using System; using System.Linq; using GLTF.Schema; using UnityEngine; using Object = UnityEngine.Object; #if UNITY_EDITOR using UnityEditor; #endif namespace UnityGLTF { public static class ShaderConverters { [InitializeOnLoadMethod] static void InitShaderConverters() { GLTFMaterialHelper.RegisterMaterialConversionToGLTF(ConvertStandardAndURPLit); GLTFMaterialHelper.RegisterMaterialConversionToGLTF(ConvertUnityGLTFGraphs); } private static bool ConvertUnityGLTFGraphs(Material material, Shader oldShader, Shader newShader) { // update legacy shaders that didn't have material overrides available if (oldShader.name.StartsWith("Hidden/UnityGLTF/PBRGraph") || oldShader.name.StartsWith("Hidden/UnityGLTF/UnlitGraph")) { material.shader = newShader; var meta = AssetImporter.GetAtPath(AssetDatabase.GetAssetPath(material.shader)) as IUnityGltfShaderUpgradeMeta; if (meta != null) { // Debug.Log("Updating shader from " + material.shader + " to " + meta.SourceShader + // " (transparent: " + meta.IsTransparent + ", double sided: " + meta.IsDoublesided + ")"); var isUnlit = meta.SourceShader.name.Contains("Unlit"); material.shader = meta.SourceShader; var mapper = isUnlit ? (IUniformMap) new UnlitMap(material) : new PBRGraphMap(material); if (meta.IsTransparent) mapper.AlphaMode = AlphaMode.BLEND; if (meta.IsDoublesided) mapper.DoubleSided = true; EditorUtility.SetDirty(material); } return true; } if (oldShader.name != "UnityGLTF/UnlitGraph" && oldShader.name != "UnityGLTF/PBRGraph") return false; material.shader = newShader; return true; } private static bool ConvertStandardAndURPLit(Material material, Shader oldShader, Shader newShader) { var allowedConversions = new[] { StandardShader, UnlitColorShader, UnlitTextureShader, UnlitTransparentShader, UnlitTransparentCutoutShader, URPLitShader, URPUnlitShader, }; var unlitSources = new[] { UnlitColorShader, UnlitTextureShader, URPUnlitShader, UnlitTransparentShader, UnlitTransparentCutoutShader, }; var birpShaders = new[] { StandardShader, UnlitColorShader, UnlitTextureShader, UnlitTransparentShader, UnlitTransparentCutoutShader, }; if (!allowedConversions.Contains(oldShader.name)) return false; var sourceIsUnlit = unlitSources.Contains(oldShader.name); var targetIsUnlit = newShader.name == URPUnlitShader; var sourceIsTransparent = oldShader.name == UnlitTransparentShader || oldShader.name == UnlitTransparentCutoutShader; var sourceIsBirp = birpShaders.Contains(oldShader.name); var needsEmissiveColorSpaceConversion = sourceIsBirp && QualitySettings.activeColorSpace == ColorSpace.Linear; var colorProp = sourceIsBirp ? _Color : _BaseColor; var colorTexProp = sourceIsBirp ? _MainTex : _BaseMap; var color = material.GetColor(colorProp, Color.white); var albedo = material.GetTexture(colorTexProp, null); var albedoOffset = material.GetTextureOffset(colorTexProp, Vector2.zero); var albedoTiling = material.GetTextureScale(colorTexProp, Vector2.one); var isTransparent = material.GetTag("RenderType", false) == "Transparent" || sourceIsTransparent; var metallic = material.GetFloat(_Metallic, 0); var smoothness = material.HasProperty(_Smoothness) ? material.GetFloat(_Smoothness, 0) : material.HasProperty(_Glossiness) ? material.GetFloat(_Glossiness, 0) : 0.5f; var metallicGloss = material.GetTexture(_MetallicGlossMap, null); var normal = material.GetTexture(_BumpMap, null); var normalStrength = material.GetFloat(_BumpScale, 1); var occlusion = material.GetTexture(_OcclusionMap, null); var occlusionStrength = material.GetFloat(_Strength, 1); var emission = material.GetTexture(_EmissionMap, null); var emissionColor = material.GetColor(_EmissionColor, Color.black); // if emission is OFF we don't want to set it to ON during conversion if ((oldShader.name == StandardShader || oldShader.name == URPLitShader) && !material.IsKeywordEnabled("_EMISSION")) { emission = null; emissionColor = Color.black; } var cutoff = material.GetFloat(_Cutoff, 0.5f); var isCutoff = material.IsKeywordEnabled("_ALPHATEST_ON") || material.IsKeywordEnabled("_BUILTIN_ALPHATEST_ON") || material.IsKeywordEnabled("_BUILTIN_AlphaClip") || oldShader.name == UnlitTransparentCutoutShader; material.shader = newShader; material.SetColor(baseColorFactor, color); material.SetTexture(baseColorTexture, albedo); material.SetTextureOffset(baseColorTexture, albedoOffset); material.SetTextureScale(baseColorTexture, albedoTiling); if (albedoOffset != Vector2.zero || albedoTiling != Vector2.one) GLTFMaterialHelper.SetKeyword(material, "_TEXTURE_TRANSFORM", true); material.SetFloat(metallicFactor, metallic); material.SetFloat(roughnessFactor, 1 - smoothness); const string ConversionWarning = "The Metallic (R) Smoothness (A) texture needs to be converted to Roughness (B) Metallic (G). "; #if UNITY_EDITOR && UNITY_2022_1_OR_NEWER var importerPath = AssetDatabase.GetAssetPath(metallicGloss); var importer = AssetImporter.GetAtPath(importerPath) as TextureImporter; if (importer && importer.swizzleG != TextureImporterSwizzle.OneMinusR) // can't really detect if this has been done on the texture already, this is just a heuristic... { if (EditorUtility.DisplayDialog("Texture Conversion", ConversionWarning + "This is done by swizzling texture channels in the importer. Do you want to proceed?", "OK", DialogOptOutDecisionType.ForThisSession, nameof(GLTFMaterialHelper) + "_texture_conversion")) { importer.swizzleR = TextureImporterSwizzle.B; importer.swizzleG = TextureImporterSwizzle.OneMinusR; importer.swizzleB = TextureImporterSwizzle.G; Undo.RegisterImporterUndo(importerPath, "Texture Swizzles (M__G > _RM_)"); importer.SaveAndReimport(); } } else if (metallicGloss) { Debug.LogWarning(ConversionWarning + "This currently needs to be done manually. Please swap channels in an external software.", material); } #else if (metallicGloss) Debug.LogWarning(ConversionWarning + "This currently needs to be done manually. Please swap channels in an external software.", material); #endif // TODO: convert metallicGloss to metallicRoughnessTexture format: Metallic (R) + Smoothness (A) → Roughness (G) + Metallic (B) // TODO: when smoothness is not 0 or 1 and there's a texture, need to convert to a texture and set roughness = 1, otherwise the math doesn't match // TODO: figure out where to put the newly created texture, and how to avoid re-creating it several times when multiple materials may use it. material.SetTexture(metallicRoughnessTexture, metallicGloss); material.SetTexture(normalTexture, normal); material.SetFloat(normalScale, normalStrength); material.SetTexture(occlusionTexture, occlusion); material.SetFloat(occlusionStrength1, occlusionStrength); material.SetTexture(emissiveTexture, emission); material.SetFloat(alphaCutoff, isCutoff ? cutoff : -cutoff); // bit hacky, but that avoids an additional keyword for determining alpha cutoff right now var map = new PBRGraphMap(material); map.AlphaMode = isCutoff ? AlphaMode.MASK : (isTransparent ? AlphaMode.BLEND : AlphaMode.OPAQUE); material.SetColor(emissiveFactor, needsEmissiveColorSpaceConversion ? emissionColor.linear : emissionColor); // set the flags on conversion, otherwise it's confusing why they're not on - can't easily replicate the magic that Unity does in their inspectors when changing emissive on/off if (material.globalIlluminationFlags == MaterialGlobalIlluminationFlags.None || material.globalIlluminationFlags == MaterialGlobalIlluminationFlags.EmissiveIsBlack) material.globalIlluminationFlags = MaterialGlobalIlluminationFlags.BakedEmissive; // ensure keywords are correctly set after conversion GLTFMaterialHelper.ValidateMaterialKeywords(material); return true; } // ReSharper disable InconsistentNaming private const string StandardShader = "Standard"; private const string UnlitColorShader = "Unlit/Color"; private const string UnlitTextureShader = "Unlit/Texture"; private const string UnlitTransparentShader = "Unlit/Transparent"; private const string UnlitTransparentCutoutShader = "Unlit/Transparent Cutout"; private const string URPLitShader = "Universal Render Pipeline/Lit"; private const string URPUnlitShader = "Universal Render Pipeline/Unlit"; // Standard and URP-Lit property names private static readonly int _Color = Shader.PropertyToID("_Color"); private static readonly int _BaseColor = Shader.PropertyToID("_BaseColor"); private static readonly int _MainTex = Shader.PropertyToID("_MainTex"); private static readonly int _BaseMap = Shader.PropertyToID("_BaseMap"); private static readonly int _Metallic = Shader.PropertyToID("_Metallic"); private static readonly int _Glossiness = Shader.PropertyToID("_Glossiness"); private static readonly int _Smoothness = Shader.PropertyToID("_Smoothness"); private static readonly int _MetallicGlossMap = Shader.PropertyToID("_MetallicGlossMap"); private static readonly int _BumpMap = Shader.PropertyToID("_BumpMap"); private static readonly int _BumpScale = Shader.PropertyToID("_BumpScale"); private static readonly int _OcclusionMap = Shader.PropertyToID("_OcclusionMap"); private static readonly int _Strength = Shader.PropertyToID("_OcclusionStrength"); private static readonly int _EmissionMap = Shader.PropertyToID("_EmissionMap"); private static readonly int _EmissionColor = Shader.PropertyToID("_EmissionColor"); private static readonly int _Cutoff = Shader.PropertyToID("_Cutoff"); // glTF property names private static readonly int baseColorFactor = Shader.PropertyToID("baseColorFactor"); private static readonly int baseColorTexture = Shader.PropertyToID("baseColorTexture"); private static readonly int metallicFactor = Shader.PropertyToID("metallicFactor"); private static readonly int roughnessFactor = Shader.PropertyToID("roughnessFactor"); private static readonly int metallicRoughnessTexture = Shader.PropertyToID("metallicRoughnessTexture"); private static readonly int normalTexture = Shader.PropertyToID("normalTexture"); private static readonly int normalScale = Shader.PropertyToID("normalScale"); private static readonly int occlusionTexture = Shader.PropertyToID("occlusionTexture"); private static readonly int occlusionStrength1 = Shader.PropertyToID("occlusionStrength"); private static readonly int emissiveTexture = Shader.PropertyToID("emissiveTexture"); private static readonly int emissiveFactor = Shader.PropertyToID("emissiveFactor"); private static readonly int alphaCutoff = Shader.PropertyToID("alphaCutoff"); // ReSharper restore InconsistentNaming private static readonly string[] emissivePropNames = new[] { "emissiveFactor", "_EmissionColor" }; [MenuItem("CONTEXT/Material/UnityGLTF Material Helpers/Convert Emissive Colors > sRGB - weaker, darker")] private static void ConvertToSRGB(MenuCommand command) { if (!(command.context is Material mat)) return; Undo.RegisterCompleteObjectUndo(mat, "Convert emissive colors to sRGB"); foreach(var propName in emissivePropNames) if (mat.HasProperty(propName)) mat.SetColor(propName, mat.GetColor(propName).gamma); } [MenuItem("CONTEXT/Material/UnityGLTF Material Helpers/Convert Emissive Colors > Linear - brighter, stronger")] private static void ConvertToLinear(MenuCommand command) { if (!(command.context is Material mat)) return; Undo.RegisterCompleteObjectUndo(mat, "Convert emissive colors to sRGB"); foreach(var propName in emissivePropNames) if (mat.HasProperty(propName)) mat.SetColor(propName, mat.GetColor(propName).linear); } [MenuItem("CONTEXT/Material/UnityGLTF Material Helpers/ Select all materials with this shader", false, -1000)] private static void SelectAllMaterialsWithShader(MenuCommand command) { if (!(command.context is Material mat)) return; var allMaterials = AssetDatabase.FindAssets("t:Material") .Select(AssetDatabase.GUIDToAssetPath) .Select(AssetDatabase.LoadAssetAtPath) .Where(x => !AssetDatabase.IsSubAsset(x)) .Where(x => x.shader == mat.shader) .Cast() .ToArray(); foreach (var obj in allMaterials) EditorGUIUtility.PingObject(obj); Selection.objects = allMaterials; } private static bool TryGetMetadataOfType(Shader shader, out T obj) where T : ScriptableObject { obj = null; var path = AssetDatabase.GetAssetPath(shader); foreach (var asset in AssetDatabase.LoadAllAssetsAtPath(path)) { if (asset is T metadataAsset) { obj = metadataAsset; return true; } } return false; } } static class MaterialHelper { public static float GetFloat(this Material material, int propertyIdx, float fallback) { if (material.HasProperty(propertyIdx)) return material.GetFloat(propertyIdx); return fallback; } public static Color GetColor(this Material material, int propertyIdx, Color fallback) { if (material.HasProperty(propertyIdx)) return material.GetColor(propertyIdx); return fallback; } public static Texture GetTexture(this Material material, int propertyIdx, Texture fallback) { if (material.HasProperty(propertyIdx)) return material.GetTexture(propertyIdx); return fallback; } public static Vector2 GetTextureScale(this Material material, int propertyIdx, Vector2 fallback) { if (material.HasProperty(propertyIdx)) return material.GetTextureScale(propertyIdx); return fallback; } public static Vector2 GetTextureOffset(this Material material, int propertyIdx, Vector2 fallback) { if (material.HasProperty(propertyIdx)) return material.GetTextureOffset(propertyIdx); return fallback; } } }