// WL-816a — FarmingIsland 맵 배경을 데모(3DPixelArtEnvironment) 아트풍으로 통일 // · 원본(Assets/FarmingIsland/**) 수정 0 — 복사본 머티리얼 + 복사본 씬에서만 작업 // · 데모 실측값: Shader Graphs/Toon · _Shades 7 · _Brightness 0.25 · _MinimumDarkness 0.2 // _OUTLINESENABLED 1 · _Outline(0,0,0,0) · _Cull 2 · _AmbientStrength 0.1 · 구름 그림자 on // · 그림자색 비율 = 데모 Stone 의 _ShadowDiffuseColor / _DiffuseColor = (0.2164, 0.2092, 0.3713) (814s 실측) public static class WL816a_Build { const string SrcScene = "Assets/FarmingIsland/Scenes/Level01.unity"; const string DstScene = "Assets/WL/Look/Farm/Scenes/WL_FarmLook.unity"; const string MatDir = "Assets/WL/Look/Farm/Materials"; const string ResDir = "Assets/WL/Look/Farm/Resources/WL"; 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"); if (toon == null) { UnityEngine.Debug.Log("[WL816a] Shader Graphs/Toon 없음 — 중단"); return; } EnsureDir(MatDir); EnsureDir("Assets/WL/Look/Farm/Scenes"); EnsureDir(ResDir); // ── 0. 되돌리기 설정 에셋 ──────────────────────────────────────── string soPath = ResDir + "/WLFarmLookSettings.asset"; var so = UnityEditor.AssetDatabase.LoadAssetAtPath(soPath); if (so == null) { so = UnityEngine.ScriptableObject.CreateInstance(); so.enabled_ = 1; so.mode = 1; so.applyLighting = 1; so.verboseLog = 0; UnityEditor.AssetDatabase.CreateAsset(so, soPath); sb.AppendLine("SO 생성 " + soPath); } // ── 1. 팔레트 텍스처를 디스크에서 직접 읽는다(임포터 무수정) ───── 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 : "실패") + " · 섬 " + (island != null ? island.width + "x" + island.height : "실패")); // ── 2. 씬 복사 ─────────────────────────────────────────────────── if (UnityEditor.AssetDatabase.LoadAssetAtPath(DstScene) != null) UnityEditor.AssetDatabase.DeleteAsset(DstScene); if (!UnityEditor.AssetDatabase.CopyAsset(SrcScene, DstScene)) { UnityEngine.Debug.Log("[WL816a] 씬 복사 실패"); return; } UnityEditor.AssetDatabase.Refresh(); var scene = UnityEditor.SceneManagement.EditorSceneManager.OpenScene( DstScene, UnityEditor.SceneManagement.OpenSceneMode.Single); sb.AppendLine("씬 복사 " + DstScene); // ── 3. 원본 조명 기록 ──────────────────────────────────────────── var lights = UnityEngine.Object.FindObjectsByType( UnityEngine.FindObjectsInactive.Include, UnityEngine.FindObjectsSortMode.None); var lightStates = new System.Collections.Generic.List(); for (int i = 0; i < lights.Length; i++) lightStates.Add(new WL.Look.Farm.WLFarmLook.LightState { target = lights[i], color = lights[i].color, intensity = lights[i].intensity, shadows = lights[i].shadows }); int origAmbMode = (int)UnityEngine.RenderSettings.ambientMode; var origAmbSky = UnityEngine.RenderSettings.ambientSkyColor; var origAmbEq = UnityEngine.RenderSettings.ambientEquatorColor; var origAmbGnd = UnityEngine.RenderSettings.ambientGroundColor; float origAmbI = UnityEngine.RenderSettings.ambientIntensity; var origSky = UnityEngine.RenderSettings.skybox; int origRefl = (int)UnityEngine.RenderSettings.defaultReflectionMode; var origSun = UnityEngine.RenderSettings.sun; // ── 4. 렌더러 전수 → Toon 머티리얼 세트 2종 ────────────────────── var rends = UnityEngine.Object.FindObjectsByType( UnityEngine.FindObjectsInactive.Include, UnityEngine.FindObjectsSortMode.None); var texCache = new System.Collections.Generic.Dictionary(); var flatCache = new System.Collections.Generic.Dictionary(); var entries = new System.Collections.Generic.List(); int uvOk = 0, uvFail = 0, swapped = 0; for (int i = 0; i < rends.Length; i++) { var r = rends[i]; if (r == null || r is UnityEngine.ParticleSystemRenderer) continue; if (r is UnityEngine.TrailRenderer || r is UnityEngine.LineRenderer) continue; var src = r.sharedMaterials; if (src == null || src.Length == 0) continue; UnityEngine.Mesh mesh = null; var mf = r.GetComponent(); if (mf != null) mesh = mf.sharedMesh; var skin = r as UnityEngine.SkinnedMeshRenderer; if (skin != null) mesh = skin.sharedMesh; var tex = new UnityEngine.Material[src.Length]; var flat = new UnityEngine.Material[src.Length]; bool any = false; for (int s = 0; s < src.Length; s++) { var m = src[s]; if (m == null || m.shader == null || m.shader.name != "Universal Render Pipeline/Simple Lit") { tex[s] = m; flat[s] = m; continue; } any = true; tex[s] = TexVariant(m, toon, texCache); flat[s] = FlatVariant(m, toon, mesh, s, palette, island, flatCache, ref uvOk, ref uvFail); } if (!any) continue; entries.Add(new WL.Look.Farm.WLFarmLook.Entry { target = r, original = src, toonTex = tex, toonFlat = flat }); r.sharedMaterials = tex; UnityEditor.EditorUtility.SetDirty(r); swapped++; } sb.AppendLine("렌더러 교체 " + swapped + " · Toon 텍스처본 " + texCache.Count + "종 · 단색본 " + flatCache.Count + "종 · UV 샘플 성공 " + uvOk + " 실패 " + uvFail); // ── 5. 조명·앰비언트·스카이박스 = 데모/아레나 실측값 ────────────── UnityEngine.RenderSettings.ambientMode = UnityEngine.Rendering.AmbientMode.Skybox; UnityEngine.RenderSettings.ambientSkyColor = new UnityEngine.Color(0.212f, 0.227f, 0.259f, 1f); UnityEngine.RenderSettings.ambientEquatorColor = new UnityEngine.Color(0.114f, 0.125f, 0.133f, 1f); UnityEngine.RenderSettings.ambientGroundColor = new UnityEngine.Color(0.047f, 0.043f, 0.035f, 1f); UnityEngine.RenderSettings.ambientIntensity = 1f; UnityEngine.RenderSettings.fog = false; UnityEngine.RenderSettings.skybox = UnityEditor.AssetDatabase.GetBuiltinExtraResource("Default-Skybox.mat"); UnityEngine.RenderSettings.defaultReflectionMode = UnityEngine.Rendering.DefaultReflectionMode.Skybox; UnityEngine.RenderSettings.reflectionIntensity = 1f; for (int i = 0; i < lights.Length; i++) { if (lights[i].type != UnityEngine.LightType.Directional) continue; lights[i].color = UnityEngine.Color.white; lights[i].intensity = 1f; lights[i].shadows = UnityEngine.LightShadows.Soft; // 데모 = Soft (원본 Level01 = Hard) UnityEditor.EditorUtility.SetDirty(lights[i]); } sb.AppendLine("조명/앰비언트/스카이박스 = 데모·아레나 값으로 통일 (ambient Skybox 0.212/0.227/0.259 · Dir 흰색 I=1 Soft · Default-Skybox)"); // ── 6. 🔴 GraphicsManager 비활성 — 런타임에 FarmingIsland URP 에셋을 수정한다 ── var gm = UnityEngine.Object.FindFirstObjectByType(UnityEngine.FindObjectsInactive.Include); if (gm != null) { gm.gameObject.SetActive(false); UnityEditor.EditorUtility.SetDirty(gm.gameObject); sb.AppendLine("GraphicsManager 비활성(런타임 URP 에셋 수정 방지)"); } // ── 7. 스위치 오브젝트 ─────────────────────────────────────────── var go = UnityEngine.GameObject.Find("WL_FarmLook"); if (go == null) go = new UnityEngine.GameObject("WL_FarmLook"); var comp = go.GetComponent(); if (comp == null) comp = go.AddComponent(); comp.entries = entries.ToArray(); comp.originalLights = lightStates.ToArray(); comp.origAmbientMode = origAmbMode; comp.origAmbientSky = origAmbSky; comp.origAmbientEquator = origAmbEq; comp.origAmbientGround = origAmbGnd; comp.origAmbientIntensity = origAmbI; comp.origSkybox = origSky; comp.origReflectionMode = origRefl; comp.origSun = origSun; UnityEditor.EditorUtility.SetDirty(comp); // ── 8. 아레나와 같은 레퍼런스 룩 SO 재사용 ──────────────────────── var rl = UnityEngine.GameObject.Find("WL_ReferenceLook"); if (rl == null) rl = new UnityEngine.GameObject("WL_ReferenceLook"); if (rl.GetComponent() == null) rl.AddComponent(); UnityEditor.EditorUtility.SetDirty(rl); sb.AppendLine("WL_ReferenceLook 추가 — 아레나와 같은 Resources/WL/WLReferenceLookSettings.asset 를 그대로 재사용"); UnityEditor.SceneManagement.EditorSceneManager.MarkSceneDirty(scene); UnityEditor.SceneManagement.EditorSceneManager.SaveScene(scene); UnityEditor.AssetDatabase.SaveAssets(); UnityEditor.AssetDatabase.Refresh(); sb.AppendLine("저장 완료"); UnityEngine.Debug.Log("[WL816a Build]\n" + sb.ToString()); } // ── 텍스처 유지안 ──────────────────────────────────────────────────── static UnityEngine.Material TexVariant(UnityEngine.Material src, UnityEngine.Shader toon, System.Collections.Generic.Dictionary cache) { UnityEngine.Material m; if (cache.TryGetValue(src, out m) && m != null) return m; string path = MatDir + "/Farm_" + src.name + "_ToonTex.mat"; m = UnityEditor.AssetDatabase.LoadAssetAtPath(path); if (m == null) { m = new UnityEngine.Material(toon); UnityEditor.AssetDatabase.CreateAsset(m, path); } m.shader = toon; var baseTex = src.HasProperty("_BaseMap") ? src.GetTexture("_BaseMap") : null; var baseCol = src.HasProperty("_BaseColor") ? src.GetColor("_BaseColor") : UnityEngine.Color.white; baseCol.a = 0f; m.SetTexture("_BaseMap", baseTex); m.SetTexture("_ShadowBaseMap", baseTex); m.SetColor("_DiffuseColor", baseCol); m.SetColor("_ShadowDiffuseColor", new UnityEngine.Color(baseCol.r * ShadowRatio.r, baseCol.g * ShadowRatio.g, baseCol.b * ShadowRatio.b, 0f)); Common(m); UnityEditor.EditorUtility.SetDirty(m); cache[src] = m; return m; } // ── 단색안 (데모처럼 텍스처 0 · 메시별 팔레트 평균색) ──────────────── static UnityEngine.Material FlatVariant(UnityEngine.Material src, UnityEngine.Shader toon, UnityEngine.Mesh mesh, int sub, UnityEngine.Texture2D palette, UnityEngine.Texture2D island, System.Collections.Generic.Dictionary cache, ref int uvOk, ref int uvFail) { 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 (baseTex != null) { var lut = baseTex.name == "Palette" ? palette : (baseTex.name == "Island01" ? island : null); if (lut != null && mesh != null) { UnityEngine.Color c; if (SampleMesh(mesh, sub, lut, out c)) { col = c; uvOk++; key = src.name + "_" + mesh.name + "_" + sub; } else { col = MeanOf(lut); uvFail++; key = src.name + "_mean"; } } else if (lut != null) { col = MeanOf(lut); uvFail++; key = src.name + "_mean"; } } col.a = 0f; UnityEngine.Material m; if (cache.TryGetValue(key, out m) && m != null) return m; string path = MatDir + "/Flat/Farm_" + Safe(key) + "_ToonFlat.mat"; EnsureDir(MatDir + "/Flat"); 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; } /// 서브메시 삼각형의 UV 로 팔레트를 찍어 면적 가중 평균색을 낸다. static bool SampleMesh(UnityEngine.Mesh mesh, int sub, UnityEngine.Texture2D lut, out UnityEngine.Color col) { col = UnityEngine.Color.white; try { if (sub >= mesh.subMeshCount) return false; 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(sub); 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); } /// 데모 실측 공통값(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 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 bytes = System.IO.File.ReadAllBytes(full); var t = new UnityEngine.Texture2D(2, 2, UnityEngine.TextureFormat.RGBA32, false, false); if (!UnityEngine.ImageConversion.LoadImage(t, bytes)) 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); } }