// WL-816j — ② PD 경로(구조 재현)에서 무늬 3단 · 움직임 3단 · 가장자리 물보라 · 성능 전/후 // 🔴 Play 중 에셋(.mat/.asset)을 고치지 않는다 — 런타임 Material 인스턴스만 만들어 렌더러에 건다(§9). using System.Collections; using System.Text; using UnityEngine; using UnityEngine.Rendering.Universal; using UnityEngine.SceneManagement; public static class WL816j_Verify { public const string Dir = "Screenshots_WL/WL816j/"; public const int W = 1080, H = 1920; public const float PlaneW = 1000f; // 실측 public const float Cells = 6f; // 보로노이 1타일에 든 칸 수(실측) public static void Open() { UnityEditor.SceneManagement.EditorSceneManager.OpenScene( "Assets/Scenes/InGame.unity", UnityEditor.SceneManagement.OpenSceneMode.Single); Debug.Log("[816j] InGame 열림"); } public static void Start() { var go = GameObject.Find("~WL816jVerify"); if (go != null) Object.DestroyImmediate(go); go = new GameObject("~WL816jVerify"); go.AddComponent(); Debug.Log("[816j] verify 러너 시작"); } // ── 카메라 ───────────────────────────────────────────────────── public static Camera Cam(string name, Vector3 pos, Vector3 euler, float fov) { var go = GameObject.Find(name); if (go == null) { go = new GameObject(name); go.hideFlags = HideFlags.DontSave; } var c = go.GetComponent(); if (c == null) c = go.AddComponent(); c.orthographic = false; c.fieldOfView = fov; c.nearClipPlane = 0.3f; c.farClipPlane = 2000f; c.clearFlags = CameraClearFlags.Skybox; c.cullingMask = ~0; c.depth = -100f; go.transform.position = pos; go.transform.rotation = Quaternion.Euler(euler); var ac = go.GetComponent(); if (ac == null) ac = go.AddComponent(); ac.renderPostProcessing = true; return c; } /// 🔴 자로 재는 카메라 — 정사영 · 바다만 · 1 m 가 정확히 몇 픽셀인지 안다. public static Camera RulerCam(float orthoSize) { var go = GameObject.Find("~WL816jRuler"); if (go == null) { go = new GameObject("~WL816jRuler"); go.hideFlags = HideFlags.DontSave; } var c = go.GetComponent(); if (c == null) c = go.AddComponent(); c.orthographic = true; c.orthographicSize = orthoSize; c.nearClipPlane = 0.3f; c.farClipPlane = 400f; c.clearFlags = CameraClearFlags.SolidColor; c.backgroundColor = Color.black; c.cullingMask = ~0; c.depth = -100f; go.transform.position = new Vector3(-160f, 60f, -160f); // 섬 밖 열린 바다 go.transform.rotation = Quaternion.Euler(90f, 0f, 0f); var ac = go.GetComponent(); if (ac == null) ac = go.AddComponent(); ac.renderPostProcessing = false; // 자로 잴 때는 후처리 없이 return c; } public static Camera WaterCam() { return Cam("~WL816jWaterCam", new Vector3(-16f, 5.5f, -16f), new Vector3(10f, 45f, 0f), 45f); } public static Camera HorizonCam(){ return Cam("~WL816jHorizonCam", new Vector3(-14f, 8f, -14f), new Vector3(4f, 45f, 0f), 55f); } // ── 캡처 ─────────────────────────────────────────────────────── public static Texture2D Grab(Camera cam, int w, int h) { var rt = new RenderTexture(w, h, 24, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB); rt.antiAliasing = 1; rt.Create(); var pT = cam.targetTexture; var pA = RenderTexture.active; cam.targetTexture = rt; cam.Render(); RenderTexture.active = rt; var tex = new Texture2D(w, h, TextureFormat.RGB24, false); tex.ReadPixels(new Rect(0, 0, w, h), 0, 0); tex.Apply(); RenderTexture.active = pA; cam.targetTexture = pT; rt.Release(); Object.DestroyImmediate(rt); return tex; } public static string Shot(Camera cam, string path) { return Shot(cam, path, W, H); } public static string Shot(Camera cam, string path, int w, int h) { if (cam == null) return "카메라 없음"; System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(path)); var tex = Grab(cam, w, h); System.IO.File.WriteAllBytes(path, tex.EncodeToPNG()); string s = "평균=" + Band(tex, 0f, 1f); Object.DestroyImmediate(tex); return s + " → " + path; } public static string Band(Texture2D t, float y0, float y1) { int a = Mathf.RoundToInt(y0 * (t.height - 1)), b = Mathf.RoundToInt(y1 * (t.height - 1)); var px = t.GetPixels32(); long r = 0, g = 0, bl = 0; int n = 0; for (int y = a; y < b; y += 3) for (int x = 0; x < t.width; x += 3) { var c = px[y * t.width + x]; r += c.r; g += c.g; bl += c.b; n++; } if (n == 0) return "-"; return "#" + ((int)(r / n)).ToString("X2") + ((int)(g / n)).ToString("X2") + ((int)(bl / n)).ToString("X2"); } /// 흰 픽셀(거품) 비율 % — 물보라가 실제로 생겼는지 숫자로. public static float FoamPct(Camera cam, int w, int h) { var t = Grab(cam, w, h); var px = t.GetPixels32(); int n = 0; for (int i = 0; i < px.Length; i++) if (px[i].r > 215 && px[i].g > 215 && px[i].b > 215) n++; Object.DestroyImmediate(t); return 100f * n / px.Length; } public static float Ms(Camera cam, int frames) { var rt = new RenderTexture(W, H, 24, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB); rt.antiAliasing = 1; rt.Create(); var pT = cam.targetTexture; 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 = pT; rt.Release(); Object.DestroyImmediate(rt); return (float)sw.Elapsed.TotalMilliseconds / frames; } public static void Strip(string[] paths, string outPath) { var ts = new Texture2D[paths.Length]; int w = 0, h = 0; for (int i = 0; i < paths.Length; i++) { ts[i] = Load(paths[i]); if (ts[i] == null) return; w += ts[i].width; h = Mathf.Max(h, ts[i].height); } var o = new Texture2D(w, h, TextureFormat.RGB24, false); var fill = new Color32[w * h]; for (int i = 0; i < fill.Length; i++) fill[i] = new Color32(24, 24, 28, 255); o.SetPixels32(fill); int x = 0; for (int i = 0; i < ts.Length; i++) { o.SetPixels32(x, 0, ts[i].width, ts[i].height, ts[i].GetPixels32()); x += ts[i].width; } o.Apply(); System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(outPath)); System.IO.File.WriteAllBytes(outPath, o.EncodeToPNG()); Object.DestroyImmediate(o); for (int i = 0; i < ts.Length; i++) Object.DestroyImmediate(ts[i]); } public static void Pair(string a, string b, string o) { Strip(new[] { a, b }, o); } public static Texture2D Load(string p) { if (!System.IO.File.Exists(p)) return null; var t = new Texture2D(2, 2, TextureFormat.RGB24, false); t.LoadImage(System.IO.File.ReadAllBytes(p)); return t; } // ── 값 만들기 ────────────────────────────────────────────────── public const string R_Size = "Vector1_07238257334b4a149e675e2451801030"; public const string R_Speed = "Vector1_8e22a98f75c94b218c49e6c4805e799d"; public const string R_Strength = "Vector1_99cc56b5c6fd474082f07f00a6df94f9"; public const string R_NoiseSpeed = "Vector1_ad7e0fcd37f44d68b68de6f06bb43b96"; public const string R_FoamScale = "Vector1_11671d7a4cdb4d059a6c69e39f3f7b3f"; public const string R_FoamSpeed = "Vector1_3089b6a325a44c1686907af4ee3b0776"; public const string R_FoamDistance = "Vector1_101dc4546b684d40bc2ff2fc59ab43d5"; public const string R_FoamEdge = "Vector1_34f757bec6b8422b9aef3b9d97117a6e"; public const string R_WaweNormalSize = "Vector1_68f0c21b684f4b7c9ebef8789113b107"; public const string R_WaweNormalSpeed = "Vector1_85ea9246e72a44ed9e0482861767c826"; public const string R_WaweNormalStrength = "Vector1_2b4963cef1de4859983ffcbae94c6f60"; /// 무늬 1칸을 cellM 미터로, 무늬가 흐르는 속도를 flowMs m/s 로 (색·거품은 손대지 않는다). public static Material Make(Material src, string name, float cellM, float flowMs) { var m = new Material(src); m.name = name; float size = PlaneW / (cellM * Cells); m.SetFloat(R_Size, size); m.SetFloat(R_Strength, 0.3085f / size); // 텍스처 기준 뒤틀림량을 816h 와 똑같이 유지 m.SetFloat(R_Speed, flowMs * size / PlaneW); m.SetFloat(R_NoiseSpeed, flowMs * size / PlaneW); return m; } /// 가장자리 거품 — 거품알 크기 foamM(m) · 띠 폭 bandM(깊이차 m) · 흐름 churnMs(m/s). public static void Foam(Material m, float foamM, float bandM, float churnMs) { float fs = PlaneW / foamM; m.SetFloat(R_FoamScale, fs); m.SetFloat(R_FoamEdge, 4f); m.SetFloat(R_FoamDistance, bandM * 4f); // edge=saturate(d/FoamDistance)*FoamEdge → 띠 = FoamDistance/FoamEdge m.SetFloat(R_FoamSpeed, churnMs * fs / PlaneW); } } public class WL816j_VerifyRunner : MonoBehaviour { static StringBuilder sb; static void L(string s) { sb.AppendLine(s); } const string D = "Screenshots_WL/WL816j/"; const string MatDir = "Assets/WL/Look/Farm/Materials/"; Renderer water; Camera live; void Start() { StartCoroutine(Co()); } IEnumerator Co() { sb = new StringBuilder(); L("=== WL-816j 확인 (PD 경로 구조 재현 = InGame 활성 + Level01 Additive) ==="); bool viaGame = false; try { if (InGameInfo.Ins != null) { InGameInfo.Ins.Load_Map(900); viaGame = true; } } catch (System.Exception) { } L("Load_Map(900) = " + (viaGame ? "성공" : "🔴 로그인 없이는 못 탄다 → LoadSceneAsync(Level01, Additive) 로 대체(816f·816h 와 같음)")); if (!viaGame) { var op = SceneManager.LoadSceneAsync("Level01", LoadSceneMode.Additive); while (op != null && !op.isDone) yield return null; } for (int i = 0; i < 7; i++) yield return new WaitForSeconds(1f); foreach (var r in Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None)) if (r != null && r.gameObject.scene.name == "Level01" && r.name == "Water") water = r; foreach (var c in Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None)) { if (!c.gameObject.activeInHierarchy || !c.enabled || c.targetTexture != null) continue; if (c.name.StartsWith("~WL816j")) continue; if (live == null || c.depth > live.depth) live = c; } L("물 렌더러 = " + (water == null ? "🔴 없음" : water.name + " mat=" + water.sharedMaterial.name + " shader=" + water.sharedMaterial.shader.name) + " · 물 교체 " + WL.Look.Farm.WLIslandLook.WaterSwapped + " · 보이는 카메라 = " + (live ? live.name : "없음")); var baseMat = water != null ? water.sharedMaterial : null; // = 816h 채택 Farm_Water_WL_C if (baseMat == null || live == null) { Done(); yield break; } // ── 0) 해안선 찾기 (물보라 확대용) ────────────────────────── float shoreX = 0f, shoreZ = -3.78f; for (float x = 4f; x < 90f; x += 0.5f) { RaycastHit hit; bool land = Physics.Raycast(new Vector3(x, 60f, shoreZ), Vector3.down, out hit, 200f) && hit.point.y > -0.9f; if (land) shoreX = x; } L("해안선(z=" + shoreZ + " 에서 +X 방향 마지막 육지) x = " + shoreX.ToString("F1")); var shoreCam = WL816j_Verify.Cam("~WL816jShore", new Vector3(shoreX + 11f, 1.6f, shoreZ + 8f), new Vector3(9f, 215f, 0f), 26f); // ── 1) 지금(816h 채택 C) ──────────────────────────────────── L(""); L("1) 지금 = " + baseMat.name + " (Size " + baseMat.GetFloat(WL816j_Verify.R_Size).ToString("G4") + " → 무늬 1칸 " + (1000f / (baseMat.GetFloat(WL816j_Verify.R_Size) * 6f)).ToString("F1") + " m" + " · FoamScale " + baseMat.GetFloat(WL816j_Verify.R_FoamScale).ToString("G4") + ")"); L(" 게임화면 " + WL816j_Verify.Shot(live, D + "a_now_game.png")); L(" 바다근접 " + WL816j_Verify.Shot(WL816j_Verify.WaterCam(), D + "a_now_water.png")); L(" 자(정사영 40 m 폭 · 1 m = 27.0 px) " + WL816j_Verify.Shot(WL816j_Verify.RulerCam(20f), D + "a_now_ruler.png", 1080, 1080)); L(" 가장자리 확대 " + WL816j_Verify.Shot(shoreCam, D + "a_now_shore.png")); L(" 가장자리 흰픽셀 = " + WL816j_Verify.FoamPct(shoreCam, 540, 960).ToString("F2") + " %"); // ── 2) 무늬 3단 (움직임은 지금과 같은 1.28 m/s 로 고정) ───── L(""); L("2) 무늬 3단 — 1칸 3 m / 1.67 m / 1 m (움직임은 전부 1.28 m/s 로 고정 · 색·거품 그대로)"); float[] cells = { 3f, 1.67f, 1f }; var sizeShots = new string[4]; var rulerShots = new string[4]; sizeShots[0] = D + "a_now_water.png"; rulerShots[0] = D + "a_now_ruler.png"; var mats = new Material[3]; for (int i = 0; i < 3; i++) { mats[i] = WL816j_Verify.Make(baseMat, "J_cell" + cells[i], cells[i], 1.28f); water.sharedMaterial = mats[i]; yield return null; yield return null; yield return new WaitForSeconds(0.3f); sizeShots[i + 1] = D + "b_cell" + (i + 1) + "_water.png"; rulerShots[i + 1] = D + "b_cell" + (i + 1) + "_ruler.png"; L(" [1칸 " + cells[i] + " m · Size " + mats[i].GetFloat(WL816j_Verify.R_Size).ToString("F0") + " · Strength " + mats[i].GetFloat(WL816j_Verify.R_Strength).ToString("G3") + "]"); L(" 바다근접 " + WL816j_Verify.Shot(WL816j_Verify.WaterCam(), sizeShots[i + 1])); L(" 자 " + WL816j_Verify.Shot(WL816j_Verify.RulerCam(20f), rulerShots[i + 1], 1080, 1080)); } WL816j_Verify.Strip(sizeShots, D + "b_pattern_steps.png"); WL816j_Verify.Strip(rulerShots, D + "b_pattern_steps_ruler.png"); L(" 3단 한 장 = b_pattern_steps.png(바다근접) · b_pattern_steps_ruler.png(자 · 좌부터 지금/3 m/1.67 m/1 m)"); // ── 3) 움직임 3단 (무늬는 1.67 m 고정) ────────────────────── L(""); L("3) 움직임 3단 — 0.4 / 0.8 / 1.5 m/s (무늬 1칸 1.67 m 고정) · 1.0 초 간격 두 장씩(흐른 거리 확인용)"); float[] flows = { 0.4f, 0.8f, 1.5f }; var flowShots = new string[3]; for (int i = 0; i < 3; i++) { var m = WL816j_Verify.Make(baseMat, "J_flow" + flows[i], 1.67f, flows[i]); water.sharedMaterial = m; yield return null; yield return null; yield return new WaitForSeconds(0.3f); string p0 = D + "c_flow" + (i + 1) + "_t0.png"; string p1 = D + "c_flow" + (i + 1) + "_t1.png"; float t0 = Time.time; WL816j_Verify.Shot(WL816j_Verify.RulerCam(10f), p0, 1080, 1080); yield return new WaitForSeconds(1f); float dt = Time.time - t0; WL816j_Verify.Shot(WL816j_Verify.RulerCam(10f), p1, 1080, 1080); flowShots[i] = p1; L(" [" + flows[i] + " m/s] dt=" + dt.ToString("F2") + " s · 두 장 = " + p0 + " , " + p1 + " (정사영 20 m 폭 · 1 m = 54.0 px)"); } WL816j_Verify.Strip(flowShots, D + "c_flow_steps.png"); // ── 4) 가장자리 물보라 — 거품알 3단 + 띠 폭 3단 ───────────── L(""); L("4) 🔴 섬 가장자리 물보라 = 셰이더에 **이미 있는 Foam**(깊이 기반)의 값만 조정 — 새 시스템 0"); float[] foamCell = { 1.5f, 0.75f, 0.375f }; var foamShots = new string[4]; foamShots[0] = D + "a_now_shore.png"; for (int i = 0; i < 3; i++) { var m = WL816j_Verify.Make(baseMat, "J_foam" + i, 1.67f, 0.8f); WL816j_Verify.Foam(m, foamCell[i], 0.55f, 0.5f); water.sharedMaterial = m; yield return null; yield return null; yield return new WaitForSeconds(0.3f); foamShots[i + 1] = D + "d_foamcell" + (i + 1) + ".png"; L(" [거품알 " + foamCell[i] + " m · FoamScale " + m.GetFloat(WL816j_Verify.R_FoamScale).ToString("F0") + "] " + WL816j_Verify.Shot(shoreCam, foamShots[i + 1]) + " 흰픽셀 " + WL816j_Verify.FoamPct(shoreCam, 540, 960).ToString("F2") + " %"); } WL816j_Verify.Strip(foamShots, D + "d_foam_cell_steps.png"); float[] band = { 0.55f, 1.1f, 1.8f }; var bandShots = new string[3]; for (int i = 0; i < 3; i++) { var m = WL816j_Verify.Make(baseMat, "J_band" + i, 1.67f, 0.8f); WL816j_Verify.Foam(m, 0.75f, band[i], 0.5f); water.sharedMaterial = m; yield return null; yield return null; yield return new WaitForSeconds(0.3f); bandShots[i] = D + "d_band" + (i + 1) + ".png"; L(" [띠 폭(깊이차) " + band[i] + " m · FoamDistance " + m.GetFloat(WL816j_Verify.R_FoamDistance).ToString("G3") + "] " + WL816j_Verify.Shot(shoreCam, bandShots[i]) + " 흰픽셀 " + WL816j_Verify.FoamPct(shoreCam, 540, 960).ToString("F2") + " %"); } WL816j_Verify.Strip(bandShots, D + "d_foam_band_steps.png"); // ── 5) 잔물결 노멀(공짜 — 텍스처는 이미 뽑고 있고 Strength 0 이라 꺼져 있었다) ── L(""); L("5) 잔물결 노멀맵 on/off — 셰이더가 이미 샘플링하는 텍스처(WaweNormalStrength 0 이라 꺼져 있던 것)"); var mOff = WL816j_Verify.Make(baseMat, "J_normOff", 1.67f, 0.8f); WL816j_Verify.Foam(mOff, 0.75f, 1.1f, 0.5f); var mOn = new Material(mOff); mOn.name = "J_normOn"; mOn.SetFloat(WL816j_Verify.R_WaweNormalSize, 250f); // 잔물결 1타일 4 m mOn.SetFloat(WL816j_Verify.R_WaweNormalSpeed, 0.075f); // 4 m × 0.075 = 0.3 m/s mOn.SetFloat(WL816j_Verify.R_WaweNormalStrength, 0.3f); string[] nShots = new string[2]; var pairM = new[] { mOff, mOn }; for (int i = 0; i < 2; i++) { water.sharedMaterial = pairM[i]; yield return null; yield return null; yield return new WaitForSeconds(0.3f); nShots[i] = D + "e_normal_" + (i == 0 ? "off" : "on") + ".png"; L(" [" + (i == 0 ? "off(지금)" : "on 4 m·0.3") + "] " + WL816j_Verify.Shot(WL816j_Verify.WaterCam(), nShots[i])); } WL816j_Verify.Strip(nShots, D + "e_normal_pair.png"); // ── 6) 성능 — 전(816h C) / 후(후보) 교대 2회 ──────────────── L(""); L("6) 성능 (같은 카메라 1080×1920 · 60프레임 오프스크린 · 순서 교대 2회)"); var cand = mOff; // 후보 = 무늬 1.67 m · 흐름 0.8 · 거품알 0.75 m · 띠 1.1 m for (int round = 0; round < 2; round++) { foreach (var pr in new[] { new object[] { "전(816h C)", baseMat }, new object[] { "후(816j 후보)", cand } }) { water.sharedMaterial = pr[1] as Material; yield return null; yield return null; L(" " + round + "회차 " + (string)pr[0] + " = " + WL816j_Verify.Ms(live, 60).ToString("F3") + " ms/frame"); } } L(" 드로우콜/렌더러 변화 = 같은 평면 1장 · 머티리얼 1개 · 셰이더 동일 → 0 (값만 다름)"); // ── 7) 후보로 마무리 캡처 ─────────────────────────────────── water.sharedMaterial = cand; yield return null; yield return new WaitForSeconds(0.3f); L(""); L("7) 후보 적용 화면"); L(" 게임화면 " + WL816j_Verify.Shot(live, D + "f_after_game.png")); L(" 바다근접 " + WL816j_Verify.Shot(WL816j_Verify.WaterCam(), D + "f_after_water.png")); L(" 수평선 " + WL816j_Verify.Shot(WL816j_Verify.HorizonCam(), D + "f_after_horizon.png")); L(" 가장자리 " + WL816j_Verify.Shot(shoreCam, D + "f_after_shore.png") + " 흰픽셀 " + WL816j_Verify.FoamPct(shoreCam, 540, 960).ToString("F2") + " %"); L(" 자 " + WL816j_Verify.Shot(WL816j_Verify.RulerCam(20f), D + "f_after_ruler.png", 1080, 1080)); WL816j_Verify.Pair(D + "a_now_game.png", D + "f_after_game.png", D + "g_side_by_side.png"); WL816j_Verify.Pair(D + "a_now_shore.png", D + "f_after_shore.png", D + "g_shore_before_after.png"); L(" 좌우 = g_side_by_side.png(좌 지금/우 후보) · g_shore_before_after.png(좌 지금/우 후보 · 가장자리 확대)"); water.sharedMaterial = baseMat; yield return null; Done(); } void Done() { System.IO.File.WriteAllText("AgentScripts/WL816j_VERIFY.txt", sb.ToString()); Debug.Log("[816j verify 완료]\n" + sb); } }