// WL814u_Capture.cs — 시제품 텍스처 임포트 설정 + 현재/도트 비교 캡처 + 읽힘 실측 (에디터 전용) // 🔴 씬 저장 0 · 프리팹/머티리얼 에셋 수정 0 (씬 인스턴스에 임시 머티리얼만 꽂았다가 버린다) · ProjectSettings 0 using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.Rendering.Universal; public static class WL814u_Capture { const string SCENE = "Assets/WL/Look/Arena/Scenes/WL_ArenaProto.unity"; const string TEX = "Assets/WL/Look/Character/Textures/"; const string SHOT = "Screenshots_WL/WL814u/"; const string OUT = "AgentScripts/WL814u_CAPTURE.txt"; const int LAYER = 31; const int W = 1080, H = 1920; static readonly StringBuilder sb = new StringBuilder(); static void L(string s) { sb.AppendLine(s); Console.WriteLine("[814uC] " + s); } // ── 1) 임포트 설정 ─────────────────────────────────────────────── public static void Import() { try { foreach (var f in new[] { "face02_Pixel.png", "Hair05_Pixel.png" }) { var p = TEX + f; AssetDatabase.ImportAsset(p, ImportAssetOptions.ForceUpdate); var ti = (TextureImporter)AssetImporter.GetAtPath(p); if (ti == null) { L("!! importer null " + p); continue; } ti.textureType = TextureImporterType.Default; ti.alphaSource = TextureImporterAlphaSource.FromInput; ti.alphaIsTransparency = true; ti.mipmapEnabled = true; // 1/59 축소 → 밉이 있어야 한다(끄면 심하게 반짝인다) ti.filterMode = FilterMode.Point; // 도트: 보간 금지 ti.wrapMode = TextureWrapMode.Clamp; ti.sRGBTexture = true; ti.maxTextureSize = 512; // 814s 채택값과 동일 조건에서 비교 ti.textureCompression = TextureImporterCompression.Uncompressed; // 하드 엣지 보존(블록압축은 계단을 뭉갠다) ti.SaveAndReimport(); var t = AssetDatabase.LoadAssetAtPath(p); L(string.Format("import {0} → {1}×{2} · {3} · mip {4} · {5}", f, t.width, t.height, t.format, t.mipmapCount, t.filterMode)); } } catch (Exception e) { L("EXCEPTION " + e); } finally { File.WriteAllText(OUT, sb.ToString(), new UTF8Encoding(true)); } } // ── 2) 캡처 ───────────────────────────────────────────────────── class Shot { public Color32[] px; public int w, h; } static Shot Render(Camera cam, int w, int h) { var rt = new RenderTexture(w, h, 24, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB); rt.antiAliasing = 1; rt.filterMode = FilterMode.Point; var pT = cam.targetTexture; var pA = RenderTexture.active; cam.targetTexture = rt; cam.Render(); RenderTexture.active = rt; var t = new Texture2D(w, h, TextureFormat.RGBA32, false); t.ReadPixels(new Rect(0, 0, w, h), 0, 0); t.Apply(); cam.targetTexture = pT; RenderTexture.active = pA; var s = new Shot { px = t.GetPixels32(), w = w, h = h }; UnityEngine.Object.DestroyImmediate(t); rt.Release(); UnityEngine.Object.DestroyImmediate(rt); return s; } static void Save(Shot s, string file) { var t = new Texture2D(s.w, s.h, TextureFormat.RGBA32, false); t.SetPixels32(s.px); t.Apply(); Directory.CreateDirectory(SHOT); File.WriteAllBytes(SHOT + file, t.EncodeToPNG()); UnityEngine.Object.DestroyImmediate(t); L(" saved " + SHOT + file + " (" + s.w + "×" + s.h + ")"); } // 여러 Shot 을 가로로 붙이고 zoom 배 점 확대 (crop = null 이면 전체) static Shot Compose(Shot[] shots, int zoom, RectInt? crop, int gap) { int cw = crop.HasValue ? crop.Value.width : shots[0].w; int ch = crop.HasValue ? crop.Value.height : shots[0].h; int ow = (cw * zoom) * shots.Length + gap * (shots.Length - 1); int oh = ch * zoom; var o = new Shot { w = ow, h = oh, px = new Color32[ow * oh] }; for (int i = 0; i < o.px.Length; i++) o.px[i] = new Color32(24, 24, 28, 255); for (int k = 0; k < shots.Length; k++) { int xoff = k * (cw * zoom + gap); for (int y = 0; y < oh; y++) for (int x = 0; x < cw * zoom; x++) { int sx = (crop.HasValue ? crop.Value.x : 0) + x / zoom; int sy = (crop.HasValue ? crop.Value.y : 0) + y / zoom; if (sx < 0 || sy < 0 || sx >= shots[k].w || sy >= shots[k].h) continue; o.px[y * ow + xoff + x] = shots[k].px[sy * shots[k].w + sx]; } } return o; } public static void Capture() { try { CapBody(); } catch (Exception e) { L("EXCEPTION " + e); } finally { File.WriteAllText(OUT, sb.ToString(), new UTF8Encoding(true)); } } static void CapBody() { EditorSceneManager.OpenScene(SCENE, OpenSceneMode.Single); GameObject root = null; foreach (var go in UnityEngine.SceneManagement.SceneManager.GetActiveScene().GetRootGameObjects()) if (go.name.Contains("LH_M05")) { root = go; break; } var smrs = root.GetComponentsInChildren(true).Where(r => r.enabled).ToArray(); var faceSMR = smrs.First(r => r.name == "Face"); var hairSMR = smrs.First(r => r.name == "Hair05"); var scam = Camera.main ?? UnityEngine.Object.FindObjectsByType(FindObjectsInactive.Exclude, FindObjectsSortMode.None).First(c => c.orthographic); // 🔴 씬의 기본 포즈(yaw 200)에서는 캐릭터가 카메라를 등지고 있어 얼굴이 거의 안 보인다. // 「얼굴이 몇 px 인가」는 카메라를 정면으로 볼 때가 최대치이므로 그 자세로 잰다(메모리에서만 · 씬 저장 0). float camYaw = scam.transform.eulerAngles.y; float poseYaw = root.transform.eulerAngles.y; root.transform.rotation = Quaternion.Euler(0, camYaw + 180f, 0); L(string.Format("포즈: 씬 기본 yaw {0:F0}° → 카메라 정면 yaw {1:F0}° 로 돌려서 측정·캡처 (카메라 yaw {2:F0}°)", poseYaw, camYaw + 180f, camYaw)); // 캐릭터 중심 프레이밍 (씬 카메라는 건드리지 않고 복사본만 쓴다) var b = new Bounds(root.transform.position, Vector3.zero); foreach (var r in smrs) b.Encapsulate(r.bounds); var go2 = new GameObject("WL814u_Cap"); var cam = go2.AddComponent(); cam.CopyFrom(scam); cam.orthographic = true; cam.orthographicSize = 10f; cam.aspect = (float)W / H; cam.targetTexture = null; cam.transform.SetPositionAndRotation(b.center - scam.transform.forward * 50f, scam.transform.rotation); var ucd = cam.GetUniversalAdditionalCameraData(); if (ucd != null) { ucd.renderPostProcessing = false; ucd.SetRenderer(0); } // 🔴 배치 모드 첫 렌더는 텍스처가 아직 안 올라와 캐릭터가 하얗게 나온다(1차 캡처에서 실측) → 워밍업 3장 버린다 for (int i = 0; i < 3; i++) Render(cam, W, H); var before = Render(cam, W, H); // ── 도트 텍스처를 「임시 머티리얼 인스턴스」에만 꽂는다 (에셋 무변경) ── var faceOrig = faceSMR.sharedMaterial; var hairOrig = hairSMR.sharedMaterial; var facePx = AssetDatabase.LoadAssetAtPath(TEX + "face02_Pixel.png"); var hairPx = AssetDatabase.LoadAssetAtPath(TEX + "Hair05_Pixel.png"); if (facePx == null || hairPx == null) { L("!! 시제품 텍스처를 못 찾음"); return; } var fm = new Material(faceOrig) { name = "TEMP_face_px" }; if (fm.HasProperty("_MainTex")) fm.SetTexture("_MainTex", facePx); if (fm.HasProperty("_BaseMap")) fm.SetTexture("_BaseMap", facePx); var hm = new Material(hairOrig) { name = "TEMP_hair_px" }; if (hm.HasProperty("_BaseMap")) hm.SetTexture("_BaseMap", hairPx); if (hm.HasProperty("_ShadowBaseMap")) hm.SetTexture("_ShadowBaseMap", hairPx); if (hm.HasProperty("_MainTex")) hm.SetTexture("_MainTex", hairPx); faceSMR.sharedMaterial = fm; hairSMR.sharedMaterial = hm; L("프리뷰 적용: Face ← face02_Pixel · Hair05 ← Hair05_Pixel (임시 Material 인스턴스 · 에셋 저장 0)"); var after = Render(cam, W, H); // 원복 (씬은 어차피 저장 안 하지만 확실히) faceSMR.sharedMaterial = faceOrig; hairSMR.sharedMaterial = hairOrig; // ── 캐릭터 bbox 를 찾아 머리 크롭 ── var vpMin = cam.WorldToViewportPoint(b.min); var vpMax = cam.WorldToViewportPoint(b.max); int cx = Mathf.RoundToInt(W * 0.5f), cy = Mathf.RoundToInt(H * 0.5f); // §1 실측: 키 98 px · y[912..1009] (같은 프레이밍) → 머리는 위쪽 40 px var headCrop = new RectInt(cx - 32, cy + 4, 64, 56); var bodyCrop = new RectInt(cx - 40, cy - 56, 80, 120); Save(Compose(new[] { before, after }, 1, null, 16), "u_1_ingame_compare.png"); Save(Compose(new[] { before, after }, 4, bodyCrop, 12), "u_2_char_4x_compare.png"); Save(Compose(new[] { before, after }, 12, headCrop, 12), "u_3_head_12x_compare.png"); // ── 읽힘 실측: 얼굴 데칼 영역의 색·대비 ── // 캐릭터만 남기는 레이어 마스크 카메라로 face 만 렌더 → 그 마스크 안에서 before/after 비교 var all = root.GetComponentsInChildren(true); var orig = new Dictionary(); foreach (var t in all) { orig[t] = t.gameObject.layer; t.gameObject.layer = LAYER; } var mcamGO = new GameObject("WL814u_Mask"); var mcam = mcamGO.AddComponent(); mcam.CopyFrom(cam); mcam.cullingMask = 1 << LAYER; mcam.clearFlags = CameraClearFlags.SolidColor; mcam.aspect = (float)W / H; var md = mcam.GetUniversalAdditionalCameraData(); if (md != null) { md.renderPostProcessing = false; md.SetRenderer(0); } Func mask = sel => { foreach (var r in smrs) r.enabled = sel.Contains(r); mcam.backgroundColor = Color.black; var k1 = Render(mcam, W, H); mcam.backgroundColor = Color.white; var k2 = Render(mcam, W, H); var m = new bool[W * H]; for (int i = 0; i < m.Length; i++) { int d = Mathf.Max(Mathf.Abs(k1.px[i].r - k2.px[i].r), Mathf.Max(Mathf.Abs(k1.px[i].g - k2.px[i].g), Mathf.Abs(k1.px[i].b - k2.px[i].b))); m[i] = d < 128; } foreach (var r in smrs) r.enabled = true; return m; }; var mFaceBefore = mask(new[] { faceSMR }); faceSMR.sharedMaterial = fm; hairSMR.sharedMaterial = hm; var mFaceAfter = mask(new[] { faceSMR }); faceSMR.sharedMaterial = faceOrig; hairSMR.sharedMaterial = hairOrig; var mAll = mask(smrs); var mHair = mask(new[] { hairSMR }); L(""); L("== 읽힘 실측 (얼굴 데칼 · 화면 1080×1920 · ortho 10 · 카메라 정면) =="); Report("현재 face02", before, mFaceBefore); Report("도트 face02_Pixel", after, mFaceAfter); // 얼굴 데칼 bbox → 눈 1개 환산 (텍스처 그려진 영역 889×617 텍셀 기준) foreach (var kv in new[] { new KeyValuePair("현재", mFaceBefore), new KeyValuePair("도트", mFaceAfter) }) { int fx0 = W, fx1 = -1, fy0 = H, fy1 = -1; for (int i = 0; i < kv.Value.Length; i++) if (kv.Value[i]) { int x = i % W, y = i / W; if (x < fx0) fx0 = x; if (x > fx1) fx1 = x; if (y < fy0) fy0 = y; if (y > fy1) fy1 = y; } if (fx1 < 0) continue; float sx = (fx1 - fx0 + 1) / 889f, sy = (fy1 - fy0 + 1) / 617f; L(string.Format(" {0} 얼굴 데칼 {1}×{2} px → 눈 1개 {3:F2}×{4:F2} px · 눈썹 {5:F2}×{6:F2} · 입 {7:F2}×{8:F2}", kv.Key, fx1 - fx0 + 1, fy1 - fy0 + 1, 277 * sx, 183 * sy, 165 * sx, 107 * sy, 67 * sx, 14 * sy)); } L(""); L("== 부위 색 수 (캐릭터 전체 / 머리칼) =="); Report("현재 전체", before, mAll); Report("도트 전체", after, mAll); Report("현재 머리칼", before, mHair); Report("도트 머리칼", after, mHair); foreach (var kv in orig) if (kv.Key != null) kv.Key.gameObject.layer = kv.Value; UnityEngine.Object.DestroyImmediate(mcamGO); UnityEngine.Object.DestroyImmediate(go2); UnityEngine.Object.DestroyImmediate(fm); UnityEngine.Object.DestroyImmediate(hm); } static void Report(string tag, Shot s, bool[] m) { var set = new HashSet(); int n = 0; float lmin = 999, lmax = -1; double lsum = 0; int x0 = W, x1 = -1, y0 = H, y1 = -1; for (int i = 0; i < m.Length; i++) { if (!m[i]) continue; n++; var c = s.px[i]; set.Add((c.r << 16) | (c.g << 8) | c.b); float l = 0.299f * c.r + 0.587f * c.g + 0.114f * c.b; lsum += l; if (l < lmin) lmin = l; if (l > lmax) lmax = l; int x = i % W, y = i / W; if (x < x0) x0 = x; if (x > x1) x1 = x; if (y < y0) y0 = y; if (y > y1) y1 = y; } if (n == 0) { L(" " + tag + ": 0 px"); return; } L(string.Format(" {0}: {1} px ({2}×{3}) · 고유색 {4} ({5:F0} % 가 서로 다른 색) · 밝기 {6:F0}~{7:F0}(폭 {8:F0}) 평균 {9:F0}", tag, n, x1 - x0 + 1, y1 - y0 + 1, set.Count, 100f * set.Count / n, lmin, lmax, lmax - lmin, lsum / n)); } }