Files
2025-11-30 08:35:03 +02:00

273 lines
9.6 KiB
C#

#if !NO_INTERNALS_ACCESS
using System.Linq;
using System.Text;
using UnityEngine;
#if UNITY_EDITOR
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEditorInternal;
#endif
namespace UnityGLTF
{
public static class GLTFMaterialHelper
{
/// <summary>
/// Return false if other delegates should take over. Only return true if you did work and assigned material.shader = newShader.
/// </summary>
public delegate bool ConvertMaterialToGLTFDelegate(Material material, Shader oldShader, Shader newShader);
private static event ConvertMaterialToGLTFDelegate ConvertMaterialDelegates;
public static void RegisterMaterialConversionToGLTF(ConvertMaterialToGLTFDelegate converter) => ConvertMaterialDelegates += converter;
public static void UnregisterMaterialConversionToGLTF(ConvertMaterialToGLTFDelegate converter) => ConvertMaterialDelegates -= converter;
public static void ConvertMaterialToGLTF(Material material, Shader oldShader, Shader newShader)
{
if (ConvertMaterialDelegates != null)
{
var list = ConvertMaterialDelegates.GetInvocationList();
foreach (var entry in list)
{
var cb = (ConvertMaterialToGLTFDelegate) entry;
if (cb != null && cb.Invoke(material, oldShader, newShader))
{
return;
}
}
}
// IDEA ideally this would use the same code path as material export/import - would reduce the amount of code duplication considerably.
// E.g. calling something like
// var glTFMaterial = ExportMaterial(material);
// ImportAndOverrideMaterial(material, glTFMaterial);
// that uses all the same heuristics, texture conversions, ...
var msg = "No automatic conversion\nfrom " + oldShader.name + "\nto " + newShader.name + "\nfound.\n\nYou can create a conversion script, adjust which old properties map to which new properties, and switch the shader again.";
#if UNITY_EDITOR
var choice = EditorUtility.DisplayDialogComplex("Shader Conversion", msg, "Just set shader", "Cancel", "Create and open conversion script");
switch (choice)
{
case 0: // OK
material.shader = newShader;
break;
case 1: // Cancel
break;
case 2: // Alt
var path = ShaderConversion.CreateConversionScript(oldShader, newShader);
InternalEditorUtility.OpenFileAtLineExternal(path, 0);
break;
}
#else
Debug.Log(msg + " Make sure your material properties match the new shader. You can add your own conversion callbacks via `RegisterMaterialConversionToGLTF`. If you think this should have been converted automatically: please open a feature request!");
material.shader = newShader;
#endif
}
public static void ValidateMaterialKeywords(Material material)
{
// TODO ensure we're setting correct keywords for
// - existence of a normal map
// - existence of emission color values or texture
// -
// var needsVolumeTransmission = false;
// needsVolumeTransmission |= material.HasProperty(thicknessFactor) && material.GetFloat(thicknessFactor) > 0;
// needsVolumeTransmission |= material.HasProperty(transmissionFactor) && material.GetFloat(transmissionFactor) > 0;
// material.SetKeyword("_VOLUME_TRANSMISSION", needsVolumeTransmission);
//
// var needsIridescence = material.HasProperty(iridescenceFactor) && material.GetFloat(iridescenceFactor) > 0;
// material.SetKeyword("_IRIDESCENCE", needsIridescence);
//
// var needsSpecular = material.HasProperty(specularFactor) && material.GetFloat(specularFactor) > 0;
// material.SetKeyword("_SPECULAR", needsSpecular);
var isImplicitBlendMode = true;
const string blendModeProp = "_OverrideSurfaceMode";
if (material.HasProperty(blendModeProp))
{
var blendMode = material.GetInt(blendModeProp);
isImplicitBlendMode = blendMode == 0;
}
#if UNITY_VISIONOS
// VisionOS doesn't support our roughness opaque rendererfeature,
// so on material import we set Volume/Transmission Materials to Transparent
isImplicitBlendMode = false;
#endif
if (isImplicitBlendMode)
{
if (material.IsKeywordEnabled("_VOLUME_TRANSMISSION_ON"))
{
// We want to enforce opaque rendering if
// - Transmission is enabled
// - Roughness is > 0
// - The material isn't set to explicitly render as transparent
// enforce Opaque
if (material.HasProperty("_BUILTIN_Surface")) material.SetFloat("_BUILTIN_Surface", 0);
if (material.HasProperty("_Surface")) material.SetFloat("_Surface", 0);
material.DisableKeyword("_SURFACE_TYPE_TRANSPARENT");
material.DisableKeyword("_BUILTIN_SURFACE_TYPE_TRANSPARENT");
// enforce queue control and render queue 3000
if (material.HasProperty("_QueueControl")) material.SetFloat("_QueueControl", 1);
if (material.HasProperty("_BUILTIN_QueueControl")) material.SetFloat("_BUILTIN_QueueControl", 1);
// not a great choice: using 2999 as magic value for "we automatically set the queue for you"
// so the change can be reverted if someone toggles transmission on and then off again.
material.renderQueue = 2999;
}
else
{
if (material.renderQueue == 2999)
{
if (material.HasProperty("_QueueControl")) material.SetFloat("_QueueControl", 0);
if (material.HasProperty("_BUILTIN_QueueControl")) material.SetFloat("_BUILTIN_QueueControl", 0);
material.renderQueue = -1;
}
}
}
if (material.HasProperty("emissiveFactor"))
material.globalIlluminationFlags = MaterialEditor.FixupEmissiveFlag(material.GetColor("emissiveFactor"), material.globalIlluminationFlags);
}
public static void SetKeyword(Material material, string keyword, bool state)
{
if (state)
{
material.EnableKeyword(keyword + "_ON");
material.EnableKeyword(keyword);
}
else
{
material.DisableKeyword(keyword + "_ON");
material.DisableKeyword(keyword);
}
if (material.HasProperty(keyword))
material.SetFloat(keyword, state ? 1 : 0);
}
}
#if UNITY_EDITOR
internal static class ShaderConversion
{
public static string CreateConversionScript(Shader oldShader, Shader newShader)
{
var classShaderName = oldShader.name
.Replace("/", "_")
.Replace(" ", "_")
.Replace("\\", "_");
var scriptFile = ShaderConversionScriptTemplate;
scriptFile = scriptFile.Replace("<OldShader>", classShaderName);
scriptFile = scriptFile.Replace("<OldShaderName>", oldShader.name);
var sb = new StringBuilder();
foreach(var (propName, propDisplayName, type) in GetShaderProperties(oldShader))
{
sb.AppendLine($"\t\tvar {propName} = material.{MethodFromType("Get", type)}(\"{propName}\"); // {propDisplayName}");
}
var sb2 = new StringBuilder();
foreach(var (propName, propDisplayName, type) in GetShaderProperties(newShader))
{
sb2.AppendLine($"\t\t// material.{MethodFromType("Set", type)}(\"{propName}\", insert_value_here); // {propDisplayName}");
}
scriptFile = scriptFile.Replace("\t\t<OldProperties>", sb.ToString());
scriptFile = scriptFile.Replace("\t\t<NewProperties>", sb2.ToString());
const string dir = "Assets/Editor/ShaderConversions";
Directory.CreateDirectory(dir);
var fileName = dir + "/" + classShaderName + ".cs";
if (!File.Exists(fileName) || EditorUtility.DisplayDialog("File already exists", $"The file \"{fileName}\" already exists. Replace?", "Replace", "Cancel"))
File.WriteAllText(fileName, scriptFile);
AssetDatabase.Refresh();
return fileName;
}
private static IEnumerable<(string propName, string propDisplayName, ShaderUtil.ShaderPropertyType type)> GetShaderProperties(Shader shader)
{
var c = ShaderUtil.GetPropertyCount(shader);
for (var i = 0; i < c; i++)
{
if (ShaderUtil.IsShaderPropertyHidden(shader, i)) continue;
if (ShaderUtil.IsShaderPropertyNonModifiableTexureProperty(shader, i)) continue;
var propName = ShaderUtil.GetPropertyName(shader, i);
if(propName.StartsWith("unity_")) continue;
var propDisplayName = ShaderUtil.GetPropertyDescription(shader, i);
var type = ShaderUtil.GetPropertyType(shader, i);
yield return (propName, propDisplayName, type);
}
}
private static string MethodFromType(string prefix, ShaderUtil.ShaderPropertyType propertyType)
{
switch (propertyType)
{
case ShaderUtil.ShaderPropertyType.Color: return prefix + "Color";
case ShaderUtil.ShaderPropertyType.Float: return prefix + "Float";
#if UNITY_2021_1_OR_NEWER
case ShaderUtil.ShaderPropertyType.Int: return prefix + "Int";
#endif
case ShaderUtil.ShaderPropertyType.Range: return prefix + "Float";
case ShaderUtil.ShaderPropertyType.Vector: return prefix + "Vector";
case ShaderUtil.ShaderPropertyType.TexEnv: return prefix + "Texture";
}
return prefix + "UnknownPropertyType"; // compiler error
}
private const string ShaderConversionScriptTemplate =
@"using UnityEditor;
using UnityEngine;
using UnityGLTF;
class Convert_<OldShader>_to_GLTF
{
const string shaderName = ""<OldShaderName>"";
[InitializeOnLoadMethod]
private static void Register()
{
GLTFMaterialHelper.RegisterMaterialConversionToGLTF(ConvertMaterialProperties);
}
private static bool ConvertMaterialProperties(Material material, Shader oldShader, Shader newShader)
{
if (oldShader.name != shaderName) return false;
// Reading old shader properties.
<OldProperties>
material.shader = newShader;
// Assigning new shader properties.
// Uncomment lines you need, and set properties from values from the section above.
<NewProperties>
// Ensure keywords are correctly set after conversion.
// Example:
// if (material.GetFloat(""_VERTEX_COLORS"") > 0.5f) material.EnableKeyword(""_VERTEX_COLORS_ON"");
ShaderGraphHelpers.ValidateMaterialKeywords(material);
return true;
}
}
";
}
#endif
}
#endif