// WL-816a — 단색안(데모처럼 텍스처 0) 머티리얼을 「메시·서브메시별 팔레트 평균색」으로 다시 만든다. // WL_FarmLook 씬의 WLFarmLook.entries[].original 을 근거로 계산하고 entries[].toonFlat 에 다시 꽂는다. public static class WL816a_Flat { const string DstScene = "Assets/WL/Look/Farm/Scenes/WL_FarmLook.unity"; const string FlatDir = "Assets/WL/Look/Farm/Materials/Flat"; static readonly UnityEngine.Color ShadowRatio = new UnityEngine.Color(0.2164f, 0.2092f, 0.3713f, 0f); public static void Run() { var sb = new System.Text.StringBuilder(); var toon = UnityEngine.Shader.Find("Shader Graphs/Toon"); var scene = UnityEditor.SceneManagement.EditorSceneManager.OpenScene( DstScene, UnityEditor.SceneManagement.OpenSceneMode.Single); var comp = UnityEngine.Object.FindFirstObjectByType(UnityEngine.FindObjectsInactive.Include); if (comp == null) { UnityEngine.Debug.Log("[WL816a Flat] WLFarmLook 없음"); return; } var palette = LoadPng("Assets/FarmingIsland/Textures/Palette.png"); var island = LoadPng("Assets/FarmingIsland/Textures/Islands/Island01.png"); sb.AppendLine("팔레트 " + (palette != null ? palette.width + "x" + palette.height : "null") + " · 섬 " + (island != null ? island.width + "x" + island.height : "null")); var cache = new System.Collections.Generic.Dictionary(); int ok = 0, fail = 0, notex = 0, dbg = 0; for (int i = 0; i < comp.entries.Length; i++) { var e = comp.entries[i]; if (e == null || e.target == null || e.original == null) continue; UnityEngine.Mesh mesh = null; var mf = e.target.GetComponent(); if (mf != null) mesh = mf.sharedMesh; var sk = e.target as UnityEngine.SkinnedMeshRenderer; if (sk != null) mesh = sk.sharedMesh; var flat = new UnityEngine.Material[e.original.Length]; for (int s = 0; s < e.original.Length; s++) { var src = e.original[s]; if (src == null || src.shader == null || src.shader.name != "Universal Render Pipeline/Simple Lit") { flat[s] = src; continue; } var baseTex = src.HasProperty("_BaseMap") ? src.GetTexture("_BaseMap") : null; var baseCol = src.HasProperty("_BaseColor") ? src.GetColor("_BaseColor") : UnityEngine.Color.white; UnityEngine.Color col = baseCol; string key = src.name; if (dbg < 4) { sb.AppendLine(" dbg[" + dbg + "] rend=" + e.target.name + " src=" + src.name + " tex=" + (baseTex == null ? "null" : "'" + baseTex.name + "'") + " mesh=" + (mesh == null ? "null" : mesh.name)); dbg++; } UnityEngine.Texture2D lut = null; if (baseTex != null) { if (baseTex.name == "Palette") lut = palette; else if (baseTex.name == "Island01") lut = island; } if (lut == null) { notex++; } else if (mesh == null) { col = MeanOf(lut); key = src.name + "_mean"; fail++; } else { UnityEngine.Color c; if (SampleMesh(mesh, s, lut, out c)) { col = c; key = src.name + "_" + mesh.name + "_" + s; ok++; } else { col = MeanOf(lut); key = src.name + "_mean"; fail++; } } col.a = 0f; flat[s] = Get(key, col, toon, cache); } e.toonFlat = flat; } UnityEditor.EditorUtility.SetDirty(comp); UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(scene); UnityEditor.SceneManagement.EditorSceneManager.SaveScene(scene); UnityEditor.AssetDatabase.SaveAssets(); sb.AppendLine("단색 머티리얼 " + cache.Count + "종 · UV 샘플 성공 " + ok + " · 실패 " + fail + " · 텍스처없음(원래단색) " + notex); UnityEngine.Debug.Log("[WL816a Flat]\n" + sb.ToString()); } static UnityEngine.Material Get(string key, UnityEngine.Color col, UnityEngine.Shader toon, System.Collections.Generic.Dictionary cache) { UnityEngine.Material m; if (cache.TryGetValue(key, out m) && m != null) return m; EnsureDir(FlatDir); string path = FlatDir + "/Farm_" + Safe(key) + "_ToonFlat.mat"; m = UnityEditor.AssetDatabase.LoadAssetAtPath(path); if (m == null) { m = new UnityEngine.Material(toon); UnityEditor.AssetDatabase.CreateAsset(m, path); } m.shader = toon; m.SetTexture("_BaseMap", null); m.SetTexture("_ShadowBaseMap", null); m.SetColor("_DiffuseColor", col); m.SetColor("_ShadowDiffuseColor", new UnityEngine.Color(col.r * ShadowRatio.r, col.g * ShadowRatio.g, col.b * ShadowRatio.b, 0f)); Common(m); UnityEditor.EditorUtility.SetDirty(m); cache[key] = m; return m; } /// 데모 실측 공통값(3DPixelArtEnvironment/Materials/Stone.mat 기준). static void Common(UnityEngine.Material m) { m.SetFloat("_Shades", 7f); m.SetFloat("_Brightness", 0.25f); m.SetFloat("_MinimumDarkness", 0.2f); m.SetFloat("_AmbientStrength", 0.1f); m.SetFloat("_OUTLINESENABLED", 1f); m.SetColor("_Outline", new UnityEngine.Color(0f, 0f, 0f, 0f)); m.SetFloat("_CLOUDSENABLED", 1f); m.SetFloat("_Cloud_Change", 0.005f); m.SetFloat("_Cloud_Cover", 0.5f); m.SetFloat("_Cloud_Density", 0.01f); m.SetFloat("_Cloud_Strength", 1f); m.SetVector("_Cloud_Movement", new UnityEngine.Vector4(1, 1, 0, 0)); m.SetVector("_Cloud_Step", new UnityEngine.Vector4(13, 17, 0, 0)); m.SetFloat("_Cull", 2f); m.SetFloat("_Surface", 0f); m.SetFloat("_MAIN_LIGHT", 0f); m.SetFloat("_SHADOWS_SOFT", 1f); m.SetFloat("_CastShadows", 1f); m.SetFloat("_ReceiveShadows", 1f); m.SetFloat("_NormalEdgeStrength", 0.3f); m.SetFloat("_DepthEdgeStrength", 0.5f); m.SetFloat("_DepthThreshold", 0.01f); m.SetFloat("_NormalThreshold", 1f); m.SetVector("_NormalBias", new UnityEngine.Vector4(1, 1, 1, 0)); m.EnableKeyword("_OUTLINESENABLED"); m.EnableKeyword("_CLOUDSENABLED"); m.EnableKeyword("_SHADOWS_SOFT"); } static bool SampleMesh(UnityEngine.Mesh mesh, int sub, UnityEngine.Texture2D lut, out UnityEngine.Color col) { col = UnityEngine.Color.white; try { int si = sub < mesh.subMeshCount ? sub : 0; var uv = mesh.uv; var vt = mesh.vertices; if (uv == null || uv.Length == 0 || vt == null || vt.Length == 0) return false; var tri = mesh.GetTriangles(si); if (tri == null || tri.Length < 3) return false; double r = 0, g = 0, b = 0, wsum = 0; for (int t = 0; t + 2 < tri.Length; t += 3) { int i0 = tri[t], i1 = tri[t + 1], i2 = tri[t + 2]; if (i0 >= uv.Length || i1 >= uv.Length || i2 >= uv.Length) continue; float w = UnityEngine.Vector3.Cross(vt[i1] - vt[i0], vt[i2] - vt[i0]).magnitude * 0.5f; if (w <= 0f) w = 1e-6f; var c0 = Pick(lut, uv[i0]); var c1 = Pick(lut, uv[i1]); var c2 = Pick(lut, uv[i2]); r += w * (c0.r + c1.r + c2.r) / 3.0; g += w * (c0.g + c1.g + c2.g) / 3.0; b += w * (c0.b + c1.b + c2.b) / 3.0; wsum += w; } if (wsum <= 0) return false; col = new UnityEngine.Color((float)(r / wsum), (float)(g / wsum), (float)(b / wsum), 0f); return true; } catch { return false; } } static UnityEngine.Color Pick(UnityEngine.Texture2D t, UnityEngine.Vector2 uv) { int x = UnityEngine.Mathf.Clamp((int)(uv.x * t.width), 0, t.width - 1); int y = UnityEngine.Mathf.Clamp((int)(uv.y * t.height), 0, t.height - 1); return t.GetPixel(x, y); } static UnityEngine.Color MeanOf(UnityEngine.Texture2D t) { var px = t.GetPixels(); double r = 0, g = 0, b = 0; for (int i = 0; i < px.Length; i++) { r += px[i].r; g += px[i].g; b += px[i].b; } return new UnityEngine.Color((float)(r / px.Length), (float)(g / px.Length), (float)(b / px.Length), 0f); } static UnityEngine.Texture2D LoadPng(string assetPath) { try { string full = System.IO.Path.Combine(System.Environment.CurrentDirectory, assetPath); if (!System.IO.File.Exists(full)) return null; var t = new UnityEngine.Texture2D(2, 2, UnityEngine.TextureFormat.RGBA32, false, false); if (!UnityEngine.ImageConversion.LoadImage(t, System.IO.File.ReadAllBytes(full))) return null; t.filterMode = UnityEngine.FilterMode.Point; return t; } catch { return null; } } static string Safe(string s) { var sb = new System.Text.StringBuilder(); for (int i = 0; i < s.Length; i++) sb.Append(char.IsLetterOrDigit(s[i]) || s[i] == '_' ? s[i] : '_'); return sb.ToString(); } static void EnsureDir(string p) { if (UnityEditor.AssetDatabase.IsValidFolder(p)) return; string parent = System.IO.Path.GetDirectoryName(p).Replace('\\', '/'); string leaf = System.IO.Path.GetFileName(p); if (!UnityEditor.AssetDatabase.IsValidFolder(parent)) EnsureDir(parent); UnityEditor.AssetDatabase.CreateFolder(parent, leaf); } }