1295 lines
66 KiB
C#
1295 lines
66 KiB
C#
// ─────────────────────────────────────────────────────────────────────────────
|
||
// 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) { Render(new Color(0.10f, 0.10f, 0.12f)); Save(Render(new Color(0.10f, 0.10f, 0.12f)), RAW + name + ".png"); } // 1장 버리고 찍는다: 메시를 갈아끼운 직후 첫 프레임은 스킨이 아직 안 올라온다
|
||
|
||
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 static Vector3 SrgbToLin(Vector3 v) { return new Vector3(S2L(v.x), S2L(v.y), S2L(v.z)); }
|
||
|
||
// 🔴 SrgbAvg = 평균을 sRGB(보이는 밝기)에서 낸다. 선형광 평균은 텍스처의 어두운 선을
|
||
// 날려 버려 화면에서 머리칼/피부가 밝고 진해진다(1~3회차 실측: L +20 · S +0.11).
|
||
public static bool SrgbAvg = true;
|
||
// RestoreSat = 팔레트 칸의 채도를 「그 칸에 속한 덩어리들의 원본 채도」로 되돌린다.
|
||
public static bool RestoreSat = true;
|
||
// 부위 그룹별 채도/명도 보정 계수 (1 = 그대로). Hue 는 절대 건드리지 않는다.
|
||
public static Dictionary<string, float> SGain = new Dictionary<string, float>();
|
||
public static Dictionary<string, float> VGain = new Dictionary<string, float>();
|
||
public static string[] palGroup = new string[0];
|
||
public static string Group(string meshName)
|
||
{
|
||
if (meshName == "Head") return "Skin";
|
||
if (meshName == "Hair05") return "Hair";
|
||
return "M05";
|
||
}
|
||
static Vector3 ToWorkSrgb(Vector3 v) { return SrgbAvg ? v : LinToSrgb(v); }
|
||
static Lab ToLabAuto(Vector3 v) { return ToLab(SrgbAvg ? SrgbToLin(v) : v); }
|
||
|
||
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] = SrgbAvg ? new Vector3(px[i].r / 255f, px[i].g / 255f, px[i].b / 255f) : 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 = ToLabAuto(regions[roots[i]].Col);
|
||
float bd = float.MaxValue; int bj = 0;
|
||
for (int j = 0; j < N; j++) { float d = LabD(lab, ToLabAuto(pal[j])); if (d < bd) { bd = d; bj = j; } }
|
||
assign[i] = bj;
|
||
}
|
||
// 🔴 Hue 복원: 팔레트 칸의 H/S 를 그 칸 소속 덩어리들의 원본 H/S 로 되돌린다(채도·면적 가중)
|
||
var palS = new Vector3[N];
|
||
palGroup = new string[N];
|
||
for (int j = 0; j < N; j++)
|
||
{
|
||
double hx = 0, hy = 0, sw = 0, ssum = 0, aw = 0;
|
||
var garea = new double[M];
|
||
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(ToWorkSrgb(reg.Col), out h, out s, out v);
|
||
ssum += s * reg.area; aw += reg.area;
|
||
garea[reg.mesh] += reg.area;
|
||
}
|
||
float ph, ps, pv; RgbToHsv(ToWorkSrgb(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 = (RestoreSat && aw > 1e-6) ? (float)(ssum / aw) : ps;
|
||
// 🔴 부위별 채도·명도 보정 — Hue 는 손대지 않는다(발주 §1 「V 만 조절 · Hue ±8°」).
|
||
// 3~4회차 실측에서 팔레트가 「지금」보다 진하고(S +0.10) 밝게(L +12~23) 나온 것을 되돌린다.
|
||
int dom = 0; for (int m = 1; m < M; m++) if (garea[m] > garea[dom]) dom = m;
|
||
string grp = Group(names[dom]);
|
||
float sg, vg;
|
||
sat *= SGain.TryGetValue(grp, out sg) ? sg : 1f;
|
||
pv *= VGain.TryGetValue(grp, out vg) ? vg : 1f;
|
||
palS[j] = HsvToRgb(hue, sat, pv);
|
||
palGroup[j] = grp;
|
||
}
|
||
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(ToWorkSrgb(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} · srcBW {3} newBW {4} bindposes {5} normals {6} tangents {7}", names[m], srcs[m].vertexCount, nm.vertexCount, srcs[m].boneWeights.Length, nm.boneWeights.Length, nm.bindposes.Length, nm.normals.Length, nm.tangents.Length));
|
||
}
|
||
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(ToWorkSrgb(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(ToLabAuto(a.Col), ToLabAuto(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(ToLabAuto).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] = ToLabAuto(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;
|
||
// 🔴 레거시 boneWeights(4본) 로 옮긴다 — SetBoneWeights(bonesPerVertex, BoneWeight1[]) 로 만든 메시를
|
||
// .asset 으로 저장하면 SkinnedMeshRenderer 가 "mesh data size and vertex stride" 로 렌더를 멈춘다(실측).
|
||
var bws = src.boneWeights;
|
||
|
||
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 nbw = new List<BoneWeight>();
|
||
|
||
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 (bws != null && bws.Length > 0) nbw.Add(bws[vi]);
|
||
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);
|
||
mesh.bindposes = src.bindposes;
|
||
if (nbw.Count == nv.Count) mesh.boneWeights = nbw.ToArray();
|
||
mesh.UploadMeshData(false);
|
||
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"); }
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// WLXIter3 — 3회차: 얼굴을 고친 상태에서 덩어리·팔레트·셰이드 전수 + 덩어리별 색단계 실측
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
public static class WLXIter3
|
||
{
|
||
public static Texture2D F32, F64, FV1, H64, H32;
|
||
|
||
public static void LoadTex()
|
||
{
|
||
F32 = AssetDatabase.LoadAssetAtPath<Texture2D>(WLXBake.TEX_DIR + "face02_P32.png");
|
||
F64 = AssetDatabase.LoadAssetAtPath<Texture2D>(WLXBake.TEX_DIR + "face02_P64.png");
|
||
FV1 = AssetDatabase.LoadAssetAtPath<Texture2D>(WLXBake.TEX_DIR + "face02_V1.png");
|
||
H64 = AssetDatabase.LoadAssetAtPath<Texture2D>(WLXBake.TEX_DIR + "Hair05_P64.png");
|
||
H32 = AssetDatabase.LoadAssetAtPath<Texture2D>(WLXBake.TEX_DIR + "Hair05_P32.png");
|
||
}
|
||
|
||
public 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;
|
||
}
|
||
}
|
||
|
||
/// <summary>덩어리 ID 를 색으로 찍는다(빛 0) — 「부위(덩어리)당 색 단계」를 재기 위한 지도.</summary>
|
||
public static void ShootIdMap(WLXBake.Result res, string name)
|
||
{
|
||
int N = res.paletteUsed, G = Mathf.CeilToInt(Mathf.Sqrt(N)), S = G * 8;
|
||
var t = new Texture2D(S, S, TextureFormat.RGBA32, false);
|
||
t.filterMode = FilterMode.Point; t.wrapMode = TextureWrapMode.Clamp;
|
||
var px = new Color32[S * S];
|
||
for (int i = 0; i < px.Length; i++) px[i] = new Color32(0, 0, 0, 255);
|
||
for (int j = 0; j < N; j++)
|
||
{
|
||
int gx = j % G, gy = j / G;
|
||
var c = new Color32((byte)(8 + j * 7), (byte)(200 - j * 5), (byte)(40 + j * 3), 255);
|
||
for (int y = 0; y < 8; y++) for (int x = 0; x < 8; x++) px[(gy * 8 + y) * S + gx * 8 + x] = c;
|
||
}
|
||
t.SetPixels32(px); t.Apply();
|
||
var sh = Shader.Find("Unlit/Texture");
|
||
var keep = new List<SkinnedMeshRenderer>();
|
||
WLXCap.NoLook();
|
||
for (int i = 0; i < WLXCap.Smrs.Length; i++)
|
||
{
|
||
var r = WLXCap.Smrs[i];
|
||
Mesh nm;
|
||
if (res.meshes.TryGetValue(r.name, out nm))
|
||
{
|
||
r.sharedMesh = nm;
|
||
var m = new Material(sh) { name = "TMP_id" }; m.SetTexture("_MainTex", t);
|
||
r.sharedMaterials = Enumerable.Repeat(m, WLXCap.OrigMats(i).Length).ToArray();
|
||
}
|
||
else { r.enabled = false; keep.Add(r); }
|
||
}
|
||
WLXCap.Shoot(name);
|
||
foreach (var r in keep) r.enabled = true;
|
||
UnityEngine.Object.DestroyImmediate(t);
|
||
}
|
||
|
||
static readonly int[] CH = { 20, 40, 80 };
|
||
static readonly int[] PN = { 10, 16, 24 };
|
||
|
||
public static void Run()
|
||
{
|
||
try
|
||
{
|
||
WLXCap.Setup();
|
||
WLXBake.SaveAssets = false;
|
||
LoadTex();
|
||
WLXCap.L("tex loaded f32=" + (F32 != null) + " h64=" + (H64 != null));
|
||
|
||
foreach (var ch in CH)
|
||
foreach (var pn in PN)
|
||
{
|
||
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));
|
||
foreach (var sh in new[] { 3, 2 })
|
||
{
|
||
ApplyPalette(res, F32, null);
|
||
WLXCap.ApplyLook(WLXCap.LookOpt(true, true, true, false, sh));
|
||
WLXCap.Shoot("i3_" + tag + "_s" + sh);
|
||
WLXCap.NoLook(); WLXCap.RestoreMeshes();
|
||
}
|
||
// 머리칼 텍스처안
|
||
ApplyPalette(res, F32, H64);
|
||
WLXCap.ApplyLook(WLXCap.LookOpt(true, true, true, false, 3));
|
||
WLXCap.Shoot("i3_" + tag + "_s3_hairtex");
|
||
WLXCap.NoLook(); WLXCap.RestoreMeshes();
|
||
ShootIdMap(res, "idmap_" + tag);
|
||
WLXCap.RestoreMats(); WLXCap.RestoreMeshes();
|
||
}
|
||
}
|
||
catch (Exception e) { WLXCap.L("EXCEPTION " + e); }
|
||
finally { WLXCap.Teardown(); WLXCap.Flush("WL814x_ITER3.txt"); }
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// WLXIter4 — 4회차: 팔레트 색을 고르는 「평균 공간」과 「채도 복원」이 화면 색조에 미치는 영향
|
||
// 3회차 실측에서 머리칼/피부가 원본보다 밝고(L +20) 진해졌다(S +0.11) → 원인 후보 2개를 A/B
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
public static class WLXIter4
|
||
{
|
||
public static void Run()
|
||
{
|
||
try
|
||
{
|
||
WLXCap.Setup();
|
||
WLXBake.SaveAssets = false;
|
||
WLXIter3.LoadTex();
|
||
var combos = new[] {
|
||
new { name = "srgbSat", srgb = true, sat = true },
|
||
new { name = "srgbNos", srgb = true, sat = false },
|
||
new { name = "linNos", srgb = false, sat = false },
|
||
};
|
||
foreach (var c in combos)
|
||
{
|
||
WLXBake.SrgbAvg = c.srgb; WLXBake.RestoreSat = c.sat;
|
||
WLXBake.Bake(40, 16, "v_" + c.name);
|
||
var res = WLXBake.Last;
|
||
WLXCap.L(string.Format("{0}: regions {1} pal {2} dHue {3:F2}/{4:F2}", c.name, res.regionCount, res.paletteUsed, res.meanDHue, res.maxDHue));
|
||
foreach (var sh in new[] { 3, 2 })
|
||
{
|
||
WLXIter3.ApplyPalette(res, WLXIter3.F32, null);
|
||
WLXCap.ApplyLook(WLXCap.LookOpt(true, true, true, false, sh));
|
||
WLXCap.Shoot("i4_" + c.name + "_s" + sh);
|
||
WLXCap.NoLook(); WLXCap.RestoreMeshes();
|
||
}
|
||
}
|
||
WLXBake.SrgbAvg = true; WLXBake.RestoreSat = true;
|
||
}
|
||
catch (Exception e) { WLXCap.L("EXCEPTION " + e); }
|
||
finally { WLXCap.Teardown(); WLXCap.Flush("WL814x_ITER4.txt"); }
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// WLXIter5 — 5회차: 「지금」과 색조(채도·밝기)를 맞추는 보정 계수 탐색 (Hue 무변경)
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
public static class WLXIter5
|
||
{
|
||
static void SetGain(float sM, float sS, float sH, float vM, float vS, float vH)
|
||
{
|
||
WLXBake.SGain = new Dictionary<string, float> { { "M05", sM }, { "Skin", sS }, { "Hair", sH } };
|
||
WLXBake.VGain = new Dictionary<string, float> { { "M05", vM }, { "Skin", vS }, { "Hair", vH } };
|
||
}
|
||
|
||
public static void Run()
|
||
{
|
||
try
|
||
{
|
||
WLXCap.Setup();
|
||
WLXBake.SaveAssets = false;
|
||
WLXIter3.LoadTex();
|
||
WLXBake.SrgbAvg = true; WLXBake.RestoreSat = true;
|
||
|
||
var sets = new[] {
|
||
new { n = "g0none", s = new[]{1f,1f,1f}, v = new[]{1f,1f,1f} },
|
||
new { n = "g1soft", s = new[]{0.80f,0.86f,0.85f}, v = new[]{1.00f,0.93f,0.93f} },
|
||
new { n = "g2hard", s = new[]{0.72f,0.78f,0.78f}, v = new[]{1.00f,0.86f,0.88f} },
|
||
new { n = "g3sat", s = new[]{0.80f,0.86f,0.85f}, v = new[]{1f,1f,1f} },
|
||
};
|
||
foreach (var g in sets)
|
||
{
|
||
SetGain(g.s[0], g.s[1], g.s[2], g.v[0], g.v[1], g.v[2]);
|
||
WLXBake.Bake(40, 16, "g_" + g.n);
|
||
var res = WLXBake.Last;
|
||
WLXCap.L(string.Format("{0}: dHue {1:F2}/{2:F2}", g.n, res.meanDHue, res.maxDHue));
|
||
WLXIter3.ApplyPalette(res, WLXIter3.F32, null);
|
||
WLXCap.ApplyLook(WLXCap.LookOpt(true, true, true, false, 3));
|
||
WLXCap.Shoot("i5_" + g.n + "_s3");
|
||
WLXCap.NoLook(); WLXCap.RestoreMeshes();
|
||
}
|
||
SetGain(1f, 1f, 1f, 1f, 1f, 1f);
|
||
}
|
||
catch (Exception e) { WLXCap.L("EXCEPTION " + e); }
|
||
finally { WLXCap.Teardown(); WLXCap.Flush("WL814x_ITER5.txt"); }
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// WLXFinal — 최종 채택본 굽기 + 에셋 배선 (덩어리 40 · 팔레트 16 · sRGB 평균 · g1soft 보정)
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
public static class WLXFinal
|
||
{
|
||
const string MAT = "Assets/WL/Look/Character/Materials/";
|
||
const string TEX = "Assets/WL/Look/Character/Textures/";
|
||
const string SO = "Assets/WL/Look/Character/Resources/WL/WLCharacterPaletteSettings.asset";
|
||
public const int CHUNKS = 40, PAL = 16;
|
||
public const float SHADES = 3f;
|
||
|
||
static readonly StringBuilder sb = new StringBuilder();
|
||
static void L(string s) { sb.AppendLine(s); }
|
||
|
||
public static void SetFinalGain()
|
||
{
|
||
WLXBake.SrgbAvg = true; WLXBake.RestoreSat = true;
|
||
WLXBake.SGain = new Dictionary<string, float> { { "M05", 0.80f }, { "Skin", 0.86f }, { "Hair", 0.85f } };
|
||
WLXBake.VGain = new Dictionary<string, float> { { "M05", 1.00f }, { "Skin", 0.93f }, { "Hair", 0.93f } };
|
||
}
|
||
|
||
public static string Build()
|
||
{
|
||
sb.Length = 0;
|
||
try
|
||
{
|
||
SetFinalGain();
|
||
WLXBake.SaveAssets = true;
|
||
L(WLXBake.Bake(CHUNKS, PAL, "FINAL"));
|
||
var res = WLXBake.Last;
|
||
var pal = AssetDatabase.LoadAssetAtPath<Texture2D>(res.palPath);
|
||
|
||
// 얼굴 점텍스처(1텍셀 = 1화면 px) — 814v V1 의 격자 1칸을 1텍셀로
|
||
string fp = WLXBake.MakePointTex(TEX + "face02_V1.png", 32, "face02_P32", true);
|
||
var faceTex = AssetDatabase.LoadAssetAtPath<Texture2D>(fp);
|
||
L("face tex " + fp + " " + (faceTex != null ? faceTex.width + "x" + faceTex.height : "?"));
|
||
|
||
// 머티리얼 복사본
|
||
var mM05 = MakeMat("M05_ToonS", "M05_ToonP", pal, true);
|
||
var mSkin = MakeMat("Skin_ToonS", "Skin_ToonP", pal, true);
|
||
var mHair = MakeMat("Hair05_ToonS", "Hair05_ToonP", pal, true);
|
||
var mFace = MakeMat("face02_ToonS", "face02_ToonP", faceTex, false);
|
||
|
||
// SO
|
||
var so = AssetDatabase.LoadAssetAtPath<WL.Look.Character.WLCharacterPaletteSettings>(SO);
|
||
if (so == null)
|
||
{
|
||
so = ScriptableObject.CreateInstance<WL.Look.Character.WLCharacterPaletteSettings>();
|
||
Directory.CreateDirectory(Path.GetDirectoryName(SO));
|
||
AssetDatabase.CreateAsset(so, SO);
|
||
}
|
||
var names = new[] { "Arm_m05", "Body_m05", "Leg_M05", "Head", "Hair05", "Face" };
|
||
var meshes = new Mesh[names.Length];
|
||
var mats = new Material[names.Length];
|
||
for (int i = 0; i < names.Length; i++)
|
||
{
|
||
if (names[i] != "Face")
|
||
meshes[i] = AssetDatabase.LoadAssetAtPath<Mesh>(WLXBake.MESH_DIR + names[i] + "_PAL_FINAL.asset");
|
||
mats[i] = names[i] == "Head" ? mSkin : names[i] == "Hair05" ? mHair : names[i] == "Face" ? mFace : mM05;
|
||
L(string.Format(" [{0}] {1,-9} mesh={2} mat={3}", i, names[i], meshes[i] != null ? meshes[i].name : "-", mats[i] != null ? mats[i].name : "-"));
|
||
}
|
||
so.enabled_ = 1; so.verboseLog = 0;
|
||
so.rendererNames = names; so.paletteMeshes = meshes; so.paletteMaterials = mats;
|
||
EditorUtility.SetDirty(so);
|
||
|
||
// 임시 산출물 정리 (비교용으로 만든 팔레트/텍스처)
|
||
foreach (var g in AssetDatabase.FindAssets("WLPalette_", new[] { "Assets/WL/Look/Character/Textures" }))
|
||
{
|
||
string p = AssetDatabase.GUIDToAssetPath(g);
|
||
if (!p.EndsWith("WLPalette_FINAL.png")) { AssetDatabase.DeleteAsset(p); L(" del " + p); }
|
||
}
|
||
foreach (var f in new[] { "face02_P64.png", "Hair05_P32.png" })
|
||
if (AssetDatabase.LoadAssetAtPath<Texture2D>(TEX + f) != null) { AssetDatabase.DeleteAsset(TEX + f); L(" del " + TEX + f); }
|
||
foreach (var g in AssetDatabase.FindAssets("_PAL_", new[] { "Assets/WL/Look/Character/Meshes" }))
|
||
{
|
||
string p = AssetDatabase.GUIDToAssetPath(g);
|
||
if (!p.Contains("_PAL_FINAL")) { AssetDatabase.DeleteAsset(p); L(" del " + p); }
|
||
}
|
||
AssetDatabase.SaveAssets(); AssetDatabase.Refresh();
|
||
L("DONE");
|
||
}
|
||
catch (Exception e) { L("EXCEPTION " + e); }
|
||
finally { File.WriteAllText("AgentScripts/WL814x_FINAL.txt", sb.ToString(), new UTF8Encoding(true)); }
|
||
return sb.ToString();
|
||
}
|
||
|
||
static Material MakeMat(string src, string dst, Texture2D tex, bool toon)
|
||
{
|
||
var s = AssetDatabase.LoadAssetAtPath<Material>(MAT + src + ".mat");
|
||
if (s == null) { L("!! no src mat " + src); return null; }
|
||
var m = AssetDatabase.LoadAssetAtPath<Material>(MAT + dst + ".mat");
|
||
if (m == null) { m = new Material(s); AssetDatabase.CreateAsset(m, MAT + dst + ".mat"); }
|
||
else { m.shader = s.shader; m.CopyPropertiesFromMaterial(s); }
|
||
foreach (var pn in new[] { "_BaseMap", "_ShadowBaseMap", "_MainTex" }) if (m.HasProperty(pn)) m.SetTexture(pn, tex);
|
||
if (toon && m.HasProperty("_Shades")) m.SetFloat("_Shades", SHADES); // 부위당 색 단계 ≤ 4
|
||
EditorUtility.SetDirty(m);
|
||
L(" mat " + dst + " shader=" + m.shader.name + " tex=" + (tex != null ? tex.name : "-") + (toon ? " shades=" + SHADES : ""));
|
||
return m;
|
||
}
|
||
|
||
/// <summary>최종 검증 — 런타임 경로(WLCharacterLook.Apply)로 적용해서 찍는다 + 되돌리기 확인.</summary>
|
||
public static void Verify()
|
||
{
|
||
try
|
||
{
|
||
WLXCap.Setup();
|
||
var lookCfg = WL.Look.Character.WLCharacterLookSettings.Instance;
|
||
var palCfg = WL.Look.Character.WLCharacterPaletteSettings.Instance;
|
||
WLXCap.L("palette SO = " + (palCfg != null ? "found enabled_=" + palCfg.enabled_ : "NULL"));
|
||
// 🔴 워밍업: 세션에서 스킨 메시를 처음 갈아끼운 직후의 첫 Camera.Render() 는
|
||
// "mesh data size and vertex stride" 로 렌더가 멈춘다(에디터 한정 · 실측).
|
||
// 한 번 갈아끼우고 버리는 렌더를 돌린 뒤 본 촬영을 한다.
|
||
palCfg.enabled_ = 1;
|
||
WL.Look.Character.WLCharacterLook.Apply(WLXCap.Root);
|
||
for (int w = 0; w < 3; w++) WLXCap.Render(Color.black);
|
||
WLXCap.RestoreMeshes(); WLXCap.RestoreMats();
|
||
for (int w = 0; w < 2; w++) WLXCap.Render(Color.black);
|
||
|
||
// ① enabled_ = 0 → 814s+814t 상태 100 % 여야 한다
|
||
int keep = palCfg.enabled_;
|
||
palCfg.enabled_ = 0;
|
||
WL.Look.Character.WLCharacterLook.Apply(WLXCap.Root);
|
||
WLXCap.L("off: " + WL.Look.Character.WLCharacterPalette.LastLog);
|
||
for (int i = 0; i < WLXCap.Smrs.Length; i++)
|
||
WLXCap.L(" off " + WLXCap.Smrs[i].name + " mesh=" + WLXCap.Smrs[i].sharedMesh.name + " mat=" + WLXCap.Smrs[i].sharedMaterial.name);
|
||
WLXCap.ApplyLook(WLXCap.LookOpt(true, true, true, true, 0));
|
||
WLXCap.Shoot("z_off_now"); WLXCap.NoLook();
|
||
|
||
// ② enabled_ = 1 → 팔레트
|
||
palCfg.enabled_ = 1;
|
||
WL.Look.Character.WLCharacterLook.Apply(WLXCap.Root);
|
||
// 🔴 스킨 메시를 갈아끼운 직후 에디터에서 곧바로 Camera.Render() 하면
|
||
// "mesh data size and vertex stride" 로 렌더가 멈춘다 → 에디터 틱을 한 번 돌린다.
|
||
AssetDatabase.Refresh();
|
||
EditorApplication.QueuePlayerLoopUpdate();
|
||
UnityEditorInternal.InternalEditorUtility.RepaintAllViews();
|
||
WLXCap.L("on: " + WL.Look.Character.WLCharacterPalette.LastLog);
|
||
for (int i = 0; i < WLXCap.Smrs.Length; i++)
|
||
{
|
||
var r0 = WLXCap.Smrs[i]; var m0 = r0.sharedMaterial;
|
||
WLXCap.L(" on " + r0.name + " mesh=" + r0.sharedMesh.name + " mat=" + m0.name
|
||
+ " tris=" + (r0.sharedMesh.triangles.Length / 3) + " verts=" + r0.sharedMesh.vertexCount
|
||
+ " en=" + r0.enabled + " vis=" + r0.isVisible + " shader=" + (m0.shader != null ? m0.shader.name : "NULL")
|
||
+ " base=" + (m0.HasProperty("_BaseMap") ? (m0.GetTexture("_BaseMap") != null ? m0.GetTexture("_BaseMap").name : "NULL") : "-")
|
||
+ " shadow=" + (m0.HasProperty("_ShadowBaseMap") ? (m0.GetTexture("_ShadowBaseMap") != null ? m0.GetTexture("_ShadowBaseMap").name : "NULL") : "-")
|
||
+ " shades=" + (m0.HasProperty("_Shades") ? m0.GetFloat("_Shades").ToString("F1") : "-")
|
||
+ " bnd=" + r0.bounds.size.ToString("F2") + " lay=" + r0.gameObject.layer);
|
||
}
|
||
WLXCap.ApplyLook(WLXCap.LookOpt(true, true, true, false, 0));
|
||
WLXCap.Shoot("z_final"); WLXCap.NoLook();
|
||
// 림을 켠 종합안도 한 장(대조)
|
||
WLXCap.ApplyLook(WLXCap.LookOpt(true, true, true, true, 0));
|
||
WLXCap.Shoot("z_final_rimon"); WLXCap.NoLook();
|
||
// 외곽선 없는 것(외곽선 1px 확인용)
|
||
WLXCap.ApplyLook(WLXCap.LookOpt(false, true, true, false, 0));
|
||
WLXCap.Shoot("z_final_noout"); WLXCap.NoLook();
|
||
|
||
// 덩어리 지도
|
||
WLXFinal.SetFinalGain();
|
||
WLXBake.SaveAssets = false;
|
||
WLXBake.Bake(CHUNKS, PAL, "idm");
|
||
WLXIter3.ShootIdMap(WLXBake.Last, "idmap_FINAL");
|
||
AssetDatabase.DeleteAsset(WLXBake.Last.palPath);
|
||
|
||
palCfg.enabled_ = keep;
|
||
}
|
||
catch (Exception e) { WLXCap.L("EXCEPTION " + e); }
|
||
finally { WLXCap.Teardown(); WLXCap.Flush("WL814x_VERIFY.txt"); }
|
||
}
|
||
|
||
/// <summary>회전 떨림 — 연속 8프레임(1°씩)의 픽셀 변화율. 814u 와 같은 축.</summary>
|
||
public static void Jitter()
|
||
{
|
||
try
|
||
{
|
||
WLXCap.Setup();
|
||
var palCfg = WL.Look.Character.WLCharacterPaletteSettings.Instance;
|
||
float yaw0 = WLXCap.Root.transform.eulerAngles.y;
|
||
foreach (var on in new[] { 0, 1 })
|
||
{
|
||
palCfg.enabled_ = on;
|
||
WL.Look.Character.WLCharacterLook.Apply(WLXCap.Root);
|
||
WLXCap.ApplyLook(WLXCap.LookOpt(true, true, true, on == 0, 0));
|
||
var prev = (WLXCap.Shot)null;
|
||
double sum = 0; int n = 0; double mx = 0;
|
||
for (int f = 0; f < 8; f++)
|
||
{
|
||
WLXCap.Root.transform.rotation = Quaternion.Euler(0, yaw0 + f, 0);
|
||
var s = WLXCap.Render(new Color(0.10f, 0.10f, 0.12f));
|
||
if (prev != null)
|
||
{
|
||
int diff = 0, tot = 0;
|
||
for (int i = 0; i < s.px.Length; i++)
|
||
{
|
||
var a = prev.px[i]; var b = s.px[i];
|
||
bool bg = a.r == 25 && a.g == 25 && a.b == 30 && b.r == 25 && b.g == 25 && b.b == 30;
|
||
if (bg) continue;
|
||
tot++;
|
||
if (Mathf.Abs(a.r - b.r) + Mathf.Abs(a.g - b.g) + Mathf.Abs(a.b - b.b) > 12) diff++;
|
||
}
|
||
double r = tot > 0 ? 100.0 * diff / tot : 0;
|
||
sum += r; n++; if (r > mx) mx = r;
|
||
}
|
||
prev = s;
|
||
}
|
||
WLXCap.L(string.Format("회전 떨림 {0}: 평균 {1:F2} % · 최대 {2:F2} % (8프레임 · 1°/프레임)", on == 0 ? "지금" : "팔레트", sum / Mathf.Max(n, 1), mx));
|
||
WLXCap.NoLook();
|
||
}
|
||
WLXCap.Root.transform.rotation = Quaternion.Euler(0, yaw0, 0);
|
||
palCfg.enabled_ = 1;
|
||
}
|
||
catch (Exception e) { WLXCap.L("EXCEPTION " + e); }
|
||
finally { WLXCap.Teardown(); WLXCap.Flush("WL814x_JITTER.txt"); }
|
||
}
|
||
|
||
/// <summary>저장된 메시 에셋이 스킨 데이터를 제대로 들고 있는지 확인(렌더 정지 원인 추적).</summary>
|
||
public static string CheckMesh()
|
||
{
|
||
var sb2 = new StringBuilder();
|
||
foreach (var n in new[] { "Arm_m05", "Body_m05", "Leg_M05", "Head", "Hair05" })
|
||
{
|
||
var p = WLXBake.MESH_DIR + n + "_PAL_FINAL.asset";
|
||
var m = AssetDatabase.LoadAssetAtPath<Mesh>(p);
|
||
var src = AssetDatabase.LoadAllAssetsAtPath("Assets/Suriyun/Characters/RedKnight/FBX/Characters/M05.fbx").OfType<Mesh>().FirstOrDefault(x => x.name == n);
|
||
sb2.AppendLine(n + " : " + (m == null ? "NULL" :
|
||
"verts " + m.vertexCount + " bw " + m.boneWeights.Length + " bind " + m.bindposes.Length
|
||
+ " attrs [" + string.Join(",", m.GetVertexAttributes().Select(a => a.attribute + ":" + a.format + "x" + a.dimension + "@" + a.stream)) + "]"));
|
||
if (src != null)
|
||
sb2.AppendLine(" src: attrs [" + string.Join(",", src.GetVertexAttributes().Select(a => a.attribute + ":" + a.format + "x" + a.dimension + "@" + a.stream)) + "]");
|
||
}
|
||
File.WriteAllText("AgentScripts/WL814x_MESHCHK.txt", sb2.ToString(), new UTF8Encoding(true));
|
||
return sb2.ToString();
|
||
}
|
||
|
||
/// <summary>메모리 메시 vs 에셋 메시 — 렌더 정지의 원인이 「에셋 저장」인지 가린다.</summary>
|
||
public static void Diag2()
|
||
{
|
||
try
|
||
{
|
||
WLXCap.Setup();
|
||
SetFinalGain();
|
||
WLXBake.SaveAssets = false;
|
||
WLXBake.Bake(CHUNKS, PAL, "memchk");
|
||
var res = WLXBake.Last;
|
||
WLXIter3.ApplyPalette(res, WLXIter3.F32, null);
|
||
WLXCap.ApplyLook(WLXCap.LookOpt(true, true, true, false, 0));
|
||
WLXCap.Shoot("q_mem"); WLXCap.NoLook(); WLXCap.RestoreMeshes(); WLXCap.RestoreMats();
|
||
|
||
// 같은 팔레트 텍스처로 에셋 메시만 바꿔 끼운다
|
||
var palTex = AssetDatabase.LoadAssetAtPath<Texture2D>(res.palPath);
|
||
foreach (var r in WLXCap.Smrs)
|
||
{
|
||
var am = AssetDatabase.LoadAssetAtPath<Mesh>(WLXBake.MESH_DIR + r.name + "_PAL_FINAL.asset");
|
||
if (am == null) continue;
|
||
r.sharedMesh = am;
|
||
WLXCap.L("asset " + r.name + " verts " + am.vertexCount + " bw " + am.boneWeights.Length + " readable " + am.isReadable);
|
||
}
|
||
var finalPal = AssetDatabase.LoadAssetAtPath<Texture2D>(WLXBake.TEX_DIR + "WLPalette_FINAL.png");
|
||
for (int i = 0; i < WLXCap.Smrs.Length; i++)
|
||
{
|
||
var mats = (Material[])WLXCap.OrigMats(i).Clone();
|
||
var tex = WLXCap.Smrs[i].name == "Face" ? WLXIter3.F32 : finalPal;
|
||
for (int m = 0; m < mats.Length; m++)
|
||
{
|
||
var c = new Material(mats[m]) { name = "TMP2" };
|
||
foreach (var pn in new[] { "_BaseMap", "_ShadowBaseMap", "_MainTex" }) if (c.HasProperty(pn)) c.SetTexture(pn, tex);
|
||
if (c.HasProperty("_Shades")) c.SetFloat("_Shades", 3f);
|
||
mats[m] = c;
|
||
}
|
||
WLXCap.Smrs[i].sharedMaterials = mats;
|
||
}
|
||
WLXCap.ApplyLook(WLXCap.LookOpt(true, true, true, false, 0));
|
||
WLXCap.Shoot("q_asset"); WLXCap.NoLook();
|
||
// 같은 런에서 런타임 경로(SO 머티리얼 + SO 메시)로도 찍어 본다
|
||
WLXCap.RestoreMeshes(); WLXCap.RestoreMats();
|
||
WL.Look.Character.WLCharacterLook.Apply(WLXCap.Root);
|
||
WLXCap.L("runtime: " + WL.Look.Character.WLCharacterPalette.LastLog);
|
||
WLXCap.ApplyLook(WLXCap.LookOpt(true, true, true, false, 0));
|
||
WLXCap.Shoot("q_runtime"); WLXCap.NoLook();
|
||
AssetDatabase.DeleteAsset(res.palPath);
|
||
}
|
||
catch (Exception e) { WLXCap.L("EXCEPTION " + e); }
|
||
finally { WLXCap.Teardown(); WLXCap.Flush("WL814x_DIAG2.txt"); }
|
||
}
|
||
}
|