243 lines
14 KiB
C#
243 lines
14 KiB
C#
// 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));
|
||
}
|
||
}
|