Project_WL/AgentScripts/WL816b_Bake.cs

546 lines
28 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// WL-816b — 무기 팔레트 아틀라스 UV 굽기 (814x `WLXBake` 방식을 무기(정적 메시)용으로)
// run_script --file AgentScripts/WL816b_Bake.cs --entry WL816bBake.Sweep (에디트 모드)
//
// 🔴 원본 수정 0 — 원본 메시·텍스처는 **읽기만** 한다. 산출물은 전부 Assets/WL/Look/Character/ 아래 신규 파일.
// 🔴 Hue 는 원본 복원 + ±8° 하드 클램프(814v 올리브·814x 초록 사고 재발 방지 · 전역 채널 이득 금지).
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEngine;
public static class WL816bBake
{
public const string MESH_DIR = "Assets/WL/Look/Character/Meshes/";
public const string TEX_DIR = "Assets/WL/Look/Character/Textures/";
public const string MAT_DIR = "Assets/WL/Look/Character/Materials/";
public const string WDIR = "Assets/Res_Addr/Weapon/";
const int CELL = 8;
const float HUE_LIMIT = 8f; // 🔴 ±8°
static readonly StringBuilder sb = new StringBuilder();
static void L(string s) { sb.AppendLine(s); }
// ───────────────────────────── 색공간 (814x §4: 평균은 sRGB 에서 낸다)
static float S2L(float c) { return c <= 0.04045f ? c / 12.92f : Mathf.Pow((c + 0.055f) / 1.055f, 2.4f); }
public struct Lab { public float L, a, b; }
public static Lab ToLab(Vector3 srgb)
{
var lin = new Vector3(S2L(srgb.x), S2L(srgb.y), S2L(srgb.z));
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) };
}
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 c, out float h, out float s, out float v)
{
float r = c.x, g = c.y, b = c.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; }
// ───────────────────────────── 텍스처 읽기 (원본 임포터 무수정)
/// <summary>비readable 텍스처를 임포터를 건드리지 않고 sRGB 바이트로 읽는다(원본 meta 수정 0).</summary>
public static Texture2D ReadAny(Texture src)
{
var rt = RenderTexture.GetTemporary(src.width, src.height, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB);
var t2d = src as Texture2D; var prevF = t2d != null ? t2d.filterMode : FilterMode.Point;
if (t2d != null) t2d.filterMode = FilterMode.Point;
Graphics.Blit(src, rt);
if (t2d != null) t2d.filterMode = prevF;
var prev = RenderTexture.active; RenderTexture.active = rt;
var t = new Texture2D(src.width, src.height, TextureFormat.RGBA32, false);
t.ReadPixels(new Rect(0, 0, src.width, src.height), 0, 0); t.Apply();
RenderTexture.active = prev; RenderTexture.ReleaseTemporary(rt);
return t;
}
class Tex { public int w, h; public Vector3[] px; }
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;
Texture2D tmp = null; bool destroy = true;
if (path.EndsWith(".png", StringComparison.OrdinalIgnoreCase) && File.Exists(path))
{
tmp = new Texture2D(2, 2, TextureFormat.RGBA32, false);
tmp.LoadImage(File.ReadAllBytes(path)); // 🔴 디스크 원본(무압축) — 압축 아티팩트 배제
}
else
{
tmp = ReadAny(AssetDatabase.LoadAssetAtPath<Texture2D>(path)); // .psd 등은 RT 블릿(임포터 무수정)
}
var p32 = tmp.GetPixels32();
t = new Tex { w = tmp.width, h = tmp.height, px = new Vector3[p32.Length] };
for (int i = 0; i < p32.Length; i++) t.px[i] = new Vector3(p32[i].r / 255f, p32[i].g / 255f, p32[i].b / 255f);
if (destroy) 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.px[y * t.w + x];
}
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;
}
// ───────────────────────────── 덩어리
class Face { public int i0, i1, i2; public Vector3 col; public float area; }
class Region { public Vector3 sum; public float area; public int count; public float hx, hy, sw; public Vector3 Col { get { return sum / Mathf.Max(area, 1e-9f); } } }
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 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 void AccHue(Region r, Vector3 c, float area)
{
float h, s, v; RgbToHsv(c, 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 int Merge(int[] uf, Region[] reg, List<KeyValuePair<int, int>> adj, int target)
{
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;
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; }
if (count <= 2) break;
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--;
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 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;
}
// ───────────────────────────── 결과
public class WRes
{
public string weapon; public Mesh mesh; public Vector3[] palette;
public int regions, palUsed, vBefore, vAfter, tris;
public float meanDHue, maxDHue; public string palPath, meshPath;
}
public static Mesh SrcMesh(string weapon, out string texPath, out Renderer rend)
{
texPath = null; rend = null;
var pf = AssetDatabase.LoadAssetAtPath<GameObject>(WDIR + weapon + ".prefab");
if (pf == null) return null;
var mf = pf.GetComponentInChildren<MeshFilter>(true);
if (mf == null || mf.sharedMesh == null) return null;
rend = mf.GetComponent<Renderer>();
var mt = rend != null ? rend.sharedMaterial : null;
Texture t = null;
if (mt != null && mt.HasProperty("_BaseMap")) t = mt.GetTexture("_BaseMap");
if (t == null && mt != null && mt.HasProperty("_MainTex")) t = mt.GetTexture("_MainTex");
texPath = t == null ? null : AssetDatabase.GetAssetPath(t);
return mf.sharedMesh;
}
/// <summary>무기 1종을 굽는다. save=false 면 수치만 낸다(스윕용).</summary>
public static WRes Bake(string weapon, int chunkTarget, int paletteN, string tag, bool save)
{
string texPath; Renderer rend;
var src = SrcMesh(weapon, out texPath, out rend);
if (src == null || string.IsNullOrEmpty(texPath)) { L(weapon + " : 메시/텍스처 없음 → 건너뜀"); return null; }
var tex = LoadTex(texPath);
var vt = src.vertices; var uv = src.uv; var tr = src.triangles;
var faces = new List<Face>(tr.Length / 3);
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
{
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])
});
}
// 인접 = 위치 용접 후 공유 엣지
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>(); var adj = new List<KeyValuePair<int, int>>();
for (int f = 0; f < faces.Count; 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) adj.Add(new KeyValuePair<int, int>(other, f)); }
else edgeMap[k] = f;
}
}
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 };
AccHue(r, faces[i].col, faces[i].area);
regions[i] = r;
}
int regionCount = Merge(uf, regions, adj, chunkTarget);
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;
}
// 🔴 칸의 H·S 를 원본(채도·면적 가중 원형평균)으로 복원 + ±8° 클램프. 전역 채널 이득 금지.
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 h2, s2, v2; RgbToHsv(reg.Col, out h2, out s2, out v2);
ssum += s2 * reg.area; aw += reg.area;
}
float ph, ps, pv; RgbToHsv(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 d8 = DHue(ph, hue);
if (Mathf.Abs(d8) > HUE_LIMIT) hue = ph + Mathf.Sign(d8) * HUE_LIMIT; // 🔴 ±8° 하드 클램프
// 🔴 채도는 **되돌리지 않는다** — 칸 평균 채도(ssum/aw)를 쓰면 은·강철처럼 「평균은 회색인데
// texel 마다 채도가 있는」 면에서 채도가 부풀어 **금속이 갈색으로 뜬다**(816b 1차 육안 기각의 원인).
// ps = 면적가중 평균색 자체의 채도 = 정직한 값. (814x 는 부위별 SGain 으로 이 부풀림을 눌렀다)
float satRestored = aw > 1e-6 ? (float)(ssum / aw) : ps;
float sat = Mathf.Min(ps, satRestored);
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(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;
}
int G = Mathf.NextPowerOfTwo(Mathf.CeilToInt(Mathf.Sqrt(N)));
var res = new WRes
{
weapon = weapon, palette = palS, regions = regionCount, palUsed = N,
vBefore = src.vertexCount, tris = tr.Length / 3,
meanDHue = dw > 1e-6 ? (float)(dsum / dw) : 0f, maxDHue = dmax
};
var rootIndex = new Dictionary<int, int>();
for (int i = 0; i < roots.Count; i++) rootIndex[roots[i]] = i;
var nm = BuildMesh(src, faces, uf, rootIndex, assign, G);
nm.name = weapon + "_PAL" + (string.IsNullOrEmpty(tag) ? "" : "_" + tag);
res.mesh = nm; res.vAfter = nm.vertexCount;
if (save)
{
int S = G * CELL;
res.palPath = TEX_DIR + "WLPaletteW_" + weapon + ".png";
WritePalettePng(palS, N, G, S, res.palPath);
Directory.CreateDirectory(MESH_DIR);
res.meshPath = MESH_DIR + nm.name + ".asset";
if (AssetDatabase.LoadAssetAtPath<Mesh>(res.meshPath) != null) AssetDatabase.DeleteAsset(res.meshPath);
AssetDatabase.CreateAsset(nm, res.meshPath);
AssetDatabase.SaveAssets();
}
L(string.Format("{0,-24} c{1,-3} p{2,-3} → 덩어리 {3,-3} 칸 {4,-3} verts {5}→{6} tris {7} dHue mean {8:F2} max {9:F2}",
weapon, chunkTarget, paletteN, res.regions, res.palUsed, res.vBefore, res.vAfter, res.tris, res.meanDHue, res.maxDHue));
return res;
}
static Mesh BuildMesh(Mesh src, List<Face> faces, int[] uf, Dictionary<int, int> rootIndex, int[] assign, int G)
{
var vt = src.vertices; var nmv = src.normals; var tg = src.tangents;
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>();
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));
return id;
};
for (int f = 0; f < faces.Count; 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.RecalculateBounds();
return mesh;
}
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();
}
// ───────────────────────────── 최종 굽기 + 배선
// 🔴 무기용 값 = **덩어리 16 · 팔레트 12칸** (인게임 Play 실측 + 육안으로 결정 · WL816b_ITER.txt)
// · 캐릭터(40/16)를 그대로 쓰면 과하다 — 무기는 화면이 캐릭터의 1/10(90~1,309 px)이라
// 덩어리 40 은 검에서 서로다른색 9.94 %, 16 은 8.33 % 로 **적은 쪽이 낫다**.
// · 그렇다고 6칸까지 줄이면 🔴 육안이 깨진다 — 08 방패의 말 문양이 배경과 한 색으로 뭉치고
// 05_Bow_004 의 노란 장식이 **초록으로 돈다**(굽기 dHue 최대 30.3° → 12칸에서 0.03°).
// · PurePoly 9종은 메시가 **분리된 껍질** 묶음이라 병합이 26~106 에서 바닥을 친다 = 목표값이 그 아래면
// 껍질 수가 곧 덩어리 수다(그래서 실제 조절 손잡이는 칸 수뿐이다).
public const int FINAL_CHUNKS = 16, FINAL_PALETTE = 12;
public static readonly string[] FINAL_WEAPONS = {
"PP_Theme_10_Mace_003", "PP_Theme_10_Shield_003", "Elven_Sword_01",
"PP_Theme_08_Mace_003", "PP_Theme_08_Shield_003",
"PP_Theme_05_Bow_003", "PP_Theme_05_Bow_004",
"PP_Theme_03_Scepter_001", "PP_Theme_03_Wand_001",
"Dagger1", "Dagger2",
// "Orb" — 렌더러가 ParticleSystem 3개뿐이고 메시가 0 이라 팔레트 UV 가 성립하지 않는다(814p 미확인 ②와 같은 이유)
};
public static void BakeFinal()
{
sb.Length = 0;
L("=== WL-816b 최종 굽기 (덩어리 " + FINAL_CHUNKS + " · 팔레트 " + FINAL_PALETTE + "칸 · 32×32 · Point · 밉 OFF · 무압축) ===");
var baseMat = AssetDatabase.LoadAssetAtPath<Material>(MAT_DIR + "M05_ToonP.mat");
if (baseMat == null) { L("🔴 기준 머티리얼 M05_ToonP.mat 없음 → 중단"); Flush(); return; }
L("기준 머티리얼 " + baseMat.name + " shader=" + baseMat.shader.name
+ " _Shades=" + (baseMat.HasProperty("_Shades") ? baseMat.GetFloat("_Shades").ToString("F0") : "-")
+ " _DiffuseColor=" + (baseMat.HasProperty("_DiffuseColor") ? baseMat.GetColor("_DiffuseColor").ToString("F3") : "-"));
var names = new List<string>(); var meshes = new List<Mesh>(); var mats = new List<Material>();
foreach (var w in FINAL_WEAPONS)
{
var r = Bake(w, FINAL_CHUNKS, FINAL_PALETTE, null, true);
if (r == null) continue;
// 머티리얼 = 캐릭터 팔레트 머티리얼(M05_ToonP)의 사본 + _BaseMap 만 이 무기의 팔레트로
string mp = MAT_DIR + w + "_ToonP.mat";
var m = AssetDatabase.LoadAssetAtPath<Material>(mp);
if (m == null) { m = new Material(baseMat); AssetDatabase.CreateAsset(m, mp); }
m.shader = baseMat.shader; m.CopyPropertiesFromMaterial(baseMat);
var ptex = AssetDatabase.LoadAssetAtPath<Texture2D>(r.palPath);
if (m.HasProperty("_BaseMap")) m.SetTexture("_BaseMap", ptex);
if (m.HasProperty("_MainTex")) m.SetTexture("_MainTex", ptex);
EditorUtility.SetDirty(m);
names.Add(w); meshes.Add(AssetDatabase.LoadAssetAtPath<Mesh>(r.meshPath)); mats.Add(m);
L(" → mesh " + r.meshPath + " · pal " + r.palPath + " · mat " + mp);
}
AssetDatabase.SaveAssets();
// SO 확장 편입 (새 SO 를 만들지 않는다)
var so = AssetDatabase.LoadAssetAtPath<WL.Look.Character.WLCharacterPaletteSettings>("Assets/WL/Look/Character/Resources/WL/WLCharacterPaletteSettings.asset");
if (so == null) { L("🔴 WLCharacterPaletteSettings.asset 없음 → 배선 실패"); Flush(); return; }
so.weaponEnabled_ = 1;
so.weaponNames = names.ToArray(); so.weaponMeshes = meshes.ToArray(); so.weaponMaterials = mats.ToArray();
EditorUtility.SetDirty(so); AssetDatabase.SaveAssets(); AssetDatabase.Refresh();
L("");
L("SO 배선: 무기 " + names.Count + "종 · weaponEnabled_=1 · 캐릭터 행 " + (so.rendererNames == null ? 0 : so.rendererNames.Length) + " 무변경");
Flush();
Debug.Log("[WL816b] bake final done · " + names.Count);
}
static void Flush() { Directory.CreateDirectory("AgentScripts"); File.WriteAllText("AgentScripts/WL816b_BAKE_FINAL.txt", sb.ToString(), new UTF8Encoding(true)); }
// ───────────────────────────── 스윕 (덩어리·칸 수 실측 결정)
public static void Sweep()
{
sb.Length = 0;
L("=== WL-816b 무기 덩어리·팔레트 스윕 (에디트 모드 · 굽기 수치만) ===");
string[] ws = { "Elven_Sword_01", "PP_Theme_10_Mace_003", "PP_Theme_10_Shield_003", "Dagger1", "PP_Theme_05_Bow_003" };
int[] chunks = { 6, 10, 16, 24, 40 };
int[] pals = { 4, 6, 8, 12, 16 };
foreach (var w in ws)
{
L("");
L("── " + w);
L("| 덩어리목표 | 칸 | 실덩어리 | 실칸 | verts | dHue평균 | dHue최대 |");
L("|---|---|---|---|---|---|---|");
foreach (var c in chunks)
foreach (var p in pals)
{
if (p > c) continue;
var r = Bake(w, c, p, null, false);
if (r == null) continue;
L(string.Format("| {0} | {1} | {2} | {3} | {4}→{5} | {6:F2} | {7:F2} |", c, p, r.regions, r.palUsed, r.vBefore, r.vAfter, r.meanDHue, r.maxDHue));
UnityEngine.Object.DestroyImmediate(r.mesh);
}
}
File.WriteAllText("AgentScripts/WL816b_SWEEP.txt", sb.ToString(), new UTF8Encoding(true));
Debug.Log("[WL816b] sweep done");
}
}