[WL-814x] 팔레트 아틀라스 UV R&D 하니스 + 진단(색의 출처 실측) + 1~2회차 (#814)
- 진단: 캐릭터 색 2,676(89.3%)의 출처를 실측으로 분해 · 텍스처 = 주범(단색 알베도로 바꾸면 2,676 -> 334) · 림라이트 = 2차 주범(98 -> 1,282) · 무드(Flat ambient) = 색을 줄인다(311 -> 85) · 점광/그림자/AA = 기여 0 - 팔레트 아틀라스 UV 굽기(WLXBake): 면 평균색 -> 덩어리 병합(Ward) -> 팔레트 k-means(Lab, Hue 복원) -> UV 를 칸 중심으로 - 1회차 덩어리 20/40/80 x 팔레트 16/24/32: 서로 다른 색 89.3% -> 4.0~5.1% - 2회차 얼굴 점텍스처(1텍셀=1화면px): 고유색 126 -> 55(shades3) / 41(shades2) Co-Authored-By: Claude Opus 5 <noreply\@anthropic.com>
This commit is contained in:
parent
2b1c8aeb20
commit
d285e7edea
|
|
@ -0,0 +1,19 @@
|
|||
setup: char bounds (0.648, 1.217, 0.907) · 화면 키 117 px · 포즈 yaw 240
|
||||
SO: enabled_=1 mode=3
|
||||
ApplyMode(3): mode 3 · 렌더러 6 · 슬롯 6
|
||||
slot Arm_m05 = M05_ToonF
|
||||
slot Body_m05 = M05_ToonF
|
||||
slot Face = face02_ToonF
|
||||
slot Hair05 = Hair05_ToonF
|
||||
slot Head = Skin_ToonF
|
||||
slot Leg_M05 = M05_ToonF
|
||||
mask Arm_m05 = 335 px
|
||||
mask Body_m05 = 975 px
|
||||
mask Face = 47 px
|
||||
mask Hair05 = 1154 px
|
||||
mask Head = 1047 px
|
||||
mask Leg_M05 = 729 px
|
||||
mask all = 2996 px
|
||||
mask body3 = 1756 px
|
||||
c0 Ⓐ136 Ⓑ12mat/md0.12 Ⓒ0.55 Ⓓ5
|
||||
DONE
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
setup: bounds (0.648, 1.217, 0.907) / screen H 117 px / pose yaw 240
|
||||
mask Arm_m05 = 335 px
|
||||
mask Body_m05 = 975 px
|
||||
mask Face = 50 px
|
||||
mask Hair05 = 1154 px
|
||||
mask Head = 1047 px
|
||||
mask Leg_M05 = 729 px
|
||||
mask all = 2996 px
|
||||
mask body3 = 1756 px
|
||||
c20p16: regions 21 pal 16 dHue 1.06/4.58 verts 2520->2573
|
||||
c20p24: regions 21 pal 21 dHue 1.06/4.58 verts 2520->2573
|
||||
c20p32: regions 21 pal 21 dHue 1.06/4.58 verts 2520->2573
|
||||
c40p16: regions 40 pal 16 dHue 1.06/5.15 verts 2520->2733
|
||||
c40p24: regions 40 pal 24 dHue 0.53/3.58 verts 2520->2781
|
||||
c40p32: regions 40 pal 32 dHue 0.50/2.37 verts 2520->2781
|
||||
c80p16: regions 80 pal 16 dHue 1.16/16.86 verts 2520->3145
|
||||
c80p24: regions 80 pal 24 dHue 0.67/16.86 verts 2520->3163
|
||||
c80p32: regions 80 pal 32 dHue 0.54/16.86 verts 2520->3164
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
setup: bounds (0.648, 1.217, 0.907) / screen H 117 px / pose yaw 240
|
||||
face P32=True P64=True V1=True hair P64=True
|
||||
best: regions 40 pal 16 dHue 1.06/5.15 verts 2520->2733
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
=== LH_M05.prefab 렌더러 ===
|
||||
[R] Arm_m05 type=SkinnedMeshRenderer enabled=True activeSelf=True
|
||||
mesh=Arm_m05 path=Assets/Suriyun/Characters/RedKnight/FBX/Characters/M05.fbx verts=468 sub=1 tris=752 blendshapes=0 bones=63 bindposes=63 isReadable=True
|
||||
uv0 range x[0.395,0.661] y[0.650,0.974] uv2=False colors=False tangents=True normals=True
|
||||
mat[0] M05_ToonS shader=Shader Graphs/Toon path=Assets/WL/Look/Character/Materials/M05_ToonS.mat base=Assets/WL/Look/Character/Textures/M05_WL.png shadowbase=Assets/WL/Look/Character/Textures/M05_WL.png
|
||||
[R] Body_m05 type=SkinnedMeshRenderer enabled=True activeSelf=True
|
||||
mesh=Body_m05 path=Assets/Suriyun/Characters/RedKnight/FBX/Characters/M05.fbx verts=604 sub=1 tris=984 blendshapes=0 bones=40 bindposes=40 isReadable=True
|
||||
uv0 range x[0.002,0.998] y[0.002,0.919] uv2=False colors=False tangents=True normals=True
|
||||
mat[0] M05_ToonS shader=Shader Graphs/Toon path=Assets/WL/Look/Character/Materials/M05_ToonS.mat base=Assets/WL/Look/Character/Textures/M05_WL.png shadowbase=Assets/WL/Look/Character/Textures/M05_WL.png
|
||||
[R] Face type=SkinnedMeshRenderer enabled=True activeSelf=True
|
||||
mesh=Face path=Assets/Suriyun/Characters/RedKnight/FBX/Characters/M05.fbx verts=121 sub=1 tris=212 blendshapes=0 bones=1 bindposes=1 isReadable=True
|
||||
uv0 range x[0.030,0.970] y[0.273,0.766] uv2=False colors=False tangents=True normals=True
|
||||
mat[0] face02_ToonS shader=Unlit/Transparent path=Assets/WL/Look/Character/Materials/face02_ToonS.mat base=Assets/WL/Look/Character/Textures/face02_WL.png shadowbase=-
|
||||
[R] Hair05 type=SkinnedMeshRenderer enabled=True activeSelf=True
|
||||
mesh=Hair05 path=Assets/Suriyun/Characters/RedKnight/FBX/Characters/M05.fbx verts=611 sub=1 tris=914 blendshapes=0 bones=1 bindposes=1 isReadable=True
|
||||
uv0 range x[0.002,0.998] y[0.002,0.995] uv2=False colors=False tangents=True normals=True
|
||||
mat[0] Hair05_ToonS shader=Shader Graphs/Toon path=Assets/WL/Look/Character/Materials/Hair05_ToonS.mat base=Assets/WL/Look/Character/Textures/Hair05_WL.png shadowbase=Assets/WL/Look/Character/Textures/Hair05_WL.png
|
||||
[R] Head type=SkinnedMeshRenderer enabled=True activeSelf=True
|
||||
mesh=Head path=Assets/Suriyun/Characters/RedKnight/FBX/Characters/M05.fbx verts=339 sub=1 tris=582 blendshapes=0 bones=1 bindposes=1 isReadable=True
|
||||
uv0 range x[0.643,0.998] y[0.057,0.603] uv2=False colors=False tangents=True normals=True
|
||||
mat[0] Skin_ToonS shader=Shader Graphs/Toon path=Assets/WL/Look/Character/Materials/Skin_ToonS.mat base=Assets/WL/Look/Character/Textures/Skin_WL.png shadowbase=Assets/WL/Look/Character/Textures/Skin_WL.png
|
||||
[R] Leg_M05 type=SkinnedMeshRenderer enabled=True activeSelf=True
|
||||
mesh=Leg_M05 path=Assets/Suriyun/Characters/RedKnight/FBX/Characters/M05.fbx verts=498 sub=1 tris=844 blendshapes=0 bones=29 bindposes=29 isReadable=True
|
||||
uv0 range x[0.002,0.514] y[0.400,0.816] uv2=False colors=False tangents=True normals=True
|
||||
mat[0] M05_ToonS shader=Shader Graphs/Toon path=Assets/WL/Look/Character/Materials/M05_ToonS.mat base=Assets/WL/Look/Character/Textures/M05_WL.png shadowbase=Assets/WL/Look/Character/Textures/M05_WL.png
|
||||
[R] Sword_M05 type=MeshRenderer enabled=True activeSelf=False
|
||||
mesh=Sword_M05 path=Assets/Suriyun/Characters/RedKnight/FBX/Weapon/Sword_M05.fbx verts=188 sub=1 tris=140 blendshapes=0 bones=0 bindposes=0 isReadable=True
|
||||
uv0 range x[0.009,0.423] y[0.006,0.988] uv2=False colors=False tangents=True normals=True
|
||||
mat[0] SwordM05 shader=Toon/Toon path=Assets/Suriyun/Characters/RedKnight/Materials/Weapon/SwordM05.mat base=- shadowbase=-
|
||||
|
||||
=== 원본 텍스처 임포트 ===
|
||||
Assets/WL/Look/Character/Textures/M05_WL.png : maxSize=512 filter=Bilinear mip=True sRGB=True readable=True npot=ToNearest actual=512x512 fmt=ASTC_6x6
|
||||
Assets/WL/Look/Character/Textures/Skin_WL.png : maxSize=512 filter=Bilinear mip=False sRGB=True readable=True npot=None actual=512x512 fmt=ASTC_6x6
|
||||
Assets/WL/Look/Character/Textures/Hair05_WL.png : maxSize=512 filter=Bilinear mip=True sRGB=True readable=True npot=ToNearest actual=512x512 fmt=ASTC_6x6
|
||||
Assets/WL/Look/Character/Textures/face02_WL.png : maxSize=512 filter=Bilinear mip=True sRGB=True readable=True npot=ToNearest actual=512x512 fmt=ASTC_6x6
|
||||
Assets/WL/Look/Character/Textures/face02_V1.png : maxSize=512 filter=Point mip=True sRGB=True readable=True npot=None actual=512x512 fmt=RGBA32
|
||||
Assets/WL/Look/Character/Textures/Hair05_V1.png : maxSize=512 filter=Point mip=True sRGB=True readable=True npot=None actual=512x512 fmt=RGB24
|
||||
|
||||
=== SO 현재값 ===
|
||||
enabled_=1 mode=3 n=4
|
||||
[0] orig=M05 m1=M05_Toon m2=M05_ToonS m3=M05_ToonF
|
||||
[1] orig=skin m1=Skin_Toon m2=Skin_ToonS m3=Skin_ToonF
|
||||
[2] orig=Hair05 m1=Hair05_Toon m2=Hair05_ToonS m3=Hair05_ToonF
|
||||
[3] orig=face02 m1=face02 m2=face02_ToonS m3=face02_ToonF
|
||||
|
||||
=== 아레나 씬 카메라/캐릭터 ===
|
||||
scene=Assets/WL/Look/Arena/Scenes/WL_ArenaProto.unity
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
// WL-814x 프로브 1 — 메시·머티리얼·텍스처 실측 (팔레트 아틀라스 UV 설계 근거)
|
||||
// run_script --file AgentScripts/WL814x_Probe.cs --entry WL814x_Probe.Run
|
||||
|
||||
public static class WL814x_Probe
|
||||
{
|
||||
const string Out = @"E:\NerdNavis\WL_wt\gameplay\AgentScripts\WL814x_PROBE.txt";
|
||||
|
||||
public static string Run()
|
||||
{
|
||||
var sb = new System.Text.StringBuilder();
|
||||
var prefab = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.GameObject>("Assets/Res_Addr/PC/LH_M05.prefab");
|
||||
if (prefab == null) return "no prefab";
|
||||
|
||||
sb.AppendLine("=== LH_M05.prefab 렌더러 ===");
|
||||
var rs = prefab.GetComponentsInChildren<UnityEngine.Renderer>(true);
|
||||
foreach (var r in rs)
|
||||
{
|
||||
var smr = r as UnityEngine.SkinnedMeshRenderer;
|
||||
var mf = r.GetComponent<UnityEngine.MeshFilter>();
|
||||
UnityEngine.Mesh m = smr != null ? smr.sharedMesh : (mf != null ? mf.sharedMesh : null);
|
||||
sb.AppendLine("[R] " + r.name + " type=" + r.GetType().Name + " enabled=" + r.enabled + " activeSelf=" + r.gameObject.activeSelf);
|
||||
if (m != null)
|
||||
{
|
||||
string path = UnityEditor.AssetDatabase.GetAssetPath(m);
|
||||
sb.AppendLine(" mesh=" + m.name + " path=" + path + " verts=" + m.vertexCount + " sub=" + m.subMeshCount
|
||||
+ " tris=" + (m.triangles.Length / 3) + " blendshapes=" + m.blendShapeCount
|
||||
+ " bones=" + (smr != null && smr.bones != null ? smr.bones.Length : 0)
|
||||
+ " bindposes=" + m.bindposes.Length + " isReadable=" + m.isReadable);
|
||||
var uv = m.uv;
|
||||
if (uv != null && uv.Length > 0)
|
||||
{
|
||||
float mnx = 9e9f, mxx = -9e9f, mny = 9e9f, mxy = -9e9f;
|
||||
for (int i = 0; i < uv.Length; i++) { if (uv[i].x < mnx) mnx = uv[i].x; if (uv[i].x > mxx) mxx = uv[i].x; if (uv[i].y < mny) mny = uv[i].y; if (uv[i].y > mxy) mxy = uv[i].y; }
|
||||
sb.AppendLine(" uv0 range x[" + mnx.ToString("F3") + "," + mxx.ToString("F3") + "] y[" + mny.ToString("F3") + "," + mxy.ToString("F3") + "]"
|
||||
+ " uv2=" + (m.uv2 != null && m.uv2.Length > 0) + " colors=" + (m.colors != null && m.colors.Length > 0)
|
||||
+ " tangents=" + (m.tangents != null && m.tangents.Length > 0) + " normals=" + (m.normals != null && m.normals.Length > 0));
|
||||
}
|
||||
}
|
||||
var mats = r.sharedMaterials;
|
||||
for (int i = 0; i < mats.Length; i++)
|
||||
{
|
||||
var mt = mats[i];
|
||||
if (mt == null) { sb.AppendLine(" mat[" + i + "] = null"); continue; }
|
||||
var tex = mt.HasProperty("_BaseMap") ? mt.GetTexture("_BaseMap") : (mt.HasProperty("_MainTex") ? mt.GetTexture("_MainTex") : null);
|
||||
var stex = mt.HasProperty("_ShadowBaseMap") ? mt.GetTexture("_ShadowBaseMap") : null;
|
||||
sb.AppendLine(" mat[" + i + "] " + mt.name + " shader=" + mt.shader.name
|
||||
+ " path=" + UnityEditor.AssetDatabase.GetAssetPath(mt)
|
||||
+ " base=" + (tex != null ? UnityEditor.AssetDatabase.GetAssetPath(tex) : "-")
|
||||
+ " shadowbase=" + (stex != null ? UnityEditor.AssetDatabase.GetAssetPath(stex) : "-"));
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("=== 원본 텍스처 임포트 ===");
|
||||
string[] texPaths = {
|
||||
"Assets/WL/Look/Character/Textures/M05_WL.png",
|
||||
"Assets/WL/Look/Character/Textures/Skin_WL.png",
|
||||
"Assets/WL/Look/Character/Textures/Hair05_WL.png",
|
||||
"Assets/WL/Look/Character/Textures/face02_WL.png",
|
||||
"Assets/WL/Look/Character/Textures/face02_V1.png",
|
||||
"Assets/WL/Look/Character/Textures/Hair05_V1.png",
|
||||
};
|
||||
foreach (var p in texPaths)
|
||||
{
|
||||
var ti = UnityEditor.AssetImporter.GetAtPath(p) as UnityEditor.TextureImporter;
|
||||
var t = UnityEditor.AssetDatabase.LoadAssetAtPath<UnityEngine.Texture2D>(p);
|
||||
if (ti == null) { sb.AppendLine(p + " : 없음"); continue; }
|
||||
sb.AppendLine(p + " : maxSize=" + ti.maxTextureSize + " filter=" + ti.filterMode + " mip=" + ti.mipmapEnabled
|
||||
+ " sRGB=" + ti.sRGBTexture + " readable=" + ti.isReadable + " npot=" + ti.npotScale
|
||||
+ " actual=" + (t != null ? t.width + "x" + t.height : "?") + " fmt=" + (t != null ? t.format.ToString() : "?"));
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("=== SO 현재값 ===");
|
||||
var so = UnityEditor.AssetDatabase.LoadAssetAtPath<WL.Look.Character.WLCharacterLookSettings>("Assets/WL/Look/Character/Resources/WL/WLCharacterLookSettings.asset");
|
||||
if (so != null)
|
||||
{
|
||||
sb.AppendLine("enabled_=" + so.enabled_ + " mode=" + so.mode + " n=" + (so.originals != null ? so.originals.Length : 0));
|
||||
for (int i = 0; i < (so.originals != null ? so.originals.Length : 0); i++)
|
||||
sb.AppendLine(" [" + i + "] orig=" + (so.originals[i] != null ? so.originals[i].name : "-")
|
||||
+ " m1=" + (so.mode1 != null && i < so.mode1.Length && so.mode1[i] != null ? so.mode1[i].name : "-")
|
||||
+ " m2=" + (so.mode2 != null && i < so.mode2.Length && so.mode2[i] != null ? so.mode2[i].name : "-")
|
||||
+ " m3=" + (so.mode3 != null && i < so.mode3.Length && so.mode3[i] != null ? so.mode3[i].name : "-"));
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("=== 아레나 씬 카메라/캐릭터 ===");
|
||||
sb.AppendLine("scene=Assets/WL/Look/Arena/Scenes/WL_ArenaProto.unity");
|
||||
|
||||
System.IO.File.WriteAllText(Out, sb.ToString());
|
||||
return "written " + Out + " (" + sb.Length + " chars)";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,783 @@
|
|||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// WL814x_Run.cs — WL-814x 팔레트 아틀라스 UV R&D 하니스 (에디터 전용)
|
||||
// WLXCap : 캡처/측정 (카메라 조건 = 814v 와 동일 · 레이어 31 단독 · 직교 10 · 1080×1920 · 포스트 off)
|
||||
// WLXBake : 메시 UV 를 팔레트 칸 중심으로 굽는다 (원본 FBX/PNG 수정 0 · 메시 복사본)
|
||||
// WLXIter : 반복 회차 드라이버
|
||||
// 🔴 씬 저장 0 · 원본 에셋 수정 0 · ProjectSettings 0
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Unity.Collections;
|
||||
using UnityEditor;
|
||||
using UnityEditor.SceneManagement;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Rendering.Universal;
|
||||
using WL.Look.Arena;
|
||||
using WL.Look.Character;
|
||||
|
||||
public static class WLXCap
|
||||
{
|
||||
public const string SCENE = "Assets/WL/Look/Arena/Scenes/WL_ArenaProto.unity";
|
||||
public const string RAW = "Screenshots_WL/WL814x/raw/";
|
||||
const int LAYER = 31;
|
||||
public const int W = 1080, H = 1920;
|
||||
|
||||
static readonly StringBuilder sb = new StringBuilder();
|
||||
public static void L(string s) { sb.AppendLine(s); }
|
||||
public static void Flush(string file) { Directory.CreateDirectory("AgentScripts"); File.WriteAllText("AgentScripts/" + file, sb.ToString(), new UTF8Encoding(true)); }
|
||||
|
||||
public static GameObject Root;
|
||||
public static SkinnedMeshRenderer[] Smrs;
|
||||
public static Camera Cam;
|
||||
static GameObject s_camGo;
|
||||
static Dictionary<Transform, int> s_layers;
|
||||
static Material[][] s_origMats;
|
||||
static Mesh[] s_origMeshes;
|
||||
|
||||
public static void Setup()
|
||||
{
|
||||
EditorSceneManager.OpenScene(SCENE, OpenSceneMode.Single);
|
||||
Root = null;
|
||||
foreach (var go in UnityEngine.SceneManagement.SceneManager.GetActiveScene().GetRootGameObjects())
|
||||
if (go.name.Contains("LH_M05")) { Root = go; break; }
|
||||
Smrs = Root.GetComponentsInChildren<SkinnedMeshRenderer>(true).Where(r => r.enabled).ToArray();
|
||||
|
||||
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); // 814u/814v 와 같은 포즈
|
||||
|
||||
s_layers = new Dictionary<Transform, int>();
|
||||
foreach (var t in Root.GetComponentsInChildren<Transform>(true)) { s_layers[t] = t.gameObject.layer; t.gameObject.layer = LAYER; }
|
||||
|
||||
s_origMats = Smrs.Select(r => r.sharedMaterials).ToArray();
|
||||
s_origMeshes = Smrs.Select(r => r.sharedMesh).ToArray();
|
||||
|
||||
var b = new Bounds(Root.transform.position, Vector3.zero);
|
||||
foreach (var r in Smrs) b.Encapsulate(r.bounds);
|
||||
|
||||
s_camGo = new GameObject("WLXCap");
|
||||
Cam = s_camGo.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);
|
||||
L("setup: bounds " + b.size.ToString("F3") + " / screen H " + (b.size.y / 20f * H).ToString("F0") + " px / pose yaw " + (camYaw + 180f).ToString("F0"));
|
||||
}
|
||||
|
||||
public static void Teardown()
|
||||
{
|
||||
WLReferenceLook.Restore();
|
||||
if (Smrs != null)
|
||||
for (int i = 0; i < Smrs.Length; i++) { Smrs[i].sharedMaterials = s_origMats[i]; Smrs[i].sharedMesh = s_origMeshes[i]; Smrs[i].enabled = true; }
|
||||
if (s_layers != null) foreach (var kv in s_layers) if (kv.Key != null) kv.Key.gameObject.layer = kv.Value;
|
||||
if (s_camGo != null) UnityEngine.Object.DestroyImmediate(s_camGo);
|
||||
s_camGo = null;
|
||||
}
|
||||
|
||||
public class Shot { public Color32[] px; public int w, h; }
|
||||
|
||||
public 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;
|
||||
}
|
||||
|
||||
public static void Save(Shot s, string path)
|
||||
{
|
||||
var t = new Texture2D(s.w, s.h, TextureFormat.RGBA32, false); t.SetPixels32(s.px); t.Apply();
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path)); File.WriteAllBytes(path, t.EncodeToPNG());
|
||||
UnityEngine.Object.DestroyImmediate(t);
|
||||
}
|
||||
|
||||
public static void Shoot(string name) { Save(Render(new Color(0.10f, 0.10f, 0.12f)), RAW + name + ".png"); }
|
||||
|
||||
public static void SaveMask(string name, params SkinnedMeshRenderer[] on)
|
||||
{
|
||||
foreach (var r in Smrs) r.enabled = on.Contains(r);
|
||||
var b = Render(Color.black); var w = Render(Color.white);
|
||||
var t = new Texture2D(W, H, TextureFormat.RGBA32, false);
|
||||
var px = new Color32[W * H]; int cnt = 0;
|
||||
for (int i = 0; i < px.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)));
|
||||
bool hit = d < 128; if (hit) cnt++;
|
||||
px[i] = hit ? new Color32(255, 255, 255, 255) : new Color32(0, 0, 0, 255);
|
||||
}
|
||||
t.SetPixels32(px); t.Apply();
|
||||
Directory.CreateDirectory(RAW); File.WriteAllBytes(RAW + "mask_" + name + ".png", t.EncodeToPNG());
|
||||
UnityEngine.Object.DestroyImmediate(t);
|
||||
foreach (var r in Smrs) r.enabled = true;
|
||||
L("mask " + name + " = " + cnt + " px");
|
||||
}
|
||||
|
||||
public static void SaveAllMasks()
|
||||
{
|
||||
WLReferenceLook.Restore();
|
||||
foreach (var r in Smrs) SaveMask(r.name, r);
|
||||
SaveMask("all", Smrs);
|
||||
SaveMask("body3", Smrs.Where(r => r.name == "Body_m05" || r.name == "Arm_m05" || r.name == "Leg_M05").ToArray());
|
||||
}
|
||||
|
||||
public static WLReferenceLook.Opt LookOpt(bool outline, bool contrast, bool mood, bool rim, int charShades)
|
||||
{
|
||||
var cfg = WLReferenceLookSettings.Instance;
|
||||
var o = WLReferenceLook.Opt.FromSettings(cfg);
|
||||
o.outline = outline; o.contrast = contrast; o.mood = mood; o.rim = rim;
|
||||
if (charShades > 0) o.charShades = charShades;
|
||||
return o;
|
||||
}
|
||||
public static void ApplyLook(WLReferenceLook.Opt o) { WLReferenceLook.Apply(o, WLReferenceLookSettings.Instance); }
|
||||
public static void NoLook() { WLReferenceLook.Restore(); }
|
||||
|
||||
public static void RestoreMats() { for (int i = 0; i < Smrs.Length; i++) Smrs[i].sharedMaterials = s_origMats[i]; }
|
||||
public static void RestoreMeshes() { for (int i = 0; i < Smrs.Length; i++) Smrs[i].sharedMesh = s_origMeshes[i]; }
|
||||
public static Material[] OrigMats(int i) { return s_origMats[i]; }
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// WLXBake — 팔레트 아틀라스 UV 굽기
|
||||
// ① 면마다 원본 텍스처의 실제 샘플 영역 평균색(선형광)
|
||||
// ② 인접 면을 덩어리로 병합(Ward · 색거리²×면적항)
|
||||
// ③ 덩어리 색을 팔레트 N색으로 k-means(Lab) → 🔴 Hue 는 원본 그대로 복원
|
||||
// ④ 면의 UV 를 팔레트 칸 중심으로 (정점은 (원정점,팔레트칸) 쌍으로만 분리 = 정점 증가 최소)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
public static class WLXBake
|
||||
{
|
||||
public const string MESH_DIR = "Assets/WL/Look/Character/Meshes/";
|
||||
public const string TEX_DIR = "Assets/WL/Look/Character/Textures/";
|
||||
const int CELL = 8;
|
||||
public static bool SaveAssets = true;
|
||||
|
||||
static readonly StringBuilder sb = new StringBuilder();
|
||||
static void L(string s) { sb.AppendLine(s); }
|
||||
|
||||
static readonly Dictionary<string, string> MeshTex = new Dictionary<string, string> {
|
||||
{ "Arm_m05", TEX_DIR + "M05_WL.png" },
|
||||
{ "Body_m05", TEX_DIR + "M05_WL.png" },
|
||||
{ "Leg_M05", TEX_DIR + "M05_WL.png" },
|
||||
{ "Head", TEX_DIR + "Skin_WL.png" },
|
||||
{ "Hair05", TEX_DIR + "Hair05_WL.png"},
|
||||
};
|
||||
|
||||
static float S2L(float c) { return c <= 0.04045f ? c / 12.92f : Mathf.Pow((c + 0.055f) / 1.055f, 2.4f); }
|
||||
static float L2S(float c) { return c <= 0.0031308f ? c * 12.92f : 1.055f * Mathf.Pow(c, 1f / 2.4f) - 0.055f; }
|
||||
public static Vector3 LinToSrgb(Vector3 v) { return new Vector3(L2S(v.x), L2S(v.y), L2S(v.z)); }
|
||||
|
||||
public struct Lab { public float L, a, b; }
|
||||
public static Lab ToLab(Vector3 lin)
|
||||
{
|
||||
float x = 0.4124f * lin.x + 0.3576f * lin.y + 0.1805f * lin.z;
|
||||
float y = 0.2126f * lin.x + 0.7152f * lin.y + 0.0722f * lin.z;
|
||||
float z = 0.0193f * lin.x + 0.1192f * lin.y + 0.9505f * lin.z;
|
||||
x /= 0.95047f; z /= 1.08883f;
|
||||
Func<float, float> f = t => t > 0.008856f ? Mathf.Pow(t, 1f / 3f) : (7.787f * t + 16f / 116f);
|
||||
float fx = f(x), fy = f(y), fz = f(z);
|
||||
return new Lab { L = 116f * fy - 16f, a = 500f * (fx - fy), b = 200f * (fy - fz) };
|
||||
}
|
||||
public static float LabD(Lab p, Lab q) { float dl = p.L - q.L, da = p.a - q.a, db = p.b - q.b; return Mathf.Sqrt(dl * dl + da * da + db * db); }
|
||||
|
||||
public static void RgbToHsv(Vector3 srgb, out float h, out float s, out float v)
|
||||
{
|
||||
float r = srgb.x, g = srgb.y, b = srgb.z;
|
||||
float mx = Mathf.Max(r, Mathf.Max(g, b)), mn = Mathf.Min(r, Mathf.Min(g, b)), d = mx - mn;
|
||||
h = 0f;
|
||||
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 Vector3 HsvToRgb(float h, float s, float v)
|
||||
{
|
||||
var c = Color.HSVToRGB(((h % 360f) + 360f) % 360f / 360f, Mathf.Clamp01(s), Mathf.Clamp01(v), false);
|
||||
return new Vector3(c.r, c.g, c.b);
|
||||
}
|
||||
static float DHue(float a, float b) { return (b - a + 540f) % 360f - 180f; }
|
||||
|
||||
class Tex { public int w, h; public Vector3[] lin; }
|
||||
static readonly Dictionary<string, Tex> s_tex = new Dictionary<string, Tex>();
|
||||
static Tex LoadTex(string path)
|
||||
{
|
||||
Tex t;
|
||||
if (s_tex.TryGetValue(path, out t)) return t;
|
||||
var tmp = new Texture2D(2, 2, TextureFormat.RGBA32, false);
|
||||
tmp.LoadImage(File.ReadAllBytes(path));
|
||||
var px = tmp.GetPixels32();
|
||||
t = new Tex { w = tmp.width, h = tmp.height, lin = new Vector3[px.Length] };
|
||||
for (int i = 0; i < px.Length; i++)
|
||||
t.lin[i] = new Vector3(S2L(px[i].r / 255f), S2L(px[i].g / 255f), S2L(px[i].b / 255f));
|
||||
UnityEngine.Object.DestroyImmediate(tmp);
|
||||
s_tex[path] = t;
|
||||
return t;
|
||||
}
|
||||
static Vector3 Sample(Tex t, float u, float v)
|
||||
{
|
||||
int x = Mathf.Clamp(Mathf.FloorToInt(u * t.w), 0, t.w - 1);
|
||||
int y = Mathf.Clamp(Mathf.FloorToInt(v * t.h), 0, t.h - 1);
|
||||
return t.lin[y * t.w + x];
|
||||
}
|
||||
|
||||
class Face { public int mesh, i0, i1, i2; public Vector3 col; public float area; }
|
||||
class Region
|
||||
{
|
||||
public Vector3 sum; public float area; public int count, mesh;
|
||||
public float hx, hy, sw;
|
||||
public Vector3 Col { get { return sum / Mathf.Max(area, 1e-9f); } }
|
||||
}
|
||||
|
||||
public class Result
|
||||
{
|
||||
public Dictionary<string, Mesh> meshes = new Dictionary<string, Mesh>();
|
||||
public Vector3[] palette;
|
||||
public int regionCount, paletteUsed;
|
||||
public float meanDHue, maxDHue;
|
||||
public int vertsBefore, vertsAfter;
|
||||
public string tag;
|
||||
public string palPath;
|
||||
}
|
||||
public static Result Last;
|
||||
|
||||
public static string Bake(int chunkTarget, int paletteN, string tag)
|
||||
{
|
||||
sb.Length = 0;
|
||||
L("=== bake chunks=" + chunkTarget + " palette=" + paletteN + " tag=" + tag + " ===");
|
||||
var prefab = AssetDatabase.LoadAssetAtPath<GameObject>("Assets/Res_Addr/PC/LH_M05.prefab");
|
||||
var smrs = prefab.GetComponentsInChildren<SkinnedMeshRenderer>(true).Where(r => MeshTex.ContainsKey(r.name)).ToArray();
|
||||
var names = smrs.Select(r => r.name).ToArray();
|
||||
var srcs = smrs.Select(r => r.sharedMesh).ToArray();
|
||||
int M = srcs.Length;
|
||||
|
||||
var faces = new List<Face>();
|
||||
var start = new int[M + 1];
|
||||
var vtxOf = new List<Vector3[]>(); var triOf = new List<int[]>();
|
||||
for (int m = 0; m < M; m++)
|
||||
{
|
||||
start[m] = faces.Count;
|
||||
var mesh = srcs[m]; var vt = mesh.vertices; var uv = mesh.uv; var tr = mesh.triangles;
|
||||
vtxOf.Add(vt); triOf.Add(tr);
|
||||
var tex = LoadTex(MeshTex[names[m]]);
|
||||
for (int t = 0; t < tr.Length; t += 3)
|
||||
{
|
||||
int a = tr[t], b = tr[t + 1], c = tr[t + 2];
|
||||
faces.Add(new Face
|
||||
{
|
||||
mesh = m, i0 = a, i1 = b, i2 = c,
|
||||
area = Vector3.Cross(vt[b] - vt[a], vt[c] - vt[a]).magnitude * 0.5f,
|
||||
col = TriAvg(tex, uv[a], uv[b], uv[c])
|
||||
});
|
||||
}
|
||||
}
|
||||
start[M] = faces.Count;
|
||||
L("faces " + faces.Count);
|
||||
|
||||
var adj = BuildAdjacency(faces, start, vtxOf, M);
|
||||
var regions = new Region[faces.Count];
|
||||
var uf = new int[faces.Count];
|
||||
for (int i = 0; i < faces.Count; i++)
|
||||
{
|
||||
uf[i] = i;
|
||||
var r = new Region { sum = faces[i].col * faces[i].area, area = faces[i].area, count = 1, mesh = faces[i].mesh };
|
||||
AccHue(r, faces[i].col, faces[i].area);
|
||||
regions[i] = r;
|
||||
}
|
||||
int regionCount = Merge(uf, regions, adj, chunkTarget, M, faces);
|
||||
L("regions " + regionCount + " (target " + chunkTarget + ")");
|
||||
for (int m = 0; m < M; m++)
|
||||
{
|
||||
var seen = new HashSet<int>();
|
||||
for (int i = start[m]; i < start[m + 1]; i++) seen.Add(Find(uf, i));
|
||||
L(" " + names[m] + " : " + seen.Count);
|
||||
}
|
||||
|
||||
var roots = new List<int>();
|
||||
{ var seen = new HashSet<int>(); for (int i = 0; i < faces.Count; i++) { int r = Find(uf, i); if (seen.Add(r)) roots.Add(r); } }
|
||||
int N = Mathf.Min(paletteN, roots.Count);
|
||||
var pal = KMeans(roots.Select(r => regions[r].Col).ToArray(), roots.Select(r => regions[r].area).ToArray(), N);
|
||||
var assign = new int[roots.Count];
|
||||
for (int i = 0; i < roots.Count; i++)
|
||||
{
|
||||
var lab = ToLab(regions[roots[i]].Col);
|
||||
float bd = float.MaxValue; int bj = 0;
|
||||
for (int j = 0; j < N; j++) { float d = LabD(lab, ToLab(pal[j])); if (d < bd) { bd = d; bj = j; } }
|
||||
assign[i] = bj;
|
||||
}
|
||||
// 🔴 Hue 복원: 팔레트 칸의 H/S 를 그 칸 소속 덩어리들의 원본 H/S 로 되돌린다(채도·면적 가중)
|
||||
var palS = new Vector3[N];
|
||||
for (int j = 0; j < N; j++)
|
||||
{
|
||||
double hx = 0, hy = 0, sw = 0, ssum = 0, aw = 0;
|
||||
for (int i = 0; i < roots.Count; i++)
|
||||
{
|
||||
if (assign[i] != j) continue;
|
||||
var reg = regions[roots[i]];
|
||||
hx += reg.hx; hy += reg.hy; sw += reg.sw;
|
||||
float h, s, v; RgbToHsv(LinToSrgb(reg.Col), out h, out s, out v);
|
||||
ssum += s * reg.area; aw += reg.area;
|
||||
}
|
||||
float ph, ps, pv; RgbToHsv(LinToSrgb(pal[j]), out ph, out ps, out pv);
|
||||
float hue = sw > 1e-6 ? (float)((Math.Atan2(hy, hx) * 180.0 / Math.PI + 360.0) % 360.0) : ph;
|
||||
float sat = aw > 1e-6 ? (float)(ssum / aw) : ps;
|
||||
palS[j] = HsvToRgb(hue, sat, pv);
|
||||
}
|
||||
double dsum = 0, dw = 0; float dmax = 0;
|
||||
for (int i = 0; i < roots.Count; i++)
|
||||
{
|
||||
var reg = regions[roots[i]];
|
||||
float h0, s0, v0, h1, s1, v1;
|
||||
RgbToHsv(LinToSrgb(reg.Col), out h0, out s0, out v0);
|
||||
RgbToHsv(palS[assign[i]], out h1, out s1, out v1);
|
||||
if (s0 < 0.08f) continue;
|
||||
float d = Mathf.Abs(DHue(h0, h1));
|
||||
dsum += d * reg.area; dw += reg.area; if (d > dmax) dmax = d;
|
||||
}
|
||||
float meanD = dw > 1e-6 ? (float)(dsum / dw) : 0f;
|
||||
L(string.Format("palette {0} · dHue mean {1:F2} max {2:F2}", N, meanD, dmax));
|
||||
|
||||
int G = Mathf.CeilToInt(Mathf.Sqrt(N));
|
||||
int S = G * CELL;
|
||||
string palPath = TEX_DIR + "WLPalette_" + tag + ".png";
|
||||
WritePalettePng(palS, N, G, S, palPath);
|
||||
|
||||
var rootIndex = new Dictionary<int, int>();
|
||||
for (int i = 0; i < roots.Count; i++) rootIndex[roots[i]] = i;
|
||||
var res = new Result { palette = palS, regionCount = regionCount, paletteUsed = N, meanDHue = meanD, maxDHue = dmax, tag = tag, palPath = palPath };
|
||||
if (SaveAssets) Directory.CreateDirectory(MESH_DIR);
|
||||
for (int m = 0; m < M; m++)
|
||||
{
|
||||
var nm = BuildMesh(srcs[m], faces, start[m], start[m + 1], uf, rootIndex, assign, G);
|
||||
nm.name = names[m] + "_PAL_" + tag;
|
||||
res.vertsBefore += srcs[m].vertexCount; res.vertsAfter += nm.vertexCount;
|
||||
if (SaveAssets)
|
||||
{
|
||||
string p = MESH_DIR + nm.name + ".asset";
|
||||
if (AssetDatabase.LoadAssetAtPath<Mesh>(p) != null) AssetDatabase.DeleteAsset(p);
|
||||
AssetDatabase.CreateAsset(nm, p);
|
||||
}
|
||||
res.meshes[names[m]] = nm;
|
||||
L(string.Format(" mesh {0,-9} verts {1,4} -> {2,4}", names[m], srcs[m].vertexCount, nm.vertexCount));
|
||||
}
|
||||
if (SaveAssets) AssetDatabase.SaveAssets();
|
||||
Last = res;
|
||||
File.WriteAllText("AgentScripts/WL814x_BAKE_" + tag + ".txt", sb.ToString(), new UTF8Encoding(true));
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
static void WritePalettePng(Vector3[] palS, int N, int G, int S, string path)
|
||||
{
|
||||
var ptex = new Texture2D(S, S, TextureFormat.RGBA32, false);
|
||||
var ppx = new Color32[S * S];
|
||||
for (int i = 0; i < ppx.Length; i++) ppx[i] = new Color32(255, 0, 255, 255);
|
||||
for (int j = 0; j < N; j++)
|
||||
{
|
||||
int gx = j % G, gy = j / G;
|
||||
var c = new Color32((byte)Mathf.RoundToInt(Mathf.Clamp01(palS[j].x) * 255f),
|
||||
(byte)Mathf.RoundToInt(Mathf.Clamp01(palS[j].y) * 255f),
|
||||
(byte)Mathf.RoundToInt(Mathf.Clamp01(palS[j].z) * 255f), 255);
|
||||
for (int y = 0; y < CELL; y++) for (int x = 0; x < CELL; x++) ppx[(gy * CELL + y) * S + gx * CELL + x] = c;
|
||||
}
|
||||
ptex.SetPixels32(ppx); ptex.Apply();
|
||||
Directory.CreateDirectory(TEX_DIR);
|
||||
File.WriteAllBytes(path, ptex.EncodeToPNG());
|
||||
UnityEngine.Object.DestroyImmediate(ptex);
|
||||
AssetDatabase.ImportAsset(path, ImportAssetOptions.ForceUpdate);
|
||||
var ti = (TextureImporter)AssetImporter.GetAtPath(path);
|
||||
ti.textureType = TextureImporterType.Default;
|
||||
ti.mipmapEnabled = false; // 🔴 밉이 있으면 이웃 칸이 섞인다
|
||||
ti.filterMode = FilterMode.Point;
|
||||
ti.wrapMode = TextureWrapMode.Clamp;
|
||||
ti.sRGBTexture = true;
|
||||
ti.npotScale = TextureImporterNPOTScale.None;
|
||||
ti.maxTextureSize = Mathf.Max(32, Mathf.NextPowerOfTwo(S));
|
||||
ti.textureCompression = TextureImporterCompression.Uncompressed;
|
||||
ti.SaveAndReimport();
|
||||
}
|
||||
|
||||
static void AccHue(Region r, Vector3 lin, float area)
|
||||
{
|
||||
float h, s, v; RgbToHsv(LinToSrgb(lin), out h, out s, out v);
|
||||
float w = s * area;
|
||||
r.hx += w * Mathf.Cos(h * Mathf.Deg2Rad); r.hy += w * Mathf.Sin(h * Mathf.Deg2Rad); r.sw += w;
|
||||
}
|
||||
|
||||
static Vector3 TriAvg(Tex tex, Vector2 a, Vector2 b, Vector2 c)
|
||||
{
|
||||
float au = a.x * tex.w, av = a.y * tex.h, bu = b.x * tex.w, bv = b.y * tex.h, cu = c.x * tex.w, cv = c.y * tex.h;
|
||||
float areaTexels = Mathf.Abs((bu - au) * (cv - av) - (cu - au) * (bv - av)) * 0.5f;
|
||||
int n = Mathf.Clamp(Mathf.CeilToInt(Mathf.Sqrt(areaTexels)) * 2, 8, 64);
|
||||
Vector3 sum = Vector3.zero; int cnt = 0;
|
||||
for (int i = 0; i < n; i++)
|
||||
for (int j = 0; j < n - i; j++)
|
||||
{
|
||||
float w0 = (i + 1f / 3f) / n, w1 = (j + 1f / 3f) / n;
|
||||
if (1f - w0 - w1 < 0f) continue;
|
||||
var uv = a * (1f - w0 - w1) + b * w0 + c * w1;
|
||||
sum += Sample(tex, uv.x, uv.y); cnt++;
|
||||
}
|
||||
if (cnt == 0) return Sample(tex, (a.x + b.x + c.x) / 3f, (a.y + b.y + c.y) / 3f);
|
||||
return sum / cnt;
|
||||
}
|
||||
|
||||
static List<KeyValuePair<int, int>> BuildAdjacency(List<Face> faces, int[] start, List<Vector3[]> vtx, int M)
|
||||
{
|
||||
var pairs = new List<KeyValuePair<int, int>>();
|
||||
for (int m = 0; m < M; m++)
|
||||
{
|
||||
var vt = vtx[m];
|
||||
var map = new Dictionary<long, int>(); var weld = new int[vt.Length];
|
||||
for (int i = 0; i < vt.Length; i++)
|
||||
{
|
||||
long k = Key(vt[i]); int id;
|
||||
if (!map.TryGetValue(k, out id)) { id = map.Count; map[k] = id; }
|
||||
weld[i] = id;
|
||||
}
|
||||
var edgeMap = new Dictionary<long, int>();
|
||||
for (int f = start[m]; f < start[m + 1]; f++)
|
||||
{
|
||||
var fa = faces[f];
|
||||
int[] ii = { weld[fa.i0], weld[fa.i1], weld[fa.i2] };
|
||||
for (int e = 0; e < 3; e++)
|
||||
{
|
||||
int p = ii[e], q = ii[(e + 1) % 3];
|
||||
long k = p < q ? ((long)p << 32) | (uint)q : ((long)q << 32) | (uint)p;
|
||||
int other;
|
||||
if (edgeMap.TryGetValue(k, out other)) { if (other != f) pairs.Add(new KeyValuePair<int, int>(other, f)); }
|
||||
else edgeMap[k] = f;
|
||||
}
|
||||
}
|
||||
}
|
||||
return pairs;
|
||||
}
|
||||
static long Key(Vector3 v)
|
||||
{
|
||||
long x = (long)Mathf.RoundToInt(v.x * 10000f) & 0x1FFFFF;
|
||||
long y = (long)Mathf.RoundToInt(v.y * 10000f) & 0x1FFFFF;
|
||||
long z = (long)Mathf.RoundToInt(v.z * 10000f) & 0x1FFFFF;
|
||||
return (x << 42) | (y << 21) | z;
|
||||
}
|
||||
static int Find(int[] uf, int i) { while (uf[i] != i) { uf[i] = uf[uf[i]]; i = uf[i]; } return i; }
|
||||
|
||||
static int Merge(int[] uf, Region[] reg, List<KeyValuePair<int, int>> adj, int target, int M, List<Face> faces)
|
||||
{
|
||||
var ver = new int[uf.Length];
|
||||
var pq = new SortedSet<(float cost, int a, int b, int va, int vb)>(Comparer<(float, int, int, int, int)>.Create((p, q) =>
|
||||
{
|
||||
int c = p.Item1.CompareTo(q.Item1); if (c != 0) return c;
|
||||
c = p.Item2.CompareTo(q.Item2); if (c != 0) return c;
|
||||
c = p.Item3.CompareTo(q.Item3); if (c != 0) return c;
|
||||
c = p.Item4.CompareTo(q.Item4); if (c != 0) return c;
|
||||
return p.Item5.CompareTo(q.Item5);
|
||||
}));
|
||||
var nb = new Dictionary<int, HashSet<int>>();
|
||||
foreach (var e in adj)
|
||||
{
|
||||
HashSet<int> sa, sbb;
|
||||
if (!nb.TryGetValue(e.Key, out sa)) { sa = new HashSet<int>(); nb[e.Key] = sa; }
|
||||
if (!nb.TryGetValue(e.Value, out sbb)) { sbb = new HashSet<int>(); nb[e.Value] = sbb; }
|
||||
sa.Add(e.Value); sbb.Add(e.Key);
|
||||
}
|
||||
foreach (var kv in nb) foreach (var o in kv.Value) if (kv.Key < o) pq.Add((Cost(reg[kv.Key], reg[o]), kv.Key, o, 0, 0));
|
||||
|
||||
int count = uf.Length;
|
||||
var perMesh = new int[M];
|
||||
for (int i = 0; i < uf.Length; i++) perMesh[faces[i].mesh]++;
|
||||
|
||||
while (count > target && pq.Count > 0)
|
||||
{
|
||||
var e = pq.Min; pq.Remove(e);
|
||||
int a = Find(uf, e.a), b = Find(uf, e.b);
|
||||
if (a == b) continue;
|
||||
int lo = Mathf.Min(a, b), hi = Mathf.Max(a, b);
|
||||
if (ver[a] != e.va || ver[b] != e.vb) { pq.Add((Cost(reg[a], reg[b]), lo, hi, ver[lo], ver[hi])); continue; }
|
||||
int mm = reg[a].mesh;
|
||||
if (perMesh[mm] <= 2) continue;
|
||||
uf[b] = a;
|
||||
reg[a].sum += reg[b].sum; reg[a].area += reg[b].area; reg[a].count += reg[b].count;
|
||||
reg[a].hx += reg[b].hx; reg[a].hy += reg[b].hy; reg[a].sw += reg[b].sw;
|
||||
ver[a]++; count--; perMesh[mm]--;
|
||||
HashSet<int> sb2;
|
||||
if (nb.TryGetValue(b, out sb2))
|
||||
{
|
||||
HashSet<int> sa2;
|
||||
if (!nb.TryGetValue(a, out sa2)) { sa2 = new HashSet<int>(); nb[a] = sa2; }
|
||||
foreach (var o in sb2)
|
||||
{
|
||||
int r = Find(uf, o);
|
||||
if (r == a) continue;
|
||||
sa2.Add(r);
|
||||
HashSet<int> sr;
|
||||
if (nb.TryGetValue(r, out sr)) sr.Add(a);
|
||||
int l2 = Mathf.Min(a, r), h2 = Mathf.Max(a, r);
|
||||
pq.Add((Cost(reg[a], reg[r]), l2, h2, ver[l2], ver[h2]));
|
||||
}
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static float Cost(Region a, Region b)
|
||||
{
|
||||
float d = LabD(ToLab(a.Col), ToLab(b.Col));
|
||||
float w = (a.area * b.area) / Mathf.Max(a.area + b.area, 1e-9f);
|
||||
return d * d * w;
|
||||
}
|
||||
|
||||
static Vector3[] KMeans(Vector3[] cols, float[] wts, int k)
|
||||
{
|
||||
int n = cols.Length;
|
||||
var labs = cols.Select(ToLab).ToArray();
|
||||
var cent = new Lab[k]; var centRgb = new Vector3[k];
|
||||
var rnd = new System.Random(12345);
|
||||
var d2 = new float[n];
|
||||
int first = 0; float bw = -1;
|
||||
for (int i = 0; i < n; i++) if (wts[i] > bw) { bw = wts[i]; first = i; }
|
||||
cent[0] = labs[first]; centRgb[0] = cols[first];
|
||||
for (int j = 1; j < k; j++)
|
||||
{
|
||||
double tot = 0;
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
float best = float.MaxValue;
|
||||
for (int q = 0; q < j; q++) { float d = LabD(labs[i], cent[q]); if (d < best) best = d; }
|
||||
d2[i] = best * best * wts[i]; tot += d2[i];
|
||||
}
|
||||
double pick = rnd.NextDouble() * tot, acc = 0; int sel = n - 1;
|
||||
for (int i = 0; i < n; i++) { acc += d2[i]; if (acc >= pick) { sel = i; break; } }
|
||||
cent[j] = labs[sel]; centRgb[j] = cols[sel];
|
||||
}
|
||||
var asg = new int[n];
|
||||
for (int it = 0; it < 80; it++)
|
||||
{
|
||||
bool ch = false;
|
||||
for (int i = 0; i < n; i++)
|
||||
{
|
||||
float bd = float.MaxValue; int bj = 0;
|
||||
for (int j = 0; j < k; j++) { float d = LabD(labs[i], cent[j]); if (d < bd) { bd = d; bj = j; } }
|
||||
if (asg[i] != bj) { asg[i] = bj; ch = true; }
|
||||
}
|
||||
var sumRgb = new Vector3[k]; var sw = new double[k];
|
||||
for (int i = 0; i < n; i++) { sumRgb[asg[i]] += cols[i] * wts[i]; sw[asg[i]] += wts[i]; }
|
||||
for (int j = 0; j < k; j++) if (sw[j] > 1e-9) { centRgb[j] = sumRgb[j] / (float)sw[j]; cent[j] = ToLab(centRgb[j]); }
|
||||
if (!ch) break;
|
||||
}
|
||||
return centRgb;
|
||||
}
|
||||
|
||||
static Mesh BuildMesh(Mesh src, List<Face> faces, int f0, int f1, int[] uf, Dictionary<int, int> rootIndex, int[] assign, int G)
|
||||
{
|
||||
var vt = src.vertices; var nmv = src.normals; var tg = src.tangents;
|
||||
var bpv = src.GetBonesPerVertex(); var bw = src.GetAllBoneWeights();
|
||||
var bwStart = new int[vt.Length];
|
||||
{ int acc = 0; for (int i = 0; i < vt.Length; i++) { bwStart[i] = acc; acc += (bpv.Length > 0 ? bpv[i] : 0); } }
|
||||
|
||||
var map = new Dictionary<long, int>();
|
||||
var nv = new List<Vector3>(); var nn = new List<Vector3>(); var nt = new List<Vector4>();
|
||||
var nuv = new List<Vector2>(); var ntri = new List<int>();
|
||||
var nbpv = new List<byte>(); var nbw = new List<BoneWeight1>();
|
||||
|
||||
Func<int, int, int> get = (vi, pal) =>
|
||||
{
|
||||
long k = ((long)vi << 16) | (uint)pal; int id;
|
||||
if (map.TryGetValue(k, out id)) return id;
|
||||
id = nv.Count; map[k] = id;
|
||||
nv.Add(vt[vi]);
|
||||
if (nmv != null && nmv.Length > 0) nn.Add(nmv[vi]);
|
||||
if (tg != null && tg.Length > 0) nt.Add(tg[vi]);
|
||||
nuv.Add(new Vector2((pal % G + 0.5f) / G, (pal / G + 0.5f) / G));
|
||||
if (bpv.Length > 0) { nbpv.Add(bpv[vi]); for (int q = 0; q < bpv[vi]; q++) nbw.Add(bw[bwStart[vi] + q]); }
|
||||
return id;
|
||||
};
|
||||
|
||||
for (int f = f0; f < f1; f++)
|
||||
{
|
||||
int pal = assign[rootIndex[Find(uf, f)]];
|
||||
var fa = faces[f];
|
||||
ntri.Add(get(fa.i0, pal)); ntri.Add(get(fa.i1, pal)); ntri.Add(get(fa.i2, pal));
|
||||
}
|
||||
|
||||
var mesh = new Mesh();
|
||||
mesh.indexFormat = nv.Count > 65000 ? UnityEngine.Rendering.IndexFormat.UInt32 : UnityEngine.Rendering.IndexFormat.UInt16;
|
||||
mesh.SetVertices(nv);
|
||||
if (nn.Count == nv.Count) mesh.SetNormals(nn);
|
||||
if (nt.Count == nv.Count) mesh.SetTangents(nt);
|
||||
mesh.SetUVs(0, nuv);
|
||||
mesh.SetTriangles(ntri, 0);
|
||||
if (nbpv.Count == nv.Count)
|
||||
{
|
||||
var a1 = new NativeArray<byte>(nbpv.ToArray(), Allocator.Temp);
|
||||
var a2 = new NativeArray<BoneWeight1>(nbw.ToArray(), Allocator.Temp);
|
||||
mesh.SetBoneWeights(a1, a2); a1.Dispose(); a2.Dispose();
|
||||
}
|
||||
mesh.bindposes = src.bindposes;
|
||||
mesh.RecalculateBounds();
|
||||
return mesh;
|
||||
}
|
||||
|
||||
// ── 얼굴/머리칼 「점 텍스처」: 814v 안의 격자 1칸 = 1텍셀 로 축소해 화면 1px 이 정확히 1칸을 읽게 한다.
|
||||
public static string MakePointTex(string srcPng, int outSize, string outName, bool alpha)
|
||||
{
|
||||
var tmp = new Texture2D(2, 2, TextureFormat.RGBA32, false);
|
||||
tmp.LoadImage(File.ReadAllBytes(srcPng));
|
||||
int f = tmp.width / outSize;
|
||||
var src = tmp.GetPixels32();
|
||||
var dst = new Color32[outSize * outSize];
|
||||
for (int y = 0; y < outSize; y++)
|
||||
for (int x = 0; x < outSize; x++)
|
||||
dst[y * outSize + x] = src[(y * f + f / 2) * tmp.width + (x * f + f / 2)]; // 칸 중심 텍셀 = 뭉개지 않는다
|
||||
var o = new Texture2D(outSize, outSize, TextureFormat.RGBA32, false);
|
||||
o.SetPixels32(dst); o.Apply();
|
||||
string p = TEX_DIR + outName + ".png";
|
||||
File.WriteAllBytes(p, o.EncodeToPNG());
|
||||
UnityEngine.Object.DestroyImmediate(o); UnityEngine.Object.DestroyImmediate(tmp);
|
||||
AssetDatabase.ImportAsset(p, ImportAssetOptions.ForceUpdate);
|
||||
var ti = (TextureImporter)AssetImporter.GetAtPath(p);
|
||||
ti.textureType = TextureImporterType.Default;
|
||||
ti.alphaSource = alpha ? TextureImporterAlphaSource.FromInput : TextureImporterAlphaSource.None;
|
||||
ti.alphaIsTransparency = alpha;
|
||||
ti.mipmapEnabled = true;
|
||||
ti.filterMode = FilterMode.Point;
|
||||
ti.wrapMode = TextureWrapMode.Clamp;
|
||||
ti.sRGBTexture = true;
|
||||
ti.npotScale = TextureImporterNPOTScale.None;
|
||||
ti.maxTextureSize = Mathf.Max(32, outSize);
|
||||
ti.textureCompression = TextureImporterCompression.Uncompressed;
|
||||
ti.SaveAndReimport();
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
// WLXIter — 반복 회차 드라이버
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
public static class WLXIter
|
||||
{
|
||||
static void ApplyPalette(WLXBake.Result res, Texture2D faceTex, Texture2D hairTex)
|
||||
{
|
||||
var pal = AssetDatabase.LoadAssetAtPath<Texture2D>(res.palPath);
|
||||
for (int i = 0; i < WLXCap.Smrs.Length; i++)
|
||||
{
|
||||
var r = WLXCap.Smrs[i];
|
||||
var mats = (Material[])WLXCap.OrigMats(i).Clone();
|
||||
Texture2D tex = null;
|
||||
Mesh nm;
|
||||
bool usePal = res.meshes.TryGetValue(r.name, out nm);
|
||||
if (r.name == "Hair05" && hairTex != null) { usePal = false; tex = hairTex; }
|
||||
if (r.name == "Face") { usePal = false; tex = faceTex; }
|
||||
if (usePal) { r.sharedMesh = nm; tex = pal; }
|
||||
if (tex == null) { r.sharedMaterials = mats; continue; }
|
||||
for (int m = 0; m < mats.Length; m++)
|
||||
{
|
||||
if (mats[m] == null) continue;
|
||||
var c = new Material(mats[m]) { name = "TMP_" + mats[m].name };
|
||||
foreach (var pn in new[] { "_BaseMap", "_ShadowBaseMap", "_MainTex" }) if (c.HasProperty(pn)) c.SetTexture(pn, tex);
|
||||
mats[m] = c;
|
||||
}
|
||||
r.sharedMaterials = mats;
|
||||
}
|
||||
}
|
||||
|
||||
static readonly int[] CHUNKS = { 20, 40, 80 };
|
||||
static readonly int[] PALS = { 16, 24, 32 };
|
||||
|
||||
/// <summary>1회차 — 덩어리 20/40/80 × 팔레트 16/24/32 전수 비교 (얼굴은 지금 그대로).</summary>
|
||||
public static void Iter1()
|
||||
{
|
||||
try
|
||||
{
|
||||
WLXCap.Setup();
|
||||
WLXCap.SaveAllMasks();
|
||||
WLXBake.SaveAssets = false;
|
||||
var cfg = WLCharacterLookSettings.Instance;
|
||||
|
||||
// 기준 = main 현재 상태(mode3 + 814t 종합안)
|
||||
WLCharacterLook.ApplyMode(WLXCap.Root, 3, cfg);
|
||||
WLXCap.ApplyLook(WLXCap.LookOpt(true, true, true, true, 0));
|
||||
WLXCap.Shoot("i1_a_base_now"); WLXCap.NoLook();
|
||||
|
||||
foreach (var ch in CHUNKS)
|
||||
foreach (var pn in PALS)
|
||||
{
|
||||
string tag = "c" + ch + "p" + pn;
|
||||
WLXBake.Bake(ch, pn, tag);
|
||||
var res = WLXBake.Last;
|
||||
WLXCap.L(string.Format("{0}: regions {1} pal {2} dHue {3:F2}/{4:F2} verts {5}->{6}",
|
||||
tag, res.regionCount, res.paletteUsed, res.meanDHue, res.maxDHue, res.vertsBefore, res.vertsAfter));
|
||||
ApplyPalette(res, null, null);
|
||||
WLXCap.ApplyLook(WLXCap.LookOpt(true, true, true, false, 3));
|
||||
WLXCap.Shoot("i1_" + tag);
|
||||
WLXCap.NoLook();
|
||||
WLXCap.RestoreMeshes();
|
||||
}
|
||||
}
|
||||
catch (Exception e) { WLXCap.L("EXCEPTION " + e); }
|
||||
finally { WLXCap.Teardown(); WLXCap.Flush("WL814x_ITER1.txt"); }
|
||||
}
|
||||
|
||||
/// <summary>2회차 — 얼굴 점텍스처(32/64) · 머리칼 팔레트 vs 텍스처 · 셰이드 단수.</summary>
|
||||
public static void Iter2(int chunks, int palN)
|
||||
{
|
||||
try
|
||||
{
|
||||
WLXCap.Setup();
|
||||
WLXBake.SaveAssets = false;
|
||||
// 얼굴·머리칼 점텍스처 생성(원본 수정 0 · 814v V1 을 격자 1칸 = 1텍셀로 축소)
|
||||
WLXBake.MakePointTex(WLXBake.TEX_DIR + "face02_V1.png", 32, "face02_P32", true);
|
||||
WLXBake.MakePointTex(WLXBake.TEX_DIR + "face02_V1.png", 64, "face02_P64", true);
|
||||
WLXBake.MakePointTex(WLXBake.TEX_DIR + "Hair05_V1.png", 64, "Hair05_P64", false);
|
||||
WLXBake.MakePointTex(WLXBake.TEX_DIR + "Hair05_V1.png", 32, "Hair05_P32", false);
|
||||
AssetDatabase.Refresh();
|
||||
var f32 = AssetDatabase.LoadAssetAtPath<Texture2D>(WLXBake.TEX_DIR + "face02_P32.png");
|
||||
var f64 = AssetDatabase.LoadAssetAtPath<Texture2D>(WLXBake.TEX_DIR + "face02_P64.png");
|
||||
var fV1 = AssetDatabase.LoadAssetAtPath<Texture2D>(WLXBake.TEX_DIR + "face02_V1.png");
|
||||
var h64 = AssetDatabase.LoadAssetAtPath<Texture2D>(WLXBake.TEX_DIR + "Hair05_P64.png");
|
||||
var h32 = AssetDatabase.LoadAssetAtPath<Texture2D>(WLXBake.TEX_DIR + "Hair05_P32.png");
|
||||
WLXCap.L("face P32=" + (f32 != null) + " P64=" + (f64 != null) + " V1=" + (fV1 != null) + " hair P64=" + (h64 != null));
|
||||
|
||||
WLXBake.Bake(chunks, palN, "best");
|
||||
var res = WLXBake.Last;
|
||||
WLXCap.L(string.Format("best: regions {0} pal {1} dHue {2:F2}/{3:F2} verts {4}->{5}",
|
||||
res.regionCount, res.paletteUsed, res.meanDHue, res.maxDHue, res.vertsBefore, res.vertsAfter));
|
||||
|
||||
var faces = new Dictionary<string, Texture2D> { { "fV1", fV1 }, { "fP64", f64 }, { "fP32", f32 } };
|
||||
foreach (var kv in faces)
|
||||
{
|
||||
ApplyPalette(res, kv.Value, null);
|
||||
WLXCap.ApplyLook(WLXCap.LookOpt(true, true, true, false, 3));
|
||||
WLXCap.Shoot("i2_" + kv.Key); WLXCap.NoLook(); WLXCap.RestoreMeshes();
|
||||
}
|
||||
// 머리칼: 팔레트(=위) vs 텍스처
|
||||
foreach (var kv in new Dictionary<string, Texture2D> { { "hairP64", h64 }, { "hairP32", h32 } })
|
||||
{
|
||||
ApplyPalette(res, f32, kv.Value);
|
||||
WLXCap.ApplyLook(WLXCap.LookOpt(true, true, true, false, 3));
|
||||
WLXCap.Shoot("i2_" + kv.Key); WLXCap.NoLook(); WLXCap.RestoreMeshes();
|
||||
}
|
||||
// 셰이드 단수 비교 (얼굴 P32 · 머리 팔레트)
|
||||
foreach (var sh in new[] { 7, 4, 3, 2 })
|
||||
{
|
||||
ApplyPalette(res, f32, null);
|
||||
WLXCap.ApplyLook(WLXCap.LookOpt(true, true, true, false, sh == 7 ? 0 : sh));
|
||||
WLXCap.Shoot("i2_shades" + sh); WLXCap.NoLook(); WLXCap.RestoreMeshes();
|
||||
}
|
||||
// 림 켠 것(레퍼런스 종합안 유지) 대조
|
||||
ApplyPalette(res, f32, null);
|
||||
WLXCap.ApplyLook(WLXCap.LookOpt(true, true, true, true, 3));
|
||||
WLXCap.Shoot("i2_rimon"); WLXCap.NoLook(); WLXCap.RestoreMeshes();
|
||||
}
|
||||
catch (Exception e) { WLXCap.L("EXCEPTION " + e); }
|
||||
finally { WLXCap.Teardown(); WLXCap.Flush("WL814x_ITER2.txt"); }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,180 @@
|
|||
# WL-814x 측정기 — 렌더 PNG + 마스크 PNG 를 읽어 합격선 6항목을 잰다.
|
||||
# python AgentScripts/WL814x_metrics.py <raw폴더> [파일접두어필터]
|
||||
#
|
||||
# 정의(보고서와 동일)
|
||||
# · 서로 다른 색 비율 = 고유색 수 / 캐릭터 영역 픽셀 수 × 100
|
||||
# · 부위당 색 단계 = 그 부위의 색을 RGB 거리 16 이내로 묶었을 때, 면적 90 % 를 덮는 군집 수
|
||||
# · 진한 점(눈) = 얼굴 데칼 마스크 안에서 L < 피부평균L × 0.75 인 픽셀 수 (814v 와 동일)
|
||||
# · Hue 오차 = 부위별 채도가중 원형평균 Hue 의 기준 대비 차 (부호 있는 각도)
|
||||
import sys, os, glob, json
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
RAW = sys.argv[1] if len(sys.argv) > 1 else "Screenshots_WL/WL814x/raw"
|
||||
FILT = sys.argv[2] if len(sys.argv) > 2 else ""
|
||||
PARTS = ["all", "body3", "Body_m05", "Arm_m05", "Leg_M05", "Head", "Hair05", "Face"]
|
||||
|
||||
|
||||
def load(p):
|
||||
return np.asarray(Image.open(p).convert("RGB"), dtype=np.uint8)
|
||||
|
||||
|
||||
def masks():
|
||||
m = {}
|
||||
for p in PARTS:
|
||||
f = os.path.join(RAW, "mask_%s.png" % p)
|
||||
if os.path.exists(f):
|
||||
m[p] = load(f)[:, :, 0] > 127
|
||||
return m
|
||||
|
||||
|
||||
def lum(px):
|
||||
return 0.299 * px[:, 0] + 0.587 * px[:, 1] + 0.114 * px[:, 2]
|
||||
|
||||
|
||||
def hsv(px):
|
||||
a = px.astype(np.float32)
|
||||
mx = a.max(1); mn = a.min(1); d = mx - mn
|
||||
h = np.zeros(len(a), np.float32)
|
||||
nz = d > 1e-6
|
||||
r, g, b = a[:, 0], a[:, 1], a[:, 2]
|
||||
i = nz & (mx == r); h[i] = ((g[i] - b[i]) / d[i]) % 6
|
||||
i = nz & (mx == g) & ~(mx == r); h[i] = (b[i] - r[i]) / d[i] + 2
|
||||
i = nz & (mx == b) & ~(mx == r) & ~(mx == g); h[i] = (r[i] - g[i]) / d[i] + 4
|
||||
h *= 60.0
|
||||
s = np.where(mx > 1e-6, d / np.maximum(mx, 1e-6), 0)
|
||||
return h, s, mx
|
||||
|
||||
|
||||
def mean_hue(px):
|
||||
h, s, v = hsv(px)
|
||||
w = np.where(s > 0.08, s, 0.0)
|
||||
if w.sum() < 1e-6:
|
||||
return float("nan")
|
||||
x = (w * np.cos(np.radians(h))).sum(); y = (w * np.sin(np.radians(h))).sum()
|
||||
return (np.degrees(np.arctan2(y, x)) + 360.0) % 360.0
|
||||
|
||||
|
||||
def dhue(a, b):
|
||||
d = (b - a + 180.0) % 360.0 - 180.0
|
||||
return d
|
||||
|
||||
|
||||
def steps(px, thr=16.0, cover=0.90):
|
||||
"""색을 RGB 거리 thr 이내로 묶어 면적 cover 를 덮는 군집 수."""
|
||||
if len(px) == 0:
|
||||
return 0, 0
|
||||
key = (px[:, 0].astype(np.int32) << 16) | (px[:, 1].astype(np.int32) << 8) | px[:, 2].astype(np.int32)
|
||||
u, cnt = np.unique(key, return_counts=True)
|
||||
order = np.argsort(-cnt)
|
||||
u, cnt = u[order], cnt[order]
|
||||
cols = np.stack([(u >> 16) & 255, (u >> 8) & 255, u & 255], 1).astype(np.float32)
|
||||
cent = []; wts = []
|
||||
for i in range(len(u)):
|
||||
c = cols[i]; w = cnt[i]
|
||||
best = -1; bd = 1e9
|
||||
for j, cc in enumerate(cent):
|
||||
d = np.linalg.norm(cc - c)
|
||||
if d < bd:
|
||||
bd = d; best = j
|
||||
if best >= 0 and bd < thr:
|
||||
n = wts[best] + w
|
||||
cent[best] = (cent[best] * wts[best] + c * w) / n
|
||||
wts[best] = n
|
||||
else:
|
||||
cent.append(c.copy()); wts.append(float(w))
|
||||
if len(cent) > 400:
|
||||
break
|
||||
wts = np.array(wts); o = np.argsort(-wts); wts = wts[o]
|
||||
tot = len(px); acc = 0; k = 0
|
||||
for w in wts:
|
||||
acc += w; k += 1
|
||||
if acc >= cover * tot:
|
||||
break
|
||||
return k, len(u)
|
||||
|
||||
|
||||
def measure(img, msk, ref_hue=None, skinL=None):
|
||||
px = img[msk]
|
||||
n = len(px)
|
||||
if n == 0:
|
||||
return None
|
||||
key = (px[:, 0].astype(np.int32) << 16) | (px[:, 1].astype(np.int32) << 8) | px[:, 2].astype(np.int32)
|
||||
uc = len(np.unique(key))
|
||||
st, _ = steps(px)
|
||||
L = lum(px)
|
||||
r = dict(px=n, colors=uc, ratio=100.0 * uc / n, steps=st,
|
||||
lmin=float(L.min()), lmax=float(L.max()), hue=mean_hue(px))
|
||||
if ref_hue is not None and not np.isnan(r["hue"]) and not np.isnan(ref_hue):
|
||||
r["dhue"] = float(dhue(ref_hue, r["hue"]))
|
||||
if skinL:
|
||||
r["dark"] = int((L < skinL * 0.75).sum())
|
||||
return r
|
||||
|
||||
|
||||
def main():
|
||||
M = masks()
|
||||
files = sorted(glob.glob(os.path.join(RAW, "*.png")))
|
||||
files = [f for f in files if not os.path.basename(f).startswith("mask_")]
|
||||
if FILT:
|
||||
files = [f for f in files if os.path.basename(f).startswith(FILT)]
|
||||
ref = {}
|
||||
rows = []
|
||||
for f in files:
|
||||
img = load(f)
|
||||
name = os.path.splitext(os.path.basename(f))[0]
|
||||
# 피부 밝기 기준 = Head 마스크에서 Face 를 뺀 영역 평균 L
|
||||
skinL = None
|
||||
if "Head" in M and "Face" in M:
|
||||
hm = M["Head"] & ~M["Face"]
|
||||
if hm.sum() > 0:
|
||||
skinL = float(lum(img[hm]).mean())
|
||||
rec = {"name": name, "skinL": skinL}
|
||||
for p in PARTS:
|
||||
if p not in M:
|
||||
continue
|
||||
r = measure(img, M[p], ref.get(p), skinL if p == "Face" else None)
|
||||
if r is None:
|
||||
continue
|
||||
if p not in ref and not np.isnan(r["hue"]):
|
||||
ref[p] = r["hue"]
|
||||
# 눈 크기(얼굴 왼쪽 절반의 진한 점 바운딩박스)
|
||||
if p == "Face" and skinL:
|
||||
ys, xs = np.where(M[p])
|
||||
x0, x1 = xs.min(), xs.max()
|
||||
half = (x0 + x1) // 2
|
||||
sub = M[p].copy(); sub[:, half:] = False
|
||||
idx = np.where(sub)
|
||||
if len(idx[0]):
|
||||
Ls = lum(img[sub])
|
||||
d = Ls < skinL * 0.75
|
||||
if d.sum() > 0:
|
||||
yy = idx[0][d]; xx = idx[1][d]
|
||||
r["eye"] = "%dx%d" % (xx.max() - xx.min() + 1, yy.max() - yy.min() + 1)
|
||||
rec[p] = r
|
||||
rows.append(rec)
|
||||
|
||||
hdr = "%-28s %6s %6s %5s | %s | %s | %s" % (
|
||||
"render", "전체색", "비율%", "단계",
|
||||
"색:몸통/피부/머리칼/얼굴", "단계:몸통/피부/머리칼", "얼굴 진한점/눈")
|
||||
print(hdr); print("-" * 118)
|
||||
for r in rows:
|
||||
a = r.get("all"); b3 = r.get("body3"); hd = r.get("Head"); hr = r.get("Hair05"); fc = r.get("Face")
|
||||
def c(x): return x["colors"] if x else 0
|
||||
def s(x): return x["steps"] if x else 0
|
||||
print("%-28s %6d %6.1f %5d | %5d/%4d/%4d/%4d | %6d/%3d/%3d | %4s / %s" % (
|
||||
r["name"], a["colors"], a["ratio"], a["steps"],
|
||||
c(b3), c(hd), c(hr), c(fc), s(b3), s(hd), s(hr),
|
||||
fc.get("dark", "-") if fc else "-", fc.get("eye", "-") if fc else "-"))
|
||||
print()
|
||||
print("%-28s %8s %8s %8s %8s" % ("render", "dH몸통", "dH피부", "dH머리칼", "dH얼굴"))
|
||||
for r in rows:
|
||||
def g(p):
|
||||
x = r.get(p)
|
||||
return ("%+.1f" % x["dhue"]) if (x and "dhue" in x) else "ref"
|
||||
print("%-28s %8s %8s %8s %8s" % (r["name"], g("body3"), g("Head"), g("Hair05"), g("Face")))
|
||||
with open(os.path.join(RAW, "..", "metrics.json"), "w", encoding="utf-8") as fp:
|
||||
json.dump(rows, fp, ensure_ascii=False, indent=1, default=float)
|
||||
|
||||
|
||||
main()
|
||||
Loading…
Reference in New Issue