235 lines
11 KiB
C#
235 lines
11 KiB
C#
// 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<string, Material>();
|
|
foreach (var name in CuratedPrefabNames)
|
|
{
|
|
var go = AssetDatabase.LoadAssetAtPath<GameObject>(ArtPrefabs + name + ".prefab");
|
|
if (go == null) { Debug.LogWarning("[EnvArt] missing prefab: " + name); continue; }
|
|
foreach (var r in go.GetComponentsInChildren<Renderer>(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<Material>(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<Material>(FallbackPath);
|
|
int swapped = 0, fell = 0;
|
|
foreach (var r in root.GetComponentsInChildren<Renderer>(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<Material>(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<Material>(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
|