2026-09-12 15:31:06 +00:00
|
|
|
|
// 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-12 15:35:53 +00:00
|
|
|
|
|
|
|
|
|
|
// ── 올리브 해결 확인: 814u 시제품 vs 이번 안, 화면에서 직접 비교 ──
|
|
|
|
|
|
public static void Olive()
|
|
|
|
|
|
{
|
|
|
|
|
|
try { OliveBody(); }
|
|
|
|
|
|
catch (Exception e) { L("EXCEPTION " + e); }
|
|
|
|
|
|
finally { File.WriteAllText("AgentScripts/WL814v_OLIVE.txt", sb.ToString(), new UTF8Encoding(true)); }
|
|
|
|
|
|
}
|
|
|
|
|
|
static void OliveBody()
|
|
|
|
|
|
{
|
|
|
|
|
|
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 hairSMR = smrs.First(r => r.name == "Hair05");
|
|
|
|
|
|
var scam = Camera.main ?? UnityEngine.Object.FindObjectsByType<Camera>(FindObjectsInactive.Exclude, FindObjectsSortMode.None).First(c => c.orthographic);
|
|
|
|
|
|
root.transform.rotation = Quaternion.Euler(0, scam.transform.eulerAngles.y + 180f, 0);
|
|
|
|
|
|
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_Olive");
|
|
|
|
|
|
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 hairOrig = hairSMR.sharedMaterial;
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
int cH; RectInt bbH; var mHair = MaskOf(smrs, new[] { hairSMR }, out cH, out bbH);
|
|
|
|
|
|
L(""); L("== 🔴 올리브 해결 확인 (머리칼 " + cH + " px · 같은 프레임·포즈) ==");
|
|
|
|
|
|
var shots = new List<Shot>(); var names = new List<string> { "원본", "814u 시제품", "814v V1", "814v V2", "814v V3", "814v V4" };
|
|
|
|
|
|
var files = new List<string> { null, "Hair05_Pixel", "Hair05_V1", "Hair05_V2", "Hair05_V3", "Hair05_V4" };
|
|
|
|
|
|
float h0 = 0;
|
|
|
|
|
|
for (int k = 0; k < files.Count; k++)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (files[k] != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var tx = AssetDatabase.LoadAssetAtPath<Texture2D>(TEX + files[k] + ".png");
|
|
|
|
|
|
var hm = new Material(hairOrig) { name = "TMP_" + files[k] };
|
|
|
|
|
|
foreach (var pn in new[] { "_MainTex", "_BaseMap", "_ShadowBaseMap" }) if (hm.HasProperty(pn)) hm.SetTexture(pn, tx);
|
|
|
|
|
|
hairSMR.sharedMaterial = hm;
|
|
|
|
|
|
}
|
|
|
|
|
|
var s = Render(new Color(0.10f, 0.10f, 0.12f)); shots.Add(s);
|
|
|
|
|
|
var hs = new List<float>(); var ws = new List<float>(); int olive = 0, oliveS = 0, n = 0;
|
|
|
|
|
|
for (int i = 0; i < mHair.Length; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!mHair[i]) continue;
|
|
|
|
|
|
float h, sa, v; Hsv(s.px[i], out h, out sa, out v);
|
|
|
|
|
|
n++;
|
|
|
|
|
|
if (sa > 0.08f) { hs.Add(h); ws.Add(sa); if (h >= 45f && h <= 110f) { olive++; if (sa > 0.25f && v < 210f) oliveS++; } }
|
|
|
|
|
|
}
|
|
|
|
|
|
float hm2 = CMeanHue(hs, ws); if (k == 0) h0 = hm2;
|
|
|
|
|
|
L(string.Format(" {0,-12} Hue {1,5:F1}° · ΔHue {2,6:+0.0;-0.0;0.0}° · 🔴 짙은 올리브(H45~110°·S>0.25·V<210) {3,4} px = {4,5:F1} % · (넓은 기준 H45~110° {5,5:F1} %)",
|
|
|
|
|
|
names[k], hm2, Mathf.DeltaAngle(h0, hm2), oliveS, 100f * oliveS / Mathf.Max(n, 1), 100f * olive / Mathf.Max(n, 1)));
|
|
|
|
|
|
hairSMR.sharedMaterial = hairOrig;
|
|
|
|
|
|
}
|
|
|
|
|
|
var crop = new RectInt(bbH.x - 3, bbH.y - 3, bbH.width + 6, bbH.height + 6);
|
|
|
|
|
|
Save(HStack(shots.Select(s => Crop(s, crop, 4)).ToList(), 8), "v_olive_fix.png");
|
|
|
|
|
|
}
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
hairSMR.sharedMaterial = hairOrig;
|
|
|
|
|
|
foreach (var kv in origLayer) if (kv.Key != null) kv.Key.gameObject.layer = kv.Value;
|
|
|
|
|
|
UnityEngine.Object.DestroyImmediate(go2);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-12 15:31:06 +00:00
|
|
|
|
public static string BEST_FACE = "V1", BEST_HAIR = "V1";
|
|
|
|
|
|
}
|