Project_WL/AgentScripts/WL814u_Capture.cs

243 lines
14 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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<Texture2D>(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<SkinnedMeshRenderer>(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<Camera>(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<Camera>(); 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<Texture2D>(TEX + "face02_Pixel.png");
var hairPx = AssetDatabase.LoadAssetAtPath<Texture2D>(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<Transform>(true);
var orig = new Dictionary<Transform, int>(); foreach (var t in all) { orig[t] = t.gameObject.layer; t.gameObject.layer = LAYER; }
var mcamGO = new GameObject("WL814u_Mask"); var mcam = mcamGO.AddComponent<Camera>();
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<SkinnedMeshRenderer[], bool[]> 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<string, bool[]>("현재", mFaceBefore), new KeyValuePair<string, bool[]>("도트", 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>(); 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));
}
}