// Assets/Editor/URPMaterialConverterWindow.cs using System.Collections.Generic; using UnityEditor; using UnityEngine; public class URPMaterialConverterWindow : EditorWindow { private DefaultAsset _folder; // project folder reference private bool _dryRun = false; private bool _includeSubfolders = true; [MenuItem("Tools/URP Material Converter")] public static void Open() => GetWindow("URP Material Converter"); private void OnGUI() { GUILayout.Label("Convert Built-in/BiRP materials to URP", EditorStyles.boldLabel); EditorGUILayout.HelpBox("Select a project folder that contains .mat assets. This tool will scan and convert them to URP-compatible shaders.", MessageType.Info); _folder = (DefaultAsset)EditorGUILayout.ObjectField("Folder", _folder, typeof(DefaultAsset), false); _includeSubfolders = EditorGUILayout.Toggle("Include Subfolders", _includeSubfolders); _dryRun = EditorGUILayout.Toggle(new GUIContent("Dry Run (report only)"), _dryRun); using (new EditorGUI.DisabledScope(_folder == null)) { if (GUILayout.Button(_dryRun ? "Analyze" : "Convert to URP")) { var path = AssetDatabase.GetAssetPath(_folder); if (!AssetDatabase.IsValidFolder(path)) { EditorUtility.DisplayDialog("Invalid Folder", "Please select a valid project folder.", "OK"); return; } ConvertAllInFolder(path, _includeSubfolders, _dryRun); } } GUILayout.Space(8); if (GUILayout.Button("Select URP Sample Shaders (Ping)")) { PingURPShaders(); } } private static void ConvertAllInFolder(string folderPath, bool recursive, bool dryRun) { string[] searchIn = new[] { folderPath }; string filter = "t:Material"; var guids = AssetDatabase.FindAssets(filter, searchIn); int converted = 0; int analyzed = 0; try { for (int i = 0; i < guids.Length; i++) { string assetPath = AssetDatabase.GUIDToAssetPath(guids[i]); if (!assetPath.EndsWith(".mat")) continue; var mat = AssetDatabase.LoadAssetAtPath(assetPath); if (mat == null) continue; if (EditorUtility.DisplayCancelableProgressBar( dryRun ? "Analyzing Materials…" : "Converting Materials…", $"{mat.name} ({i+1}/{guids.Length})", (float)(i + 1) / guids.Length)) { break; } bool willConvert = WillConvert(mat); analyzed++; if (willConvert && !dryRun) { Undo.RecordObject(mat, "Convert Material to URP"); ConvertMaterialToURP(mat); EditorUtility.SetDirty(mat); converted++; } } } finally { EditorUtility.ClearProgressBar(); if (!dryRun) { AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } } EditorUtility.DisplayDialog( dryRun ? "URP Converter — Analysis" : "URP Converter — Complete", dryRun ? $"Analyzed {analyzed} materials.\n{CountConvertible(guids)} can be converted." : $"Processed {analyzed} materials.\nConverted: {converted}", "OK"); // Local helper to count convertible without loading twice int CountConvertible(string[] g) { int c = 0; foreach (var guid in g) { var p = AssetDatabase.GUIDToAssetPath(guid); var m = AssetDatabase.LoadAssetAtPath(p); if (m != null && WillConvert(m)) c++; } return c; } } private static bool WillConvert(Material mat) { if (mat == null || mat.shader == null) return false; string s = mat.shader.name; // Skip if already URP if (s.StartsWith("Universal Render Pipeline/")) return false; // Common convertible buckets if (s == "Standard" || s == "Standard (Specular setup)") return true; if (s.StartsWith("Unlit/")) return true; if (s.StartsWith("Particles/Standard")) return true; // You can add more mappings here as needed. return false; } private static void ConvertMaterialToURP(Material mat) { if (mat == null || mat.shader == null) return; string src = mat.shader.name; if (src == "Standard" || src == "Standard (Specular setup)") { ConvertStandardToURPLit(mat, specularWorkflow: src.Contains("Specular")); return; } if (src.StartsWith("Unlit/")) { var urpUnlit = Shader.Find("Universal Render Pipeline/Unlit"); if (urpUnlit != null) { var data = CaptureCommon(mat); mat.shader = urpUnlit; ApplyCommon(mat, data); // Unlit ignores metallic/smoothness, but we still keep base/albedo/alpha/emission ApplySurfaceSettingsFromStandard(mat); ApplyEmission(mat, data); } return; } if (src.StartsWith("Particles/Standard Unlit")) { var urpParticlesUnlit = Shader.Find("Universal Render Pipeline/Particles/Unlit"); if (urpParticlesUnlit != null) { var data = CaptureCommon(mat); mat.shader = urpParticlesUnlit; ApplyCommon(mat, data); ApplySurfaceSettingsFromStandard(mat); ApplyEmission(mat, data); } return; } if (src.StartsWith("Particles/Standard Surface")) { var urpParticlesLit = Shader.Find("Universal Render Pipeline/Particles/Lit"); if (urpParticlesLit != null) { var data = CapturePBR(mat); mat.shader = urpParticlesLit; ApplyCommon(mat, data.common); ApplyPBR(mat, data); ApplySurfaceSettingsFromStandard(mat); ApplyEmission(mat, data.common); } return; } // Fallback: try to place it on URP/Lit retaining as much as possible var fallback = Shader.Find("Universal Render Pipeline/Lit"); if (fallback != null) { var data = CapturePBR(mat); mat.shader = fallback; ApplyCommon(mat, data.common); ApplyPBR(mat, data); ApplySurfaceSettingsFromStandard(mat); ApplyEmission(mat, data.common); } } // ----- Data capture structs ----- private struct CommonData { public Texture mainTex; public Color baseColor; public Texture baseMap; // alias for mainTex public float cutoff; public bool alphaClip; public int renderQueue; public Texture emissionMap; public Color emissionColor; public bool emissionEnabled; } private struct PBRData { public CommonData common; // Metallic workflow public Texture metallicGlossMap; public float metallic; public float glossiness; // Specular workflow public Texture specGlossMap; public Color specColor; // Normals public Texture bumpMap; public float bumpScale; // Occlusion public Texture occlusionMap; public float occlusionStrength; } // ----- Capture/Apply helpers ----- private static CommonData CaptureCommon(Material m) { var d = new CommonData(); d.renderQueue = m.renderQueue; if (m.HasProperty("_MainTex")) d.mainTex = m.GetTexture("_MainTex"); if (m.HasProperty("_BaseMap")) d.baseMap = m.GetTexture("_BaseMap"); if (m.HasProperty("_Color")) d.baseColor = m.GetColor("_Color"); else if (m.HasProperty("_BaseColor")) d.baseColor = m.GetColor("_BaseColor"); else d.baseColor = Color.white; if (m.HasProperty("_Cutoff")) d.cutoff = m.GetFloat("_Cutoff"); if (m.HasProperty("_AlphaClip")) d.alphaClip = m.GetFloat("_AlphaClip") > 0.5f; // Emission capture d.emissionEnabled = m.IsKeywordEnabled("_EMISSION"); if (m.HasProperty("_EmissionMap")) d.emissionMap = m.GetTexture("_EmissionMap"); if (m.HasProperty("_EmissionColor")) d.emissionColor = m.GetColor("_EmissionColor"); return d; } private static PBRData CapturePBR(Material m) { var d = new PBRData { common = CaptureCommon(m) }; if (m.HasProperty("_MetallicGlossMap")) d.metallicGlossMap = m.GetTexture("_MetallicGlossMap"); if (m.HasProperty("_Metallic")) d.metallic = m.GetFloat("_Metallic"); if (m.HasProperty("_Glossiness")) d.glossiness = m.GetFloat("_Glossiness"); // Standard if (m.HasProperty("_Smoothness")) d.glossiness = m.GetFloat("_Smoothness"); // URP Lit also uses _Smoothness if (m.HasProperty("_SpecGlossMap")) d.specGlossMap = m.GetTexture("_SpecGlossMap"); if (m.HasProperty("_SpecColor")) d.specColor = m.GetColor("_SpecColor"); else if (m.HasProperty("_SpecularColor")) d.specColor = m.GetColor("_SpecularColor"); if (m.HasProperty("_BumpMap")) d.bumpMap = m.GetTexture("_BumpMap"); if (m.HasProperty("_BumpScale")) d.bumpScale = m.GetFloat("_BumpScale"); if (m.HasProperty("_OcclusionMap")) d.occlusionMap = m.GetTexture("_OcclusionMap"); if (m.HasProperty("_OcclusionStrength")) d.occlusionStrength = m.GetFloat("_OcclusionStrength"); return d; } private static void ApplyCommon(Material m, CommonData d) { // Base map/color if (m.HasProperty("_BaseMap")) { var tex = d.baseMap != null ? d.baseMap : d.mainTex; m.SetTexture("_BaseMap", tex); } if (m.HasProperty("_BaseColor")) m.SetColor("_BaseColor", d.baseColor == default ? Color.white : d.baseColor); // Alpha clip and cutoff if (m.HasProperty("_AlphaClip")) m.SetFloat("_AlphaClip", d.alphaClip ? 1f : 0f); if (m.HasProperty("_Cutoff")) m.SetFloat("_Cutoff", d.cutoff); // Keep render queue if custom if (d.renderQueue != -1) m.renderQueue = d.renderQueue; } private static void ApplyPBR(Material m, PBRData d) { // Metallic workflow if (m.HasProperty("_MetallicGlossMap")) m.SetTexture("_MetallicGlossMap", d.metallicGlossMap); if (m.HasProperty("_Metallic")) m.SetFloat("_Metallic", d.metallic); if (m.HasProperty("_Smoothness")) m.SetFloat("_Smoothness", d.glossiness); // Specular workflow (URP Lit supports both via keyword) if (m.HasProperty("_SpecGlossMap")) m.SetTexture("_SpecGlossMap", d.specGlossMap); if (m.HasProperty("_SpecColor")) m.SetColor("_SpecColor", d.specColor == default ? Color.white : d.specColor); // Normals if (m.HasProperty("_BumpMap")) m.SetTexture("_BumpMap", d.bumpMap); if (m.HasProperty("_BumpScale")) m.SetFloat("_BumpScale", d.bumpScale); // Occlusion if (m.HasProperty("_OcclusionMap")) m.SetTexture("_OcclusionMap", d.occlusionMap); if (m.HasProperty("_OcclusionStrength")) m.SetFloat("_OcclusionStrength", Mathf.Approximately(d.occlusionStrength, 0f) ? 1f : d.occlusionStrength); } private static void ApplyEmission(Material m, CommonData d) { bool hasMap = d.emissionMap != null; bool hasColor = d.emissionColor.maxColorComponent > 0.0001f; if (m.HasProperty("_EmissionMap")) m.SetTexture("_EmissionMap", d.emissionMap); if (m.HasProperty("_EmissionColor")) m.SetColor("_EmissionColor", hasColor ? d.emissionColor : Color.black); if (hasMap || hasColor || d.emissionEnabled) m.EnableKeyword("_EMISSION"); else m.DisableKeyword("_EMISSION"); } private static void ConvertStandardToURPLit(Material mat, bool specularWorkflow) { var urpLit = Shader.Find("Universal Render Pipeline/Lit"); if (urpLit == null) return; var data = CapturePBR(mat); mat.shader = urpLit; ApplyCommon(mat, data.common); ApplyPBR(mat, data); ApplySurfaceSettingsFromStandard(mat); ApplyEmission(mat, data.common); } /// /// Maps Standard's _Mode to URP's _Surface/_AlphaClip. Also propagates cutoff. /// private static void ApplySurfaceSettingsFromStandard(Material m) { // Standard _Mode: 0 Opaque, 1 Cutout, 2 Fade, 3 Transparent int mode = m.HasProperty("_Mode") ? Mathf.RoundToInt(m.GetFloat("_Mode")) : 0; if (m.HasProperty("_Surface")) { if (mode == 0) // Opaque { m.SetFloat("_Surface", 0f); if (m.HasProperty("_AlphaClip")) m.SetFloat("_AlphaClip", 0f); } else if (mode == 1) // Cutout { m.SetFloat("_Surface", 0f); if (m.HasProperty("_AlphaClip")) m.SetFloat("_AlphaClip", 1f); } else // Fade or Transparent { m.SetFloat("_Surface", 1f); // Transparent if (m.HasProperty("_AlphaClip")) m.SetFloat("_AlphaClip", 0f); } } // Keep cutoff if present if (m.HasProperty("_Cutoff") && !m.HasProperty("_AlphaClip")) { // Some shaders only rely on cutoff without explicit _AlphaClip. // Nothing special to set here; URP Lit uses _AlphaClip. } // Optional: set blending for Fade (2) vs Transparent (3) if (m.HasProperty("_Blend")) { // URP Lit doesn't use _Blend the same way; leaving it alone. } } private static void PingURPShaders() { var candidates = new[] { "Universal Render Pipeline/Lit", "Universal Render Pipeline/Unlit", "Universal Render Pipeline/Particles/Unlit", "Universal Render Pipeline/Particles/Lit" }; foreach (var n in candidates) { var s = Shader.Find(n); if (s != null) EditorGUIUtility.PingObject(s); } } }