Project_WL/AgentScripts/WL814v_Capture.cs

299 lines
18 KiB
C#
Raw 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.

// WL814v_Capture.cs — 안별 임포트 설정 + 화면 실측 + 비교 캡처 (에디터 전용)
// 🔴 씬 저장 0 · 머티리얼/프리팹/URP 에셋 수정 0 (씬 인스턴스에 임시 Material 만 꽂았다 버린다) · ProjectSettings 0 · 원본 png 수정 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 WL814v_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/WL814v/";
const string OUT = "AgentScripts/WL814v_CAPTURE.txt";
const int LAYER = 31;
const int W = 1080, H = 1920;
static readonly string[] VS = { "V1", "V2", "V3", "V4" };
static readonly StringBuilder sb = new StringBuilder();
static void L(string s) { sb.AppendLine(s); Console.WriteLine("[814vC] " + s); }
public static void Import()
{
try
{
foreach (var v in VS)
foreach (var f in new[] { "face02_" + v + ".png", "Hair05_" + v + ".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/40 축소 → 밉 없으면 심하게 반짝인다
ti.filterMode = FilterMode.Point; // 도트: 보간 금지
ti.wrapMode = TextureWrapMode.Clamp;
ti.sRGBTexture = true;
ti.maxTextureSize = 512; // 원본 face02_WL/Hair05_WL 의 실효 크기와 동일 조건
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 { Directory.CreateDirectory(Path.GetDirectoryName(OUT)); File.WriteAllText(OUT, sb.ToString(), new UTF8Encoding(true)); }
}
class Shot { public Color32[] px; public int w, h; }
static Camera cam;
static Shot Render(Color bg)
{
var rt = new RenderTexture(W, H, 24, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB);
rt.antiAliasing = 1; rt.filterMode = FilterMode.Point;
var pA = RenderTexture.active; var pC = cam.clearFlags; var pB = cam.backgroundColor;
cam.clearFlags = CameraClearFlags.SolidColor; cam.backgroundColor = bg; 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 = null; RenderTexture.active = pA; cam.clearFlags = pC; cam.backgroundColor = pB;
var s = new Shot { px = t.GetPixels32(), w = W, h = H };
UnityEngine.Object.DestroyImmediate(t); rt.Release(); UnityEngine.Object.DestroyImmediate(rt);
return s;
}
static bool[] MaskOf(SkinnedMeshRenderer[] all, SkinnedMeshRenderer[] on, out int cnt, out RectInt bb)
{
foreach (var r in all) r.enabled = on.Contains(r);
var b = Render(Color.black); var w = Render(Color.white);
var m = new bool[W * H]; cnt = 0; int x0 = W, y0 = H, x1 = -1, y1 = -1;
for (int i = 0; i < m.Length; i++)
{
int d = Mathf.Max(Mathf.Abs(b.px[i].r - w.px[i].r), Mathf.Max(Mathf.Abs(b.px[i].g - w.px[i].g), Mathf.Abs(b.px[i].b - w.px[i].b)));
if (d < 128) { m[i] = true; cnt++; 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; }
}
foreach (var r in all) r.enabled = true;
bb = (x1 < 0) ? new RectInt(0, 0, 0, 0) : new RectInt(x0, y0, x1 - x0 + 1, y1 - y0 + 1);
return m;
}
static float Lum(Color32 c) { return 0.299f * c.r + 0.587f * c.g + 0.114f * c.b; }
static void Hsv(Color32 c, out float h, out float s, out float v)
{
float r = c.r, g = c.g, b = c.b; float mx = Mathf.Max(r, Mathf.Max(g, b)), mn = Mathf.Min(r, Mathf.Min(g, b)); float d = mx - mn;
h = 0; if (d > 1e-6f) { if (mx == r) h = ((g - b) / d + 6f) % 6f; else if (mx == g) h = (b - r) / d + 2f; else h = (r - g) / d + 4f; }
h *= 60f; s = mx > 1e-6f ? d / mx : 0f; v = mx;
}
static float CMeanHue(List<float> hs, List<float> ws)
{
double x = 0, y = 0;
for (int i = 0; i < hs.Count; i++) { x += ws[i] * Math.Cos(hs[i] * Math.PI / 180.0); y += ws[i] * Math.Sin(hs[i] * Math.PI / 180.0); }
return (float)(((Math.Atan2(y, x) * 180.0 / Math.PI) + 360.0) % 360.0);
}
class Met { public int px, colors; public float ratio, lmin, lmax, hue, darkPx, eyeW, eyeH; }
static Met Measure(Shot s, bool[] mask, RectInt bb, float skinL)
{
var m = new Met(); var set = new HashSet<int>(); var hs = new List<float>(); var ws = new List<float>();
m.lmin = 999; m.lmax = -1;
int dx0 = W, dy0 = H, dx1 = -1, dy1 = -1; int half = bb.x + bb.width / 2;
for (int i = 0; i < mask.Length; i++)
{
if (!mask[i]) continue;
var c = s.px[i]; m.px++;
set.Add((c.r << 16) | (c.g << 8) | c.b);
float l = Lum(c); if (l < m.lmin) m.lmin = l; if (l > m.lmax) m.lmax = l;
float h, sa, v; Hsv(c, out h, out sa, out v);
if (sa > 0.08f) { hs.Add(h); ws.Add(sa); }
if (skinL > 0 && l < skinL * 0.75f)
{
m.darkPx++;
int x = i % W, y = i / W;
if (x < half) { if (x < dx0) dx0 = x; if (x > dx1) dx1 = x; if (y < dy0) dy0 = y; if (y > dy1) dy1 = y; }
}
}
m.colors = set.Count; m.ratio = m.px > 0 ? 100f * set.Count / m.px : 0;
m.hue = hs.Count > 0 ? CMeanHue(hs, ws) : -1;
m.eyeW = dx1 >= dx0 ? dx1 - dx0 + 1 : 0; m.eyeH = dy1 >= dy0 ? dy1 - dy0 + 1 : 0;
return m;
}
static Shot Crop(Shot s, RectInt r, int zoom)
{
var o = new Shot { w = r.width * zoom, h = r.height * zoom, px = new Color32[r.width * zoom * r.height * zoom] };
for (int y = 0; y < o.h; y++)
for (int x = 0; x < o.w; x++)
{
int sx = r.x + x / zoom, sy = r.y + y / zoom;
o.px[y * o.w + x] = (sx >= 0 && sy >= 0 && sx < s.w && sy < s.h) ? s.px[sy * s.w + sx] : new Color32(24, 24, 28, 255);
}
return o;
}
static Shot HStack(List<Shot> a, int gap)
{
int h = a.Max(s => s.h), w = a.Sum(s => s.w) + gap * (a.Count - 1);
var o = new Shot { w = w, h = h, px = new Color32[w * h] };
for (int i = 0; i < o.px.Length; i++) o.px[i] = new Color32(24, 24, 28, 255);
int xo = 0;
foreach (var s in a) { for (int y = 0; y < s.h; y++) for (int x = 0; x < s.w; x++) o.px[y * w + xo + x] = s.px[y * s.w + x]; xo += s.w + gap; }
return o;
}
static Shot VStack(List<Shot> a, int gap)
{
int w = a.Max(s => s.w), h = a.Sum(s => s.h) + gap * (a.Count - 1);
var o = new Shot { w = w, h = h, px = new Color32[w * h] };
for (int i = 0; i < o.px.Length; i++) o.px[i] = new Color32(24, 24, 28, 255);
int yo = 0;
for (int k = a.Count - 1; k >= 0; k--) { var s = a[k]; for (int y = 0; y < s.h; y++) for (int x = 0; x < s.w; x++) o.px[(yo + y) * w + x] = s.px[y * s.w + x]; yo += s.h + gap; }
return o;
}
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 + ")");
}
public static void Capture()
{
try { Body(); }
catch (Exception e) { L("EXCEPTION " + e); }
finally { Directory.CreateDirectory(Path.GetDirectoryName(OUT)); File.WriteAllText(OUT, sb.ToString(), new UTF8Encoding(true)); }
}
static void Body()
{
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 headSMR = smrs.First(r => r.name == "Head");
var scam = Camera.main ?? UnityEngine.Object.FindObjectsByType<Camera>(FindObjectsInactive.Exclude, FindObjectsSortMode.None).First(c => c.orthographic);
float camYaw = scam.transform.eulerAngles.y;
root.transform.rotation = Quaternion.Euler(0, camYaw + 180f, 0);
L(string.Format("포즈 = 카메라 정면 yaw {0:F0}° · ortho 10 · {1}×{2} (814u 동일)", camYaw + 180f, W, H));
var all = root.GetComponentsInChildren<Transform>(true);
var origLayer = new Dictionary<Transform, int>(); foreach (var t in all) { origLayer[t] = t.gameObject.layer; t.gameObject.layer = LAYER; }
var b = new Bounds(root.transform.position, Vector3.zero); foreach (var r in smrs) b.Encapsulate(r.bounds);
var go2 = new GameObject("WL814v_Cap");
cam = go2.AddComponent<Camera>(); cam.CopyFrom(scam);
cam.orthographic = true; cam.orthographicSize = 10f; cam.aspect = (float)W / H; cam.targetTexture = null; cam.cullingMask = 1 << LAYER;
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); }
for (int i = 0; i < 3; i++) Render(Color.black); // 배치 워밍업
var faceOrig = faceSMR.sharedMaterial; var hairOrig = hairSMR.sharedMaterial;
try
{
int cF, cH, cHead, cAll; RectInt bbF, bbH, bbHead, bbAll;
var mFace = MaskOf(smrs, new[] { faceSMR }, out cF, out bbF);
var mHair = MaskOf(smrs, new[] { hairSMR }, out cH, out bbH);
var mHead = MaskOf(smrs, new[] { headSMR }, out cHead, out bbHead);
var mAll = MaskOf(smrs, smrs, out cAll, out bbAll);
L(string.Format("마스크: 얼굴데칼 {0} px {1} · 머리칼 {2} px · 머리(피부) {3} px · 전체 {4} px {5}", cF, bbF, cH, cHead, cAll, bbAll));
var baseShot = Render(new Color(0.10f, 0.10f, 0.12f));
// 피부 밝기 기준 = Head 마스크의 평균 L
float skinL = 0; int nh = 0;
for (int i = 0; i < mHead.Length; i++) if (mHead[i] && !mFace[i]) { skinL += Lum(baseShot.px[i]); nh++; }
skinL = nh > 0 ? skinL / nh : 128f;
L(string.Format("피부(Head) 평균 밝기 L = {0:F0} → 「진하게 읽히는 점」 문턱 = L < {1:F0}", skinL, skinL * 0.75f));
var m0f = Measure(baseShot, mFace, bbF, skinL);
var m0h = Measure(baseShot, mHair, bbH, 0);
L("");
L("== 얼굴 안별 화면 실측 (얼굴 데칼 " + cF + " px) ==");
L(string.Format(" {0,-8} 고유색 {1,4} · 서로다른색 {2,5:F0} % · 밝기 {3,3:F0}~{4,3:F0}(폭 {5,3:F0}) · Hue {6,5:F1}° · ΔHue {7,6} · 진한점 {8,3:F0} px · 눈1개 진한부분 {9}×{10} px",
"원본", m0f.colors, m0f.ratio, m0f.lmin, m0f.lmax, m0f.lmax - m0f.lmin, m0f.hue, "-", m0f.darkPx, m0f.eyeW, m0f.eyeH));
var faceShots = new List<Shot>(); faceShots.Add(baseShot);
foreach (var v in VS)
{
var tx = AssetDatabase.LoadAssetAtPath<Texture2D>(TEX + "face02_" + v + ".png");
if (tx == null) { L(" !! 없음 face02_" + v); continue; }
var fm = new Material(faceOrig) { name = "TMP_face_" + v };
if (fm.HasProperty("_MainTex")) fm.SetTexture("_MainTex", tx);
if (fm.HasProperty("_BaseMap")) fm.SetTexture("_BaseMap", tx);
if (fm.HasProperty("_ShadowBaseMap")) fm.SetTexture("_ShadowBaseMap", tx);
faceSMR.sharedMaterial = fm;
var s = Render(new Color(0.10f, 0.10f, 0.12f)); faceShots.Add(s);
var m = Measure(s, mFace, bbF, skinL);
L(string.Format(" {0,-8} 고유색 {1,4} · 서로다른색 {2,5:F0} % · 밝기 {3,3:F0}~{4,3:F0}(폭 {5,3:F0}) · Hue {6,5:F1}° · ΔHue {7,6:+0.0;-0.0} · 진한점 {8,3:F0} px · 눈1개 진한부분 {9}×{10} px",
"face " + v, m.colors, m.ratio, m.lmin, m.lmax, m.lmax - m.lmin, m.hue, Mathf.DeltaAngle(m0f.hue, m.hue), m.darkPx, m.eyeW, m.eyeH));
faceSMR.sharedMaterial = faceOrig;
}
L("");
L("== 머리칼 안별 화면 실측 (머리칼 " + cH + " px) ==");
L(string.Format(" {0,-8} 고유색 {1,4} · 서로다른색 {2,5:F0} % · 밝기 {3,3:F0}~{4,3:F0}(폭 {5,3:F0}) · Hue {6,5:F1}° · ΔHue {7,6}",
"원본", m0h.colors, m0h.ratio, m0h.lmin, m0h.lmax, m0h.lmax - m0h.lmin, m0h.hue, "-"));
var hairShots = new List<Shot>(); hairShots.Add(baseShot);
foreach (var v in VS)
{
var tx = AssetDatabase.LoadAssetAtPath<Texture2D>(TEX + "Hair05_" + v + ".png");
if (tx == null) { L(" !! 없음 Hair05_" + v); continue; }
var hm = new Material(hairOrig) { name = "TMP_hair_" + v };
if (hm.HasProperty("_MainTex")) hm.SetTexture("_MainTex", tx);
if (hm.HasProperty("_BaseMap")) hm.SetTexture("_BaseMap", tx);
if (hm.HasProperty("_ShadowBaseMap")) hm.SetTexture("_ShadowBaseMap", tx);
hairSMR.sharedMaterial = hm;
var s = Render(new Color(0.10f, 0.10f, 0.12f)); hairShots.Add(s);
var m = Measure(s, mHair, bbH, 0);
L(string.Format(" {0,-8} 고유색 {1,4} · 서로다른색 {2,5:F0} % · 밝기 {3,3:F0}~{4,3:F0}(폭 {5,3:F0}) · Hue {6,5:F1}° · ΔHue {7,6:+0.0;-0.0}",
"hair " + v, m.colors, m.ratio, m.lmin, m.lmax, m.lmax - m.lmin, m.hue, Mathf.DeltaAngle(m0h.hue, m.hue)));
hairSMR.sharedMaterial = hairOrig;
}
// ── 캡처 ──
var headCrop = new RectInt(bbAll.x - 4, bbAll.y + bbAll.height - 58, bbAll.width + 8, 58);
L("");
Save(VStack(new List<Shot> {
HStack(faceShots.Select(s => Crop(s, headCrop, 1)).ToList(), 4),
HStack(faceShots.Select(s => Crop(s, headCrop, 12)).ToList(), 12) }, 16), "v_face_grid.png");
Save(VStack(new List<Shot> {
HStack(hairShots.Select(s => Crop(s, headCrop, 1)).ToList(), 4),
HStack(hairShots.Select(s => Crop(s, headCrop, 4)).ToList(), 8) }, 16), "v_hair_grid.png");
// 최적 조합 = BEST_FACE / BEST_HAIR (인자로 바꿀 수 있게 상수)
{
var ftx = AssetDatabase.LoadAssetAtPath<Texture2D>(TEX + "face02_" + BEST_FACE + ".png");
var htx = AssetDatabase.LoadAssetAtPath<Texture2D>(TEX + "Hair05_" + BEST_HAIR + ".png");
var fm = new Material(faceOrig) { name = "TMP_bestF" }; var hm = new Material(hairOrig) { name = "TMP_bestH" };
foreach (var pn in new[] { "_MainTex", "_BaseMap", "_ShadowBaseMap" }) { if (fm.HasProperty(pn)) fm.SetTexture(pn, ftx); if (hm.HasProperty(pn)) hm.SetTexture(pn, htx); }
faceSMR.sharedMaterial = fm; hairSMR.sharedMaterial = hm;
var best = Render(new Color(0.10f, 0.10f, 0.12f));
var full = new RectInt(bbAll.x - 6, bbAll.y - 6, bbAll.width + 12, bbAll.height + 12);
Save(VStack(new List<Shot> {
HStack(new List<Shot>{ Crop(baseShot, full, 1), Crop(best, full, 1) }, 6),
HStack(new List<Shot>{ Crop(baseShot, full, 6), Crop(best, full, 6) }, 12) }, 16), "v_best_full.png");
var mf = Measure(best, mFace, bbF, skinL); var mh = Measure(best, mHair, bbH, 0); var ma = Measure(best, mAll, bbAll, 0);
var ma0 = Measure(baseShot, mAll, bbAll, 0);
L("");
L(string.Format("== 추천 조합 얼굴 {0} + 머리 {1} ==", BEST_FACE, BEST_HAIR));
L(string.Format(" 캐릭터 전체: 고유색 {0} → {1} · 서로다른색 {2:F0} % → {3:F0} %", ma0.colors, ma.colors, ma0.ratio, ma.ratio));
L(string.Format(" 얼굴 진한점 {0:F0} → {1:F0} px · 눈1개 {2}×{3} px · 대비폭 {4:F0} → {5:F0}", m0f.darkPx, mf.darkPx, mf.eyeW, mf.eyeH, m0f.lmax - m0f.lmin, mf.lmax - mf.lmin));
L(string.Format(" 머리칼 고유색 {0} → {1} · ΔHue {2:+0.0;-0.0}°", m0h.colors, mh.colors, Mathf.DeltaAngle(m0h.hue, mh.hue)));
faceSMR.sharedMaterial = faceOrig; hairSMR.sharedMaterial = hairOrig;
}
}
finally
{
faceSMR.sharedMaterial = faceOrig; hairSMR.sharedMaterial = hairOrig;
foreach (var kv in origLayer) if (kv.Key != null) kv.Key.gameObject.layer = kv.Value;
UnityEngine.Object.DestroyImmediate(go2);
}
}
public static string BEST_FACE = "V1", BEST_HAIR = "V1";
}