399 lines
14 KiB
C#
399 lines
14 KiB
C#
|
// 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<URPMaterialConverterWindow>("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<Material>(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<Material>(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);
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Maps Standard's _Mode to URP's _Surface/_AlphaClip. Also propagates cutoff.
|
||
|
/// </summary>
|
||
|
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);
|
||
|
}
|
||
|
}
|
||
|
}
|