Merge branch 'wl/gameplay/WL-816o-grass-cloud-edge'
This commit is contained in:
commit
323630badc
|
|
@ -0,0 +1,172 @@
|
|||
// WL-816o 진단 — 화면에 그려진 구름 띠 경계 ↔ CPU 가 계산한 띠 경계가 왜 어긋나는가.
|
||||
public static class WL816o_Diag
|
||||
{
|
||||
public static void Start()
|
||||
{
|
||||
var go = UnityEngine.GameObject.Find("~WL816oDiag");
|
||||
if (go != null) UnityEngine.Object.DestroyImmediate(go);
|
||||
go = new UnityEngine.GameObject("~WL816oDiag");
|
||||
go.AddComponent<WL816o_DiagRunner>();
|
||||
}
|
||||
}
|
||||
|
||||
public class WL816o_DiagRunner : UnityEngine.MonoBehaviour
|
||||
{
|
||||
const int TW = 512;
|
||||
static System.Text.StringBuilder sb;
|
||||
void Start() { StartCoroutine(Co()); }
|
||||
|
||||
System.Collections.IEnumerator Co()
|
||||
{
|
||||
sb = new System.Text.StringBuilder();
|
||||
if (UnityEngine.SceneManagement.SceneManager.sceneCount < 2)
|
||||
{
|
||||
var op = UnityEngine.SceneManagement.SceneManager.LoadSceneAsync("Level01", UnityEngine.SceneManagement.LoadSceneMode.Additive);
|
||||
while (op != null && !op.isDone) yield return null;
|
||||
}
|
||||
for (int i = 0; i < 6; i++) yield return new UnityEngine.WaitForSeconds(1f);
|
||||
|
||||
var g = UnityEngine.Object.FindFirstObjectByType<WL.Look.Farm.WLIslandGrass>(UnityEngine.FindObjectsInactive.Include);
|
||||
var cfg = WL.Look.Farm.WLIslandLookSettings.Instance;
|
||||
var mat = cfg.cloudEdgeSourceMaterial != null ? cfg.cloudEdgeSourceMaterial : cfg.islandTopMaterial;
|
||||
|
||||
// 실제로 화면에 보이는 섬 윗면 렌더러가 무슨 머티리얼을 쓰는가 (런타임 교체 후)
|
||||
var isls = UnityEngine.Object.FindObjectsByType<CryingSnow.FarmingIsland.Island>(UnityEngine.FindObjectsInactive.Exclude, UnityEngine.FindObjectsSortMode.None);
|
||||
for (int i = 0; i < isls.Length && i < 3; i++)
|
||||
{
|
||||
var r = isls[i].GetComponent<UnityEngine.Renderer>();
|
||||
L("Island[" + i + "] pos=" + isls[i].transform.position + " mats=" + (r ? Names(r) : "-"));
|
||||
}
|
||||
|
||||
var b = g.CalculateInstancesBounds();
|
||||
float size = UnityEngine.Mathf.Max(b.size.x, b.size.z) * 0.5f;
|
||||
float cx = b.center.x, cz = b.center.z;
|
||||
float y = isls.Length > 0 ? isls[0].transform.position.y : b.center.y;
|
||||
L("bounds c=" + b.center + " size=" + b.size + " · ortho size=" + size + " · 윗면 y=" + y);
|
||||
|
||||
bool was = g.enabled; g.enabled = false;
|
||||
var cam = MakeTop(cx, cz, size);
|
||||
var tex = GrabCam(cam, TW, TW);
|
||||
g.enabled = was;
|
||||
var px = tex.GetPixels32();
|
||||
|
||||
var f = new WL.Look.Farm.WLCloudShadowField();
|
||||
if (!f.Configure(mat)) { L("Configure 실패 " + f.Why); Flush(); yield break; }
|
||||
L("field: " + f.Dump());
|
||||
L("_Time.y(global)=" + UnityEngine.Shader.GetGlobalVector("_Time").y + " timeSinceLevelLoad=" + UnityEngine.Time.timeSinceLevelLoad);
|
||||
|
||||
// 화면 밝기 계단 확인 (초록 픽셀만)
|
||||
var hist = new System.Collections.Generic.Dictionary<int, int>();
|
||||
for (int i = 0; i < px.Length; i++)
|
||||
{
|
||||
var p = px[i];
|
||||
if (!(p.g > p.b + 12 && p.g > p.r + 8 && p.g > 60)) continue;
|
||||
int l = Lum(p); if (!hist.ContainsKey(l)) hist[l] = 0; hist[l]++;
|
||||
}
|
||||
var keys = new System.Collections.Generic.List<int>(hist.Keys); keys.Sort();
|
||||
var top = new System.Collections.Generic.List<string>();
|
||||
for (int i = 0; i < keys.Count; i++) if (hist[keys[i]] > 2000) top.Add(keys[i] + "×" + hist[keys[i]]);
|
||||
L("초록 밝기 계단(2000픽셀 이상) = " + string.Join(" ", top.ToArray()) + " (고유값 " + keys.Count + ")");
|
||||
|
||||
// 가운데 줄 — 화면 경계 위치 vs CPU 띠 경계 위치 (월드 X)
|
||||
for (int r = 0; r < 3; r++)
|
||||
{
|
||||
int iy = 128 + r * 128;
|
||||
float wz = cz + (((iy + 0.5f) / TW) - 0.5f) * 2f * size;
|
||||
var scr = new System.Collections.Generic.List<string>();
|
||||
var cpu = new System.Collections.Generic.List<string>();
|
||||
int prevBand = int.MinValue;
|
||||
for (int ix = 1; ix < TW - 1; ix++)
|
||||
{
|
||||
float wx = cx + (((ix + 0.5f) / TW) - 0.5f) * 2f * size;
|
||||
var A = px[iy * TW + ix - 1]; var B = px[iy * TW + ix];
|
||||
if (Green(A) && Green(B)) { int d = Lum(B) - Lum(A); if (d < 0) d = -d; if (d >= 6 && d <= 45) scr.Add(wx.ToString("F2")); }
|
||||
int bd = f.Band(wx, y, wz);
|
||||
if (prevBand != int.MinValue && bd != prevBand) cpu.Add(wx.ToString("F2") + "(" + prevBand + "→" + bd + ")");
|
||||
prevBand = bd;
|
||||
}
|
||||
L("row z=" + wz.ToString("F2") + " 화면경계X=[" + string.Join(" ", scr.ToArray()) + "]");
|
||||
L(" CPU경계X=[" + string.Join(" ", cpu.ToArray()) + "] cloudiness(x=" + cx.ToString("F1") + ")="
|
||||
+ f.Cloudiness(cx, y, wz).ToString("F4"));
|
||||
}
|
||||
|
||||
// 시간 오프셋 스캔 — 가장 잘 맞는 Δt 를 찾는다
|
||||
float t0 = f.TimeValue;
|
||||
string best = ""; float bestScore = -1f, bestDt = 0f;
|
||||
for (float dt = -12f; dt <= 12.01f; dt += 0.5f)
|
||||
{
|
||||
f.TimeValue = t0 + dt;
|
||||
float s = Agree(px, f, cx, cz, size, y);
|
||||
if (s > bestScore) { bestScore = s; bestDt = dt; }
|
||||
if (System.Math.Abs(dt) < 0.01f) best = "Δt=0 일치율 " + (s * 100f).ToString("F1") + "%";
|
||||
}
|
||||
f.TimeValue = t0;
|
||||
L(best + " · 최적 Δt=" + bestDt.ToString("F1") + " 일치율 " + (bestScore * 100f).ToString("F1") + "%");
|
||||
|
||||
UnityEngine.Object.DestroyImmediate(tex);
|
||||
Flush();
|
||||
}
|
||||
|
||||
static float Agree(UnityEngine.Color32[] px, WL.Look.Farm.WLCloudShadowField f, float cx, float cz, float size, float y)
|
||||
{
|
||||
int shown = 0, matched = 0;
|
||||
for (int iy = 8; iy < TW - 8; iy += 4)
|
||||
{
|
||||
float wz = cz + (((iy + 0.5f) / TW) - 0.5f) * 2f * size;
|
||||
for (int ix = 2; ix < TW - 3; ix += 2)
|
||||
{
|
||||
var A = px[iy * TW + ix]; var B = px[iy * TW + ix + 1];
|
||||
if (!Green(A) || !Green(B)) continue;
|
||||
int d = Lum(B) - Lum(A); if (d < 0) d = -d;
|
||||
if (d < 6 || d > 45) continue;
|
||||
shown++;
|
||||
float w0 = cx + (((ix + 0.5f) / TW) - 0.5f) * 2f * size;
|
||||
float w1 = cx + (((ix + 3.5f) / TW) - 0.5f) * 2f * size;
|
||||
float wm = cx + (((ix - 1.5f) / TW) - 0.5f) * 2f * size;
|
||||
int b0 = f.Band(wm, y, wz), b1 = f.Band(w1, y, wz);
|
||||
if (b0 != b1) matched++;
|
||||
_ = w0;
|
||||
}
|
||||
}
|
||||
return shown == 0 ? 0f : (float)matched / shown;
|
||||
}
|
||||
|
||||
static bool Green(UnityEngine.Color32 p) { return p.g > p.b + 12 && p.g > p.r + 8 && p.g > 60; }
|
||||
static int Lum(UnityEngine.Color32 p) { return (p.r * 77 + p.g * 150 + p.b * 29) >> 8; }
|
||||
static string Names(UnityEngine.Renderer r)
|
||||
{ var m = r.sharedMaterials; var s = ""; for (int i = 0; i < m.Length; i++) s += (m[i] ? m[i].name : "-") + " "; return s; }
|
||||
|
||||
static UnityEngine.Camera MakeTop(float cx, float cz, float size)
|
||||
{
|
||||
var go = UnityEngine.GameObject.Find("~816oTopD");
|
||||
if (go == null) go = new UnityEngine.GameObject("~816oTopD");
|
||||
var c = go.GetComponent<UnityEngine.Camera>(); if (c == null) c = go.AddComponent<UnityEngine.Camera>();
|
||||
c.orthographic = true; c.orthographicSize = size;
|
||||
c.nearClipPlane = 0.1f; c.farClipPlane = 200f;
|
||||
c.transform.rotation = UnityEngine.Quaternion.Euler(90f, 0f, 0f);
|
||||
c.transform.position = new UnityEngine.Vector3(cx, 40f, cz);
|
||||
c.enabled = false;
|
||||
return c;
|
||||
}
|
||||
|
||||
static UnityEngine.Texture2D GrabCam(UnityEngine.Camera cam, int w, int h)
|
||||
{
|
||||
var rt = new UnityEngine.RenderTexture(w, h, 24, UnityEngine.RenderTextureFormat.ARGB32, UnityEngine.RenderTextureReadWrite.sRGB);
|
||||
rt.Create(); cam.targetTexture = rt; cam.Render();
|
||||
var prev = UnityEngine.RenderTexture.active; UnityEngine.RenderTexture.active = rt;
|
||||
var tex = new UnityEngine.Texture2D(w, h, UnityEngine.TextureFormat.RGB24, false);
|
||||
tex.ReadPixels(new UnityEngine.Rect(0, 0, w, h), 0, 0); tex.Apply();
|
||||
UnityEngine.RenderTexture.active = prev; cam.targetTexture = null;
|
||||
rt.Release(); UnityEngine.Object.DestroyImmediate(rt);
|
||||
System.IO.Directory.CreateDirectory("Screenshots_WL/WL816o");
|
||||
System.IO.File.WriteAllBytes("Screenshots_WL/WL816o/diag_top.png", UnityEngine.ImageConversion.EncodeToPNG(tex));
|
||||
return tex;
|
||||
}
|
||||
|
||||
static void L(string s) { sb.AppendLine(s); }
|
||||
static void Flush()
|
||||
{
|
||||
System.IO.File.WriteAllText("AgentScripts/WL816o_DIAG.txt", sb.ToString());
|
||||
UnityEngine.Debug.Log("[816o diag]\n" + sb.ToString());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
public static class WL816o_Kw
|
||||
{
|
||||
public static void Run()
|
||||
{
|
||||
var sb = new System.Text.StringBuilder();
|
||||
string[] paths = {
|
||||
"Assets/WL/Look/Farm/Materials/Farm_IslandTop_Arena.mat",
|
||||
"Assets/WL/Look/Farm/Materials/Farm_Grass_Arena.mat" };
|
||||
for (int k = 0; k < paths.Length; k++)
|
||||
{
|
||||
var m = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.Material>(paths[k]);
|
||||
if (m == null) { sb.AppendLine(paths[k] + " = null"); continue; }
|
||||
sb.AppendLine("== " + m.name);
|
||||
sb.AppendLine(" IsKeywordEnabled(_CLOUDSENABLED) = " + m.IsKeywordEnabled("_CLOUDSENABLED"));
|
||||
sb.AppendLine(" HasProperty(_CLOUDSENABLED) = " + m.HasProperty("_CLOUDSENABLED")
|
||||
+ (m.HasProperty("_CLOUDSENABLED") ? (" val=" + m.GetFloat("_CLOUDSENABLED")) : ""));
|
||||
sb.AppendLine(" shaderKeywords = " + string.Join(",", m.shaderKeywords));
|
||||
var ek = m.enabledKeywords;
|
||||
var names = new System.Collections.Generic.List<string>();
|
||||
for (int i = 0; i < ek.Length; i++) names.Add(ek[i].name);
|
||||
sb.AppendLine(" enabledKeywords = " + string.Join(",", names));
|
||||
sb.AppendLine(" _Cloud_Density=" + m.GetFloat("_Cloud_Density") + " _Cloud_Strength=" + m.GetFloat("_Cloud_Strength")
|
||||
+ " _Shades=" + m.GetFloat("_Shades") + " _Brightness=" + m.GetFloat("_Brightness"));
|
||||
}
|
||||
System.IO.File.WriteAllText("AgentScripts/WL816o_KW.txt", sb.ToString());
|
||||
UnityEngine.Debug.Log("[816o kw]\n" + sb.ToString());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,280 @@
|
|||
// WL-816o — 구름 그림자 경계 배치: 전(816n) / 후 실측 + CPU 포팅 검증.
|
||||
// 에셋은 건드리지 않는다 (cfg 는 Instantiate 복제본 · 816f/816n 방식).
|
||||
public static class WL816o_Play
|
||||
{
|
||||
public static void Start()
|
||||
{
|
||||
var go = UnityEngine.GameObject.Find("~WL816oPlay");
|
||||
if (go != null) UnityEngine.Object.DestroyImmediate(go);
|
||||
go = new UnityEngine.GameObject("~WL816oPlay");
|
||||
go.AddComponent<WL816o_PlayRunner>();
|
||||
}
|
||||
}
|
||||
|
||||
public class WL816o_PlayRunner : UnityEngine.MonoBehaviour
|
||||
{
|
||||
static System.Text.StringBuilder sb;
|
||||
const int W = 1080, H = 1920;
|
||||
const int TW = 512;
|
||||
|
||||
void Start() { StartCoroutine(Co()); }
|
||||
|
||||
System.Collections.IEnumerator Co()
|
||||
{
|
||||
sb = new System.Text.StringBuilder();
|
||||
if (UnityEngine.SceneManagement.SceneManager.sceneCount < 2)
|
||||
{
|
||||
var op = UnityEngine.SceneManagement.SceneManager.LoadSceneAsync("Level01", UnityEngine.SceneManagement.LoadSceneMode.Additive);
|
||||
while (op != null && !op.isDone) yield return null;
|
||||
}
|
||||
for (int i = 0; i < 7; i++) yield return new UnityEngine.WaitForSeconds(1f);
|
||||
|
||||
var g = UnityEngine.Object.FindFirstObjectByType<WL.Look.Farm.WLIslandGrass>(UnityEngine.FindObjectsInactive.Include);
|
||||
if (g == null) { L("🔴 WLIslandGrass 없음 — 중단"); Flush(); yield break; }
|
||||
var src = WL.Look.Farm.WLIslandLookSettings.Instance;
|
||||
var rt = UnityEngine.Object.Instantiate(src); // 에셋 무변경
|
||||
g.cfg = rt;
|
||||
|
||||
var mat = rt.cloudEdgeSourceMaterial != null ? rt.cloudEdgeSourceMaterial : rt.islandTopMaterial;
|
||||
L("바닥 머티리얼 = " + (mat ? mat.name : "-") + " · 셰이더 " + (mat && mat.shader ? mat.shader.name : "-"));
|
||||
L("SO 값 : cell=" + rt.cloudEdgeCell + " maxCells=" + rt.cloudEdgeMaxCells + " dilate=" + rt.cloudEdgeDilate
|
||||
+ " driftCover=" + rt.cloudEdgeDriftCover + " refresh=" + rt.cloudEdgeRefreshSeconds + "s densityScale=" + rt.cloudEdgeDensityScale);
|
||||
|
||||
var cam = MakeCam();
|
||||
L("카메라 " + cam.transform.position + " euler=" + cam.transform.eulerAngles + " fov=" + cam.fieldOfView + " (타깃까지 12 m · 45°)");
|
||||
|
||||
// ── BEFORE (816n 상태 = 균일 격자) ─────────────────────────────
|
||||
rt.cloudEdgeEnabled = 0;
|
||||
g.Rebuild(); yield return null; yield return null;
|
||||
L(Shot(cam, "a_before", "BEFORE 816n(균일 격자)"));
|
||||
L(" " + Stat() + " · " + Bench(cam, 60).ToString("F3") + " ms/frame");
|
||||
int bInst = WL.Look.Farm.WLIslandGrass.Instances;
|
||||
long bTri = WL.Look.Farm.WLIslandGrass.Triangles;
|
||||
int bDraw = WL.Look.Farm.WLIslandGrass.DrawnConfigs;
|
||||
|
||||
// ── AFTER (구름 경계 배치) ─────────────────────────────────────
|
||||
rt.cloudEdgeEnabled = 1;
|
||||
g.Rebuild(); yield return null; yield return null;
|
||||
L(Shot(cam, "b_after", "AFTER 816o(구름 경계 배치)"));
|
||||
L(" " + Stat() + " · " + Bench(cam, 60).ToString("F3") + " ms/frame");
|
||||
int aInst = WL.Look.Farm.WLIslandGrass.Instances;
|
||||
long aTri = WL.Look.Farm.WLIslandGrass.Triangles;
|
||||
int aDraw = WL.Look.Farm.WLIslandGrass.DrawnConfigs;
|
||||
L(" 로그: " + WL.Look.Farm.WLIslandGrass.LastLog);
|
||||
|
||||
// 교대 2회 (측정 노이즈 확인)
|
||||
rt.cloudEdgeEnabled = 0; g.Rebuild(); yield return null;
|
||||
float b2 = Bench(cam, 60);
|
||||
rt.cloudEdgeEnabled = 1; g.Rebuild(); yield return null;
|
||||
float a2 = Bench(cam, 60);
|
||||
L("[교대 2회] before=" + b2.ToString("F3") + " ms · after=" + a2.ToString("F3") + " ms");
|
||||
|
||||
L("[예산] 인스턴스 " + bInst + " → " + aInst + " (" + Pct(aInst, bInst) + ")"
|
||||
+ " · 삼각형 " + bTri + " → " + aTri + " (" + Pct((int)aTri, (int)bTri) + ")"
|
||||
+ " · 드로우콜 +" + bDraw + " → +" + aDraw
|
||||
+ " · 후보 " + WL.Look.Farm.WLIslandGrass.Candidates);
|
||||
|
||||
// 주기 갱신 비용
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
g.RefreshCloudEdges();
|
||||
sw.Stop();
|
||||
L("[주기 갱신] RefreshCloudEdges 1회 = " + sw.Elapsed.TotalMilliseconds.ToString("F2") + " ms"
|
||||
+ " (굽기 " + WL.Look.Farm.WLIslandGrass.BakeMs.ToString("F2") + " · 고르기 " + WL.Look.Farm.WLIslandGrass.FilterMs.ToString("F2")
|
||||
+ ") → 인스턴스 " + WL.Look.Farm.WLIslandGrass.Instances);
|
||||
|
||||
// ── CPU 포팅 검증 (셰이더가 그린 띠 경계 ↔ CPU 가 계산한 띠 경계) ──
|
||||
L(Verify(g, rt, mat));
|
||||
|
||||
// 최종 상태 = 구름 경계 ON
|
||||
rt.cloudEdgeEnabled = 1; g.Rebuild(); yield return null;
|
||||
Shot(cam, "b_after", "final");
|
||||
TopShot(g, "t_after");
|
||||
rt.cloudEdgeEnabled = 0; g.Rebuild(); yield return null;
|
||||
TopShot(g, "t_before");
|
||||
rt.cloudEdgeEnabled = 1; g.Rebuild(); yield return null;
|
||||
|
||||
Flush();
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────── 검증
|
||||
static string Verify(WL.Look.Farm.WLIslandGrass g, WL.Look.Farm.WLIslandLookSettings rt, UnityEngine.Material mat)
|
||||
{
|
||||
var f = new WL.Look.Farm.WLCloudShadowField();
|
||||
if (!f.Configure(mat)) return "[검증] 실패 — " + f.Why;
|
||||
|
||||
var b = g.CalculateInstancesBounds();
|
||||
float size = UnityEngine.Mathf.Max(b.size.x, b.size.z) * 0.5f;
|
||||
float cx = b.center.x, cz = b.center.z, y = b.center.y;
|
||||
|
||||
bool was = g.enabled;
|
||||
g.enabled = false; // 풀을 빼고 바닥만 본다
|
||||
var cam = MakeTop(cx, cz, size);
|
||||
var tex = GrabCam(cam, TW, TW);
|
||||
g.enabled = was;
|
||||
|
||||
var px = tex.GetPixels32();
|
||||
// 픽셀 ↔ 월드 (ortho · Euler(90,0,0) → screen +x = world +x, screen +y = world +z)
|
||||
var band = new int[TW * TW];
|
||||
var ok = new bool[TW * TW];
|
||||
for (int iy = 0; iy < TW; iy++)
|
||||
{
|
||||
float wz = cz + (((iy + 0.5f) / TW) - 0.5f) * 2f * size;
|
||||
for (int ix = 0; ix < TW; ix++)
|
||||
{
|
||||
float wx = cx + (((ix + 0.5f) / TW) - 0.5f) * 2f * size;
|
||||
int i = iy * TW + ix;
|
||||
band[i] = f.Band(wx, y, wz);
|
||||
var p = px[i];
|
||||
ok[i] = (p.g > p.b + 12 && p.g > p.r + 8 && p.g > 60); // 풀밭 초록만
|
||||
}
|
||||
}
|
||||
|
||||
// CPU 가 「여기가 경계」라고 한 곳에 화면에도 색 계단이 있는가 (이 방향이 포팅 검증이다)
|
||||
int cpuEdges = 0, cpuHit = 0, flatTot = 0, flatHit = 0;
|
||||
for (int iy = 1; iy < TW - 1; iy++)
|
||||
{
|
||||
for (int ix = 3; ix < TW - 4; ix++)
|
||||
{
|
||||
int i = iy * TW + ix;
|
||||
if (!ok[i - 3] || !ok[i + 4]) continue;
|
||||
if (band[i] != band[i + 1])
|
||||
{
|
||||
cpuEdges++;
|
||||
if (Stepy(px, i - 3, 7)) cpuHit++;
|
||||
}
|
||||
else if (band[i - 3] == band[i] && band[i] == band[i + 4])
|
||||
{
|
||||
flatTot++; // CPU 가 평평하다고 한 곳
|
||||
if (Stepy(px, i - 3, 7)) flatHit++; // 그런데 화면에 계단이 있으면 오차
|
||||
}
|
||||
}
|
||||
}
|
||||
UnityEngine.Object.DestroyImmediate(tex);
|
||||
|
||||
string s = "[검증] CPU 가 띠 경계라 한 " + cpuEdges + "곳 중 화면에도 색 계단이 있는 것 = "
|
||||
+ (cpuEdges > 0 ? (100f * cpuHit / cpuEdges).ToString("F1") : "0") + "%"
|
||||
+ " · CPU 가 평평하다 한 " + flatTot + "곳 중 화면에 계단이 있는 것 = "
|
||||
+ (flatTot > 0 ? (100f * flatHit / flatTot).ToString("F1") : "0") + "% (낮을수록 좋다 · 타일·소품 경계 포함)";
|
||||
s += "\n (_Time.y=" + UnityEngine.Shader.GetGlobalVector("_Time").y.ToString("F3")
|
||||
+ " · timeSinceLevelLoad=" + UnityEngine.Time.timeSinceLevelLoad.ToString("F3")
|
||||
+ " · 띠비율=" + (WL.Look.Farm.WLIslandGrass.EdgeFraction * 100f).ToString("F1") + "%"
|
||||
+ " · 칸=" + WL.Look.Farm.WLIslandGrass.EdgeCellSize.ToString("F2") + "m×" + WL.Look.Farm.WLIslandGrass.EdgeCells
|
||||
+ " · 노이즈=" + WL.Look.Farm.WLIslandGrass.EdgeSampleCell.ToString("F2") + "m×" + WL.Look.Farm.WLIslandGrass.EdgeSamples
|
||||
+ " · 넓힘=" + WL.Look.Farm.WLIslandGrass.EdgeDilate + ")";
|
||||
return s;
|
||||
}
|
||||
|
||||
static int Lum(UnityEngine.Color32 p) { return (p.r * 77 + p.g * 150 + p.b * 29) >> 8; }
|
||||
|
||||
/// <summary>i 부터 n 픽셀 안에 6~45 크기의 밝기 계단이 있는가.</summary>
|
||||
static bool Stepy(UnityEngine.Color32[] px, int i, int n)
|
||||
{
|
||||
for (int k = 0; k < n - 1; k++)
|
||||
{
|
||||
int d = Lum(px[i + k + 1]) - Lum(px[i + k]); if (d < 0) d = -d;
|
||||
if (d >= 6 && d <= 45) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static string Pct(int a, int b) { return b <= 0 ? "-" : ((100f * a / b).ToString("F1") + "%"); }
|
||||
|
||||
static string Stat()
|
||||
{
|
||||
return "인스턴스=" + WL.Look.Farm.WLIslandGrass.Instances
|
||||
+ " 삼각형=" + WL.Look.Farm.WLIslandGrass.Triangles
|
||||
+ " 드로우콜+" + WL.Look.Farm.WLIslandGrass.DrawnConfigs
|
||||
+ " 타일=" + WL.Look.Farm.WLIslandGrass.Tiles;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────── helpers
|
||||
static UnityEngine.Camera MakeCam()
|
||||
{
|
||||
UnityEngine.Vector3 tgt = UnityEngine.Vector3.zero;
|
||||
var pc = UnityEngine.GameObject.FindGameObjectWithTag("Player");
|
||||
if (pc != null) tgt = pc.transform.position;
|
||||
var go = UnityEngine.GameObject.Find("~816oCam");
|
||||
if (go == null) go = new UnityEngine.GameObject("~816oCam");
|
||||
var c = go.GetComponent<UnityEngine.Camera>();
|
||||
if (c == null) c = go.AddComponent<UnityEngine.Camera>();
|
||||
c.fieldOfView = 60f; c.orthographic = false;
|
||||
c.nearClipPlane = 0.1f; c.farClipPlane = 300f;
|
||||
c.transform.rotation = UnityEngine.Quaternion.Euler(45f, 45f, 0f);
|
||||
c.transform.position = tgt - c.transform.forward * 12f;
|
||||
c.enabled = false;
|
||||
return c;
|
||||
}
|
||||
|
||||
static UnityEngine.Camera MakeTop(float cx, float cz, float size)
|
||||
{
|
||||
var go = UnityEngine.GameObject.Find("~816oTop");
|
||||
if (go == null) go = new UnityEngine.GameObject("~816oTop");
|
||||
var c = go.GetComponent<UnityEngine.Camera>();
|
||||
if (c == null) c = go.AddComponent<UnityEngine.Camera>();
|
||||
c.orthographic = true; c.orthographicSize = size;
|
||||
c.nearClipPlane = 0.1f; c.farClipPlane = 200f;
|
||||
c.transform.rotation = UnityEngine.Quaternion.Euler(90f, 0f, 0f);
|
||||
c.transform.position = new UnityEngine.Vector3(cx, 40f, cz);
|
||||
c.enabled = false;
|
||||
return c;
|
||||
}
|
||||
|
||||
static UnityEngine.Texture2D GrabCam(UnityEngine.Camera cam, int w, int h)
|
||||
{
|
||||
var rt = new UnityEngine.RenderTexture(w, h, 24, UnityEngine.RenderTextureFormat.ARGB32, UnityEngine.RenderTextureReadWrite.sRGB);
|
||||
rt.Create(); cam.targetTexture = rt; cam.Render();
|
||||
var prev = UnityEngine.RenderTexture.active; UnityEngine.RenderTexture.active = rt;
|
||||
var tex = new UnityEngine.Texture2D(w, h, UnityEngine.TextureFormat.RGB24, false);
|
||||
tex.ReadPixels(new UnityEngine.Rect(0, 0, w, h), 0, 0); tex.Apply();
|
||||
UnityEngine.RenderTexture.active = prev; cam.targetTexture = null;
|
||||
rt.Release(); UnityEngine.Object.DestroyImmediate(rt);
|
||||
return tex;
|
||||
}
|
||||
|
||||
static void TopShot(WL.Look.Farm.WLIslandGrass g, string name)
|
||||
{
|
||||
var b = g.CalculateInstancesBounds();
|
||||
float size = UnityEngine.Mathf.Max(b.size.x, b.size.z) * 0.5f;
|
||||
var cam = MakeTop(b.center.x, b.center.z, size);
|
||||
var tex = GrabCam(cam, 768, 768);
|
||||
System.IO.Directory.CreateDirectory("Screenshots_WL/WL816o");
|
||||
System.IO.File.WriteAllBytes("Screenshots_WL/WL816o/" + name + ".png", UnityEngine.ImageConversion.EncodeToPNG(tex));
|
||||
UnityEngine.Object.DestroyImmediate(tex);
|
||||
}
|
||||
|
||||
static string Shot(UnityEngine.Camera cam, string name, string tag)
|
||||
{
|
||||
var tex = GrabCam(cam, W, H);
|
||||
System.IO.Directory.CreateDirectory("Screenshots_WL/WL816o");
|
||||
System.IO.File.WriteAllBytes("Screenshots_WL/WL816o/" + name + ".png", UnityEngine.ImageConversion.EncodeToPNG(tex));
|
||||
var px = tex.GetPixels32();
|
||||
long r = 0, gg = 0, bb = 0; int n = 0;
|
||||
for (int i = 0; i < px.Length; i++)
|
||||
{
|
||||
var p = px[i];
|
||||
if (p.g > p.b + 12 && p.g > p.r + 8 && p.g > 60) { r += p.r; gg += p.g; bb += p.b; n++; }
|
||||
}
|
||||
UnityEngine.Object.DestroyImmediate(tex);
|
||||
if (n == 0) return tag + " : 초록 픽셀 0";
|
||||
return string.Format("{0} : 풀밭 초록 픽셀 {1} ({2:F1}%) 평균 #{3:X2}{4:X2}{5:X2}", tag, n, 100f * n / px.Length, r / n, gg / n, bb / n);
|
||||
}
|
||||
|
||||
static float Bench(UnityEngine.Camera cam, int frames)
|
||||
{
|
||||
var rt = new UnityEngine.RenderTexture(W, H, 24, UnityEngine.RenderTextureFormat.ARGB32, UnityEngine.RenderTextureReadWrite.sRGB);
|
||||
rt.Create(); cam.targetTexture = rt;
|
||||
for (int i = 0; i < 10; i++) cam.Render();
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
for (int i = 0; i < frames; i++) cam.Render();
|
||||
sw.Stop(); cam.targetTexture = null;
|
||||
rt.Release(); UnityEngine.Object.DestroyImmediate(rt);
|
||||
return (float)sw.Elapsed.TotalMilliseconds / frames;
|
||||
}
|
||||
|
||||
static void L(string s) { sb.AppendLine(s); }
|
||||
static void Flush()
|
||||
{
|
||||
System.IO.File.WriteAllText("AgentScripts/WL816o_PLAY.txt", sb.ToString());
|
||||
UnityEngine.Debug.Log("[816o play]\n" + sb.ToString());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
# WL-816o — 캡처 합성 (같은 높이로 맞춰 나란히)
|
||||
import os
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
BASE = r"E:\NerdNavis\WL_wt\gameplay\Screenshots_WL"
|
||||
OUT = os.path.join(BASE, "WL816o")
|
||||
os.makedirs(OUT, exist_ok=True)
|
||||
|
||||
|
||||
def strip(items, out, h=900, gap=10):
|
||||
ims = []
|
||||
for path, label in items:
|
||||
if not os.path.exists(path):
|
||||
print("MISSING", path)
|
||||
return
|
||||
im = Image.open(path).convert("RGB")
|
||||
w = int(im.width * h / im.height)
|
||||
ims.append((im.resize((w, h), Image.LANCZOS), label))
|
||||
W = sum(i.width for i, _ in ims) + gap * (len(ims) - 1)
|
||||
canvas = Image.new("RGB", (W, h + 34), (24, 24, 24))
|
||||
d = ImageDraw.Draw(canvas)
|
||||
x = 0
|
||||
for im, label in ims:
|
||||
canvas.paste(im, (x, 34))
|
||||
d.text((x + 8, 10), label, fill=(255, 255, 255))
|
||||
x += im.width + gap
|
||||
canvas.save(out)
|
||||
print("wrote", out, canvas.size)
|
||||
|
||||
|
||||
strip([(os.path.join(BASE, "WL816n", "demo_A_baseline.png"), "DEMO (3DPixelArt)"),
|
||||
(os.path.join(OUT, "a_before.png"), "BEFORE = 816n (uniform grid)"),
|
||||
(os.path.join(OUT, "b_after.png"), "AFTER = 816o (cloud-edge only)")],
|
||||
os.path.join(OUT, "a_demo_vs_before_after.png"), h=900)
|
||||
|
||||
strip([(os.path.join(OUT, "t_before.png"), "TOP BEFORE (uniform grid)"),
|
||||
(os.path.join(OUT, "t_after.png"), "TOP AFTER (cloud-edge only)")],
|
||||
os.path.join(OUT, "c_top_before_after.png"), h=768)
|
||||
|
|
@ -0,0 +1,424 @@
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// WLCloudShadow.cs — 데모 Toon 셰이더의 「구름 그림자 띠」를 CPU 에서 **같은 식으로** 재현한다
|
||||
// (WL-816o · #816)
|
||||
//
|
||||
// ■ 왜 필요한가 (PD 지시 2026-09-15)
|
||||
// 「데모와 같은 비주얼. 하지만 잔디를 많이 심어서 퍼포먼스에 영향을 주지 않는 방향으로
|
||||
// 최적화된 로직(예: 구름 그림자 기준으로만 배치)」
|
||||
// 816n 실측 — 데모 풀은 바닥과 거의 같은 색(밝기비 0.933)이라 **구름 그림자 경계에서만**
|
||||
// 눈에 띈다(보이는 풀의 88 % 가 경계 띠 15.7 % 면적에 몰림). 즉 평평한 색 면 안쪽의 풀은
|
||||
// 삼각형만 먹고 화면에는 거의 기여하지 않는다 → **경계 띠에만 심으면 그림은 같고 비용은 준다**.
|
||||
//
|
||||
// ■ 무엇을 그대로 옮겼나 (원본 0줄 — 읽기만 했다)
|
||||
// `Assets/3DPixelArtEnvironment/Shaders/Subgraphs/CloudShadows.shadersubgraph`
|
||||
// worldPos ─▶ ShadowProjection(SunDir) ─▶ TilingAndOffset(offset = _Time.y × _Cloud_Movement)
|
||||
// ─▶ CloudNoise(Scale=_Cloud_Density, VerticalSpeed=_Cloud_Change,
|
||||
// Step=_Cloud_Step, Coverage=_Cloud_Cover, Time=_Time.y)
|
||||
// ─▶ × _Cloud_Strength = Cloudiness
|
||||
// `Subgraphs/ToonLighting.shadersubgraph` Lighting = step(ShadowAtten) − Cloudiness
|
||||
// `Subgraphs/ToonRamp.shadersubgraph` t = saturate(ceil(Lighting × _Shades + _Brightness) / _Shades)
|
||||
// → 섬 윗면은 **평면**이라 ShadowAtten·법선이 상수다. 그래서 화면에 보이는 색 띠의 경계는
|
||||
// 오로지 `ceil((1 − Cloudiness) × _Shades + _Brightness)` 가 바뀌는 지점이다.
|
||||
// `Includes/CloudNoise.hlsl`(6 옥타브 FBM) · `Includes/SimplexNoise3D.hlsl`(Ashima snoise) 를
|
||||
// C# 으로 1:1 이식했다.
|
||||
//
|
||||
// ■ C45 — _Cloud_*, _Shades, _Brightness 는 전부 **머티리얼에서 읽는다**(코드 상수 0).
|
||||
// 셰이더 값이 바뀌면 이 판정도 자동으로 따라간다.
|
||||
//
|
||||
// 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다.
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace WL.Look.Farm
|
||||
{
|
||||
/// <summary>
|
||||
/// 구름 그림자의 「색 띠 경계」를 XZ 격자에 한 번 구워 두고, 점 하나가 경계 근처인지 O(1) 로 답한다.
|
||||
/// </summary>
|
||||
public sealed class WLCloudShadowField
|
||||
{
|
||||
// ── 머티리얼에서 읽은 값 ────────────────────────────────────────
|
||||
public bool Ready;
|
||||
public bool CloudsOn;
|
||||
public string Why = "";
|
||||
|
||||
float _scale, _cover, _strength, _change, _shades, _brightness;
|
||||
float _movX, _movY, _stepX, _stepY;
|
||||
float _sunX, _sunY, _sunZ;
|
||||
float _time;
|
||||
|
||||
// ── 구운 격자 ──────────────────────────────────────────────────
|
||||
int[] _band;
|
||||
byte[] _edge, _tmp;
|
||||
float[] _smp;
|
||||
int _nx, _nz;
|
||||
float _minX, _minZ, _cell, _inv;
|
||||
|
||||
public int Cells { get { return _nx * _nz; } }
|
||||
public float CellSize { get { return _cell; } }
|
||||
public int Samples; // 노이즈를 실제로 계산한 점 수(비용은 여기서만 난다)
|
||||
public float SampleCellSize;
|
||||
public float EdgeFraction; // 격자에서 경계로 잡힌 칸의 비율(예산 추정에 쓴다)
|
||||
|
||||
/// <summary>구름 값이 1초에 월드 몇 m 를 흐르는가 — 갱신 주기 ↔ 띠 폭을 맞추는 데 쓴다.</summary>
|
||||
public float DriftPerSecond { get { return Mathf.Sqrt(_movX * _movX + _movY * _movY); } }
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// ① 셰이더 값 읽기
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
public bool Configure(Material groundMat)
|
||||
{
|
||||
Ready = false; Why = "";
|
||||
if (groundMat == null) { Why = "바닥 머티리얼이 없다"; return false; }
|
||||
if (!groundMat.HasProperty("_Cloud_Density")) { Why = "머티리얼에 _Cloud_* 가 없다(Toon 계열이 아니다)"; return false; }
|
||||
|
||||
_scale = groundMat.GetFloat("_Cloud_Density");
|
||||
_cover = groundMat.HasProperty("_Cloud_Cover") ? groundMat.GetFloat("_Cloud_Cover") : 0.5f;
|
||||
_strength = groundMat.HasProperty("_Cloud_Strength") ? groundMat.GetFloat("_Cloud_Strength") : 1f;
|
||||
_change = groundMat.HasProperty("_Cloud_Change") ? groundMat.GetFloat("_Cloud_Change") : 0f;
|
||||
|
||||
var mv = groundMat.HasProperty("_Cloud_Movement") ? groundMat.GetVector("_Cloud_Movement") : Vector4.zero;
|
||||
_movX = mv.x; _movY = mv.y;
|
||||
var st = groundMat.HasProperty("_Cloud_Step") ? groundMat.GetVector("_Cloud_Step") : Vector4.zero;
|
||||
_stepX = st.x; _stepY = st.y;
|
||||
|
||||
_shades = groundMat.HasProperty("_Shades") ? groundMat.GetFloat("_Shades") : 1f;
|
||||
_brightness = groundMat.HasProperty("_Brightness") ? groundMat.GetFloat("_Brightness") : 0f;
|
||||
if (_shades < 1f) _shades = 1f;
|
||||
|
||||
// 🔴 실측(816o) — `_CLOUDSENABLED` 는 **전역 키워드**라 `Material.IsKeywordEnabled`(로컬 공간)로는
|
||||
// 항상 false 가 나온다. 머티리얼에 실제로 켜져 있는지는 `shaderKeywords` 배열로 봐야 한다.
|
||||
CloudsOn = false;
|
||||
var kws = groundMat.shaderKeywords;
|
||||
for (int i = 0; i < kws.Length; i++) if (kws[i] == "_CLOUDSENABLED") { CloudsOn = true; break; }
|
||||
if (!CloudsOn)
|
||||
CloudsOn = groundMat.IsKeywordEnabled("_CLOUDSENABLED")
|
||||
|| (groundMat.HasProperty("_CLOUDSENABLED") && groundMat.GetFloat("_CLOUDSENABLED") > 0.5f);
|
||||
if (!CloudsOn) { Why = "이 머티리얼은 구름이 꺼져 있다"; return false; }
|
||||
if (_strength <= 0f || _scale <= 0f) { Why = "_Cloud_Strength/_Cloud_Density 가 0"; return false; }
|
||||
|
||||
ResolveSun();
|
||||
ResolveTime();
|
||||
|
||||
Ready = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void ResolveSun()
|
||||
{
|
||||
// URP `Main Light` = RenderSettings.sun(지정돼 있으면) · Direction = 빛으로 향하는 방향 = −forward
|
||||
Light sun = RenderSettings.sun;
|
||||
if (sun == null || !sun.isActiveAndEnabled || sun.type != LightType.Directional)
|
||||
{
|
||||
sun = null;
|
||||
float best = -1f;
|
||||
var lights = Object.FindObjectsByType<Light>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
|
||||
for (int i = 0; i < lights.Length; i++)
|
||||
{
|
||||
var l = lights[i];
|
||||
if (l == null || l.type != LightType.Directional || !l.isActiveAndEnabled) continue;
|
||||
if (l.intensity > best) { best = l.intensity; sun = l; }
|
||||
}
|
||||
}
|
||||
Vector3 d = sun != null ? -sun.transform.forward : Vector3.up;
|
||||
_sunX = d.x; _sunY = d.y; _sunZ = d.z;
|
||||
}
|
||||
|
||||
void ResolveTime()
|
||||
{
|
||||
// 셰이더가 실제로 쓰는 _Time.y 를 우선 읽는다. 못 읽으면(0) 레벨 로드 후 경과 시간.
|
||||
var t = Shader.GetGlobalVector("_Time");
|
||||
_time = (t.y != 0f) ? t.y : Time.timeSinceLevelLoad;
|
||||
}
|
||||
|
||||
/// <summary>시간만 다시 읽는다(주기 갱신용 — 머티리얼·태양은 그대로).</summary>
|
||||
public void Tick() { ResolveSun(); ResolveTime(); }
|
||||
|
||||
/// <summary>셰이더가 쓰는 `_Time.y`. 진단·검증에서 직접 넣어 볼 수 있다.</summary>
|
||||
public float TimeValue { get { return _time; } set { _time = value; } }
|
||||
|
||||
public Vector3 SunDir { get { return new Vector3(_sunX, _sunY, _sunZ); } }
|
||||
public string Dump()
|
||||
{
|
||||
return "scale=" + _scale + " cover=" + _cover + " strength=" + _strength + " change=" + _change
|
||||
+ " mov=(" + _movX + "," + _movY + ") step=(" + _stepX + "," + _stepY + ")"
|
||||
+ " shades=" + _shades + " brightness=" + _brightness
|
||||
+ " sun=(" + _sunX.ToString("F3") + "," + _sunY.ToString("F3") + "," + _sunZ.ToString("F3") + ")"
|
||||
+ " time=" + _time.ToString("F3");
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// ② 셰이더와 같은 식
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
/// <summary>CloudShadows 서브그래프의 최종 출력 `Cloudiness`.</summary>
|
||||
public float Cloudiness(float wx, float wy, float wz)
|
||||
{
|
||||
// ShadowProjection.hlsl — 그림자가 바닥에 떨어지는 지점
|
||||
float m = (Mathf.Abs(_sunY) > 1e-4f) ? (wy / -_sunY) : 0f;
|
||||
float px = wx + m * _sunX;
|
||||
float pz = wz + m * _sunZ;
|
||||
|
||||
// TilingAndOffset (Tiling = 1) — offset = _Time.y × _Cloud_Movement
|
||||
float ux = px + _time * _movX;
|
||||
float uy = pz + _time * _movY;
|
||||
|
||||
// CloudNoise.hlsl — 6 옥타브 FBM
|
||||
float S = _scale;
|
||||
float tz = _time * _change;
|
||||
float n = Snoise(ux * S, uy * S, tz);
|
||||
n += 0.5f * Snoise((ux * 2f - _stepX) * S, (uy * 2f - _stepY) * S, tz);
|
||||
n += 0.25f * Snoise((ux * 4f - 2f * _stepX) * S, (uy * 4f - 2f * _stepY) * S, tz);
|
||||
n += 0.125f * Snoise((ux * 8f - 3f * _stepX) * S, (uy * 8f - 3f * _stepY) * S, tz);
|
||||
n += 0.0625f * Snoise((ux * 16f - 4f * _stepX) * S, (uy * 16f - 4f * _stepY) * S, tz);
|
||||
n += 0.03125f * Snoise((ux * 32f - 5f * _stepX) * S, (uy * 32f - 5f * _stepY) * S, tz);
|
||||
|
||||
return (_cover + 0.5f * n) * _strength;
|
||||
}
|
||||
|
||||
/// <summary>ToonRamp 가 만드는 색 띠 번호. 이 값이 바뀌는 곳이 화면에서 보이는 경계다.</summary>
|
||||
public int Band(float wx, float wy, float wz) { return BandOf(Cloudiness(wx, wy, wz)); }
|
||||
|
||||
/// <summary>구름 값 하나를 띠 번호로. (보간한 값에도 쓴다)</summary>
|
||||
public int BandOf(float cloudiness)
|
||||
{
|
||||
float lighting = 1f - cloudiness; // step(ShadowAtten)=1 (평면·그림자 없음)
|
||||
int k = Mathf.CeilToInt(lighting * _shades + _brightness);
|
||||
// ToonRamp 의 saturate — 위아래로 넘친 칸은 한 색으로 뭉친다(=경계가 없다)
|
||||
int top = Mathf.CeilToInt(_shades);
|
||||
if (k < 0) k = 0;
|
||||
if (k > top) k = top;
|
||||
return k;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// ③ 격자에 굽기
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// 주어진 범위의 XZ 평면(높이 y)에 대해 띠 번호를 구우고, 번호가 바뀌는 칸을 경계로 표시한다.
|
||||
/// </summary>
|
||||
/// <param name="cell">격자 칸(m). 작을수록 띠가 얇고 정확하다.</param>
|
||||
/// <param name="maxCells">칸 수 상한 — 섬이 커지면 칸을 자동으로 키운다(모바일 예산).</param>
|
||||
/// <param name="dilate">경계에서 몇 칸 더 넓힐 것인가(구름이 흐르는 만큼 미리 덮어 둔다).</param>
|
||||
public void Bake(Bounds b, float y, float cell, int maxCells, int dilate,
|
||||
float sampleCell, int maxSamples)
|
||||
{
|
||||
EdgeFraction = 0f;
|
||||
if (!Ready) return;
|
||||
if (cell <= 0.01f) cell = 0.01f;
|
||||
if (dilate < 0) dilate = 0;
|
||||
if (sampleCell < cell) sampleCell = cell;
|
||||
|
||||
int pad = dilate + 2;
|
||||
float w = Mathf.Max(b.size.x, 0.1f);
|
||||
float h = Mathf.Max(b.size.z, 0.1f);
|
||||
|
||||
// 칸 수 상한 — 넘으면 칸을 키운다
|
||||
for (int guard = 0; guard < 24; guard++)
|
||||
{
|
||||
int tx = Mathf.CeilToInt(w / cell) + 1 + pad * 2;
|
||||
int tz = Mathf.CeilToInt(h / cell) + 1 + pad * 2;
|
||||
if (maxCells <= 0 || (long)tx * tz <= maxCells) { _nx = tx; _nz = tz; break; }
|
||||
cell *= 1.25f;
|
||||
_nx = tx; _nz = tz;
|
||||
}
|
||||
_cell = cell;
|
||||
_inv = 1f / cell;
|
||||
_minX = b.min.x - pad * cell;
|
||||
_minZ = b.min.z - pad * cell;
|
||||
|
||||
int n = _nx * _nz;
|
||||
if (_band == null || _band.Length < n) { _band = new int[n]; _edge = new byte[n]; _tmp = new byte[n]; }
|
||||
|
||||
// ── ① 구름 값은 **성긴 격자**에서만 계산한다 (6 옥타브 노이즈 = 유일한 비싼 부분) ──
|
||||
// 구름의 가장 작은 무늬가 1/(32×_Cloud_Density) ≈ 3 m 라 1 m 간격이면 충분히 따라간다.
|
||||
float spanX = (_nx - 1) * cell, spanZ = (_nz - 1) * cell;
|
||||
int sx = 0, sz = 0;
|
||||
for (int guard = 0; guard < 24; guard++)
|
||||
{
|
||||
sx = Mathf.CeilToInt(spanX / sampleCell) + 2;
|
||||
sz = Mathf.CeilToInt(spanZ / sampleCell) + 2;
|
||||
if (maxSamples <= 0 || (long)sx * sz <= maxSamples) break;
|
||||
sampleCell *= 1.25f;
|
||||
}
|
||||
SampleCellSize = sampleCell; Samples = sx * sz;
|
||||
if (_smp == null || _smp.Length < sx * sz) _smp = new float[sx * sz];
|
||||
float s0x = _minX + 0.5f * cell - sampleCell; // 가장자리 보간을 위해 한 칸 바깥에서 시작
|
||||
float s0z = _minZ + 0.5f * cell - sampleCell;
|
||||
for (int j = 0; j < sz; j++)
|
||||
{
|
||||
float wz = s0z + j * sampleCell;
|
||||
int row = j * sx;
|
||||
for (int i2 = 0; i2 < sx; i2++) _smp[row + i2] = Cloudiness(s0x + i2 * sampleCell, y, wz);
|
||||
}
|
||||
|
||||
// ── ② 촘촘한 격자에서는 **보간한 값**만 띠 번호로 바꾼다(노이즈 재계산 없음) ──
|
||||
float invS = 1f / sampleCell;
|
||||
for (int iz = 0; iz < _nz; iz++)
|
||||
{
|
||||
float wz = _minZ + (iz + 0.5f) * cell;
|
||||
float fz = (wz - s0z) * invS;
|
||||
int jz = (int)fz; if (jz < 0) jz = 0; if (jz > sz - 2) jz = sz - 2;
|
||||
float tz = fz - jz;
|
||||
int row = iz * _nx, r0 = jz * sx, r1 = r0 + sx;
|
||||
for (int ix = 0; ix < _nx; ix++)
|
||||
{
|
||||
float wx = _minX + (ix + 0.5f) * cell;
|
||||
float fx = (wx - s0x) * invS;
|
||||
int jx = (int)fx; if (jx < 0) jx = 0; if (jx > sx - 2) jx = sx - 2;
|
||||
float tx = fx - jx;
|
||||
float a = _smp[r0 + jx] + (_smp[r0 + jx + 1] - _smp[r0 + jx]) * tx;
|
||||
float c = _smp[r1 + jx] + (_smp[r1 + jx + 1] - _smp[r1 + jx]) * tx;
|
||||
_band[row + ix] = BandOf(a + (c - a) * tz);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < n; i++) _edge[i] = 0;
|
||||
for (int iz = 1; iz < _nz - 1; iz++)
|
||||
{
|
||||
int row = iz * _nx;
|
||||
for (int ix = 1; ix < _nx - 1; ix++)
|
||||
{
|
||||
int i = row + ix;
|
||||
int c = _band[i];
|
||||
if (_band[i - 1] != c || _band[i + 1] != c || _band[i - _nx] != c || _band[i + _nx] != c)
|
||||
_edge[i] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (int p = 0; p < dilate; p++)
|
||||
{
|
||||
System.Array.Copy(_edge, _tmp, n);
|
||||
for (int iz = 1; iz < _nz - 1; iz++)
|
||||
{
|
||||
int row = iz * _nx;
|
||||
for (int ix = 1; ix < _nx - 1; ix++)
|
||||
{
|
||||
int i = row + ix;
|
||||
if (_tmp[i] != 0) continue;
|
||||
if (_tmp[i - 1] != 0 || _tmp[i + 1] != 0 || _tmp[i - _nx] != 0 || _tmp[i + _nx] != 0)
|
||||
_edge[i] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 패딩을 뺀 안쪽에서만 비율을 센다(예산 추정용)
|
||||
int cnt = 0, tot = 0;
|
||||
for (int iz = pad; iz < _nz - pad; iz++)
|
||||
{
|
||||
int row = iz * _nx;
|
||||
for (int ix = pad; ix < _nx - pad; ix++) { tot++; if (_edge[row + ix] != 0) cnt++; }
|
||||
}
|
||||
EdgeFraction = tot > 0 ? (float)cnt / tot : 0f;
|
||||
}
|
||||
|
||||
/// <summary>이 점이 색 띠 경계 근처인가. 격자 밖이면 자르지 않는다(안전 쪽).</summary>
|
||||
public bool IsEdge(float wx, float wz)
|
||||
{
|
||||
if (_edge == null) return true;
|
||||
int cx = (int)((wx - _minX) * _inv);
|
||||
int cz = (int)((wz - _minZ) * _inv);
|
||||
if (cx < 0 || cz < 0 || cx >= _nx || cz >= _nz) return true;
|
||||
return _edge[cz * _nx + cx] != 0;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// ④ SimplexNoise3D.hlsl `snoise` 이식 (Ashima Arts · MIT · 원본 파일 0줄 수정)
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
static float Mod289(float x) { return x - Mathf.Floor(x * (1f / 289f)) * 289f; }
|
||||
static float Permute(float x) { return Mod289(((x * 34f) + 10f) * x); }
|
||||
static float TaylorInvSqrt(float r) { return 1.79284291400159f - 0.85373472095314f * r; }
|
||||
|
||||
public static float Snoise(float vx, float vy, float vz)
|
||||
{
|
||||
const float Cx = 1f / 6f, Cy = 1f / 3f;
|
||||
|
||||
// First corner
|
||||
float dv = (vx + vy + vz) * Cy;
|
||||
float ix = Mathf.Floor(vx + dv), iy = Mathf.Floor(vy + dv), iz = Mathf.Floor(vz + dv);
|
||||
float di = (ix + iy + iz) * Cx;
|
||||
float x0x = vx - ix + di, x0y = vy - iy + di, x0z = vz - iz + di;
|
||||
|
||||
// Other corners — g = step(x0.yzx, x0.xyz)
|
||||
float gx = x0x >= x0y ? 1f : 0f;
|
||||
float gy = x0y >= x0z ? 1f : 0f;
|
||||
float gz = x0z >= x0x ? 1f : 0f;
|
||||
float lx = 1f - gx, ly = 1f - gy, lz = 1f - gz;
|
||||
float i1x = gx < lz ? gx : lz, i1y = gy < lx ? gy : lx, i1z = gz < ly ? gz : ly;
|
||||
float i2x = gx > lz ? gx : lz, i2y = gy > lx ? gy : lx, i2z = gz > ly ? gz : ly;
|
||||
|
||||
float x1x = x0x - i1x + Cx, x1y = x0y - i1y + Cx, x1z = x0z - i1z + Cx;
|
||||
float x2x = x0x - i2x + Cy, x2y = x0y - i2y + Cy, x2z = x0z - i2z + Cy;
|
||||
float x3x = x0x - 0.5f, x3y = x0y - 0.5f, x3z = x0z - 0.5f;
|
||||
|
||||
// Permutations
|
||||
ix = Mod289(ix); iy = Mod289(iy); iz = Mod289(iz);
|
||||
float p0 = Permute(iz + 0f);
|
||||
float p1 = Permute(iz + i1z);
|
||||
float p2 = Permute(iz + i2z);
|
||||
float p3 = Permute(iz + 1f);
|
||||
p0 = Permute(p0 + iy + 0f);
|
||||
p1 = Permute(p1 + iy + i1y);
|
||||
p2 = Permute(p2 + iy + i2y);
|
||||
p3 = Permute(p3 + iy + 1f);
|
||||
p0 = Permute(p0 + ix + 0f);
|
||||
p1 = Permute(p1 + ix + i1x);
|
||||
p2 = Permute(p2 + ix + i2x);
|
||||
p3 = Permute(p3 + ix + 1f);
|
||||
|
||||
// Gradients — ns = n_ * D.wyz − D.xzx, D = (0, 0.5, 1, 2)
|
||||
const float n_ = 0.142857142857f;
|
||||
const float nsx = n_ * 2f, nsy = n_ * 0.5f - 1f, nsz = n_;
|
||||
const float nszz = nsz * nsz;
|
||||
|
||||
float j0 = p0 - 49f * Mathf.Floor(p0 * nszz);
|
||||
float j1 = p1 - 49f * Mathf.Floor(p1 * nszz);
|
||||
float j2 = p2 - 49f * Mathf.Floor(p2 * nszz);
|
||||
float j3 = p3 - 49f * Mathf.Floor(p3 * nszz);
|
||||
|
||||
float xa = Mathf.Floor(j0 * nsz), xb = Mathf.Floor(j1 * nsz), xc = Mathf.Floor(j2 * nsz), xd = Mathf.Floor(j3 * nsz);
|
||||
float ya = Mathf.Floor(j0 - 7f * xa), yb = Mathf.Floor(j1 - 7f * xb), yc = Mathf.Floor(j2 - 7f * xc), yd = Mathf.Floor(j3 - 7f * xd);
|
||||
|
||||
float X0 = xa * nsx + nsy, X1 = xb * nsx + nsy, X2 = xc * nsx + nsy, X3 = xd * nsx + nsy;
|
||||
float Y0 = ya * nsx + nsy, Y1 = yb * nsx + nsy, Y2 = yc * nsx + nsy, Y3 = yd * nsx + nsy;
|
||||
float h0 = 1f - Mathf.Abs(X0) - Mathf.Abs(Y0);
|
||||
float h1 = 1f - Mathf.Abs(X1) - Mathf.Abs(Y1);
|
||||
float h2 = 1f - Mathf.Abs(X2) - Mathf.Abs(Y2);
|
||||
float h3 = 1f - Mathf.Abs(X3) - Mathf.Abs(Y3);
|
||||
|
||||
// b0 = (X0, X1, Y0, Y1) · s0 = floor(b0) * 2 + 1 · sh = −step(h, 0)
|
||||
float sX0 = Mathf.Floor(X0) * 2f + 1f, sX1 = Mathf.Floor(X1) * 2f + 1f;
|
||||
float sY0 = Mathf.Floor(Y0) * 2f + 1f, sY1 = Mathf.Floor(Y1) * 2f + 1f;
|
||||
float sX2 = Mathf.Floor(X2) * 2f + 1f, sX3 = Mathf.Floor(X3) * 2f + 1f;
|
||||
float sY2 = Mathf.Floor(Y2) * 2f + 1f, sY3 = Mathf.Floor(Y3) * 2f + 1f;
|
||||
float sh0 = h0 <= 0f ? -1f : 0f, sh1 = h1 <= 0f ? -1f : 0f;
|
||||
float sh2 = h2 <= 0f ? -1f : 0f, sh3 = h3 <= 0f ? -1f : 0f;
|
||||
|
||||
// a0 = b0.xzyw + s0.xzyw * sh.xxyy → (X0, Y0, X1, Y1) + (sX0, sY0, sX1, sY1) * (sh0, sh0, sh1, sh1)
|
||||
float g0x = X0 + sX0 * sh0, g0y = Y0 + sY0 * sh0, g0z = h0;
|
||||
float g1x = X1 + sX1 * sh1, g1y = Y1 + sY1 * sh1, g1z = h1;
|
||||
float g2x = X2 + sX2 * sh2, g2y = Y2 + sY2 * sh2, g2z = h2;
|
||||
float g3x = X3 + sX3 * sh3, g3y = Y3 + sY3 * sh3, g3z = h3;
|
||||
|
||||
float n0 = TaylorInvSqrt(g0x * g0x + g0y * g0y + g0z * g0z);
|
||||
float n1 = TaylorInvSqrt(g1x * g1x + g1y * g1y + g1z * g1z);
|
||||
float n2 = TaylorInvSqrt(g2x * g2x + g2y * g2y + g2z * g2z);
|
||||
float n3 = TaylorInvSqrt(g3x * g3x + g3y * g3y + g3z * g3z);
|
||||
g0x *= n0; g0y *= n0; g0z *= n0;
|
||||
g1x *= n1; g1y *= n1; g1z *= n1;
|
||||
g2x *= n2; g2y *= n2; g2z *= n2;
|
||||
g3x *= n3; g3y *= n3; g3z *= n3;
|
||||
|
||||
float m0 = 0.5f - (x0x * x0x + x0y * x0y + x0z * x0z); if (m0 < 0f) m0 = 0f;
|
||||
float m1 = 0.5f - (x1x * x1x + x1y * x1y + x1z * x1z); if (m1 < 0f) m1 = 0f;
|
||||
float m2 = 0.5f - (x2x * x2x + x2y * x2y + x2z * x2z); if (m2 < 0f) m2 = 0f;
|
||||
float m3 = 0.5f - (x3x * x3x + x3y * x3y + x3z * x3z); if (m3 < 0f) m3 = 0f;
|
||||
m0 *= m0; m1 *= m1; m2 *= m2; m3 *= m3;
|
||||
m0 *= m0; m1 *= m1; m2 *= m2; m3 *= m3;
|
||||
|
||||
return 105f * (m0 * (g0x * x0x + g0y * x0y + g0z * x0z)
|
||||
+ m1 * (g1x * x1x + g1y * x1y + g1z * x1z)
|
||||
+ m2 * (g2x * x2x + g2y * x2y + g2z * x2z)
|
||||
+ m3 * (g3x * x3x + g3y * x3y + g3z * x3z));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
fileFormatVersion: 2
|
||||
guid: a2212bf029b1a9341ada8c8c84ebff63
|
||||
|
|
@ -38,28 +38,68 @@ namespace WL.Look.Farm
|
|||
public static float UsedDensity;
|
||||
public static string LastLog = "";
|
||||
|
||||
// 816o — 구름 경계 배치 진단
|
||||
public static int Candidates, CloudRefreshes, CloudExcluded, EdgeCells, EdgeDilate, EdgeSamples;
|
||||
public static float EdgeFraction, EdgeCellSize, EdgeSampleCell, BakeMs, FilterMs;
|
||||
public static bool CloudEdgeOn;
|
||||
public static string CloudWhy = "";
|
||||
|
||||
public WLIslandLookSettings cfg;
|
||||
|
||||
Bounds _bounds = new Bounds(Vector3.zero, Vector3.one * 16f);
|
||||
readonly List<Rect> _blockers = new List<Rect>(128);
|
||||
|
||||
// ── 816o 캐시 — 한 번 뽑은 후보를 들고 있다가, 구름이 흐르면 「고르기」만 다시 한다 ──
|
||||
struct Cand { public float x, z, y; public int def; public float sc; public float yaw; }
|
||||
Cand[] _cand;
|
||||
int _candCount;
|
||||
InstancingSettings[] _cset;
|
||||
WLScatterDef[] _cdef;
|
||||
long[] _ctri;
|
||||
bool _cacheValid;
|
||||
float _cacheY;
|
||||
bool _fastRefresh;
|
||||
readonly WLCloudShadowField _field = new WLCloudShadowField();
|
||||
|
||||
public override Bounds CalculateInstancesBounds() { return _bounds; }
|
||||
|
||||
/// <summary>다시 깐다(섬이 확장됐을 때). 멱등 — 몇 번 불러도 안전.</summary>
|
||||
public void Rebuild()
|
||||
{
|
||||
_cacheValid = false;
|
||||
_fastRefresh = false;
|
||||
if (!isActiveAndEnabled) { enabled = true; return; } // OnEnable 이 알아서 만든다
|
||||
enabled = false; // OnDisable → 버퍼 해제
|
||||
enabled = true; // OnEnable → 다시 생성
|
||||
Rebuilds++;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 816o — 구름이 흐른 만큼 **띠만** 다시 고른다. 타일·제외 사각형은 다시 훑지 않는다
|
||||
/// (`FindObjectsByType` 를 타지 않으므로 전체 재생성보다 훨씬 싸다).
|
||||
/// </summary>
|
||||
public void RefreshCloudEdges()
|
||||
{
|
||||
if (!_cacheValid || !isActiveAndEnabled) return;
|
||||
if (cfg == null || cfg.cloudEdgeEnabled == 0) return;
|
||||
_fastRefresh = true;
|
||||
enabled = false;
|
||||
enabled = true;
|
||||
_fastRefresh = false;
|
||||
CloudRefreshes++;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// 점 뽑기 — 데모 `TerrainInstancesBehaviour.GetInstanceData` 와 같은 절차
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
public override Dictionary<InstancingSettings, List<InstanceData>> GetInstanceData()
|
||||
{
|
||||
if (_fastRefresh && _cacheValid) return BuildFromCache(true);
|
||||
|
||||
_cacheValid = false;
|
||||
Instances = 0; Tiles = 0; Excluded = 0; Triangles = 0; DrawnConfigs = 0; UsedDensity = 0f;
|
||||
Candidates = 0; EdgeFraction = 1f; EdgeCells = 0; EdgeCellSize = 0f; EdgeDilate = 0;
|
||||
BakeMs = 0f; FilterMs = 0f; CloudEdgeOn = false; CloudWhy = ""; _edgeOn = false;
|
||||
|
||||
if (cfg == null) cfg = WLIslandLookSettings.Instance;
|
||||
if (cfg == null || cfg.enabled_ == 0 || cfg.grassEnabled == 0) { LastLog = "꺼짐"; return null; }
|
||||
|
|
@ -84,37 +124,7 @@ namespace WL.Look.Farm
|
|||
|
||||
BuildBlockers(tiles);
|
||||
|
||||
// 밀도 자동 조절(모바일 예산)
|
||||
float budgetScale = 1f;
|
||||
if (cfg.maxInstances > 0)
|
||||
{
|
||||
float area = 0f;
|
||||
for (int i = 0; i < tiles.Count; i++) area += 64f; // 타일 8×8
|
||||
float topLayer = 0f;
|
||||
for (int i = 0; i < cfg.scatter.Length; i++)
|
||||
{
|
||||
var s = cfg.scatter[i];
|
||||
if (s == null || s.enabled_ == 0) continue;
|
||||
if (s.density > topLayer) topLayer = s.density;
|
||||
}
|
||||
float est = area * topLayer * topLayer * 0.93f; // 윗면은 8×8 의 약 93 %
|
||||
if (est > cfg.maxInstances) budgetScale = Mathf.Sqrt(cfg.maxInstances / est);
|
||||
}
|
||||
|
||||
// 같은 density 끼리 한 층으로 묶는다(데모의 FirstLayer/SecondLayer 와 같은 구조)
|
||||
var layers = new List<float>(4);
|
||||
for (int i = 0; i < cfg.scatter.Length; i++)
|
||||
{
|
||||
var s = cfg.scatter[i];
|
||||
if (s == null || s.enabled_ == 0 || s.mesh == null || s.material == null || s.scale <= 0f) continue;
|
||||
float d = s.density * budgetScale;
|
||||
if (d <= 0f) continue;
|
||||
bool found = false;
|
||||
for (int k = 0; k < layers.Count; k++) if (Mathf.Abs(layers[k] - d) < 1e-5f) { found = true; break; }
|
||||
if (!found) layers.Add(d);
|
||||
}
|
||||
|
||||
var result = new Dictionary<InstancingSettings, List<InstanceData>>();
|
||||
// 경계 박스 — 구름 격자를 굽기 전에 정해져야 한다
|
||||
var bmin = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
|
||||
var bmax = new Vector3(float.MinValue, float.MinValue, float.MinValue);
|
||||
for (int i = 0; i < tiles.Count; i++)
|
||||
|
|
@ -124,7 +134,57 @@ namespace WL.Look.Farm
|
|||
bmax = Vector3.Max(bmax, new Vector3(t.center.x + 4f, t.center.y + 3f, t.center.z + 4f));
|
||||
}
|
||||
_bounds = new Bounds((bmin + bmax) * 0.5f, bmax - bmin);
|
||||
Vector3 bc = _bounds.center;
|
||||
_cacheY = tiles[0].center.y;
|
||||
|
||||
// ── 816o — 바닥과 **같은 식**으로 구름 그림자 색 띠의 경계를 굽는다 ──
|
||||
float coverage = 1f;
|
||||
if (cfg.cloudEdgeEnabled != 0)
|
||||
{
|
||||
var src = cfg.cloudEdgeSourceMaterial != null ? cfg.cloudEdgeSourceMaterial : cfg.islandTopMaterial;
|
||||
if (_field.Configure(src))
|
||||
{
|
||||
BakeField();
|
||||
coverage = Mathf.Clamp(_field.EdgeFraction, 0.02f, 1f);
|
||||
_edgeOn = true; CloudEdgeOn = true;
|
||||
}
|
||||
else CloudWhy = _field.Why;
|
||||
}
|
||||
|
||||
// 밀도 자동 조절(모바일 예산) — 띠 면적(coverage)을 실측해서 반영한다
|
||||
float edgeScale = _edgeOn ? Mathf.Max(0.05f, cfg.cloudEdgeDensityScale) : 1f;
|
||||
float budgetScale = 1f;
|
||||
if (cfg.maxInstances > 0)
|
||||
{
|
||||
float area = tiles.Count * 64f; // 타일 8×8
|
||||
float topLayer = 0f;
|
||||
for (int i = 0; i < cfg.scatter.Length; i++)
|
||||
{
|
||||
var s = cfg.scatter[i];
|
||||
if (s == null || s.enabled_ == 0) continue;
|
||||
if (s.density > topLayer) topLayer = s.density;
|
||||
}
|
||||
float d0 = topLayer * edgeScale;
|
||||
float est = area * d0 * d0 * 0.93f * coverage; // 윗면은 8×8 의 약 93 %
|
||||
if (est > cfg.maxInstances) budgetScale = Mathf.Sqrt(cfg.maxInstances / est);
|
||||
}
|
||||
float densScale = budgetScale * edgeScale;
|
||||
|
||||
// 같은 density 끼리 한 층으로 묶는다(데모의 FirstLayer/SecondLayer 와 같은 구조)
|
||||
var layers = new List<float>(4);
|
||||
for (int i = 0; i < cfg.scatter.Length; i++)
|
||||
{
|
||||
var s = cfg.scatter[i];
|
||||
if (s == null || s.enabled_ == 0 || s.mesh == null || s.material == null || s.scale <= 0f) continue;
|
||||
float d = s.density * densScale;
|
||||
if (d <= 0f) continue;
|
||||
bool found = false;
|
||||
for (int k = 0; k < layers.Count; k++) if (Mathf.Abs(layers[k] - d) < 1e-5f) { found = true; break; }
|
||||
if (!found) layers.Add(d);
|
||||
}
|
||||
|
||||
var allDefs = new List<WLScatterDef>(8);
|
||||
var allSets = new List<InstancingSettings>(8);
|
||||
var cands = new List<Cand>(8192);
|
||||
|
||||
for (int L = 0; L < layers.Count; L++)
|
||||
{
|
||||
|
|
@ -137,24 +197,23 @@ namespace WL.Look.Farm
|
|||
{
|
||||
var s = cfg.scatter[i];
|
||||
if (s == null || s.enabled_ == 0 || s.mesh == null || s.material == null || s.scale <= 0f) continue;
|
||||
if (Mathf.Abs(s.density * budgetScale - density) > 1e-5f) continue;
|
||||
if (Mathf.Abs(s.density * densScale - density) > 1e-5f) continue;
|
||||
defs.Add(s); wsum += Mathf.Max(0f, s.probability);
|
||||
}
|
||||
if (defs.Count == 0 || wsum <= 0f) continue;
|
||||
|
||||
var buckets = new List<InstanceData>[defs.Count];
|
||||
var iset = new InstancingSettings[defs.Count];
|
||||
int baseIdx = allDefs.Count;
|
||||
for (int i = 0; i < defs.Count; i++)
|
||||
{
|
||||
buckets[i] = new List<InstanceData>(1024);
|
||||
iset[i] = new InstancingSettings
|
||||
allDefs.Add(defs[i]);
|
||||
allSets.Add(new InstancingSettings
|
||||
{
|
||||
Mesh = defs[i].mesh,
|
||||
Material = defs[i].material,
|
||||
Probability = defs[i].probability,
|
||||
Scale = defs[i].scale,
|
||||
NormalOffset = defs[i].normalOffset,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
float step = 1f / density;
|
||||
|
|
@ -194,36 +253,111 @@ namespace WL.Look.Farm
|
|||
}
|
||||
|
||||
float sc = 1f + (Frac(h, 3) - 0.5f) * 2f * sv;
|
||||
var rot = cfg.randomYaw != 0 ? Quaternion.Euler(0f, Frac(h, 4) * 360f, 0f) : Quaternion.identity;
|
||||
var pos = new Vector3(wx, t.center.y, wz) - bc;
|
||||
float yaw = cfg.randomYaw != 0 ? Frac(h, 4) * 360f : 0f;
|
||||
|
||||
var trs = Matrix4x4.TRS(pos, rot, Vector3.one * sc);
|
||||
var def = defs[pick];
|
||||
trs *= Matrix4x4.TRS(def.normalOffset * Vector3.up, Quaternion.identity,
|
||||
new Vector3(def.scale, def.scale, def.scale));
|
||||
|
||||
buckets[pick].Add(new InstanceData { TRS = trs, Normal = Vector3.up });
|
||||
cands.Add(new Cand { x = wx, z = wz, y = t.center.y, def = baseIdx + pick, sc = sc, yaw = yaw });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < defs.Count; i++)
|
||||
{
|
||||
if (buckets[i].Count == 0) continue;
|
||||
result.Add(iset[i], buckets[i]);
|
||||
Instances += buckets[i].Count;
|
||||
DrawnConfigs++;
|
||||
long tri = 0;
|
||||
for (int s = 0; s < defs[i].mesh.subMeshCount; s++) tri += defs[i].mesh.GetIndexCount(s) / 3;
|
||||
Triangles += tri * buckets[i].Count;
|
||||
if (density > UsedDensity) UsedDensity = density;
|
||||
}
|
||||
if (density > UsedDensity) UsedDensity = density;
|
||||
}
|
||||
|
||||
LastLog = "타일 " + Tiles + " · 인스턴스 " + Instances + " · 드로우콜 " + DrawnConfigs
|
||||
+ " · 삼각형 " + Triangles + " · 제외점 " + Excluded
|
||||
_cdef = allDefs.ToArray();
|
||||
_cset = allSets.ToArray();
|
||||
_ctri = new long[_cdef.Length];
|
||||
for (int i = 0; i < _cdef.Length; i++)
|
||||
{
|
||||
long tri = 0;
|
||||
var m = _cdef[i].mesh;
|
||||
for (int s = 0; s < m.subMeshCount; s++) tri += m.GetIndexCount(s) / 3;
|
||||
_ctri[i] = tri;
|
||||
}
|
||||
_cand = cands.ToArray();
|
||||
_candCount = _cand.Length;
|
||||
Candidates = _candCount;
|
||||
_budgetScale = budgetScale;
|
||||
_cacheValid = _cdef.Length > 0 && _candCount > 0;
|
||||
if (!_cacheValid) { LastLog = "후보 0"; cfg.Log(LastLog); return null; }
|
||||
|
||||
return BuildFromCache(false);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
// 816o — 띠 굽기 + 캐시에서 고르기
|
||||
// ─────────────────────────────────────────────────────────────────
|
||||
float _budgetScale = 1f;
|
||||
bool _edgeOn;
|
||||
|
||||
void BakeField()
|
||||
{
|
||||
float cell = Mathf.Max(0.05f, cfg.cloudEdgeCell);
|
||||
int dil = Mathf.Max(0, cfg.cloudEdgeDilate);
|
||||
// 갱신 주기 동안 구름이 흐르는 만큼 미리 덮어 둔다 — 띠가 움직여도 빈틈이 안 생긴다
|
||||
if (cfg.cloudEdgeDriftCover != 0 && cfg.cloudEdgeRefreshSeconds > 0f)
|
||||
dil += Mathf.CeilToInt(_field.DriftPerSecond * cfg.cloudEdgeRefreshSeconds / cell);
|
||||
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
_field.Bake(_bounds, _cacheY, cell, cfg.cloudEdgeMaxCells, dil,
|
||||
cfg.cloudEdgeSampleCell, cfg.cloudEdgeMaxSamples);
|
||||
sw.Stop();
|
||||
BakeMs = (float)sw.Elapsed.TotalMilliseconds;
|
||||
EdgeCells = _field.Cells; EdgeCellSize = _field.CellSize;
|
||||
EdgeSamples = _field.Samples; EdgeSampleCell = _field.SampleCellSize;
|
||||
EdgeFraction = _field.EdgeFraction; EdgeDilate = dil;
|
||||
}
|
||||
|
||||
Dictionary<InstancingSettings, List<InstanceData>> BuildFromCache(bool refresh)
|
||||
{
|
||||
if (refresh)
|
||||
{
|
||||
_field.Tick();
|
||||
BakeField();
|
||||
}
|
||||
|
||||
Instances = 0; Triangles = 0; DrawnConfigs = 0; CloudExcluded = 0;
|
||||
|
||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||
var buckets = new List<InstanceData>[_cdef.Length];
|
||||
for (int i = 0; i < buckets.Length; i++) buckets[i] = new List<InstanceData>(256);
|
||||
|
||||
Vector3 bc = _bounds.center;
|
||||
bool edge = _edgeOn;
|
||||
for (int i = 0; i < _candCount; i++)
|
||||
{
|
||||
var c = _cand[i];
|
||||
if (edge && !_field.IsEdge(c.x, c.z)) { CloudExcluded++; continue; }
|
||||
|
||||
var rot = c.yaw != 0f ? Quaternion.Euler(0f, c.yaw, 0f) : Quaternion.identity;
|
||||
var trs = Matrix4x4.TRS(new Vector3(c.x, c.y, c.z) - bc, rot, Vector3.one * c.sc);
|
||||
var def = _cdef[c.def];
|
||||
trs *= Matrix4x4.TRS(def.normalOffset * Vector3.up, Quaternion.identity,
|
||||
new Vector3(def.scale, def.scale, def.scale));
|
||||
buckets[c.def].Add(new InstanceData { TRS = trs, Normal = Vector3.up });
|
||||
}
|
||||
sw.Stop();
|
||||
FilterMs = (float)sw.Elapsed.TotalMilliseconds;
|
||||
|
||||
var result = new Dictionary<InstancingSettings, List<InstanceData>>();
|
||||
for (int i = 0; i < buckets.Length; i++)
|
||||
{
|
||||
if (buckets[i].Count == 0) continue;
|
||||
result.Add(_cset[i], buckets[i]);
|
||||
Instances += buckets[i].Count;
|
||||
DrawnConfigs++;
|
||||
Triangles += _ctri[i] * buckets[i].Count;
|
||||
}
|
||||
|
||||
LastLog = "타일 " + Tiles + " · 후보 " + Candidates + " · 인스턴스 " + Instances
|
||||
+ " · 드로우콜 " + DrawnConfigs + " · 삼각형 " + Triangles + " · 제외점 " + Excluded
|
||||
+ " · 밀도 " + UsedDensity.ToString("F2") + "(=" + (UsedDensity * UsedDensity).ToString("F2") + "개/㎡)"
|
||||
+ (budgetScale < 1f ? " · 예산으로 밀도 ×" + budgetScale.ToString("F2") : "");
|
||||
+ (_budgetScale < 1f ? " · 예산으로 밀도 ×" + _budgetScale.ToString("F2") : "")
|
||||
+ (_edgeOn
|
||||
? " · 구름띠 " + (EdgeFraction * 100f).ToString("F1") + "%(칸 " + EdgeCellSize.ToString("F2")
|
||||
+ "m×" + EdgeCells + " · 넓힘 " + EdgeDilate + " · 노이즈 " + EdgeSampleCell.ToString("F2")
|
||||
+ "m×" + EdgeSamples + ") · 띠밖 제외 " + CloudExcluded
|
||||
+ " · 굽기 " + BakeMs.ToString("F1") + "ms · 고르기 " + FilterMs.ToString("F1") + "ms"
|
||||
: " · 구름띠 off" + (CloudWhy.Length > 0 ? "(" + CloudWhy + ")" : ""));
|
||||
cfg.Log(LastLog);
|
||||
return result.Count == 0 ? null : result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,6 +166,7 @@ namespace WL.Look.Farm
|
|||
if (cfg.rebuildPollSeconds > 0f)
|
||||
{
|
||||
int last = CountUnlocked(scene);
|
||||
float nextCloud = Time.time + Mathf.Max(0f, cfg.cloudEdgeRefreshSeconds);
|
||||
while (scene.isLoaded)
|
||||
{
|
||||
yield return new WaitForSeconds(cfg.rebuildPollSeconds);
|
||||
|
|
@ -177,6 +178,16 @@ namespace WL.Look.Farm
|
|||
last = now;
|
||||
if (cfg.applyLook != 0) SwapMaterials(cfg, scene);
|
||||
RequestGrassRebuild(cfg);
|
||||
nextCloud = Time.time + Mathf.Max(0f, cfg.cloudEdgeRefreshSeconds);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 816o — 구름이 흐른 만큼 띠를 다시 고른다(타일·제외 사각형은 다시 훑지 않는다)
|
||||
if (cfg.cloudEdgeEnabled != 0 && cfg.cloudEdgeRefreshSeconds > 0f && Time.time >= nextCloud)
|
||||
{
|
||||
nextCloud = Time.time + cfg.cloudEdgeRefreshSeconds;
|
||||
var g = Object.FindFirstObjectByType<WLIslandGrass>(FindObjectsInactive.Include);
|
||||
if (g != null) g.RefreshCloudEdges();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -270,6 +270,47 @@ namespace WL.Look.Farm
|
|||
[Tooltip("확장 애니메이션(DOTween 1초)이 끝나기를 기다리는 초.")]
|
||||
public float rebuildDelaySeconds = 1.4f;
|
||||
|
||||
// ───────────────────────────────── §2 구름 경계 배치 (816o)
|
||||
[Header("§2 — 구름 그림자 경계에만 풀을 심는다 (816o · PD 「모바일 부하 없게」)")]
|
||||
[Tooltip("🔴 되돌리기 스위치 — 0 이면 816n 상태 100 % 복귀(섬 전체에 균일 격자). " +
|
||||
"1 이면 바닥 색 띠가 바뀌는 곳(구름 그림자 경계) 근처에만 심는다. " +
|
||||
"816n 실측: 보이는 풀의 88 % 가 이 띠에 몰려 있다 = 나머지는 삼각형만 먹는다.")]
|
||||
public int cloudEdgeEnabled = 1;
|
||||
|
||||
[Tooltip("경계 판정 격자 칸(m). 띠 폭의 최소 단위 = 이 값. 여기는 노이즈를 계산하지 않는다(보간).")]
|
||||
public float cloudEdgeCell = 0.3f;
|
||||
|
||||
[Tooltip("판정 격자 칸 수 상한. 섬이 커지면 칸을 자동으로 키운다(0 = 무제한).")]
|
||||
public int cloudEdgeMaxCells = 120000;
|
||||
|
||||
[Tooltip("🔴 비용이 나는 유일한 곳 — 구름 노이즈(6 옥타브)를 실제로 계산하는 간격(m). " +
|
||||
"구름의 가장 작은 무늬가 1/(32×_Cloud_Density) ≈ 3 m 라 1 m 면 충분하다. " +
|
||||
"사이 값은 겹선형 보간으로 채운다.")]
|
||||
public float cloudEdgeSampleCell = 1f;
|
||||
|
||||
[Tooltip("노이즈를 계산하는 점의 개수 상한. 섬이 커지면 간격을 자동으로 늘려 비용을 묶어 둔다.")]
|
||||
public int cloudEdgeMaxSamples = 4096;
|
||||
|
||||
[Tooltip("경계에서 몇 칸 더 넓힐 것인가. 띠 폭 ≈ (1 + 2 × 이 값) × 칸(m). " +
|
||||
"구름이 흐르는 만큼(`cloudEdgeDriftCover`)은 여기에 자동으로 더해진다.")]
|
||||
public int cloudEdgeDilate = 1;
|
||||
|
||||
[Tooltip("1 이면 갱신 주기 동안 구름이 흐르는 거리만큼 띠를 자동으로 더 넓힌다 — " +
|
||||
"띠가 움직여도 풀이 뒤따라오지 못해 생기는 빈틈을 막는다. 0 이면 위 값만 쓴다.")]
|
||||
public int cloudEdgeDriftCover = 1;
|
||||
|
||||
[Tooltip("구름이 흐르므로 이 주기(초)마다 띠를 다시 고른다. 0 이면 한 번 굽고 고정. " +
|
||||
"🔴 실제 주기는 `rebuildPollSeconds` 단위로 반올림된다.")]
|
||||
public float cloudEdgeRefreshSeconds = 1f;
|
||||
|
||||
[Tooltip("띠 안의 밀도 배율. 면적이 줄어든 만큼 띠를 더 촘촘히 해 데모 느낌을 살린다. " +
|
||||
"1 = 816n 과 같은 밀도(= 인스턴스가 띠 면적 비율만큼 그대로 줄어든다).")]
|
||||
public float cloudEdgeDensityScale = 1.25f;
|
||||
|
||||
[Tooltip("구름 값을 읽어 올 머티리얼. 비우면 `islandTopMaterial`(바닥)을 쓴다 — " +
|
||||
"바닥과 **같은 식**이어야 띠가 어긋나지 않는다.")]
|
||||
public Material cloudEdgeSourceMaterial;
|
||||
|
||||
// ───────────────────────────────── 구운 마스크
|
||||
[Header("§1 — 구운 윗면 마스크 (에디터에서 생성 · 손대지 말 것)")]
|
||||
public WLTileMask[] tileMasks = new WLTileMask[0];
|
||||
|
|
|
|||
Loading…
Reference in New Issue