// WL-816e 빌드 — ① 섬 윗면 텍스처·머티리얼(데모 톤) ② 설정 SO ③ 윗면/길 마스크 굽기 // 원본(FarmingIsland · 3DPixelArtEnvironment)은 읽기만 한다. public static class WL816e_Build { const string kDir = "Assets/WL/Look/Farm/"; const string kTexPath = kDir + "Textures/WL_IslandTop_Demo.png"; const string kMatPath = kDir + "Materials/Farm_IslandTop_Demo.mat"; const string kSoPath = kDir + "Resources/WL/WLIslandLookSettings.asset"; // 🔴 「데모 지면과 **화면에서 같은 색**이 되는」 값 — 실측으로 역산했다. // 데모 ToonTerrain 의 화면 초록(구름 그림자 없는 p95) = #B1E196 = 선형(0.4397, 0.7529, 0.3050) // 우리 Shader Graphs/Toon 은 텍스처를 (1.031, 0.889, 0.717) 배로 화면에 낸다(실측) // → 텍스처 초록 = 0.4397/1.031, 0.7529/0.889, 0.3050/0.717 = (0.4265, 0.8469, 0.4254) = #AFEDAE static readonly UnityEngine.Color kGrassLin = new UnityEngine.Color(0.4265f, 0.8469f, 0.4254f, 1f); // 모래·흙은 데모 Terrain 2층(_GreenHighlight / _GreenShadow)에 같은 감쇠 0.717 을 적용 static readonly UnityEngine.Color kSandLin = new UnityEngine.Color(0.6020f, 0.5954f, 0.4629f, 1f); static readonly UnityEngine.Color kDirtLin = new UnityEngine.Color(0.2165f, 0.1632f, 0.0949f, 1f); // 그림자비 = Terrain 의 _RedShadow / _RedHighlight static readonly UnityEngine.Color kShadowRatio = new UnityEngine.Color(0.3248f, 0.3416f, 0.3936f, 1f); public static void RunTex() { var sb = new System.Text.StringBuilder(); Texture(sb); Material(sb); UnityEditor.AssetDatabase.SaveAssets(); UnityEngine.Debug.Log(sb.ToString()); } public static void Run() { var sb = new System.Text.StringBuilder(); Texture(sb); Material(sb); Settings(sb); UnityEditor.AssetDatabase.SaveAssets(); UnityEngine.Debug.Log(sb.ToString()); } // ── ① 8×8 텍스처 ────────────────────────────────────────────────── static void Texture(System.Text.StringBuilder sb) { System.IO.Directory.CreateDirectory(kDir + "Textures"); var t = new UnityEngine.Texture2D(8, 8, UnityEngine.TextureFormat.RGBA32, false, false); // 원본 Island01.png 배치: (PNG 행 0 = 위) 위 왼쪽 = 모래 · 위 오른쪽 = 흙/절벽 · 아래 = 초록 2종(체크무늬) // Unity 의 y 는 아래가 0 → v<0.5(아래 절반) = 초록. for (int y = 0; y < 8; y++) for (int x = 0; x < 8; x++) { UnityEngine.Color c; if (y < 4) c = kGrassLin; // 초록 두 칸을 **한 색**으로 = 체크무늬 소멸 else c = (x < 4) ? kSandLin : kDirtLin; t.SetPixel(x, y, c.gamma); } t.Apply(); System.IO.File.WriteAllBytes(kTexPath, UnityEngine.ImageConversion.EncodeToPNG(t)); UnityEngine.Object.DestroyImmediate(t); UnityEditor.AssetDatabase.ImportAsset(kTexPath, UnityEditor.ImportAssetOptions.ForceUpdate); var imp = UnityEditor.AssetImporter.GetAtPath(kTexPath) as UnityEditor.TextureImporter; if (imp != null) { imp.textureType = UnityEditor.TextureImporterType.Default; imp.sRGBTexture = true; imp.filterMode = UnityEngine.FilterMode.Point; imp.wrapMode = UnityEngine.TextureWrapMode.Clamp; imp.mipmapEnabled = false; imp.npotScale = UnityEditor.TextureImporterNPOTScale.None; imp.textureCompression = UnityEditor.TextureImporterCompression.Uncompressed; imp.maxTextureSize = 32; imp.SaveAndReimport(); } sb.AppendLine("텍스처 " + kTexPath + " — 초록 " + Hex(kGrassLin) + " · 모래 " + Hex(kSandLin) + " · 흙 " + Hex(kDirtLin)); } static string Hex(UnityEngine.Color lin) { var g = lin.gamma; return "#" + UnityEngine.ColorUtility.ToHtmlStringRGB(g); } // ── ② 머티리얼 ──────────────────────────────────────────────────── static void Material(System.Text.StringBuilder sb) { var src = UnityEditor.AssetDatabase.LoadAssetAtPath(kDir + "Materials/Farm_Island01_ToonTex.mat"); if (src == null) { sb.AppendLine("🔴 816a 의 Farm_Island01_ToonTex.mat 이 없다"); return; } var tex = UnityEditor.AssetDatabase.LoadAssetAtPath(kTexPath); var m = UnityEngine.Object.Instantiate(src); m.name = "Farm_IslandTop_Demo"; if (m.HasProperty("_BaseMap")) m.SetTexture("_BaseMap", tex); if (m.HasProperty("_ShadowBaseMap")) m.SetTexture("_ShadowBaseMap", tex); if (m.HasProperty("_DiffuseColor")) m.SetColor("_DiffuseColor", UnityEngine.Color.white); if (m.HasProperty("_ShadowDiffuseColor")) m.SetColor("_ShadowDiffuseColor", kShadowRatio); var old = UnityEditor.AssetDatabase.LoadAssetAtPath(kMatPath); if (old != null) { old.CopyPropertiesFromMaterial(m); UnityEngine.Object.DestroyImmediate(m); UnityEditor.EditorUtility.SetDirty(old); } else UnityEditor.AssetDatabase.CreateAsset(m, kMatPath); sb.AppendLine("머티리얼 " + kMatPath + " (shader " + src.shader.name + " · _ShadowDiffuseColor " + kShadowRatio.ToString("F4") + ")"); } // ── ③ 설정 SO + 마스크 ──────────────────────────────────────────── static void Settings(System.Text.StringBuilder sb) { System.IO.Directory.CreateDirectory(kDir + "Resources/WL"); UnityEditor.AssetDatabase.Refresh(); // 🔴 마스크를 **먼저** 굽는다 — Bake 가 씬을 열면 AssetDatabase 가 재로드돼 // 앞서 잡아 둔 SO 참조가 끊긴다(MissingReferenceException). 구운 뒤에 SO 를 잡는다. var bakedTiles = new System.Collections.Generic.List(); var bakedRoads = new System.Collections.Generic.List(); Bake(bakedTiles, bakedRoads, sb); var so = UnityEditor.AssetDatabase.LoadAssetAtPath(kSoPath); if (so == null) { so = UnityEngine.ScriptableObject.CreateInstance(); UnityEditor.AssetDatabase.CreateAsset(so, kSoPath); UnityEditor.AssetDatabase.SaveAssets(); so = UnityEditor.AssetDatabase.LoadAssetAtPath(kSoPath); } so.tileMasks = bakedTiles.ToArray(); so.roadMasks = bakedRoads.ToArray(); // 머티리얼 교체 표 var remap = new System.Collections.Generic.List(); string[,] pairs = { { "Assets/FarmingIsland/Materials/Palette.mat", kDir + "Materials/Farm_Palette_ToonTex.mat" }, { "Assets/FarmingIsland/Materials/Islands/Island01.mat", kDir + "Materials/Farm_IslandTop_Demo.mat" }, { "Assets/FarmingIsland/Materials/Islands/Road01.mat", kDir + "Materials/Farm_Road01_ToonTex.mat" }, { "Assets/FarmingIsland/Materials/Skins/Skin_Purple.mat", kDir + "Materials/Farm_Skin_Purple_ToonTex.mat" }, { "Assets/FarmingIsland/Materials/Skins/Skin_Yellow.mat", kDir + "Materials/Farm_Skin_Yellow_ToonTex.mat" }, { "Assets/FarmingIsland/Materials/Environments/SimpleWater.mat", kDir + "Materials/Farm_SimpleWater_Demo.mat" }, }; for (int i = 0; i < pairs.GetLength(0); i++) { var a = UnityEditor.AssetDatabase.LoadAssetAtPath(pairs[i, 0]); var b = UnityEditor.AssetDatabase.LoadAssetAtPath(pairs[i, 1]); if (a == null || b == null) { sb.AppendLine("🔴 remap 실패 " + pairs[i, 0] + " → " + pairs[i, 1]); continue; } remap.Add(new WL.Look.Farm.WLMatRemap { enabled_ = 1, from = a, to = b }); } so.materialRemap = remap.ToArray(); so.islandTopMaterial = UnityEditor.AssetDatabase.LoadAssetAtPath(kMatPath); so.skybox = UnityEditor.AssetDatabase.GetBuiltinExtraResource("Default-Skybox.mat"); // 흩뿌릴 것 — 데모 실측값 그대로 var grassMesh = FindMesh("Assets/3DPixelArtEnvironment/Meshes/Instanced/Grass_Leaf.fbx", "Grass_Leaf"); var flowerMesh = FindMesh("Assets/3DPixelArtEnvironment/Meshes/Instanced/Flower_Leaf.fbx", "Flower_Leaf"); var stoneMesh = FindMeshByName("Stone_1"); var mGrass = UnityEditor.AssetDatabase.LoadAssetAtPath("Assets/3DPixelArtEnvironment/Materials/Instanced_Grass.mat"); var mFlower = UnityEditor.AssetDatabase.LoadAssetAtPath("Assets/3DPixelArtEnvironment/Materials/Instanced_Flower.mat"); var mGravel = UnityEditor.AssetDatabase.LoadAssetAtPath("Assets/3DPixelArtEnvironment/Materials/Instanced_Gravel.mat"); var sc = new System.Collections.Generic.List(); sc.Add(new WL.Look.Farm.WLScatterDef { label = "풀", mesh = grassMesh, material = mGrass, probability = 100f, scale = 0.35f, normalOffset = 0.04f, density = 2.5f }); sc.Add(new WL.Look.Farm.WLScatterDef { label = "꽃", mesh = flowerMesh, material = mFlower, probability = 1f, scale = 0.35f, normalOffset = 0.04f, density = 2.5f }); sc.Add(new WL.Look.Farm.WLScatterDef { label = "자갈", mesh = stoneMesh, material = mGravel, probability = 1f, scale = 0.7f, normalOffset = 0.1f, density = 0.05f }); so.scatter = sc.ToArray(); sb.AppendLine("scatter — 풀 " + (grassMesh == null ? "메시없음" : grassMesh.name) + " · 꽃 " + (flowerMesh == null ? "메시없음" : flowerMesh.name) + " · 자갈 " + (stoneMesh == null ? "메시없음" : stoneMesh.name)); UnityEditor.EditorUtility.SetDirty(so); sb.AppendLine("SO " + kSoPath + " — remap " + so.materialRemap.Length + " · tileMasks " + so.tileMasks.Length + " · roadMasks " + so.roadMasks.Length); } static UnityEngine.Mesh FindMesh(string path, string name) { var all = UnityEditor.AssetDatabase.LoadAllAssetsAtPath(path); for (int i = 0; i < all.Length; i++) { var m = all[i] as UnityEngine.Mesh; if (m != null) return m; } return null; } static UnityEngine.Mesh FindMeshByName(string name) { var guids = UnityEditor.AssetDatabase.FindAssets(name + " t:Mesh"); for (int i = 0; i < guids.Length; i++) { var p = UnityEditor.AssetDatabase.GUIDToAssetPath(guids[i]); var all = UnityEditor.AssetDatabase.LoadAllAssetsAtPath(p); for (int k = 0; k < all.Length; k++) { var m = all[k] as UnityEngine.Mesh; if (m != null && m.name == name) return m; } } // 데모 씬의 인스턴싱 설정에서 직접 집는다 return null; } // ── 마스크 굽기 ─────────────────────────────────────────────────── public static void Bake(System.Collections.Generic.List tiles, System.Collections.Generic.List roads, System.Text.StringBuilder sb) { var prev = UnityEditor.SceneManagement.EditorSceneManager.GetActiveScene().path; UnityEditor.SceneManagement.EditorSceneManager.OpenScene("Assets/FarmingIsland/Scenes/Level01.unity", UnityEditor.SceneManagement.OpenSceneMode.Single); var mgr = UnityEngine.Object.FindFirstObjectByType(UnityEngine.FindObjectsInactive.Include); var t = typeof(CryingSnow.FarmingIsland.IslandManager); var islMeshes = t.GetField("islandMeshes", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) .GetValue(mgr) as System.Collections.Generic.List; var roadMeshes = t.GetField("roadMeshes", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) .GetValue(mgr) as System.Collections.Generic.List; int res = 64; for (int i = 0; islMeshes != null && i < islMeshes.Count; i++) { var m = islMeshes[i]; if (m == null) continue; if (m.name == "Bridge") continue; // 다리에는 안 깐다 var mask = Rasterize(m, res, 4f, true, out int on); mask.meshName = m.name; tiles.Add(mask); if (i <= 1 || i == 46 || i == 47) sb.AppendLine(" 마스크 isl[" + i + "] " + m.name + " 초록칸 " + on + "/" + (res * res) + " (" + (on * 64f / (res * res)).ToString("F1") + "㎡)"); } for (int i = 0; roadMeshes != null && i < roadMeshes.Count; i++) { var m = roadMeshes[i]; if (m == null) continue; var mask = Rasterize(m, res, 4f, false, out int on); mask.meshName = m.name; roads.Add(mask); } if (!string.IsNullOrEmpty(prev)) UnityEditor.SceneManagement.EditorSceneManager.OpenScene(prev, UnityEditor.SceneManagement.OpenSceneMode.Single); } /// /// 메시의 **윗면**(법선 +Y · 최상단)을 XZ 격자에 굽는다. /// greenOnly = true 면 **UV 가 Island01.png 의 초록 사분면(v < 0.5)** 인 삼각형만 센다 /// = FI 가 「여기는 잔디」라고 표시한 자리. 모래 해변·흙 절벽에는 풀을 안 깐다. /// static WL.Look.Farm.WLTileMask Rasterize(UnityEngine.Mesh m, int res, float half, bool greenOnly, out int onCells) { var mask = new WL.Look.Farm.WLTileMask { res = res, half = half, bits = new byte[(res * res + 7) / 8] }; var v = m.vertices; var tri = m.triangles; var uv = m.uv; float maxY = -99999f; for (int i = 0; i < v.Length; i++) if (v[i].y > maxY) maxY = v[i].y; float cell = half * 2f / res; onCells = 0; for (int i = 0; i + 2 < tri.Length; i += 3) { int i0 = tri[i], i1 = tri[i + 1], i2 = tri[i + 2]; var a = v[i0]; var b = v[i1]; var c = v[i2]; var n = UnityEngine.Vector3.Cross(b - a, c - a); if (n.y <= 0f) continue; if ((a.y + b.y + c.y) / 3f < maxY - 0.05f) continue; if (greenOnly && uv != null && uv.Length > i2) { float vv = (uv[i0].y + uv[i1].y + uv[i2].y) / 3f; if (vv >= 0.5f) continue; // 모래(위 왼쪽)·흙(위 오른쪽) = 잔디 아님 } float minx = UnityEngine.Mathf.Min(a.x, UnityEngine.Mathf.Min(b.x, c.x)); float maxx = UnityEngine.Mathf.Max(a.x, UnityEngine.Mathf.Max(b.x, c.x)); float minz = UnityEngine.Mathf.Min(a.z, UnityEngine.Mathf.Min(b.z, c.z)); float maxz = UnityEngine.Mathf.Max(a.z, UnityEngine.Mathf.Max(b.z, c.z)); int cx0 = UnityEngine.Mathf.Max(0, UnityEngine.Mathf.FloorToInt((minx + half) / cell)); int cx1 = UnityEngine.Mathf.Min(res - 1, UnityEngine.Mathf.CeilToInt((maxx + half) / cell)); int cz0 = UnityEngine.Mathf.Max(0, UnityEngine.Mathf.FloorToInt((minz + half) / cell)); int cz1 = UnityEngine.Mathf.Min(res - 1, UnityEngine.Mathf.CeilToInt((maxz + half) / cell)); for (int cx = cx0; cx <= cx1; cx++) for (int cz = cz0; cz <= cz1; cz++) { float px = -half + (cx + 0.5f) * cell; float pz = -half + (cz + 0.5f) * cell; float d1 = (px - b.x) * (a.z - b.z) - (a.x - b.x) * (pz - b.z); float d2 = (px - c.x) * (b.z - c.z) - (b.x - c.x) * (pz - c.z); float d3 = (px - a.x) * (c.z - a.z) - (c.x - a.x) * (pz - a.z); bool neg = (d1 < 0) || (d2 < 0) || (d3 < 0); bool pos = (d1 > 0) || (d2 > 0) || (d3 > 0); if (neg && pos) continue; if (!mask.At(px, pz)) onCells++; mask.Set(cx, cz, true); } } return mask; } // ── 마스크 눈으로 확인 ──────────────────────────────────────────── public static void Show() { var so = UnityEditor.AssetDatabase.LoadAssetAtPath(kSoPath); var sb = new System.Text.StringBuilder(); string[] want = { "47", "46", "33", "01" }; for (int w = 0; w < want.Length; w++) { var m = so.FindTileMask(want[w]); if (m == null) { sb.AppendLine("마스크 없음 " + want[w]); continue; } sb.AppendLine("--- TILE " + want[w] + " ---"); for (int r = 31; r >= 0; r--) { var line = new System.Text.StringBuilder(" "); for (int c = 0; c < 32; c++) line.Append(m.At(-4f + (c + 0.5f) * 0.25f, -4f + (r + 0.5f) * 0.25f) ? '#' : '.'); sb.AppendLine(line.ToString()); } } var rm = so.FindRoadMask("15"); if (rm != null) { sb.AppendLine("--- ROAD 15 ---"); for (int r = 31; r >= 0; r--) { var line = new System.Text.StringBuilder(" "); for (int c = 0; c < 32; c++) line.Append(rm.At(-4f + (c + 0.5f) * 0.25f, -4f + (r + 0.5f) * 0.25f) ? '#' : '.'); sb.AppendLine(line.ToString()); } } UnityEngine.Debug.Log(sb.ToString()); } }