// Editor-only art-pipeline helpers for Project-M. // Converts the HDRP-authored BefourStudios props to stock URP/Lit materials // (Entities-Graphics compatible) and remaps placed prop instances onto them. // Reusable for future Synty/asset packs: edit CuratedPrefabNames or call the // public static methods. Lives in Assembly-CSharp-Editor (Editor folder). // // Re-run contract: ConvertCurated() OVERWRITES existing M_Env_* materials in place // (GUID preserved so scene/prefab refs stay valid) -> any hand-tuning of a converted // material is reset to the generated values on the next run. #if UNITY_EDITOR using System.Collections.Generic; using UnityEditor; using UnityEngine; using UnityEngine.Rendering; namespace ProjectM.EditorTools { public static class EnvArtTools { const string EnvDir = "Assets/_Project/Materials/Env"; const string ArtPrefabs = "Assets/BefourStudios/Art/Prefabs/"; const string FallbackPath = EnvDir + "/M_Env_Fallback.mat"; // Candidate property names across HDRP/Lit and HDRP ShaderGraph variants. static readonly string[] BaseProps = { "_BaseColorMap", "_BaseTexture", "_BaseMap", "_MainTex", "_AlbedoTexture" }; static readonly string[] NormalProps = { "_NormalMap", "_NormalTexture", "_BumpMap" }; // REAL color tints only. NOT _BaseColorMultiply (a float scalar on these sources) — // reading a float via GetColor() returns (0,0,0) and would black out the albedo. static readonly string[] TintProps = { "_BaseColor", "_AlbedoTint", "_Color" }; static readonly string[] EmissiveProps = { "_EmissiveColor", "_EmissionColor" }; // Source prefabs whose materials get converted. Covers all placed home-base props // plus the two ghost props (SM_Storage, SM_Battery). static readonly string[] CuratedPrefabNames = { "SM_ModularBlockCentral", "SM_ModularBlockEdge01", "SM_ModularBlockEdge02", "SM_ModularBlockCorner01", "SM_ModularBlockCorner02", "SM_Dome01", "SM_DomeDoor", "SM_Battery", "SM_BatteryCharger", "SM_BatteryPod", "SM_Crate01", "SM_Crate02", "SM_Crate03", "SM_Plant07", "SM_Plant08", "SM_Plant16", "SM_LightPole", "SM_SolarPanelModule", "SM_Storage", }; [MenuItem("ProjectM/Art/1. Convert Curated Env Materials")] public static int ConvertCurated() { EnsureDir(); EnsureFallback(); // Dedup by DESTINATION path; warn on collisions (same-named sources from different folders). var byDest = new Dictionary(); foreach (var name in CuratedPrefabNames) { var go = AssetDatabase.LoadAssetAtPath(ArtPrefabs + name + ".prefab"); if (go == null) { Debug.LogWarning("[EnvArt] missing prefab: " + name); continue; } foreach (var r in go.GetComponentsInChildren(true)) foreach (var m in r.sharedMaterials) { if (m == null || string.IsNullOrEmpty(m.name) || m.name == "No Name") continue; var dest = EnvPathFor(m.name); if (byDest.TryGetValue(dest, out var prev)) { if (prev != m) Debug.LogWarning($"[EnvArt] name collision -> {dest}: keeping '{AssetDatabase.GetAssetPath(prev)}', ignoring '{AssetDatabase.GetAssetPath(m)}'"); continue; } byDest[dest] = m; } } int n = 0; foreach (var kv in byDest) { ConvertToUrp(kv.Value, kv.Key); n++; } AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); Debug.Log($"[EnvArt] Converted {n} materials into {EnvDir} (overwrites regenerate; hand edits are lost)."); return n; } public static string EnvPathFor(string srcMaterialName) { string s = srcMaterialName; if (s.StartsWith("M_")) s = s.Substring(2); else if (s.StartsWith("MI_")) s = s.Substring(3); s = s.Replace(" ", ""); return $"{EnvDir}/M_Env_{s}.mat"; } // Build (or overwrite, preserving GUID) a stock URP/Lit material from an HDRP source. public static Material ConvertToUrp(Material src, string destPath) { var sh = Shader.Find("Universal Render Pipeline/Lit"); var tmp = new Material(sh); var baseTex = FindTex(src, BaseProps) as Texture2D; var normTex = FindTex(src, NormalProps) as Texture2D; if (baseTex != null) tmp.SetTexture("_BaseMap", baseTex); Color tint = Color.white; foreach (var c in TintProps) if (HasColor(src, c)) { tint = src.GetColor(c); break; } tint.a = 1f; tmp.SetColor("_BaseColor", tint); if (normTex != null) { tmp.SetTexture("_BumpMap", normTex); tmp.EnableKeyword("_NORMALMAP"); tmp.SetFloat("_BumpScale", 1f); } // ORM channels don't match URP's _MetallicGlossMap, and there's no reflection probe here, // so keep metallic LOW (high metallic + dark skybox reads near-black) and use uniform smoothness. string nm = src.name.ToLowerInvariant(); float metallic = 0.10f, smooth = 0.45f; if (nm.Contains("metal") || nm.Contains("modular") || nm.Contains("battery") || nm.Contains("solar") || nm.Contains("dome") || nm.Contains("container") || nm.Contains("charger")) { metallic = 0.20f; smooth = 0.50f; } if (nm.Contains("glass") || nm.Contains("mirror")) { metallic = 0.0f; smooth = 0.85f; } if (nm.Contains("plant") || nm.Contains("dirt") || nm.Contains("rock") || nm.Contains("rubber") || nm.Contains("plastic")) { metallic = 0.0f; smooth = 0.30f; } tmp.SetFloat("_Metallic", metallic); tmp.SetFloat("_Smoothness", smooth); tmp.SetFloat("_WorkflowMode", 1f); // Metallic bool clip = nm.Contains("plant") || (HasFloat(src, "_AlphaCutoffEnable") && src.GetFloat("_AlphaCutoffEnable") > 0.5f) || (HasFloat(src, "_AlphaClip") && src.GetFloat("_AlphaClip") > 0.5f); if (clip) { tmp.SetFloat("_AlphaClip", 1f); tmp.EnableKeyword("_ALPHATEST_ON"); tmp.SetFloat("_Cutoff", 0.4f); tmp.renderQueue = 2450; if (nm.Contains("plant")) tmp.SetFloat("_Cull", 0f); // double-sided foliage } // Emission only when the SOURCE _Emissive flag is on AND the name marks it a light fixture, // so a default non-zero _EmissiveColor on the shader graph doesn't make everything glow // (flat color emission can't reproduce the source emission mask anyway). bool srcEmOn = HasFloat(src, "_Emissive") && src.GetFloat("_Emissive") > 0.5f; bool nameEmissive = nm.Contains("emissive") || nm.Contains("hologram") || nm.Contains("screen") || nm.Contains("lightpole") || nm.Contains("lights"); if (srcEmOn && nameEmissive) { Color ec = Color.white; foreach (var c in EmissiveProps) if (HasColor(src, c)) { ec = src.GetColor(c); break; } float emInt = HasFloat(src, "_EmissiveIntensity") ? Mathf.Clamp(src.GetFloat("_EmissiveIntensity"), 1f, 4f) : 1.5f; var e = new Color(ec.r, ec.g, ec.b) * emInt; if (e.maxColorComponent > 0.05f) { tmp.EnableKeyword("_EMISSION"); tmp.SetColor("_EmissionColor", e); tmp.globalIlluminationFlags = MaterialGlobalIlluminationFlags.BakedEmissive; } } var existing = AssetDatabase.LoadAssetAtPath(destPath); if (existing != null) { EditorUtility.CopySerialized(tmp, existing); Object.DestroyImmediate(tmp); EditorUtility.SetDirty(existing); return existing; } AssetDatabase.CreateAsset(tmp, destPath); return tmp; } // Reassign every renderer material on a placed prop instance to its converted Env equivalent. public static int RemapRenderersToEnv(GameObject root) { EnsureFallback(); var fallback = AssetDatabase.LoadAssetAtPath(FallbackPath); int swapped = 0, fell = 0; foreach (var r in root.GetComponentsInChildren(true)) { var src = r.sharedMaterials; var dst = new Material[src.Length]; for (int i = 0; i < src.Length; i++) { if (src[i] == null) { dst[i] = fallback; fell++; continue; } var env = AssetDatabase.LoadAssetAtPath(EnvPathFor(src[i].name)); if (env != null) { dst[i] = env; swapped++; } else { dst[i] = fallback; fell++; } } r.sharedMaterials = dst; } if (fell > 0) Debug.Log($"[EnvArt] RemapRenderersToEnv {root.name}: {swapped} mapped, {fell} -> fallback"); return swapped; } static bool HasColor(Material m, string prop) { if (!m.HasProperty(prop)) return false; int idx = m.shader.FindPropertyIndex(prop); if (idx < 0) return false; var t = m.shader.GetPropertyType(idx); return t == ShaderPropertyType.Color || t == ShaderPropertyType.Vector; } static bool HasFloat(Material m, string prop) { if (!m.HasProperty(prop)) return false; int idx = m.shader.FindPropertyIndex(prop); if (idx < 0) return false; var t = m.shader.GetPropertyType(idx); return t == ShaderPropertyType.Float || t == ShaderPropertyType.Range; } static Texture FindTex(Material m, string[] names) { foreach (var n in names) { if (!m.HasProperty(n)) continue; int idx = m.shader.FindPropertyIndex(n); if (idx < 0 || m.shader.GetPropertyType(idx) != ShaderPropertyType.Texture) continue; var t = m.GetTexture(n); if (t != null) return t; } return null; } static void EnsureDir() { if (!AssetDatabase.IsValidFolder(EnvDir)) AssetDatabase.CreateFolder("Assets/_Project/Materials", "Env"); } static void EnsureFallback() { if (AssetDatabase.LoadAssetAtPath(FallbackPath) != null) return; EnsureDir(); var m = new Material(Shader.Find("Universal Render Pipeline/Lit")); m.SetColor("_BaseColor", new Color(0.55f, 0.57f, 0.60f)); m.SetFloat("_Metallic", 0.1f); m.SetFloat("_Smoothness", 0.45f); AssetDatabase.CreateAsset(m, FallbackPath); } } } #endif