[WL-814x] 팔레트 아틀라스 UV 로 캐릭터를 진짜 도트로 (덩어리 40 · 팔레트 16색) (#814)

메시 UV 를 「색 하나를 가리키는 좌표」로 새로 굽는다. 삼각형 안에서 UV 가 상수라
화면에서 아무리 축소돼도 그 덩어리는 항상 단색 — 평균 날 디테일이 애초에 없다.

실측(WL_ArenaProto · 직교 10 · 1080x1920 · 캐릭터 2,996 px · 814u/814v 와 같은 축)
  서로 다른 색 비율   89.3 % -> 1.7 %      (합격선 <= 25 %)
  캐릭터 영역 고유색  2,676  -> 52         (합격선 <= 60)
  덩어리당 색 단계    -      -> 평균 2.79  (14칸 중 13칸이 2~3 · 합격선 <= 4)
  눈 진한 점          10 px  -> 36 px      (눈 1개 7x5 px)
  색상(Hue) 오차      -      -> 몸통 2.4° 피부 3.7° 머리칼 3.5°
  삼각형 수 무변경 · 정점 2,520 -> 2,776 (+10 %)

원본 수정 0 — FBX/원본 png/프리팹/씬 전부 무변경. 메시는 복사본(.asset)에 UV 만 새로 굽고
런타임에 갈아끼운다. 되돌리기 = 새 SO 의 enabled_ = 0 이면 814s+814t 상태 100 %.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
깃 관리자 2026-09-13 01:53:53 +09:00
parent d285e7edea
commit ada56c7688
67 changed files with 5690 additions and 28 deletions

View File

@ -0,0 +1,14 @@
=== bake chunks=40 palette=16 tag=FINAL ===
faces 4076
regions 40 (target 40)
Arm_m05 : 6
Body_m05 : 11
Hair05 : 14
Head : 2
Leg_M05 : 7
palette 16 · dHue mean 0.81 max 4.37
mesh Arm_m05 verts 468 -> 504 · srcBW 468 newBW 504 bindposes 63 normals 504 tangents 504
mesh Body_m05 verts 604 -> 703 · srcBW 604 newBW 703 bindposes 40 normals 703 tangents 703
mesh Hair05 verts 611 -> 647 · srcBW 611 newBW 647 bindposes 1 normals 647 tangents 647
mesh Head verts 339 -> 339 · srcBW 339 newBW 339 bindposes 1 normals 339 tangents 339
mesh Leg_M05 verts 498 -> 583 · srcBW 498 newBW 583 bindposes 29 normals 583 tangents 583

View File

@ -0,0 +1,14 @@
=== bake chunks=40 palette=16 tag=best ===
faces 4076
regions 40 (target 40)
Arm_m05 : 10
Body_m05 : 10
Hair05 : 13
Head : 2
Leg_M05 : 5
palette 16 · dHue mean 1.06 max 5.15
mesh Arm_m05 verts 468 -> 538
mesh Body_m05 verts 604 -> 682
mesh Hair05 verts 611 -> 638
mesh Head verts 339 -> 339
mesh Leg_M05 verts 498 -> 536

View File

@ -0,0 +1,14 @@
=== bake chunks=20 palette=10 tag=c20p10 ===
faces 4076
regions 21 (target 20)
Arm_m05 : 2
Body_m05 : 5
Hair05 : 10
Head : 2
Leg_M05 : 2
palette 10 · dHue mean 1.17 max 4.98
mesh Arm_m05 verts 468 -> 468
mesh Body_m05 verts 604 -> 604
mesh Hair05 verts 611 -> 611
mesh Head verts 339 -> 383
mesh Leg_M05 verts 498 -> 507

View File

@ -0,0 +1,14 @@
=== bake chunks=20 palette=16 tag=c20p16 ===
faces 4076
regions 21 (target 20)
Arm_m05 : 2
Body_m05 : 5
Hair05 : 10
Head : 2
Leg_M05 : 2
palette 16 · dHue mean 1.06 max 4.58
mesh Arm_m05 verts 468 -> 468
mesh Body_m05 verts 604 -> 604
mesh Hair05 verts 611 -> 611
mesh Head verts 339 -> 383
mesh Leg_M05 verts 498 -> 507

View File

@ -0,0 +1,14 @@
=== bake chunks=20 palette=24 tag=c20p24 ===
faces 4076
regions 21 (target 20)
Arm_m05 : 2
Body_m05 : 5
Hair05 : 10
Head : 2
Leg_M05 : 2
palette 21 · dHue mean 1.06 max 4.58
mesh Arm_m05 verts 468 -> 468
mesh Body_m05 verts 604 -> 604
mesh Hair05 verts 611 -> 611
mesh Head verts 339 -> 383
mesh Leg_M05 verts 498 -> 507

View File

@ -0,0 +1,14 @@
=== bake chunks=20 palette=32 tag=c20p32 ===
faces 4076
regions 21 (target 20)
Arm_m05 : 2
Body_m05 : 5
Hair05 : 10
Head : 2
Leg_M05 : 2
palette 21 · dHue mean 1.06 max 4.58
mesh Arm_m05 verts 468 -> 468
mesh Body_m05 verts 604 -> 604
mesh Hair05 verts 611 -> 611
mesh Head verts 339 -> 383
mesh Leg_M05 verts 498 -> 507

View File

@ -0,0 +1,14 @@
=== bake chunks=40 palette=10 tag=c40p10 ===
faces 4076
regions 40 (target 40)
Arm_m05 : 10
Body_m05 : 10
Hair05 : 13
Head : 2
Leg_M05 : 5
palette 10 · dHue mean 1.20 max 5.76
mesh Arm_m05 verts 468 -> 538
mesh Body_m05 verts 604 -> 682
mesh Hair05 verts 611 -> 638
mesh Head verts 339 -> 339
mesh Leg_M05 verts 498 -> 536

View File

@ -0,0 +1,14 @@
=== bake chunks=40 palette=16 tag=c40p16 ===
faces 4076
regions 40 (target 40)
Arm_m05 : 10
Body_m05 : 10
Hair05 : 13
Head : 2
Leg_M05 : 5
palette 16 · dHue mean 1.06 max 5.15
mesh Arm_m05 verts 468 -> 538
mesh Body_m05 verts 604 -> 682
mesh Hair05 verts 611 -> 638
mesh Head verts 339 -> 339
mesh Leg_M05 verts 498 -> 536

View File

@ -0,0 +1,14 @@
=== bake chunks=40 palette=24 tag=c40p24 ===
faces 4076
regions 40 (target 40)
Arm_m05 : 10
Body_m05 : 10
Hair05 : 13
Head : 2
Leg_M05 : 5
palette 24 · dHue mean 0.53 max 3.58
mesh Arm_m05 verts 468 -> 538
mesh Body_m05 verts 604 -> 682
mesh Hair05 verts 611 -> 642
mesh Head verts 339 -> 383
mesh Leg_M05 verts 498 -> 536

View File

@ -0,0 +1,14 @@
=== bake chunks=40 palette=32 tag=c40p32 ===
faces 4076
regions 40 (target 40)
Arm_m05 : 10
Body_m05 : 10
Hair05 : 13
Head : 2
Leg_M05 : 5
palette 32 · dHue mean 0.50 max 2.37
mesh Arm_m05 verts 468 -> 538
mesh Body_m05 verts 604 -> 682
mesh Hair05 verts 611 -> 642
mesh Head verts 339 -> 383
mesh Leg_M05 verts 498 -> 536

View File

@ -0,0 +1,14 @@
=== bake chunks=80 palette=10 tag=c80p10 ===
faces 4076
regions 80 (target 80)
Arm_m05 : 14
Body_m05 : 22
Hair05 : 28
Head : 2
Leg_M05 : 14
palette 10 · dHue mean 1.47 max 16.74
mesh Arm_m05 verts 468 -> 576
mesh Body_m05 verts 604 -> 789
mesh Hair05 verts 611 -> 751
mesh Head verts 339 -> 383
mesh Leg_M05 verts 498 -> 614

View File

@ -0,0 +1,14 @@
=== bake chunks=80 palette=16 tag=c80p16 ===
faces 4076
regions 80 (target 80)
Arm_m05 : 14
Body_m05 : 22
Hair05 : 28
Head : 2
Leg_M05 : 14
palette 16 · dHue mean 1.16 max 16.86
mesh Arm_m05 verts 468 -> 576
mesh Body_m05 verts 604 -> 801
mesh Hair05 verts 611 -> 751
mesh Head verts 339 -> 383
mesh Leg_M05 verts 498 -> 634

View File

@ -0,0 +1,14 @@
=== bake chunks=80 palette=24 tag=c80p24 ===
faces 4076
regions 80 (target 80)
Arm_m05 : 14
Body_m05 : 22
Hair05 : 28
Head : 2
Leg_M05 : 14
palette 24 · dHue mean 0.67 max 16.86
mesh Arm_m05 verts 468 -> 576
mesh Body_m05 verts 604 -> 801
mesh Hair05 verts 611 -> 751
mesh Head verts 339 -> 383
mesh Leg_M05 verts 498 -> 652

View File

@ -0,0 +1,14 @@
=== bake chunks=80 palette=32 tag=c80p32 ===
faces 4076
regions 80 (target 80)
Arm_m05 : 14
Body_m05 : 22
Hair05 : 28
Head : 2
Leg_M05 : 14
palette 32 · dHue mean 0.54 max 16.86
mesh Arm_m05 verts 468 -> 576
mesh Body_m05 verts 604 -> 801
mesh Hair05 verts 611 -> 752
mesh Head verts 339 -> 383
mesh Leg_M05 verts 498 -> 652

View File

@ -0,0 +1,14 @@
=== bake chunks=40 palette=16 tag=g_g0none ===
faces 4076
regions 40 (target 40)
Arm_m05 : 6
Body_m05 : 11
Hair05 : 14
Head : 2
Leg_M05 : 7
palette 16 · dHue mean 0.81 max 4.37
mesh Arm_m05 verts 468 -> 504
mesh Body_m05 verts 604 -> 703
mesh Hair05 verts 611 -> 647
mesh Head verts 339 -> 339
mesh Leg_M05 verts 498 -> 583

View File

@ -0,0 +1,14 @@
=== bake chunks=40 palette=16 tag=g_g1soft ===
faces 4076
regions 40 (target 40)
Arm_m05 : 6
Body_m05 : 11
Hair05 : 14
Head : 2
Leg_M05 : 7
palette 16 · dHue mean 0.81 max 4.37
mesh Arm_m05 verts 468 -> 504
mesh Body_m05 verts 604 -> 703
mesh Hair05 verts 611 -> 647
mesh Head verts 339 -> 339
mesh Leg_M05 verts 498 -> 583

View File

@ -0,0 +1,14 @@
=== bake chunks=40 palette=16 tag=g_g2hard ===
faces 4076
regions 40 (target 40)
Arm_m05 : 6
Body_m05 : 11
Hair05 : 14
Head : 2
Leg_M05 : 7
palette 16 · dHue mean 0.81 max 4.37
mesh Arm_m05 verts 468 -> 504
mesh Body_m05 verts 604 -> 703
mesh Hair05 verts 611 -> 647
mesh Head verts 339 -> 339
mesh Leg_M05 verts 498 -> 583

View File

@ -0,0 +1,14 @@
=== bake chunks=40 palette=16 tag=g_g3sat ===
faces 4076
regions 40 (target 40)
Arm_m05 : 6
Body_m05 : 11
Hair05 : 14
Head : 2
Leg_M05 : 7
palette 16 · dHue mean 0.81 max 4.37
mesh Arm_m05 verts 468 -> 504
mesh Body_m05 verts 604 -> 703
mesh Hair05 verts 611 -> 647
mesh Head verts 339 -> 339
mesh Leg_M05 verts 498 -> 583

View File

@ -0,0 +1,14 @@
=== bake chunks=40 palette=16 tag=idm ===
faces 4076
regions 40 (target 40)
Arm_m05 : 6
Body_m05 : 11
Hair05 : 14
Head : 2
Leg_M05 : 7
palette 16 · dHue mean 0.81 max 4.37
mesh Arm_m05 verts 468 -> 504 · srcBW 468 newBW 504 bindposes 63 normals 504 tangents 504
mesh Body_m05 verts 604 -> 703 · srcBW 604 newBW 703 bindposes 40 normals 703 tangents 703
mesh Hair05 verts 611 -> 647 · srcBW 611 newBW 647 bindposes 1 normals 647 tangents 647
mesh Head verts 339 -> 339 · srcBW 339 newBW 339 bindposes 1 normals 339 tangents 339
mesh Leg_M05 verts 498 -> 583 · srcBW 498 newBW 583 bindposes 29 normals 583 tangents 583

View File

@ -0,0 +1,14 @@
=== bake chunks=40 palette=16 tag=memchk ===
faces 4076
regions 40 (target 40)
Arm_m05 : 6
Body_m05 : 11
Hair05 : 14
Head : 2
Leg_M05 : 7
palette 16 · dHue mean 0.81 max 4.37
mesh Arm_m05 verts 468 -> 504 · srcBW 468 newBW 504 bindposes 63 normals 504 tangents 504
mesh Body_m05 verts 604 -> 703 · srcBW 604 newBW 703 bindposes 40 normals 703 tangents 703
mesh Hair05 verts 611 -> 647 · srcBW 611 newBW 647 bindposes 1 normals 647 tangents 647
mesh Head verts 339 -> 339 · srcBW 339 newBW 339 bindposes 1 normals 339 tangents 339
mesh Leg_M05 verts 498 -> 583 · srcBW 498 newBW 583 bindposes 29 normals 583 tangents 583

View File

@ -0,0 +1,14 @@
=== bake chunks=40 palette=16 tag=v_linNos ===
faces 4076
regions 40 (target 40)
Arm_m05 : 12
Body_m05 : 11
Hair05 : 10
Head : 2
Leg_M05 : 5
palette 16 · dHue mean 0.98 max 7.29
mesh Arm_m05 verts 468 -> 554
mesh Body_m05 verts 604 -> 697
mesh Hair05 verts 611 -> 611
mesh Head verts 339 -> 339
mesh Leg_M05 verts 498 -> 536

View File

@ -0,0 +1,14 @@
=== bake chunks=40 palette=16 tag=v_srgbNos ===
faces 4076
regions 40 (target 40)
Arm_m05 : 6
Body_m05 : 11
Hair05 : 14
Head : 2
Leg_M05 : 7
palette 16 · dHue mean 0.81 max 4.37
mesh Arm_m05 verts 468 -> 504
mesh Body_m05 verts 604 -> 703
mesh Hair05 verts 611 -> 647
mesh Head verts 339 -> 339
mesh Leg_M05 verts 498 -> 583

View File

@ -0,0 +1,14 @@
=== bake chunks=40 palette=16 tag=v_srgbSat ===
faces 4076
regions 40 (target 40)
Arm_m05 : 6
Body_m05 : 11
Hair05 : 14
Head : 2
Leg_M05 : 7
palette 16 · dHue mean 0.81 max 4.37
mesh Arm_m05 verts 468 -> 504
mesh Body_m05 verts 604 -> 703
mesh Hair05 verts 611 -> 647
mesh Head verts 339 -> 339
mesh Leg_M05 verts 498 -> 583

View File

@ -0,0 +1,7 @@
setup: bounds (0.648, 1.217, 0.907) / screen H 117 px / pose yaw 240
asset Arm_m05 verts 504 bw 504 readable True
asset Body_m05 verts 703 bw 703 readable True
asset Hair05 verts 647 bw 647 readable True
asset Head verts 339 bw 339 readable True
asset Leg_M05 verts 583 bw 583 readable True
runtime: 렌더러 6 · 메시 5

View File

@ -0,0 +1,27 @@
=== bake chunks=40 palette=16 tag=FINAL ===
faces 4076
regions 40 (target 40)
Arm_m05 : 6
Body_m05 : 11
Hair05 : 14
Head : 2
Leg_M05 : 7
palette 16 · dHue mean 0.81 max 4.37
mesh Arm_m05 verts 468 -> 504 · srcBW 468 newBW 504 bindposes 63 normals 504 tangents 504
mesh Body_m05 verts 604 -> 703 · srcBW 604 newBW 703 bindposes 40 normals 703 tangents 703
mesh Hair05 verts 611 -> 647 · srcBW 611 newBW 647 bindposes 1 normals 647 tangents 647
mesh Head verts 339 -> 339 · srcBW 339 newBW 339 bindposes 1 normals 339 tangents 339
mesh Leg_M05 verts 498 -> 583 · srcBW 498 newBW 583 bindposes 29 normals 583 tangents 583
face tex Assets/WL/Look/Character/Textures/face02_P32.png 32x32
mat M05_ToonP shader=Shader Graphs/Toon tex=WLPalette_FINAL shades=3
mat Skin_ToonP shader=Shader Graphs/Toon tex=WLPalette_FINAL shades=3
mat Hair05_ToonP shader=Shader Graphs/Toon tex=WLPalette_FINAL shades=3
mat face02_ToonP shader=Unlit/Transparent tex=face02_P32
[0] Arm_m05 mesh=Arm_m05_PAL_FINAL mat=M05_ToonP
[1] Body_m05 mesh=Body_m05_PAL_FINAL mat=M05_ToonP
[2] Leg_M05 mesh=Leg_M05_PAL_FINAL mat=M05_ToonP
[3] Head mesh=Head_PAL_FINAL mat=Skin_ToonP
[4] Hair05 mesh=Hair05_PAL_FINAL mat=Hair05_ToonP
[5] Face mesh=- mat=face02_ToonP
DONE

View File

@ -0,0 +1,11 @@
setup: bounds (0.648, 1.217, 0.907) / screen H 117 px / pose yaw 240
tex loaded f32=True h64=True
c20p10: regions 21 pal 10 dHue 1.17/4.98 verts 2520->2573
c20p16: regions 21 pal 16 dHue 1.06/4.58 verts 2520->2573
c20p24: regions 21 pal 21 dHue 1.06/4.58 verts 2520->2573
c40p10: regions 40 pal 10 dHue 1.20/5.76 verts 2520->2733
c40p16: regions 40 pal 16 dHue 1.06/5.15 verts 2520->2733
c40p24: regions 40 pal 24 dHue 0.53/3.58 verts 2520->2781
c80p10: regions 80 pal 10 dHue 1.47/16.74 verts 2520->3113
c80p16: regions 80 pal 16 dHue 1.16/16.86 verts 2520->3145
c80p24: regions 80 pal 24 dHue 0.67/16.86 verts 2520->3163

View File

@ -0,0 +1,4 @@
setup: bounds (0.648, 1.217, 0.907) / screen H 117 px / pose yaw 240
srgbSat: regions 40 pal 16 dHue 0.81/4.37
srgbNos: regions 40 pal 16 dHue 0.81/4.37
linNos: regions 40 pal 16 dHue 0.98/7.29

View File

@ -0,0 +1,5 @@
setup: bounds (0.648, 1.217, 0.907) / screen H 117 px / pose yaw 240
g0none: dHue 0.81/4.37
g1soft: dHue 0.81/4.37
g2hard: dHue 0.81/4.37
g3sat: dHue 0.81/4.37

View File

@ -0,0 +1,10 @@
Arm_m05 : verts 504 bw 504 bind 63 attrs [Position:Float32x3@0,Normal:Float32x3@0,Tangent:Float32x4@0,TexCoord0:Float32x2@1,BlendWeight:Float32x4@2,BlendIndices:UInt32x4@2]
src: attrs [Position:Float32x3@0,Normal:Float32x3@0,Tangent:Float32x4@0,TexCoord0:Float32x2@1,BlendWeight:Float32x4@2,BlendIndices:UInt32x4@2]
Body_m05 : verts 703 bw 703 bind 40 attrs [Position:Float32x3@0,Normal:Float32x3@0,Tangent:Float32x4@0,TexCoord0:Float32x2@1,BlendWeight:Float32x4@2,BlendIndices:UInt32x4@2]
src: attrs [Position:Float32x3@0,Normal:Float32x3@0,Tangent:Float32x4@0,TexCoord0:Float32x2@1,BlendWeight:Float32x4@2,BlendIndices:UInt32x4@2]
Leg_M05 : verts 583 bw 583 bind 29 attrs [Position:Float32x3@0,Normal:Float32x3@0,Tangent:Float32x4@0,TexCoord0:Float32x2@1,BlendWeight:Float32x4@2,BlendIndices:UInt32x4@2]
src: attrs [Position:Float32x3@0,Normal:Float32x3@0,Tangent:Float32x4@0,TexCoord0:Float32x2@1,BlendWeight:Float32x4@2,BlendIndices:UInt32x4@2]
Head : verts 339 bw 339 bind 1 attrs [Position:Float32x3@0,Normal:Float32x3@0,Tangent:Float32x4@0,TexCoord0:Float32x2@1,BlendWeight:Float32x4@2,BlendIndices:UInt32x4@2]
src: attrs [Position:Float32x3@0,Normal:Float32x3@0,Tangent:Float32x4@0,TexCoord0:Float32x2@1,BlendIndices:UInt32x1@2]
Hair05 : verts 647 bw 647 bind 1 attrs [Position:Float32x3@0,Normal:Float32x3@0,Tangent:Float32x4@0,TexCoord0:Float32x2@1,BlendWeight:Float32x4@2,BlendIndices:UInt32x4@2]
src: attrs [Position:Float32x3@0,Normal:Float32x3@0,Tangent:Float32x4@0,TexCoord0:Float32x2@1,BlendIndices:UInt32x1@2]

View File

@ -101,7 +101,7 @@ public static class WLXCap
UnityEngine.Object.DestroyImmediate(t); UnityEngine.Object.DestroyImmediate(t);
} }
public static void Shoot(string name) { Save(Render(new Color(0.10f, 0.10f, 0.12f)), RAW + name + ".png"); } public static void 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) public static void SaveMask(string name, params SkinnedMeshRenderer[] on)
{ {
@ -174,6 +174,25 @@ public static class WLXBake
static float S2L(float c) { return c <= 0.04045f ? c / 12.92f : Mathf.Pow((c + 0.055f) / 1.055f, 2.4f); } 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; } 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 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 struct Lab { public float L, a, b; }
public static Lab ToLab(Vector3 lin) public static Lab ToLab(Vector3 lin)
@ -219,7 +238,7 @@ public static class WLXBake
var px = tmp.GetPixels32(); var px = tmp.GetPixels32();
t = new Tex { w = tmp.width, h = tmp.height, lin = new Vector3[px.Length] }; t = new Tex { w = tmp.width, h = tmp.height, lin = new Vector3[px.Length] };
for (int i = 0; i < px.Length; i++) for (int i = 0; i < px.Length; i++)
t.lin[i] = new Vector3(S2L(px[i].r / 255f), S2L(px[i].g / 255f), S2L(px[i].b / 255f)); 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); UnityEngine.Object.DestroyImmediate(tmp);
s_tex[path] = t; s_tex[path] = t;
return t; return t;
@ -310,35 +329,46 @@ public static class WLXBake
var assign = new int[roots.Count]; var assign = new int[roots.Count];
for (int i = 0; i < roots.Count; i++) for (int i = 0; i < roots.Count; i++)
{ {
var lab = ToLab(regions[roots[i]].Col); var lab = ToLabAuto(regions[roots[i]].Col);
float bd = float.MaxValue; int bj = 0; 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; } } for (int j = 0; j < N; j++) { float d = LabD(lab, ToLabAuto(pal[j])); if (d < bd) { bd = d; bj = j; } }
assign[i] = bj; assign[i] = bj;
} }
// 🔴 Hue 복원: 팔레트 칸의 H/S 를 그 칸 소속 덩어리들의 원본 H/S 로 되돌린다(채도·면적 가중) // 🔴 Hue 복원: 팔레트 칸의 H/S 를 그 칸 소속 덩어리들의 원본 H/S 로 되돌린다(채도·면적 가중)
var palS = new Vector3[N]; var palS = new Vector3[N];
palGroup = new string[N];
for (int j = 0; j < N; j++) for (int j = 0; j < N; j++)
{ {
double hx = 0, hy = 0, sw = 0, ssum = 0, aw = 0; double hx = 0, hy = 0, sw = 0, ssum = 0, aw = 0;
var garea = new double[M];
for (int i = 0; i < roots.Count; i++) for (int i = 0; i < roots.Count; i++)
{ {
if (assign[i] != j) continue; if (assign[i] != j) continue;
var reg = regions[roots[i]]; var reg = regions[roots[i]];
hx += reg.hx; hy += reg.hy; sw += reg.sw; hx += reg.hx; hy += reg.hy; sw += reg.sw;
float h, s, v; RgbToHsv(LinToSrgb(reg.Col), out h, out s, out v); float h, s, v; RgbToHsv(ToWorkSrgb(reg.Col), out h, out s, out v);
ssum += s * reg.area; aw += reg.area; ssum += s * reg.area; aw += reg.area;
garea[reg.mesh] += reg.area;
} }
float ph, ps, pv; RgbToHsv(LinToSrgb(pal[j]), out ph, out ps, out pv); 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 hue = sw > 1e-6 ? (float)((Math.Atan2(hy, hx) * 180.0 / Math.PI + 360.0) % 360.0) : ph;
float sat = aw > 1e-6 ? (float)(ssum / aw) : ps; 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); palS[j] = HsvToRgb(hue, sat, pv);
palGroup[j] = grp;
} }
double dsum = 0, dw = 0; float dmax = 0; double dsum = 0, dw = 0; float dmax = 0;
for (int i = 0; i < roots.Count; i++) for (int i = 0; i < roots.Count; i++)
{ {
var reg = regions[roots[i]]; var reg = regions[roots[i]];
float h0, s0, v0, h1, s1, v1; float h0, s0, v0, h1, s1, v1;
RgbToHsv(LinToSrgb(reg.Col), out h0, out s0, out v0); RgbToHsv(ToWorkSrgb(reg.Col), out h0, out s0, out v0);
RgbToHsv(palS[assign[i]], out h1, out s1, out v1); RgbToHsv(palS[assign[i]], out h1, out s1, out v1);
if (s0 < 0.08f) continue; if (s0 < 0.08f) continue;
float d = Mathf.Abs(DHue(h0, h1)); float d = Mathf.Abs(DHue(h0, h1));
@ -368,7 +398,7 @@ public static class WLXBake
AssetDatabase.CreateAsset(nm, p); AssetDatabase.CreateAsset(nm, p);
} }
res.meshes[names[m]] = nm; res.meshes[names[m]] = nm;
L(string.Format(" mesh {0,-9} verts {1,4} -> {2,4}", names[m], srcs[m].vertexCount, nm.vertexCount)); 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(); if (SaveAssets) AssetDatabase.SaveAssets();
Last = res; Last = res;
@ -408,7 +438,7 @@ public static class WLXBake
static void AccHue(Region r, Vector3 lin, float area) static void AccHue(Region r, Vector3 lin, float area)
{ {
float h, s, v; RgbToHsv(LinToSrgb(lin), out h, out s, out v); float h, s, v; RgbToHsv(ToWorkSrgb(lin), out h, out s, out v);
float w = s * area; float w = s * area;
r.hx += w * Mathf.Cos(h * Mathf.Deg2Rad); r.hy += w * Mathf.Sin(h * Mathf.Deg2Rad); r.sw += w; r.hx += w * Mathf.Cos(h * Mathf.Deg2Rad); r.hy += w * Mathf.Sin(h * Mathf.Deg2Rad); r.sw += w;
} }
@ -530,7 +560,7 @@ public static class WLXBake
static float Cost(Region a, Region b) static float Cost(Region a, Region b)
{ {
float d = LabD(ToLab(a.Col), ToLab(b.Col)); float d = LabD(ToLabAuto(a.Col), ToLabAuto(b.Col));
float w = (a.area * b.area) / Mathf.Max(a.area + b.area, 1e-9f); float w = (a.area * b.area) / Mathf.Max(a.area + b.area, 1e-9f);
return d * d * w; return d * d * w;
} }
@ -538,7 +568,7 @@ public static class WLXBake
static Vector3[] KMeans(Vector3[] cols, float[] wts, int k) static Vector3[] KMeans(Vector3[] cols, float[] wts, int k)
{ {
int n = cols.Length; int n = cols.Length;
var labs = cols.Select(ToLab).ToArray(); var labs = cols.Select(ToLabAuto).ToArray();
var cent = new Lab[k]; var centRgb = new Vector3[k]; var cent = new Lab[k]; var centRgb = new Vector3[k];
var rnd = new System.Random(12345); var rnd = new System.Random(12345);
var d2 = new float[n]; var d2 = new float[n];
@ -570,7 +600,7 @@ public static class WLXBake
} }
var sumRgb = new Vector3[k]; var sw = new double[k]; 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 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]); } 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; if (!ch) break;
} }
return centRgb; return centRgb;
@ -579,14 +609,14 @@ public static class WLXBake
static Mesh BuildMesh(Mesh src, List<Face> faces, int f0, int f1, int[] uf, Dictionary<int, int> rootIndex, int[] assign, int G) static Mesh BuildMesh(Mesh src, List<Face> faces, int f0, int f1, int[] uf, Dictionary<int, int> rootIndex, int[] assign, int G)
{ {
var vt = src.vertices; var nmv = src.normals; var tg = src.tangents; var vt = src.vertices; var nmv = src.normals; var tg = src.tangents;
var bpv = src.GetBonesPerVertex(); var bw = src.GetAllBoneWeights(); // 🔴 레거시 boneWeights(4본) 로 옮긴다 — SetBoneWeights(bonesPerVertex, BoneWeight1[]) 로 만든 메시를
var bwStart = new int[vt.Length]; // .asset 으로 저장하면 SkinnedMeshRenderer 가 "mesh data size and vertex stride" 로 렌더를 멈춘다(실측).
{ int acc = 0; for (int i = 0; i < vt.Length; i++) { bwStart[i] = acc; acc += (bpv.Length > 0 ? bpv[i] : 0); } } var bws = src.boneWeights;
var map = new Dictionary<long, int>(); var map = new Dictionary<long, int>();
var nv = new List<Vector3>(); var nn = new List<Vector3>(); var nt = new List<Vector4>(); 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 nuv = new List<Vector2>(); var ntri = new List<int>();
var nbpv = new List<byte>(); var nbw = new List<BoneWeight1>(); var nbw = new List<BoneWeight>();
Func<int, int, int> get = (vi, pal) => Func<int, int, int> get = (vi, pal) =>
{ {
@ -597,7 +627,7 @@ public static class WLXBake
if (nmv != null && nmv.Length > 0) nn.Add(nmv[vi]); if (nmv != null && nmv.Length > 0) nn.Add(nmv[vi]);
if (tg != null && tg.Length > 0) nt.Add(tg[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)); nuv.Add(new Vector2((pal % G + 0.5f) / G, (pal / G + 0.5f) / G));
if (bpv.Length > 0) { nbpv.Add(bpv[vi]); for (int q = 0; q < bpv[vi]; q++) nbw.Add(bw[bwStart[vi] + q]); } if (bws != null && bws.Length > 0) nbw.Add(bws[vi]);
return id; return id;
}; };
@ -615,13 +645,9 @@ public static class WLXBake
if (nt.Count == nv.Count) mesh.SetTangents(nt); if (nt.Count == nv.Count) mesh.SetTangents(nt);
mesh.SetUVs(0, nuv); mesh.SetUVs(0, nuv);
mesh.SetTriangles(ntri, 0); mesh.SetTriangles(ntri, 0);
if (nbpv.Count == nv.Count)
{
var a1 = new NativeArray<byte>(nbpv.ToArray(), Allocator.Temp);
var a2 = new NativeArray<BoneWeight1>(nbw.ToArray(), Allocator.Temp);
mesh.SetBoneWeights(a1, a2); a1.Dispose(); a2.Dispose();
}
mesh.bindposes = src.bindposes; mesh.bindposes = src.bindposes;
if (nbw.Count == nv.Count) mesh.boneWeights = nbw.ToArray();
mesh.UploadMeshData(false);
mesh.RecalculateBounds(); mesh.RecalculateBounds();
return mesh; return mesh;
} }
@ -781,3 +807,488 @@ public static class WLXIter
finally { WLXCap.Teardown(); WLXCap.Flush("WL814x_ITER2.txt"); } 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"); }
}
}

View File

@ -0,0 +1,16 @@
setup: bounds (0.648, 1.217, 0.907) / screen H 117 px / pose yaw 240
palette SO = found enabled_=1
off: enabled_=0 → 적용 0 (814s+814t 상태)
off Arm_m05 mesh=Arm_m05 mat=M05_ToonF
off Body_m05 mesh=Body_m05 mat=M05_ToonF
off Face mesh=Face mat=face02_ToonF
off Hair05 mesh=Hair05 mat=Hair05_ToonF
off Head mesh=Head mat=Skin_ToonF
off Leg_M05 mesh=Leg_M05 mat=M05_ToonF
on: 렌더러 6 · 메시 5
on Arm_m05 mesh=Arm_m05_PAL_FINAL mat=M05_ToonP tris=752 verts=504 en=True vis=True shader=Shader Graphs/Toon base=WLPalette_FINAL shadow=WLPalette_FINAL shades=3.0 bnd=(0.50, 0.32, 0.91) lay=31
on Body_m05 mesh=Body_m05_PAL_FINAL mat=M05_ToonP tris=984 verts=703 en=True vis=True shader=Shader Graphs/Toon base=WLPalette_FINAL shadow=WLPalette_FINAL shades=3.0 bnd=(0.33, 0.44, 0.43) lay=31
on Face mesh=Face mat=face02_ToonP tris=212 verts=121 en=True vis=True shader=Unlit/Transparent base=- shadow=- shades=- bnd=(0.33, 0.29, 0.37) lay=31
on Hair05 mesh=Hair05_PAL_FINAL mat=Hair05_ToonP tris=914 verts=647 en=True vis=True shader=Shader Graphs/Toon base=WLPalette_FINAL shadow=WLPalette_FINAL shades=3.0 bnd=(0.65, 0.48, 0.66) lay=31
on Head mesh=Head_PAL_FINAL mat=Skin_ToonP tris=582 verts=339 en=True vis=True shader=Shader Graphs/Toon base=WLPalette_FINAL shadow=WLPalette_FINAL shades=3.0 bnd=(0.52, 0.40, 0.55) lay=31
on Leg_M05 mesh=Leg_M05_PAL_FINAL mat=M05_ToonP tris=844 verts=583 en=True vis=True shader=Shader Graphs/Toon base=WLPalette_FINAL shadow=WLPalette_FINAL shades=3.0 bnd=(0.36, 0.54, 0.35) lay=31

View File

@ -0,0 +1,42 @@
# WL-814x 비교 이미지 생성기 — raw PNG 을 캐릭터 영역으로 잘라 1배 + N배로 가로 배치
# python AgentScripts/WL814x_compare.py <out.png> <zoom> <name1> <name2> ...
import sys, os
import numpy as np
from PIL import Image, ImageDraw
RAW = 'Screenshots_WL/WL814x/raw'
OUT = sys.argv[1]
ZOOM = int(sys.argv[2])
NAMES = sys.argv[3:]
ma = np.asarray(Image.open(RAW + '/mask_all.png').convert('L')) > 127
ys, xs = np.where(ma)
pad = 6
x0, x1 = max(0, xs.min() - pad), min(ma.shape[1], xs.max() + pad + 1)
y0, y1 = max(0, ys.min() - pad), min(ma.shape[0], ys.max() + pad + 1)
w, h = x1 - x0, y1 - y0
cells = []
for n in NAMES:
p = RAW + '/' + n + '.png'
if not os.path.exists(p):
print('missing', p); continue
im = Image.open(p).convert('RGB').crop((x0, y0, x1, y1))
big = im.resize((w * ZOOM, h * ZOOM), Image.NEAREST)
cells.append((n, im, big))
gap = 10
label = 18
W = sum(c[2].width for c in cells) + gap * (len(cells) - 1)
H = cells[0][2].height + cells[0][1].height + gap + label
out = Image.new('RGB', (W, H), (24, 24, 28))
d = ImageDraw.Draw(out)
x = 0
for n, im, big in cells:
out.paste(big, (x, label))
out.paste(im, (x + (big.width - im.width) // 2, label + big.height + gap))
d.text((x + 2, 3), n, fill=(230, 230, 230))
x += big.width + gap
os.makedirs(os.path.dirname(OUT) or '.', exist_ok=True)
out.save(OUT)
print('saved', OUT, out.size)

View File

@ -103,8 +103,10 @@ def measure(img, msk, ref_hue=None, skinL=None):
uc = len(np.unique(key)) uc = len(np.unique(key))
st, _ = steps(px) st, _ = steps(px)
L = lum(px) L = lum(px)
h_, s_, v_ = hsv(px)
r = dict(px=n, colors=uc, ratio=100.0 * uc / n, steps=st, r = dict(px=n, colors=uc, ratio=100.0 * uc / n, steps=st,
lmin=float(L.min()), lmax=float(L.max()), hue=mean_hue(px)) lmin=float(L.min()), lmax=float(L.max()), hue=mean_hue(px),
sat=float(s_.mean()), val=float(v_.mean()), lmean=float(L.mean()))
if ref_hue is not None and not np.isnan(r["hue"]) and not np.isnan(ref_hue): if ref_hue is not None and not np.isnan(r["hue"]) and not np.isnan(ref_hue):
r["dhue"] = float(dhue(ref_hue, r["hue"])) r["dhue"] = float(dhue(ref_hue, r["hue"]))
if skinL: if skinL:
@ -118,6 +120,10 @@ def main():
files = [f for f in files if not os.path.basename(f).startswith("mask_")] files = [f for f in files if not os.path.basename(f).startswith("mask_")]
if FILT: if FILT:
files = [f for f in files if os.path.basename(f).startswith(FILT)] files = [f for f in files if os.path.basename(f).startswith(FILT)]
# 🔴 색상/채도 기준은 언제나 「지금」 상태(i1_a_base_now) 로 고정한다.
base = os.path.join(RAW, "i1_a_base_now.png")
if os.path.exists(base):
files = [base] + [f for f in files if os.path.abspath(f) != os.path.abspath(base)]
ref = {} ref = {}
rows = [] rows = []
for f in files: for f in files:
@ -167,12 +173,23 @@ def main():
c(b3), c(hd), c(hr), c(fc), s(b3), s(hd), s(hr), c(b3), c(hd), c(hr), c(fc), s(b3), s(hd), s(hr),
fc.get("dark", "-") if fc else "-", fc.get("eye", "-") if fc else "-")) fc.get("dark", "-") if fc else "-", fc.get("eye", "-") if fc else "-"))
print() print()
print("%-28s %8s %8s %8s %8s" % ("render", "dH몸통", "dH피부", "dH머리칼", "dH얼굴")) print("%-26s | %-22s | %-22s | %s" % ("render", "dHue 몸통/피부/머리칼", "채도S 몸통/피부/머리칼", "밝기L 몸통/피부/머리칼"))
b0 = rows[0]
for r in rows: for r in rows:
def g(p): def g(p):
x = r.get(p) x = r.get(p)
return ("%+.1f" % x["dhue"]) if (x and "dhue" in x) else "ref" return ("%+5.1f" % x["dhue"]) if (x and "dhue" in x) else " ref"
print("%-28s %8s %8s %8s %8s" % (r["name"], g("body3"), g("Head"), g("Hair05"), g("Face"))) def s(p):
x = r.get(p); y = b0.get(p)
if not x: return " -"
return "%.2f" % x["sat"] + ("(%+.2f)" % (x["sat"] - y["sat"]) if y and r is not b0 else "")
def lv(p):
x = r.get(p); y = b0.get(p)
if not x: return " -"
return "%3.0f" % x["lmean"] + ("(%+3.0f)" % (x["lmean"] - y["lmean"]) if y and r is not b0 else "")
print("%-26s | %s %s %s | %s %s %s | %s %s %s" % (
r["name"], g("body3"), g("Head"), g("Hair05"),
s("body3"), s("Head"), s("Hair05"), lv("body3"), lv("Head"), lv("Hair05")))
with open(os.path.join(RAW, "..", "metrics.json"), "w", encoding="utf-8") as fp: with open(os.path.join(RAW, "..", "metrics.json"), "w", encoding="utf-8") as fp:
json.dump(rows, fp, ensure_ascii=False, indent=1, default=float) json.dump(rows, fp, ensure_ascii=False, indent=1, default=float)

View File

@ -0,0 +1,107 @@
# WL-814x 보고용 캡처 묶음 + 합격선 표 이미지 + 외곽선 두께 실측
import os
import numpy as np
from PIL import Image, ImageDraw
RAW = 'Screenshots_WL/WL814x/raw'
OUT = 'Screenshots_WL/WL814x'
def L(n):
return np.asarray(Image.open(RAW + '/' + n + '.png').convert('RGB'), dtype=np.uint8)
ma = L('mask_all')[:, :, 0] > 127
ys, xs = np.where(ma)
pad = 6
X0, X1 = xs.min() - pad, xs.max() + pad + 1
Y0, Y1 = ys.min() - pad, ys.max() + pad + 1
def crop(n, z):
im = Image.open(RAW + '/' + n + '.png').convert('RGB').crop((X0, Y0, X1, Y1))
return im.resize((im.width * z, im.height * z), Image.NEAREST) if z > 1 else im
def grid(out, zoom, names, labels=None):
cells = [(labels[i] if labels else names[i], crop(n, zoom), crop(n, 1)) for i, n in enumerate(names)
if os.path.exists(RAW + '/' + n + '.png')]
gap, lab = 10, 20
W = sum(c[1].width for c in cells) + gap * (len(cells) - 1)
H = cells[0][1].height + cells[0][2].height + gap + lab
img = Image.new('RGB', (W, H), (24, 24, 28))
d = ImageDraw.Draw(img)
x = 0
for t, big, small in cells:
img.paste(big, (x, lab))
img.paste(small, (x + (big.width - small.width) // 2, lab + big.height + gap))
d.text((x + 2, 4), t, fill=(235, 235, 235))
x += big.width + gap
img.save(OUT + '/' + out)
print('saved', out, img.size)
# ── 외곽선 두께 실측: 실루엣 바깥 1px 띠가 거의 검정인가
def outline_px(name):
im = L(name)
inner = np.zeros_like(ma)
inner[1:-1, 1:-1] = ma[1:-1, 1:-1] & ma[:-2, 1:-1] & ma[2:, 1:-1] & ma[1:-1, :-2] & ma[1:-1, 2:]
band1 = ma & ~inner
inner2 = np.zeros_like(ma)
inner2[2:-2, 2:-2] = inner[2:-2, 2:-2] & inner[1:-3, 2:-2] & inner[3:-1, 2:-2] & inner[2:-2, 1:-3] & inner[2:-2, 3:-1]
band2 = inner & ~inner2
def darkfrac(b):
px = im[b]
lum = 0.299 * px[:, 0] + 0.587 * px[:, 1] + 0.114 * px[:, 2]
return 100.0 * (lum < 40).sum() / max(len(px), 1), int(b.sum())
f1, n1 = darkfrac(band1)
f2, n2 = darkfrac(band2)
return f1, n1, f2, n2
if __name__ == '__main__':
grid('iter1.png', 5, ['i1_a_base_now', 'i1_c20p16', 'i1_c40p16', 'i1_c80p16'],
['지금(89.3%/2676)', '덩어리20 팔레트16', '덩어리40 팔레트16', '덩어리80 팔레트16'])
grid('iter2.png', 5, ['i1_a_base_now', 'i2_fV1', 'i2_fP32', 'i2_shades2'],
['지금', '얼굴 814v V1(2048)', '얼굴 P32(1텍셀=1px)', '+셰이드2단'])
grid('iter3.png', 5, ['i1_a_base_now', 'i3_c20p10_s2', 'i3_c40p16_s2', 'i3_c40p16_s3', 'i3_c80p16_s2'],
['지금', 'c20p10 s2', 'c40p16 s2', 'c40p16 s3', 'c80p16 s2'])
grid('iter4.png', 5, ['i1_a_base_now', 'i4_linNos_s3', 'i5_g0none_s3', 'i5_g1soft_s3', 'i5_g2hard_s3'],
['지금', '선형평균', 'sRGB평균 보정없음', '보정 g1soft(채택)', '보정 g2hard'])
grid('z_final_compare.png', 4, ['z_off_now', 'q_runtime'], ['지금 (enabled_=0)', '최종 팔레트 UV (enabled_=1)'])
for n in ['z_off_now', 'q_runtime']:
print(n, '외곽선 1px띠 어두운비율 %.1f%% (%d px) · 2px째 띠 %.1f%% (%d px)' % outline_px(n))
# ── 합격선 표 이미지
rows = [
('항목', '지금', '최종', '합격선', '판정'),
('서로 다른 색 비율', '89.3 %', '1.7 %', '≤ 25 %', 'PASS'),
('캐릭터 영역 고유색 수', '2,676', '52', '≤ 60', 'PASS'),
('부위(덩어리)당 색 단계', '41', '평균 2.79 (14칸 중 13칸 2~3)', '≤ 4', 'PASS*'),
('눈이 어두운 덩어리로', '10 px', '36 px (눈 1개 7×5)', '36 px 유지 이상', 'PASS'),
('색상(Hue) 오차', '-', '몸통 2.4° 피부 3.7° 머리칼 3.5°', '평균 ≤ 8°', 'PASS'),
('실루엣 외곽선', '1 px', '1 px (814t 설정 무변경)', '유지', 'PASS'),
]
cw = [230, 90, 300, 130, 80]
rh = 34
W = sum(cw) + 40
H = rh * len(rows) + 70
img = Image.new('RGB', (W, H), (22, 22, 26))
d = ImageDraw.Draw(img)
d.text((20, 14), 'WL-814x 팔레트 아틀라스 UV — 합격선 6항목 (WL_ArenaProto · 직교10 · 1080x1920 · 캐릭터 2996px)', fill=(255, 235, 160))
for r, row in enumerate(rows):
y = 48 + r * rh
bg = (40, 40, 48) if r == 0 else ((30, 30, 36) if r % 2 else (26, 26, 31))
d.rectangle([16, y, W - 16, y + rh - 2], fill=bg)
x = 20
for c, cell in enumerate(row):
col = (235, 235, 235)
if r > 0 and c == 4:
col = (130, 230, 150)
if r > 0 and c == 2:
col = (150, 210, 255)
d.text((x + 6, y + 9), str(cell), fill=col)
x += cw[c]
d.text((20, 48 + len(rows) * rh + 6), '* 단계 7 인 칸 1개 = 얼굴 데칼이 얹힌 머리 피부 칸(데칼은 별도 부위로 잼)', fill=(200, 200, 200))
img.save(OUT + '/z_metrics.png')
print('saved z_metrics.png', img.size)

View File

@ -0,0 +1,47 @@
# WL-814x 「부위(덩어리)당 색 단계」 측정기
# idmap_<tag>.png = 덩어리(팔레트 칸)를 고유색으로 찍은 지도(빛 0)
# python AgentScripts/WL814x_steps.py <raw> <render이름> <idmap이름>
import sys
import numpy as np
from PIL import Image
RAW = sys.argv[1]
REN = sys.argv[2]
IDM = sys.argv[3]
def L(p):
return np.asarray(Image.open(RAW + '/' + p + '.png').convert('RGB'), dtype=np.uint8)
idm = L(IDM)
ren = L(REN)
ma = L('mask_all')[:, :, 0] > 127
kid = (idm[:, :, 0].astype(np.int32) << 16) | (idm[:, :, 1].astype(np.int32) << 8) | idm[:, :, 2].astype(np.int32)
kre = (ren[:, :, 0].astype(np.int32) << 16) | (ren[:, :, 1].astype(np.int32) << 8) | ren[:, :, 2].astype(np.int32)
# 배경(0,0,0)과 마스크 밖 제외. idmap 은 얼굴 렌더러를 끄고 찍었으므로 그 영역도 자동 제외.
valid = ma & (kid != 0)
ids, cnt = np.unique(kid[valid], return_counts=True)
rows = []
for i, c in zip(ids, cnt):
if c < 8:
continue # 8px 미만 덩어리는 단계 판정 불가 → 제외(개수만 센다)
sel = valid & (kid == i)
u, uc = np.unique(kre[sel], return_counts=True)
o = np.argsort(-uc)
cov = np.cumsum(uc[o]) / c
n90 = int(np.searchsorted(cov, 0.90) + 1)
rows.append((int(c), len(u), n90))
rows.sort(key=lambda r: -r[0])
if rows:
a = np.array(rows)
big = a[a[:, 0] >= 20]
print('%s / %s : 덩어리(팔레트칸) %d개(8px+), 픽셀 %d' % (REN, IDM, len(rows), a[:, 0].sum()))
print(' 칸당 고유색 평균 %.1f 최대 %d' % (a[:, 1].mean(), a[:, 1].max()))
print(' 칸당 색단계(90%%덮기) 평균 %.2f 최대 %d · 20px+ 칸만: 평균 %.2f 최대 %d' % (
a[:, 2].mean(), a[:, 2].max(),
big[:, 2].mean() if len(big) else 0, big[:, 2].max() if len(big) else 0))
print(' 단계 분포:', {k: int((a[:, 2] == k).sum()) for k in sorted(set(a[:, 2].tolist()))})
print(' 가장 큰 칸 6개 (px, 고유색, 단계):', rows[:6])
else:
print('no chunks')

View File

@ -0,0 +1,101 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Hair05_ToonP
m_Shader: {fileID: -6465566751694194690, guid: 378227f8a69213e419dfb0d889d8dd2f,
type: 3}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords:
- _OUTLINESENABLED
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap:
RenderType: Opaque
disabledShaderPasses:
- MOTIONVECTORS
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _BaseMap:
m_Texture: {fileID: 2800000, guid: cb59a62e22f03c44381bac391d63c8b2, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _ShadowBaseMap:
m_Texture: {fileID: 2800000, guid: cb59a62e22f03c44381bac391d63c8b2, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- unity_Lightmaps:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- unity_LightmapsInd:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- unity_ShadowMasks:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _AlphaClip: 0
- _AlphaToMask: 0
- _Blend: 0
- _Brightness: 0.25
- _CastShadows: 1
- _Cloud_Change: 0.005
- _Cloud_Cover: 0.5
- _Cloud_Density: 0.01
- _Cloud_Strength: 1
- _Cull: 2
- _DepthEdgeStrength: 0.5
- _DepthThreshold: 0.01
- _DstBlend: 0
- _DstBlendAlpha: 0
- _MinimumDarkness: 0.2
- _NormalEdgeStrength: 0.3
- _NormalThreshold: 1
- _OUTLINESENABLED: 1
- _QueueControl: 0
- _QueueOffset: 0
- _Shades: 3
- _SrcBlend: 1
- _SrcBlendAlpha: 1
- _Surface: 0
- _XRMotionVectorsPass: 1
- _ZTest: 4
- _ZWrite: 1
- _ZWriteControl: 0
m_Colors:
- _Cloud_Movement: {r: 1, g: 1, b: 0, a: 0}
- _Cloud_Step: {r: 13, g: 17, b: 0, a: 0}
- _DiffuseColor: {r: 1, g: 1, b: 1, a: 1}
- _NormalBias: {r: 1, g: 1, b: 1, a: 0}
- _Outline: {r: 0, g: 0, b: 0, a: 0}
- _ShadowDiffuseColor: {r: 0.2164, g: 0.2092, b: 0.3713, a: 0}
m_BuildTextureStacks: []
m_AllowLocking: 1
--- !u!114 &3057500309613993806
MonoBehaviour:
m_ObjectHideFlags: 11
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
version: 10

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2ad4735b15308b94ca1bd78da99c3ee1
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,101 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &-5322226680851780222
MonoBehaviour:
m_ObjectHideFlags: 11
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
version: 10
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: M05_ToonP
m_Shader: {fileID: -6465566751694194690, guid: 378227f8a69213e419dfb0d889d8dd2f,
type: 3}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords:
- _OUTLINESENABLED
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap:
RenderType: Opaque
disabledShaderPasses:
- MOTIONVECTORS
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _BaseMap:
m_Texture: {fileID: 2800000, guid: cb59a62e22f03c44381bac391d63c8b2, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _ShadowBaseMap:
m_Texture: {fileID: 2800000, guid: cb59a62e22f03c44381bac391d63c8b2, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- unity_Lightmaps:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- unity_LightmapsInd:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- unity_ShadowMasks:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _AlphaClip: 0
- _AlphaToMask: 0
- _Blend: 0
- _Brightness: 0.25
- _CastShadows: 1
- _Cloud_Change: 0.005
- _Cloud_Cover: 0.5
- _Cloud_Density: 0.01
- _Cloud_Strength: 1
- _Cull: 2
- _DepthEdgeStrength: 0.5
- _DepthThreshold: 0.01
- _DstBlend: 0
- _DstBlendAlpha: 0
- _MinimumDarkness: 0.2
- _NormalEdgeStrength: 0.3
- _NormalThreshold: 1
- _OUTLINESENABLED: 1
- _QueueControl: 0
- _QueueOffset: 0
- _Shades: 3
- _SrcBlend: 1
- _SrcBlendAlpha: 1
- _Surface: 0
- _XRMotionVectorsPass: 1
- _ZTest: 4
- _ZWrite: 1
- _ZWriteControl: 0
m_Colors:
- _Cloud_Movement: {r: 1, g: 1, b: 0, a: 0}
- _Cloud_Step: {r: 13, g: 17, b: 0, a: 0}
- _DiffuseColor: {r: 1, g: 1, b: 1, a: 1}
- _NormalBias: {r: 1, g: 1, b: 1, a: 0}
- _Outline: {r: 0, g: 0, b: 0, a: 0}
- _ShadowDiffuseColor: {r: 0.2164, g: 0.2092, b: 0.3713, a: 0}
m_BuildTextureStacks: []
m_AllowLocking: 1

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9161c6a9a87cab1438eb74cecad72893
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,101 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: Skin_ToonP
m_Shader: {fileID: -6465566751694194690, guid: 378227f8a69213e419dfb0d889d8dd2f,
type: 3}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords:
- _OUTLINESENABLED
m_InvalidKeywords: []
m_LightmapFlags: 4
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap:
RenderType: Opaque
disabledShaderPasses:
- MOTIONVECTORS
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _BaseMap:
m_Texture: {fileID: 2800000, guid: cb59a62e22f03c44381bac391d63c8b2, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _ShadowBaseMap:
m_Texture: {fileID: 2800000, guid: cb59a62e22f03c44381bac391d63c8b2, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- unity_Lightmaps:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- unity_LightmapsInd:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- unity_ShadowMasks:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _AlphaClip: 0
- _AlphaToMask: 0
- _Blend: 0
- _Brightness: 0.25
- _CastShadows: 1
- _Cloud_Change: 0.005
- _Cloud_Cover: 0.5
- _Cloud_Density: 0.01
- _Cloud_Strength: 1
- _Cull: 2
- _DepthEdgeStrength: 0.5
- _DepthThreshold: 0.01
- _DstBlend: 0
- _DstBlendAlpha: 0
- _MinimumDarkness: 0.2
- _NormalEdgeStrength: 0.3
- _NormalThreshold: 1
- _OUTLINESENABLED: 1
- _QueueControl: 0
- _QueueOffset: 0
- _Shades: 3
- _SrcBlend: 1
- _SrcBlendAlpha: 1
- _Surface: 0
- _XRMotionVectorsPass: 1
- _ZTest: 4
- _ZWrite: 1
- _ZWriteControl: 0
m_Colors:
- _Cloud_Movement: {r: 1, g: 1, b: 0, a: 0}
- _Cloud_Step: {r: 13, g: 17, b: 0, a: 0}
- _DiffuseColor: {r: 1, g: 1, b: 1, a: 1}
- _NormalBias: {r: 1, g: 1, b: 1, a: 0}
- _Outline: {r: 0, g: 0, b: 0, a: 0}
- _ShadowDiffuseColor: {r: 0.2164, g: 0.2092, b: 0.3713, a: 0}
m_BuildTextureStacks: []
m_AllowLocking: 1
--- !u!114 &2218486046608314064
MonoBehaviour:
m_ObjectHideFlags: 11
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
m_Name:
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
version: 10

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 470ec749ed9afb843a0bca59f47eea68
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,97 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!21 &2100000
Material:
serializedVersion: 8
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_Name: face02_ToonP
m_Shader: {fileID: 10750, guid: 0000000000000000f000000000000000, type: 0}
m_Parent: {fileID: 0}
m_ModifiedSerializedProperties: 0
m_ValidKeywords: []
m_InvalidKeywords:
- _ALPHAPREMULTIPLY_ON
- _EMISSION
m_LightmapFlags: 1
m_EnableInstancingVariants: 0
m_DoubleSidedGI: 0
m_CustomRenderQueue: -1
stringTagMap: {}
disabledShaderPasses: []
m_LockedProperties:
m_SavedProperties:
serializedVersion: 3
m_TexEnvs:
- _BumpMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailAlbedoMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailMask:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _DetailNormalMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _EmissionMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _FalloffSampler:
m_Texture: {fileID: 2800000, guid: 53ba3cfb9b246fd4f9098e204d897e60, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MainTex:
m_Texture: {fileID: 2800000, guid: dfcbd6a1930d90a479f2f5ecea38d377, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _MetallicGlossMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _OcclusionMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _ParallaxMap:
m_Texture: {fileID: 0}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
- _RimLightSampler:
m_Texture: {fileID: 2800000, guid: 11011750921e10846abfad2b18cadbca, type: 3}
m_Scale: {x: 1, y: 1}
m_Offset: {x: 0, y: 0}
m_Ints: []
m_Floats:
- _BumpScale: 1
- _Cutoff: 0.5
- _DepthBias: 0.00012
- _DetailNormalMapScale: 1
- _DstBlend: 10
- _EdgeThickness: 1
- _GlossMapScale: 1
- _Glossiness: 0.5
- _GlossyReflections: 1
- _Metallic: 0
- _Mode: 3
- _OcclusionStrength: 1
- _Parallax: 0.02
- _SmoothnessTextureChannel: 0
- _SpecularHighlights: 1
- _SrcBlend: 1
- _UVSec: 0
- _ZWrite: 0
m_Colors:
- _Color: {r: 1, g: 1, b: 1, a: 1}
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
- _ShadowColor: {r: 0.8, g: 0.8, b: 1, a: 1}
m_BuildTextureStacks: []
m_AllowLocking: 1

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9e63e2e61895e324099928771be402cd
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: bce6f36f9e291134e91b1ff9c61100d4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ecedbc4292abc7043ae38a52519a3d38
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 4300000
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: cd83dd62c11dabb4c8f09d9dcf907504
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 4300000
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 026afa11933facb47826e4587d3eeab9
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 4300000
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3aa19749ee2e7814ea985c18d216efc7
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 4300000
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c4b2eca5a27bfdf4e802a23edf5e8d7e
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 4300000
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,37 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 254b7d1576e32124295b43c0dd1f76a0, type: 3}
m_Name: WLCharacterPaletteSettings
m_EditorClassIdentifier: Assembly-CSharp::WL.Look.Character.WLCharacterPaletteSettings
enabled_: 1
verboseLog: 0
rendererNames:
- Arm_m05
- Body_m05
- Leg_M05
- Head
- Hair05
- Face
paletteMeshes:
- {fileID: 4300000, guid: ecedbc4292abc7043ae38a52519a3d38, type: 2}
- {fileID: 4300000, guid: cd83dd62c11dabb4c8f09d9dcf907504, type: 2}
- {fileID: 4300000, guid: c4b2eca5a27bfdf4e802a23edf5e8d7e, type: 2}
- {fileID: 4300000, guid: 3aa19749ee2e7814ea985c18d216efc7, type: 2}
- {fileID: 4300000, guid: 026afa11933facb47826e4587d3eeab9, type: 2}
- {fileID: 0}
paletteMaterials:
- {fileID: 2100000, guid: 9161c6a9a87cab1438eb74cecad72893, type: 2}
- {fileID: 2100000, guid: 9161c6a9a87cab1438eb74cecad72893, type: 2}
- {fileID: 2100000, guid: 9161c6a9a87cab1438eb74cecad72893, type: 2}
- {fileID: 2100000, guid: 470ec749ed9afb843a0bca59f47eea68, type: 2}
- {fileID: 2100000, guid: 2ad4735b15308b94ca1bd78da99c3ee1, type: 2}
- {fileID: 2100000, guid: 9e63e2e61895e324099928771be402cd, type: 2}

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ecc40027fd4cc2d418b07795efba2ed3
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,181 @@
fileFormatVersion: 2
guid: 2726b4a58d7996b4f936528bc359d323
TextureImporter:
internalIDToNameTable:
- first:
213: 6022471554731658938
second: Hair05_P64_0
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 1
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 0
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 0
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 64
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Server
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites:
- serializedVersion: 2
name: Hair05_P64_0
rect:
serializedVersion: 2
x: 0
y: 0
width: 64
height: 64
alignment: 0
pivot: {x: 0, y: 0}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: -1
bones: []
spriteID: abecca978fd149350800000000000000
internalID: 6022471554731658938
vertices: []
indices:
edges: []
weights: []
outline: []
customData:
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 203 B

View File

@ -0,0 +1,181 @@
fileFormatVersion: 2
guid: cb59a62e22f03c44381bac391d63c8b2
TextureImporter:
internalIDToNameTable:
- first:
213: 4905164880735866611
second: WLPalette_FINAL_0
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 1
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 0
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 32
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Server
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites:
- serializedVersion: 2
name: WLPalette_FINAL_0
rect:
serializedVersion: 2
x: 0
y: 0
width: 32
height: 32
alignment: 0
pivot: {x: 0, y: 0}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: -1
bones: []
spriteID: 3fe8bf86875a21440800000000000000
internalID: 4905164880735866611
vertices: []
indices:
edges: []
weights: []
outline: []
customData:
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 B

View File

@ -0,0 +1,181 @@
fileFormatVersion: 2
guid: dfcbd6a1930d90a479f2f5ecea38d377
TextureImporter:
internalIDToNameTable:
- first:
213: -6539300337229460641
second: face02_P32_0
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 1
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 0
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 32
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 0
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Server
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: iOS
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites:
- serializedVersion: 2
name: face02_P32_0
rect:
serializedVersion: 2
x: 0
y: 0
width: 32
height: 32
alignment: 0
pivot: {x: 0, y: 0}
border: {x: 0, y: 0, z: 0, w: 0}
customData:
outline: []
physicsShape: []
tessellationDetail: -1
bones: []
spriteID: f5b88df6dfcbf35a0800000000000000
internalID: -6539300337229460641
vertices: []
indices:
edges: []
weights: []
outline: []
customData:
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

View File

@ -41,6 +41,8 @@ namespace WL.Look.Character
var cfg = WLCharacterLookSettings.Instance; var cfg = WLCharacterLookSettings.Instance;
if (cfg == null || root == null) return; if (cfg == null || root == null) return;
ApplyMode(root, WLCharacterLookSettings.ActiveMode, cfg); ApplyMode(root, WLCharacterLookSettings.ActiveMode, cfg);
// WL-814x — 팔레트 아틀라스 UV. 그쪽 SO 의 enabled_ = 0 이면 아무 일도 일어나지 않는다.
WLCharacterPalette.Apply(root);
} }
/// <summary>모드를 직접 지정해 맞춘다(캡처·프로브·A/B 용).</summary> /// <summary>모드를 직접 지정해 맞춘다(캡처·프로브·A/B 용).</summary>

View File

@ -0,0 +1,89 @@
// ─────────────────────────────────────────────────────────────────────────────
// WLCharacterPalette.cs — 팔레트 아틀라스 UV 를 런타임에 끼운다 (WL-814x)
//
// PD 지시 #814 · 발주서 WL-814x
//
// ■ 왜 이렇게 하나
// 화면 속 캐릭터는 키 117 px 뿐이라 텍셀 수천 개가 평균 나 화면 1 px 이 된다(814u 실측).
// 그래서 텍스처를 아무리 곱게 그려도 「흐린 사진」이 된다.
// → 메시의 각 덩어리 UV 를 **팔레트 텍스처의 칸 한가운데**로 보내면 그 덩어리는
// 아무리 축소돼도 항상 단색이다(평균 날 디테일이 애초에 없다).
//
// ■ 되돌리기
// WLCharacterPaletteSettings.enabled_ = 0 → 아무것도 안 한다 = 814s + 814t 상태 100 %.
//
// ■ 성능 / GC
// Awake 1회. 렌더러 수(7)만큼만 돈다. 바꿀 것이 없으면 대입도 하지 않는다.
//
// 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다(813z 규칙).
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections.Generic;
using UnityEngine;
namespace WL.Look.Character
{
public static class WLCharacterPalette
{
// 진단(프로브가 읽는다)
public static int AppliedRenderers, AppliedMeshes;
public static string LastLog = "";
static readonly List<Renderer> s_buf = new List<Renderer>(16);
/// <summary>SO 가 켜져 있으면 팔레트 메시·머티리얼로 갈아끼운다.</summary>
public static void Apply(GameObject root)
{
AppliedRenderers = 0; AppliedMeshes = 0;
var cfg = WLCharacterPaletteSettings.Instance;
if (cfg == null || root == null || cfg.enabled_ == 0)
{
LastLog = cfg == null ? "SO 없음 → 적용 0" : "enabled_=0 → 적용 0 (814s+814t 상태)";
return;
}
var names = cfg.rendererNames;
if (names == null || names.Length == 0) { LastLog = "표 비어 있음 → 적용 0"; return; }
s_buf.Clear();
root.GetComponentsInChildren<Renderer>(true, s_buf);
for (int ri = 0; ri < s_buf.Count; ri++)
{
var r = s_buf[ri];
if (r == null) continue;
int idx = IndexOf(names, r.name);
if (idx < 0) continue;
// ① 메시 — 스킨 메시는 본/바인드포즈가 원본과 같으므로 그대로 갈아끼워진다.
var smr = r as SkinnedMeshRenderer;
if (smr != null && cfg.paletteMeshes != null && idx < cfg.paletteMeshes.Length)
{
var m = cfg.paletteMeshes[idx];
if (m != null && smr.sharedMesh != m) { smr.sharedMesh = m; AppliedMeshes++; }
}
// ② 머티리얼
if (cfg.paletteMaterials != null && idx < cfg.paletteMaterials.Length)
{
var want = cfg.paletteMaterials[idx];
if (want != null)
{
var mats = r.sharedMaterials;
bool changed = false;
for (int si = 0; si < mats.Length; si++)
if (mats[si] != want) { mats[si] = want; changed = true; }
if (changed) { r.sharedMaterials = mats; AppliedRenderers++; }
}
}
}
LastLog = "렌더러 " + AppliedRenderers + " · 메시 " + AppliedMeshes;
if (cfg.verboseLog != 0) Debug.Log("[WL814x CharacterPalette] " + LastLog);
}
static int IndexOf(string[] names, string n)
{
for (int i = 0; i < names.Length; i++) if (names[i] == n) return i;
return -1;
}
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: dbb1204c1ba8048468874a4a590cfe27

View File

@ -0,0 +1,61 @@
// ─────────────────────────────────────────────────────────────────────────────
// WLCharacterPaletteSettings.cs — 팔레트 아틀라스 UV(도트) 되돌리기 스위치 (WL-814x)
//
// PD 지시 #814 · 발주서 WL-814x §4
//
// 🔴 enabled_ = 0 → 이 기능 전체 off = 814s + 814t 상태 100 %
// (814s 의 WLCharacterLookSettings 는 그대로 두고, 그 **뒤에** 얹는 별도 스위치다.
// 그래서 이 SO 를 꺼도 기존 되돌리기 동작이 조금도 바뀌지 않는다.)
//
// 무엇을 바꾸나 = 렌더러별로 ① 메시를 「팔레트 UV 복사본」으로 ② 머티리얼을 팔레트 머티리얼로.
// 원본 FBX·원본 png·프리팹은 손대지 않는다 — 전부 런타임 교체다.
//
// 🔴 Debug.LogError / Debug.LogException 을 쓰지 않는다(813z 규칙).
// ─────────────────────────────────────────────────────────────────────────────
using UnityEngine;
namespace WL.Look.Character
{
public class WLCharacterPaletteSettings : ScriptableObject
{
[Header("스위치")]
[Tooltip("0 이면 이 기능 전체 off (= 814s+814t 상태 100 %)")]
public int enabled_ = 1;
[Tooltip("1 이면 적용 내역을 로그로 남긴다")]
public int verboseLog = 0;
[Header("렌더러별 표 (같은 인덱스끼리 짝)")]
[Tooltip("LH_M05 프리팹 안의 렌더러 GameObject 이름")]
public string[] rendererNames;
[Tooltip("팔레트 UV 메시 복사본 (비어 있으면 메시는 안 바꾼다 = 얼굴)")]
public Mesh[] paletteMeshes;
[Tooltip("팔레트 머티리얼")]
public Material[] paletteMaterials;
// ───────────────────────────────────────── 싱글턴(Resources)
public const string ResourcePath = "WL/WLCharacterPaletteSettings";
static WLCharacterPaletteSettings s_inst;
static bool s_tried;
public static WLCharacterPaletteSettings Instance
{
get
{
if (s_inst == null && !s_tried)
{
s_tried = true;
s_inst = Resources.Load<WLCharacterPaletteSettings>(ResourcePath);
}
return s_inst;
}
}
public static void Invalidate() { s_inst = null; s_tried = false; }
public static bool Enabled { get { var c = Instance; return c != null && c.enabled_ != 0; } }
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 254b7d1576e32124295b43c0dd1f76a0