2026-09-13 03:02:40 +00:00
|
|
|
|
// 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° 하드 클램프
|
2026-09-13 03:09:54 +00:00
|
|
|
|
// 🔴 채도는 **되돌리지 않는다** — 칸 평균 채도(ssum/aw)를 쓰면 은·강철처럼 「평균은 회색인데
|
|
|
|
|
|
// texel 마다 채도가 있는」 면에서 채도가 부풀어 **금속이 갈색으로 뜬다**(816b 1차 육안 기각의 원인).
|
|
|
|
|
|
// ps = 면적가중 평균색 자체의 채도 = 정직한 값. (814x 는 부위별 SGain 으로 이 부풀림을 눌렀다)
|
|
|
|
|
|
float satRestored = aw > 1e-6 ? (float)(ssum / aw) : ps;
|
|
|
|
|
|
float sat = Mathf.Min(ps, satRestored);
|
2026-09-13 03:02:40 +00:00
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ───────────────────────────── 최종 굽기 + 배선
|
2026-09-13 03:09:54 +00:00
|
|
|
|
// 🔴 무기용 값 = **덩어리 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;
|
2026-09-13 03:02:40 +00:00
|
|
|
|
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");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|