2026-09-12 16:29:30 +00:00
// ─────────────────────────────────────────────────────────────────────────────
// WL814x_Run.cs — WL-814x 팔레트 아틀라스 UV R&D 하니스 (에디터 전용)
// WLXCap : 캡처/측정 (카메라 조건 = 814v 와 동일 · 레이어 31 단독 · 직교 10 · 1080× 1920 · 포스트 off)
// WLXBake : 메시 UV 를 팔레트 칸 중심으로 굽는다 (원본 FBX/PNG 수정 0 · 메시 복사본)
// WLXIter : 반복 회차 드라이버
// 🔴 씬 저장 0 · 원본 에셋 수정 0 · ProjectSettings 0
// ─────────────────────────────────────────────────────────────────────────────
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.Linq ;
using System.Text ;
using Unity.Collections ;
using UnityEditor ;
using UnityEditor.SceneManagement ;
using UnityEngine ;
using UnityEngine.Rendering.Universal ;
using WL.Look.Arena ;
using WL.Look.Character ;
public static class WLXCap
{
public const string SCENE = "Assets/WL/Look/Arena/Scenes/WL_ArenaProto.unity" ;
public const string RAW = "Screenshots_WL/WL814x/raw/" ;
const int LAYER = 31 ;
public const int W = 1080 , H = 1920 ;
static readonly StringBuilder sb = new StringBuilder ( ) ;
public static void L ( string s ) { sb . AppendLine ( s ) ; }
public static void Flush ( string file ) { Directory . CreateDirectory ( "AgentScripts" ) ; File . WriteAllText ( "AgentScripts/" + file , sb . ToString ( ) , new UTF8Encoding ( true ) ) ; }
public static GameObject Root ;
public static SkinnedMeshRenderer [ ] Smrs ;
public static Camera Cam ;
static GameObject s_camGo ;
static Dictionary < Transform , int > s_layers ;
static Material [ ] [ ] s_origMats ;
static Mesh [ ] s_origMeshes ;
public static void Setup ( )
{
EditorSceneManager . OpenScene ( SCENE , OpenSceneMode . Single ) ;
Root = null ;
foreach ( var go in UnityEngine . SceneManagement . SceneManager . GetActiveScene ( ) . GetRootGameObjects ( ) )
if ( go . name . Contains ( "LH_M05" ) ) { Root = go ; break ; }
Smrs = Root . GetComponentsInChildren < SkinnedMeshRenderer > ( true ) . Where ( r = > r . enabled ) . ToArray ( ) ;
var scam = Camera . main ? ? UnityEngine . Object . FindObjectsByType < Camera > ( FindObjectsInactive . Exclude , FindObjectsSortMode . None ) . First ( c = > c . orthographic ) ;
float camYaw = scam . transform . eulerAngles . y ;
Root . transform . rotation = Quaternion . Euler ( 0 , camYaw + 180f , 0 ) ; // 814u/814v 와 같은 포즈
s_layers = new Dictionary < Transform , int > ( ) ;
foreach ( var t in Root . GetComponentsInChildren < Transform > ( true ) ) { s_layers [ t ] = t . gameObject . layer ; t . gameObject . layer = LAYER ; }
s_origMats = Smrs . Select ( r = > r . sharedMaterials ) . ToArray ( ) ;
s_origMeshes = Smrs . Select ( r = > r . sharedMesh ) . ToArray ( ) ;
var b = new Bounds ( Root . transform . position , Vector3 . zero ) ;
foreach ( var r in Smrs ) b . Encapsulate ( r . bounds ) ;
s_camGo = new GameObject ( "WLXCap" ) ;
Cam = s_camGo . AddComponent < Camera > ( ) ; Cam . CopyFrom ( scam ) ;
Cam . orthographic = true ; Cam . orthographicSize = 10f ; Cam . aspect = ( float ) W / H ;
Cam . targetTexture = null ; Cam . cullingMask = 1 < < LAYER ;
Cam . transform . SetPositionAndRotation ( b . center - scam . transform . forward * 50f , scam . transform . rotation ) ;
var ucd = Cam . GetUniversalAdditionalCameraData ( ) ; if ( ucd ! = null ) { ucd . renderPostProcessing = false ; ucd . SetRenderer ( 0 ) ; }
for ( int i = 0 ; i < 3 ; i + + ) Render ( Color . black ) ;
L ( "setup: bounds " + b . size . ToString ( "F3" ) + " / screen H " + ( b . size . y / 20f * H ) . ToString ( "F0" ) + " px / pose yaw " + ( camYaw + 180f ) . ToString ( "F0" ) ) ;
}
public static void Teardown ( )
{
WLReferenceLook . Restore ( ) ;
if ( Smrs ! = null )
for ( int i = 0 ; i < Smrs . Length ; i + + ) { Smrs [ i ] . sharedMaterials = s_origMats [ i ] ; Smrs [ i ] . sharedMesh = s_origMeshes [ i ] ; Smrs [ i ] . enabled = true ; }
if ( s_layers ! = null ) foreach ( var kv in s_layers ) if ( kv . Key ! = null ) kv . Key . gameObject . layer = kv . Value ;
if ( s_camGo ! = null ) UnityEngine . Object . DestroyImmediate ( s_camGo ) ;
s_camGo = null ;
}
public class Shot { public Color32 [ ] px ; public int w , h ; }
public static Shot Render ( Color bg )
{
var rt = new RenderTexture ( W , H , 24 , RenderTextureFormat . ARGB32 , RenderTextureReadWrite . sRGB ) ;
rt . antiAliasing = 1 ; rt . filterMode = FilterMode . Point ;
var pA = RenderTexture . active ; var pC = Cam . clearFlags ; var pB = Cam . backgroundColor ;
Cam . clearFlags = CameraClearFlags . SolidColor ; Cam . backgroundColor = bg ; Cam . targetTexture = rt ; Cam . Render ( ) ;
RenderTexture . active = rt ;
var t = new Texture2D ( W , H , TextureFormat . RGBA32 , false ) ; t . ReadPixels ( new Rect ( 0 , 0 , W , H ) , 0 , 0 ) ; t . Apply ( ) ;
Cam . targetTexture = null ; RenderTexture . active = pA ; Cam . clearFlags = pC ; Cam . backgroundColor = pB ;
var s = new Shot { px = t . GetPixels32 ( ) , w = W , h = H } ;
UnityEngine . Object . DestroyImmediate ( t ) ; rt . Release ( ) ; UnityEngine . Object . DestroyImmediate ( rt ) ;
return s ;
}
public static void Save ( Shot s , string path )
{
var t = new Texture2D ( s . w , s . h , TextureFormat . RGBA32 , false ) ; t . SetPixels32 ( s . px ) ; t . Apply ( ) ;
Directory . CreateDirectory ( Path . GetDirectoryName ( path ) ) ; File . WriteAllBytes ( path , t . EncodeToPNG ( ) ) ;
UnityEngine . Object . DestroyImmediate ( t ) ;
}
2026-09-12 16:53:53 +00:00
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장 버리고 찍는다: 메시를 갈아끼운 직후 첫 프레임은 스킨이 아직 안 올라온다
2026-09-12 16:29:30 +00:00
public static void SaveMask ( string name , params SkinnedMeshRenderer [ ] on )
{
foreach ( var r in Smrs ) r . enabled = on . Contains ( r ) ;
var b = Render ( Color . black ) ; var w = Render ( Color . white ) ;
var t = new Texture2D ( W , H , TextureFormat . RGBA32 , false ) ;
var px = new Color32 [ W * H ] ; int cnt = 0 ;
for ( int i = 0 ; i < px . Length ; i + + )
{
int d = Mathf . Max ( Mathf . Abs ( b . px [ i ] . r - w . px [ i ] . r ) , Mathf . Max ( Mathf . Abs ( b . px [ i ] . g - w . px [ i ] . g ) , Mathf . Abs ( b . px [ i ] . b - w . px [ i ] . b ) ) ) ;
bool hit = d < 128 ; if ( hit ) cnt + + ;
px [ i ] = hit ? new Color32 ( 255 , 255 , 255 , 255 ) : new Color32 ( 0 , 0 , 0 , 255 ) ;
}
t . SetPixels32 ( px ) ; t . Apply ( ) ;
Directory . CreateDirectory ( RAW ) ; File . WriteAllBytes ( RAW + "mask_" + name + ".png" , t . EncodeToPNG ( ) ) ;
UnityEngine . Object . DestroyImmediate ( t ) ;
foreach ( var r in Smrs ) r . enabled = true ;
L ( "mask " + name + " = " + cnt + " px" ) ;
}
public static void SaveAllMasks ( )
{
WLReferenceLook . Restore ( ) ;
foreach ( var r in Smrs ) SaveMask ( r . name , r ) ;
SaveMask ( "all" , Smrs ) ;
SaveMask ( "body3" , Smrs . Where ( r = > r . name = = "Body_m05" | | r . name = = "Arm_m05" | | r . name = = "Leg_M05" ) . ToArray ( ) ) ;
}
public static WLReferenceLook . Opt LookOpt ( bool outline , bool contrast , bool mood , bool rim , int charShades )
{
var cfg = WLReferenceLookSettings . Instance ;
var o = WLReferenceLook . Opt . FromSettings ( cfg ) ;
o . outline = outline ; o . contrast = contrast ; o . mood = mood ; o . rim = rim ;
if ( charShades > 0 ) o . charShades = charShades ;
return o ;
}
public static void ApplyLook ( WLReferenceLook . Opt o ) { WLReferenceLook . Apply ( o , WLReferenceLookSettings . Instance ) ; }
public static void NoLook ( ) { WLReferenceLook . Restore ( ) ; }
public static void RestoreMats ( ) { for ( int i = 0 ; i < Smrs . Length ; i + + ) Smrs [ i ] . sharedMaterials = s_origMats [ i ] ; }
public static void RestoreMeshes ( ) { for ( int i = 0 ; i < Smrs . Length ; i + + ) Smrs [ i ] . sharedMesh = s_origMeshes [ i ] ; }
public static Material [ ] OrigMats ( int i ) { return s_origMats [ i ] ; }
}
// ─────────────────────────────────────────────────────────────────────────────
// WLXBake — 팔레트 아틀라스 UV 굽기
// ① 면마다 원본 텍스처의 실제 샘플 영역 평균색(선형광)
// ② 인접 면을 덩어리로 병합(Ward · 색거리²×면적항)
// ③ 덩어리 색을 팔레트 N색으로 k-means(Lab) → 🔴 Hue 는 원본 그대로 복원
// ④ 면의 UV 를 팔레트 칸 중심으로 (정점은 (원정점,팔레트칸) 쌍으로만 분리 = 정점 증가 최소)
// ─────────────────────────────────────────────────────────────────────────────
public static class WLXBake
{
public const string MESH_DIR = "Assets/WL/Look/Character/Meshes/" ;
public const string TEX_DIR = "Assets/WL/Look/Character/Textures/" ;
const int CELL = 8 ;
public static bool SaveAssets = true ;
static readonly StringBuilder sb = new StringBuilder ( ) ;
static void L ( string s ) { sb . AppendLine ( s ) ; }
static readonly Dictionary < string , string > MeshTex = new Dictionary < string , string > {
{ "Arm_m05" , TEX_DIR + "M05_WL.png" } ,
{ "Body_m05" , TEX_DIR + "M05_WL.png" } ,
{ "Leg_M05" , TEX_DIR + "M05_WL.png" } ,
{ "Head" , TEX_DIR + "Skin_WL.png" } ,
{ "Hair05" , TEX_DIR + "Hair05_WL.png" } ,
} ;
static float S2L ( float c ) { return c < = 0.04045f ? c / 12.92f : Mathf . Pow ( ( c + 0.055f ) / 1.055f , 2.4f ) ; }
static float L2S ( float c ) { return c < = 0.0031308f ? c * 12.92f : 1.055f * Mathf . Pow ( c , 1f / 2.4f ) - 0.055f ; }
public static Vector3 LinToSrgb ( Vector3 v ) { return new Vector3 ( L2S ( v . x ) , L2S ( v . y ) , L2S ( v . z ) ) ; }
2026-09-12 16:53:53 +00:00
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 ) ; }
2026-09-12 16:29:30 +00:00
public struct Lab { public float L , a , b ; }
public static Lab ToLab ( Vector3 lin )
{
float x = 0.4124f * lin . x + 0.3576f * lin . y + 0.1805f * lin . z ;
float y = 0.2126f * lin . x + 0.7152f * lin . y + 0.0722f * lin . z ;
float z = 0.0193f * lin . x + 0.1192f * lin . y + 0.9505f * lin . z ;
x / = 0.95047f ; z / = 1.08883f ;
Func < float , float > f = t = > t > 0.008856f ? Mathf . Pow ( t , 1f / 3f ) : ( 7.787f * t + 16f / 116f ) ;
float fx = f ( x ) , fy = f ( y ) , fz = f ( z ) ;
return new Lab { L = 116f * fy - 16f , a = 500f * ( fx - fy ) , b = 200f * ( fy - fz ) } ;
}
public static float LabD ( Lab p , Lab q ) { float dl = p . L - q . L , da = p . a - q . a , db = p . b - q . b ; return Mathf . Sqrt ( dl * dl + da * da + db * db ) ; }
public static void RgbToHsv ( Vector3 srgb , out float h , out float s , out float v )
{
float r = srgb . x , g = srgb . y , b = srgb . z ;
float mx = Mathf . Max ( r , Mathf . Max ( g , b ) ) , mn = Mathf . Min ( r , Mathf . Min ( g , b ) ) , d = mx - mn ;
h = 0f ;
if ( d > 1e-6f )
{
if ( mx = = r ) h = ( ( g - b ) / d + 6f ) % 6f ;
else if ( mx = = g ) h = ( b - r ) / d + 2f ;
else h = ( r - g ) / d + 4f ;
}
h * = 60f ; s = mx > 1e-6f ? d / mx : 0f ; v = mx ;
}
static Vector3 HsvToRgb ( float h , float s , float v )
{
var c = Color . HSVToRGB ( ( ( h % 360f ) + 360f ) % 360f / 360f , Mathf . Clamp01 ( s ) , Mathf . Clamp01 ( v ) , false ) ;
return new Vector3 ( c . r , c . g , c . b ) ;
}
static float DHue ( float a , float b ) { return ( b - a + 540f ) % 360f - 180f ; }
class Tex { public int w , h ; public Vector3 [ ] lin ; }
static readonly Dictionary < string , Tex > s_tex = new Dictionary < string , Tex > ( ) ;
static Tex LoadTex ( string path )
{
Tex t ;
if ( s_tex . TryGetValue ( path , out t ) ) return t ;
var tmp = new Texture2D ( 2 , 2 , TextureFormat . RGBA32 , false ) ;
tmp . LoadImage ( File . ReadAllBytes ( path ) ) ;
var px = tmp . GetPixels32 ( ) ;
t = new Tex { w = tmp . width , h = tmp . height , lin = new Vector3 [ px . Length ] } ;
for ( int i = 0 ; i < px . Length ; i + + )
2026-09-12 16:53:53 +00:00
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 ) ) ;
2026-09-12 16:29:30 +00:00
UnityEngine . Object . DestroyImmediate ( tmp ) ;
s_tex [ path ] = t ;
return t ;
}
static Vector3 Sample ( Tex t , float u , float v )
{
int x = Mathf . Clamp ( Mathf . FloorToInt ( u * t . w ) , 0 , t . w - 1 ) ;
int y = Mathf . Clamp ( Mathf . FloorToInt ( v * t . h ) , 0 , t . h - 1 ) ;
return t . lin [ y * t . w + x ] ;
}
class Face { public int mesh , i0 , i1 , i2 ; public Vector3 col ; public float area ; }
class Region
{
public Vector3 sum ; public float area ; public int count , mesh ;
public float hx , hy , sw ;
public Vector3 Col { get { return sum / Mathf . Max ( area , 1e-9f ) ; } }
}
public class Result
{
public Dictionary < string , Mesh > meshes = new Dictionary < string , Mesh > ( ) ;
public Vector3 [ ] palette ;
public int regionCount , paletteUsed ;
public float meanDHue , maxDHue ;
public int vertsBefore , vertsAfter ;
public string tag ;
public string palPath ;
}
public static Result Last ;
public static string Bake ( int chunkTarget , int paletteN , string tag )
{
sb . Length = 0 ;
L ( "=== bake chunks=" + chunkTarget + " palette=" + paletteN + " tag=" + tag + " ===" ) ;
var prefab = AssetDatabase . LoadAssetAtPath < GameObject > ( "Assets/Res_Addr/PC/LH_M05.prefab" ) ;
var smrs = prefab . GetComponentsInChildren < SkinnedMeshRenderer > ( true ) . Where ( r = > MeshTex . ContainsKey ( r . name ) ) . ToArray ( ) ;
var names = smrs . Select ( r = > r . name ) . ToArray ( ) ;
var srcs = smrs . Select ( r = > r . sharedMesh ) . ToArray ( ) ;
int M = srcs . Length ;
var faces = new List < Face > ( ) ;
var start = new int [ M + 1 ] ;
var vtxOf = new List < Vector3 [ ] > ( ) ; var triOf = new List < int [ ] > ( ) ;
for ( int m = 0 ; m < M ; m + + )
{
start [ m ] = faces . Count ;
var mesh = srcs [ m ] ; var vt = mesh . vertices ; var uv = mesh . uv ; var tr = mesh . triangles ;
vtxOf . Add ( vt ) ; triOf . Add ( tr ) ;
var tex = LoadTex ( MeshTex [ names [ m ] ] ) ;
for ( int t = 0 ; t < tr . Length ; t + = 3 )
{
int a = tr [ t ] , b = tr [ t + 1 ] , c = tr [ t + 2 ] ;
faces . Add ( new Face
{
mesh = m , i0 = a , i1 = b , i2 = c ,
area = Vector3 . Cross ( vt [ b ] - vt [ a ] , vt [ c ] - vt [ a ] ) . magnitude * 0.5f ,
col = TriAvg ( tex , uv [ a ] , uv [ b ] , uv [ c ] )
} ) ;
}
}
start [ M ] = faces . Count ;
L ( "faces " + faces . Count ) ;
var adj = BuildAdjacency ( faces , start , vtxOf , M ) ;
var regions = new Region [ faces . Count ] ;
var uf = new int [ faces . Count ] ;
for ( int i = 0 ; i < faces . Count ; i + + )
{
uf [ i ] = i ;
var r = new Region { sum = faces [ i ] . col * faces [ i ] . area , area = faces [ i ] . area , count = 1 , mesh = faces [ i ] . mesh } ;
AccHue ( r , faces [ i ] . col , faces [ i ] . area ) ;
regions [ i ] = r ;
}
int regionCount = Merge ( uf , regions , adj , chunkTarget , M , faces ) ;
L ( "regions " + regionCount + " (target " + chunkTarget + ")" ) ;
for ( int m = 0 ; m < M ; m + + )
{
var seen = new HashSet < int > ( ) ;
for ( int i = start [ m ] ; i < start [ m + 1 ] ; i + + ) seen . Add ( Find ( uf , i ) ) ;
L ( " " + names [ m ] + " : " + seen . Count ) ;
}
var roots = new List < int > ( ) ;
{ var seen = new HashSet < int > ( ) ; for ( int i = 0 ; i < faces . Count ; i + + ) { int r = Find ( uf , i ) ; if ( seen . Add ( r ) ) roots . Add ( r ) ; } }
int N = Mathf . Min ( paletteN , roots . Count ) ;
var pal = KMeans ( roots . Select ( r = > regions [ r ] . Col ) . ToArray ( ) , roots . Select ( r = > regions [ r ] . area ) . ToArray ( ) , N ) ;
var assign = new int [ roots . Count ] ;
for ( int i = 0 ; i < roots . Count ; i + + )
{
2026-09-12 16:53:53 +00:00
var lab = ToLabAuto ( regions [ roots [ i ] ] . Col ) ;
2026-09-12 16:29:30 +00:00
float bd = float . MaxValue ; int bj = 0 ;
2026-09-12 16:53:53 +00:00
for ( int j = 0 ; j < N ; j + + ) { float d = LabD ( lab , ToLabAuto ( pal [ j ] ) ) ; if ( d < bd ) { bd = d ; bj = j ; } }
2026-09-12 16:29:30 +00:00
assign [ i ] = bj ;
}
// 🔴 Hue 복원: 팔레트 칸의 H/S 를 그 칸 소속 덩어리들의 원본 H/S 로 되돌린다(채도·면적 가중)
var palS = new Vector3 [ N ] ;
2026-09-12 16:53:53 +00:00
palGroup = new string [ N ] ;
2026-09-12 16:29:30 +00:00
for ( int j = 0 ; j < N ; j + + )
{
double hx = 0 , hy = 0 , sw = 0 , ssum = 0 , aw = 0 ;
2026-09-12 16:53:53 +00:00
var garea = new double [ M ] ;
2026-09-12 16:29:30 +00:00
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 ;
2026-09-12 16:53:53 +00:00
float h , s , v ; RgbToHsv ( ToWorkSrgb ( reg . Col ) , out h , out s , out v ) ;
2026-09-12 16:29:30 +00:00
ssum + = s * reg . area ; aw + = reg . area ;
2026-09-12 16:53:53 +00:00
garea [ reg . mesh ] + = reg . area ;
2026-09-12 16:29:30 +00:00
}
2026-09-12 16:53:53 +00:00
float ph , ps , pv ; RgbToHsv ( ToWorkSrgb ( pal [ j ] ) , out ph , out ps , out pv ) ;
2026-09-12 16:29:30 +00:00
float hue = sw > 1e-6 ? ( float ) ( ( Math . Atan2 ( hy , hx ) * 180.0 / Math . PI + 360.0 ) % 360.0 ) : ph ;
2026-09-12 16:53:53 +00:00
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 ;
2026-09-12 16:29:30 +00:00
palS [ j ] = HsvToRgb ( hue , sat , pv ) ;
2026-09-12 16:53:53 +00:00
palGroup [ j ] = grp ;
2026-09-12 16:29:30 +00:00
}
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 ;
2026-09-12 16:53:53 +00:00
RgbToHsv ( ToWorkSrgb ( reg . Col ) , out h0 , out s0 , out v0 ) ;
2026-09-12 16:29:30 +00:00
RgbToHsv ( palS [ assign [ i ] ] , out h1 , out s1 , out v1 ) ;
if ( s0 < 0.08f ) continue ;
float d = Mathf . Abs ( DHue ( h0 , h1 ) ) ;
dsum + = d * reg . area ; dw + = reg . area ; if ( d > dmax ) dmax = d ;
}
float meanD = dw > 1e-6 ? ( float ) ( dsum / dw ) : 0f ;
L ( string . Format ( "palette {0} · dHue mean {1:F2} max {2:F2}" , N , meanD , dmax ) ) ;
int G = Mathf . CeilToInt ( Mathf . Sqrt ( N ) ) ;
int S = G * CELL ;
string palPath = TEX_DIR + "WLPalette_" + tag + ".png" ;
WritePalettePng ( palS , N , G , S , palPath ) ;
var rootIndex = new Dictionary < int , int > ( ) ;
for ( int i = 0 ; i < roots . Count ; i + + ) rootIndex [ roots [ i ] ] = i ;
var res = new Result { palette = palS , regionCount = regionCount , paletteUsed = N , meanDHue = meanD , maxDHue = dmax , tag = tag , palPath = palPath } ;
if ( SaveAssets ) Directory . CreateDirectory ( MESH_DIR ) ;
for ( int m = 0 ; m < M ; m + + )
{
var nm = BuildMesh ( srcs [ m ] , faces , start [ m ] , start [ m + 1 ] , uf , rootIndex , assign , G ) ;
nm . name = names [ m ] + "_PAL_" + tag ;
res . vertsBefore + = srcs [ m ] . vertexCount ; res . vertsAfter + = nm . vertexCount ;
if ( SaveAssets )
{
string p = MESH_DIR + nm . name + ".asset" ;
if ( AssetDatabase . LoadAssetAtPath < Mesh > ( p ) ! = null ) AssetDatabase . DeleteAsset ( p ) ;
AssetDatabase . CreateAsset ( nm , p ) ;
}
res . meshes [ names [ m ] ] = nm ;
2026-09-12 16:53:53 +00:00
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 ) ) ;
2026-09-12 16:29:30 +00:00
}
if ( SaveAssets ) AssetDatabase . SaveAssets ( ) ;
Last = res ;
File . WriteAllText ( "AgentScripts/WL814x_BAKE_" + tag + ".txt" , sb . ToString ( ) , new UTF8Encoding ( true ) ) ;
return sb . ToString ( ) ;
}
static void WritePalettePng ( Vector3 [ ] palS , int N , int G , int S , string path )
{
var ptex = new Texture2D ( S , S , TextureFormat . RGBA32 , false ) ;
var ppx = new Color32 [ S * S ] ;
for ( int i = 0 ; i < ppx . Length ; i + + ) ppx [ i ] = new Color32 ( 255 , 0 , 255 , 255 ) ;
for ( int j = 0 ; j < N ; j + + )
{
int gx = j % G , gy = j / G ;
var c = new Color32 ( ( byte ) Mathf . RoundToInt ( Mathf . Clamp01 ( palS [ j ] . x ) * 255f ) ,
( byte ) Mathf . RoundToInt ( Mathf . Clamp01 ( palS [ j ] . y ) * 255f ) ,
( byte ) Mathf . RoundToInt ( Mathf . Clamp01 ( palS [ j ] . z ) * 255f ) , 255 ) ;
for ( int y = 0 ; y < CELL ; y + + ) for ( int x = 0 ; x < CELL ; x + + ) ppx [ ( gy * CELL + y ) * S + gx * CELL + x ] = c ;
}
ptex . SetPixels32 ( ppx ) ; ptex . Apply ( ) ;
Directory . CreateDirectory ( TEX_DIR ) ;
File . WriteAllBytes ( path , ptex . EncodeToPNG ( ) ) ;
UnityEngine . Object . DestroyImmediate ( ptex ) ;
AssetDatabase . ImportAsset ( path , ImportAssetOptions . ForceUpdate ) ;
var ti = ( TextureImporter ) AssetImporter . GetAtPath ( path ) ;
ti . textureType = TextureImporterType . Default ;
ti . mipmapEnabled = false ; // 🔴 밉이 있으면 이웃 칸이 섞인다
ti . filterMode = FilterMode . Point ;
ti . wrapMode = TextureWrapMode . Clamp ;
ti . sRGBTexture = true ;
ti . npotScale = TextureImporterNPOTScale . None ;
ti . maxTextureSize = Mathf . Max ( 32 , Mathf . NextPowerOfTwo ( S ) ) ;
ti . textureCompression = TextureImporterCompression . Uncompressed ;
ti . SaveAndReimport ( ) ;
}
static void AccHue ( Region r , Vector3 lin , float area )
{
2026-09-12 16:53:53 +00:00
float h , s , v ; RgbToHsv ( ToWorkSrgb ( lin ) , out h , out s , out v ) ;
2026-09-12 16:29:30 +00:00
float w = s * area ;
r . hx + = w * Mathf . Cos ( h * Mathf . Deg2Rad ) ; r . hy + = w * Mathf . Sin ( h * Mathf . Deg2Rad ) ; r . sw + = w ;
}
static Vector3 TriAvg ( Tex tex , Vector2 a , Vector2 b , Vector2 c )
{
float au = a . x * tex . w , av = a . y * tex . h , bu = b . x * tex . w , bv = b . y * tex . h , cu = c . x * tex . w , cv = c . y * tex . h ;
float areaTexels = Mathf . Abs ( ( bu - au ) * ( cv - av ) - ( cu - au ) * ( bv - av ) ) * 0.5f ;
int n = Mathf . Clamp ( Mathf . CeilToInt ( Mathf . Sqrt ( areaTexels ) ) * 2 , 8 , 64 ) ;
Vector3 sum = Vector3 . zero ; int cnt = 0 ;
for ( int i = 0 ; i < n ; i + + )
for ( int j = 0 ; j < n - i ; j + + )
{
float w0 = ( i + 1f / 3f ) / n , w1 = ( j + 1f / 3f ) / n ;
if ( 1f - w0 - w1 < 0f ) continue ;
var uv = a * ( 1f - w0 - w1 ) + b * w0 + c * w1 ;
sum + = Sample ( tex , uv . x , uv . y ) ; cnt + + ;
}
if ( cnt = = 0 ) return Sample ( tex , ( a . x + b . x + c . x ) / 3f , ( a . y + b . y + c . y ) / 3f ) ;
return sum / cnt ;
}
static List < KeyValuePair < int , int > > BuildAdjacency ( List < Face > faces , int [ ] start , List < Vector3 [ ] > vtx , int M )
{
var pairs = new List < KeyValuePair < int , int > > ( ) ;
for ( int m = 0 ; m < M ; m + + )
{
var vt = vtx [ m ] ;
var map = new Dictionary < long , int > ( ) ; var weld = new int [ vt . Length ] ;
for ( int i = 0 ; i < vt . Length ; i + + )
{
long k = Key ( vt [ i ] ) ; int id ;
if ( ! map . TryGetValue ( k , out id ) ) { id = map . Count ; map [ k ] = id ; }
weld [ i ] = id ;
}
var edgeMap = new Dictionary < long , int > ( ) ;
for ( int f = start [ m ] ; f < start [ m + 1 ] ; f + + )
{
var fa = faces [ f ] ;
int [ ] ii = { weld [ fa . i0 ] , weld [ fa . i1 ] , weld [ fa . i2 ] } ;
for ( int e = 0 ; e < 3 ; e + + )
{
int p = ii [ e ] , q = ii [ ( e + 1 ) % 3 ] ;
long k = p < q ? ( ( long ) p < < 32 ) | ( uint ) q : ( ( long ) q < < 32 ) | ( uint ) p ;
int other ;
if ( edgeMap . TryGetValue ( k , out other ) ) { if ( other ! = f ) pairs . Add ( new KeyValuePair < int , int > ( other , f ) ) ; }
else edgeMap [ k ] = f ;
}
}
}
return pairs ;
}
static long Key ( Vector3 v )
{
long x = ( long ) Mathf . RoundToInt ( v . x * 10000f ) & 0x1FFFFF ;
long y = ( long ) Mathf . RoundToInt ( v . y * 10000f ) & 0x1FFFFF ;
long z = ( long ) Mathf . RoundToInt ( v . z * 10000f ) & 0x1FFFFF ;
return ( x < < 42 ) | ( y < < 21 ) | z ;
}
static int Find ( int [ ] uf , int i ) { while ( uf [ i ] ! = i ) { uf [ i ] = uf [ uf [ i ] ] ; i = uf [ i ] ; } return i ; }
static int Merge ( int [ ] uf , Region [ ] reg , List < KeyValuePair < int , int > > adj , int target , int M , List < Face > faces )
{
var ver = new int [ uf . Length ] ;
var pq = new SortedSet < ( float cost , int a , int b , int va , int vb ) > ( Comparer < ( float , int , int , int , int ) > . Create ( ( p , q ) = >
{
int c = p . Item1 . CompareTo ( q . Item1 ) ; if ( c ! = 0 ) return c ;
c = p . Item2 . CompareTo ( q . Item2 ) ; if ( c ! = 0 ) return c ;
c = p . Item3 . CompareTo ( q . Item3 ) ; if ( c ! = 0 ) return c ;
c = p . Item4 . CompareTo ( q . Item4 ) ; if ( c ! = 0 ) return c ;
return p . Item5 . CompareTo ( q . Item5 ) ;
} ) ) ;
var nb = new Dictionary < int , HashSet < int > > ( ) ;
foreach ( var e in adj )
{
HashSet < int > sa , sbb ;
if ( ! nb . TryGetValue ( e . Key , out sa ) ) { sa = new HashSet < int > ( ) ; nb [ e . Key ] = sa ; }
if ( ! nb . TryGetValue ( e . Value , out sbb ) ) { sbb = new HashSet < int > ( ) ; nb [ e . Value ] = sbb ; }
sa . Add ( e . Value ) ; sbb . Add ( e . Key ) ;
}
foreach ( var kv in nb ) foreach ( var o in kv . Value ) if ( kv . Key < o ) pq . Add ( ( Cost ( reg [ kv . Key ] , reg [ o ] ) , kv . Key , o , 0 , 0 ) ) ;
int count = uf . Length ;
var perMesh = new int [ M ] ;
for ( int i = 0 ; i < uf . Length ; i + + ) perMesh [ faces [ i ] . mesh ] + + ;
while ( count > target & & pq . Count > 0 )
{
var e = pq . Min ; pq . Remove ( e ) ;
int a = Find ( uf , e . a ) , b = Find ( uf , e . b ) ;
if ( a = = b ) continue ;
int lo = Mathf . Min ( a , b ) , hi = Mathf . Max ( a , b ) ;
if ( ver [ a ] ! = e . va | | ver [ b ] ! = e . vb ) { pq . Add ( ( Cost ( reg [ a ] , reg [ b ] ) , lo , hi , ver [ lo ] , ver [ hi ] ) ) ; continue ; }
int mm = reg [ a ] . mesh ;
if ( perMesh [ mm ] < = 2 ) continue ;
uf [ b ] = a ;
reg [ a ] . sum + = reg [ b ] . sum ; reg [ a ] . area + = reg [ b ] . area ; reg [ a ] . count + = reg [ b ] . count ;
reg [ a ] . hx + = reg [ b ] . hx ; reg [ a ] . hy + = reg [ b ] . hy ; reg [ a ] . sw + = reg [ b ] . sw ;
ver [ a ] + + ; count - - ; perMesh [ mm ] - - ;
HashSet < int > sb2 ;
if ( nb . TryGetValue ( b , out sb2 ) )
{
HashSet < int > sa2 ;
if ( ! nb . TryGetValue ( a , out sa2 ) ) { sa2 = new HashSet < int > ( ) ; nb [ a ] = sa2 ; }
foreach ( var o in sb2 )
{
int r = Find ( uf , o ) ;
if ( r = = a ) continue ;
sa2 . Add ( r ) ;
HashSet < int > sr ;
if ( nb . TryGetValue ( r , out sr ) ) sr . Add ( a ) ;
int l2 = Mathf . Min ( a , r ) , h2 = Mathf . Max ( a , r ) ;
pq . Add ( ( Cost ( reg [ a ] , reg [ r ] ) , l2 , h2 , ver [ l2 ] , ver [ h2 ] ) ) ;
}
}
}
return count ;
}
static float Cost ( Region a , Region b )
{
2026-09-12 16:53:53 +00:00
float d = LabD ( ToLabAuto ( a . Col ) , ToLabAuto ( b . Col ) ) ;
2026-09-12 16:29:30 +00:00
float w = ( a . area * b . area ) / Mathf . Max ( a . area + b . area , 1e-9f ) ;
return d * d * w ;
}
static Vector3 [ ] KMeans ( Vector3 [ ] cols , float [ ] wts , int k )
{
int n = cols . Length ;
2026-09-12 16:53:53 +00:00
var labs = cols . Select ( ToLabAuto ) . ToArray ( ) ;
2026-09-12 16:29:30 +00:00
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 ] ; }
2026-09-12 16:53:53 +00:00
for ( int j = 0 ; j < k ; j + + ) if ( sw [ j ] > 1e-9 ) { centRgb [ j ] = sumRgb [ j ] / ( float ) sw [ j ] ; cent [ j ] = ToLabAuto ( centRgb [ j ] ) ; }
2026-09-12 16:29:30 +00:00
if ( ! ch ) break ;
}
return centRgb ;
}
static Mesh BuildMesh ( Mesh src , List < Face > faces , int f0 , int f1 , int [ ] uf , Dictionary < int , int > rootIndex , int [ ] assign , int G )
{
var vt = src . vertices ; var nmv = src . normals ; var tg = src . tangents ;
2026-09-12 16:53:53 +00:00
// 🔴 레거시 boneWeights(4본) 로 옮긴다 — SetBoneWeights(bonesPerVertex, BoneWeight1[]) 로 만든 메시를
// .asset 으로 저장하면 SkinnedMeshRenderer 가 "mesh data size and vertex stride" 로 렌더를 멈춘다(실측).
var bws = src . boneWeights ;
2026-09-12 16:29:30 +00:00
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 > ( ) ;
2026-09-12 16:53:53 +00:00
var nbw = new List < BoneWeight > ( ) ;
2026-09-12 16:29:30 +00:00
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 ) ) ;
2026-09-12 16:53:53 +00:00
if ( bws ! = null & & bws . Length > 0 ) nbw . Add ( bws [ vi ] ) ;
2026-09-12 16:29:30 +00:00
return id ;
} ;
for ( int f = f0 ; f < f1 ; f + + )
{
int pal = assign [ rootIndex [ Find ( uf , f ) ] ] ;
var fa = faces [ f ] ;
ntri . Add ( get ( fa . i0 , pal ) ) ; ntri . Add ( get ( fa . i1 , pal ) ) ; ntri . Add ( get ( fa . i2 , pal ) ) ;
}
var mesh = new Mesh ( ) ;
mesh . indexFormat = nv . Count > 65000 ? UnityEngine . Rendering . IndexFormat . UInt32 : UnityEngine . Rendering . IndexFormat . UInt16 ;
mesh . SetVertices ( nv ) ;
if ( nn . Count = = nv . Count ) mesh . SetNormals ( nn ) ;
if ( nt . Count = = nv . Count ) mesh . SetTangents ( nt ) ;
mesh . SetUVs ( 0 , nuv ) ;
mesh . SetTriangles ( ntri , 0 ) ;
mesh . bindposes = src . bindposes ;
2026-09-12 16:53:53 +00:00
if ( nbw . Count = = nv . Count ) mesh . boneWeights = nbw . ToArray ( ) ;
mesh . UploadMeshData ( false ) ;
2026-09-12 16:29:30 +00:00
mesh . RecalculateBounds ( ) ;
return mesh ;
}
// ── 얼굴/머리칼 「점 텍스처」: 814v 안의 격자 1칸 = 1텍셀 로 축소해 화면 1px 이 정확히 1칸을 읽게 한다.
public static string MakePointTex ( string srcPng , int outSize , string outName , bool alpha )
{
var tmp = new Texture2D ( 2 , 2 , TextureFormat . RGBA32 , false ) ;
tmp . LoadImage ( File . ReadAllBytes ( srcPng ) ) ;
int f = tmp . width / outSize ;
var src = tmp . GetPixels32 ( ) ;
var dst = new Color32 [ outSize * outSize ] ;
for ( int y = 0 ; y < outSize ; y + + )
for ( int x = 0 ; x < outSize ; x + + )
dst [ y * outSize + x ] = src [ ( y * f + f / 2 ) * tmp . width + ( x * f + f / 2 ) ] ; // 칸 중심 텍셀 = 뭉개지 않는다
var o = new Texture2D ( outSize , outSize , TextureFormat . RGBA32 , false ) ;
o . SetPixels32 ( dst ) ; o . Apply ( ) ;
string p = TEX_DIR + outName + ".png" ;
File . WriteAllBytes ( p , o . EncodeToPNG ( ) ) ;
UnityEngine . Object . DestroyImmediate ( o ) ; UnityEngine . Object . DestroyImmediate ( tmp ) ;
AssetDatabase . ImportAsset ( p , ImportAssetOptions . ForceUpdate ) ;
var ti = ( TextureImporter ) AssetImporter . GetAtPath ( p ) ;
ti . textureType = TextureImporterType . Default ;
ti . alphaSource = alpha ? TextureImporterAlphaSource . FromInput : TextureImporterAlphaSource . None ;
ti . alphaIsTransparency = alpha ;
ti . mipmapEnabled = true ;
ti . filterMode = FilterMode . Point ;
ti . wrapMode = TextureWrapMode . Clamp ;
ti . sRGBTexture = true ;
ti . npotScale = TextureImporterNPOTScale . None ;
ti . maxTextureSize = Mathf . Max ( 32 , outSize ) ;
ti . textureCompression = TextureImporterCompression . Uncompressed ;
ti . SaveAndReimport ( ) ;
return p ;
}
}
// ─────────────────────────────────────────────────────────────────────────────
// WLXIter — 반복 회차 드라이버
// ─────────────────────────────────────────────────────────────────────────────
public static class WLXIter
{
static void ApplyPalette ( WLXBake . Result res , Texture2D faceTex , Texture2D hairTex )
{
var pal = AssetDatabase . LoadAssetAtPath < Texture2D > ( res . palPath ) ;
for ( int i = 0 ; i < WLXCap . Smrs . Length ; i + + )
{
var r = WLXCap . Smrs [ i ] ;
var mats = ( Material [ ] ) WLXCap . OrigMats ( i ) . Clone ( ) ;
Texture2D tex = null ;
Mesh nm ;
bool usePal = res . meshes . TryGetValue ( r . name , out nm ) ;
if ( r . name = = "Hair05" & & hairTex ! = null ) { usePal = false ; tex = hairTex ; }
if ( r . name = = "Face" ) { usePal = false ; tex = faceTex ; }
if ( usePal ) { r . sharedMesh = nm ; tex = pal ; }
if ( tex = = null ) { r . sharedMaterials = mats ; continue ; }
for ( int m = 0 ; m < mats . Length ; m + + )
{
if ( mats [ m ] = = null ) continue ;
var c = new Material ( mats [ m ] ) { name = "TMP_" + mats [ m ] . name } ;
foreach ( var pn in new [ ] { "_BaseMap" , "_ShadowBaseMap" , "_MainTex" } ) if ( c . HasProperty ( pn ) ) c . SetTexture ( pn , tex ) ;
mats [ m ] = c ;
}
r . sharedMaterials = mats ;
}
}
static readonly int [ ] CHUNKS = { 20 , 40 , 80 } ;
static readonly int [ ] PALS = { 16 , 24 , 32 } ;
/// <summary>1회차 — 덩어리 20/40/80 × 팔레트 16/24/32 전수 비교 (얼굴은 지금 그대로).</summary>
public static void Iter1 ( )
{
try
{
WLXCap . Setup ( ) ;
WLXCap . SaveAllMasks ( ) ;
WLXBake . SaveAssets = false ;
var cfg = WLCharacterLookSettings . Instance ;
// 기준 = main 현재 상태(mode3 + 814t 종합안)
WLCharacterLook . ApplyMode ( WLXCap . Root , 3 , cfg ) ;
WLXCap . ApplyLook ( WLXCap . LookOpt ( true , true , true , true , 0 ) ) ;
WLXCap . Shoot ( "i1_a_base_now" ) ; WLXCap . NoLook ( ) ;
foreach ( var ch in CHUNKS )
foreach ( var pn in PALS )
{
string tag = "c" + ch + "p" + pn ;
WLXBake . Bake ( ch , pn , tag ) ;
var res = WLXBake . Last ;
WLXCap . L ( string . Format ( "{0}: regions {1} pal {2} dHue {3:F2}/{4:F2} verts {5}->{6}" ,
tag , res . regionCount , res . paletteUsed , res . meanDHue , res . maxDHue , res . vertsBefore , res . vertsAfter ) ) ;
ApplyPalette ( res , null , null ) ;
WLXCap . ApplyLook ( WLXCap . LookOpt ( true , true , true , false , 3 ) ) ;
WLXCap . Shoot ( "i1_" + tag ) ;
WLXCap . NoLook ( ) ;
WLXCap . RestoreMeshes ( ) ;
}
}
catch ( Exception e ) { WLXCap . L ( "EXCEPTION " + e ) ; }
finally { WLXCap . Teardown ( ) ; WLXCap . Flush ( "WL814x_ITER1.txt" ) ; }
}
/// <summary>2회차 — 얼굴 점텍스처(32/64) · 머리칼 팔레트 vs 텍스처 · 셰이드 단수.</summary>
public static void Iter2 ( int chunks , int palN )
{
try
{
WLXCap . Setup ( ) ;
WLXBake . SaveAssets = false ;
// 얼굴·머리칼 점텍스처 생성(원본 수정 0 · 814v V1 을 격자 1칸 = 1텍셀로 축소)
WLXBake . MakePointTex ( WLXBake . TEX_DIR + "face02_V1.png" , 32 , "face02_P32" , true ) ;
WLXBake . MakePointTex ( WLXBake . TEX_DIR + "face02_V1.png" , 64 , "face02_P64" , true ) ;
WLXBake . MakePointTex ( WLXBake . TEX_DIR + "Hair05_V1.png" , 64 , "Hair05_P64" , false ) ;
WLXBake . MakePointTex ( WLXBake . TEX_DIR + "Hair05_V1.png" , 32 , "Hair05_P32" , false ) ;
AssetDatabase . Refresh ( ) ;
var f32 = AssetDatabase . LoadAssetAtPath < Texture2D > ( WLXBake . TEX_DIR + "face02_P32.png" ) ;
var f64 = AssetDatabase . LoadAssetAtPath < Texture2D > ( WLXBake . TEX_DIR + "face02_P64.png" ) ;
var fV1 = AssetDatabase . LoadAssetAtPath < Texture2D > ( WLXBake . TEX_DIR + "face02_V1.png" ) ;
var h64 = AssetDatabase . LoadAssetAtPath < Texture2D > ( WLXBake . TEX_DIR + "Hair05_P64.png" ) ;
var h32 = AssetDatabase . LoadAssetAtPath < Texture2D > ( WLXBake . TEX_DIR + "Hair05_P32.png" ) ;
WLXCap . L ( "face P32=" + ( f32 ! = null ) + " P64=" + ( f64 ! = null ) + " V1=" + ( fV1 ! = null ) + " hair P64=" + ( h64 ! = null ) ) ;
WLXBake . Bake ( chunks , palN , "best" ) ;
var res = WLXBake . Last ;
WLXCap . L ( string . Format ( "best: regions {0} pal {1} dHue {2:F2}/{3:F2} verts {4}->{5}" ,
res . regionCount , res . paletteUsed , res . meanDHue , res . maxDHue , res . vertsBefore , res . vertsAfter ) ) ;
var faces = new Dictionary < string , Texture2D > { { "fV1" , fV1 } , { "fP64" , f64 } , { "fP32" , f32 } } ;
foreach ( var kv in faces )
{
ApplyPalette ( res , kv . Value , null ) ;
WLXCap . ApplyLook ( WLXCap . LookOpt ( true , true , true , false , 3 ) ) ;
WLXCap . Shoot ( "i2_" + kv . Key ) ; WLXCap . NoLook ( ) ; WLXCap . RestoreMeshes ( ) ;
}
// 머리칼: 팔레트(=위) vs 텍스처
foreach ( var kv in new Dictionary < string , Texture2D > { { "hairP64" , h64 } , { "hairP32" , h32 } } )
{
ApplyPalette ( res , f32 , kv . Value ) ;
WLXCap . ApplyLook ( WLXCap . LookOpt ( true , true , true , false , 3 ) ) ;
WLXCap . Shoot ( "i2_" + kv . Key ) ; WLXCap . NoLook ( ) ; WLXCap . RestoreMeshes ( ) ;
}
// 셰이드 단수 비교 (얼굴 P32 · 머리 팔레트)
foreach ( var sh in new [ ] { 7 , 4 , 3 , 2 } )
{
ApplyPalette ( res , f32 , null ) ;
WLXCap . ApplyLook ( WLXCap . LookOpt ( true , true , true , false , sh = = 7 ? 0 : sh ) ) ;
WLXCap . Shoot ( "i2_shades" + sh ) ; WLXCap . NoLook ( ) ; WLXCap . RestoreMeshes ( ) ;
}
// 림 켠 것(레퍼런스 종합안 유지) 대조
ApplyPalette ( res , f32 , null ) ;
WLXCap . ApplyLook ( WLXCap . LookOpt ( true , true , true , true , 3 ) ) ;
WLXCap . Shoot ( "i2_rimon" ) ; WLXCap . NoLook ( ) ; WLXCap . RestoreMeshes ( ) ;
}
catch ( Exception e ) { WLXCap . L ( "EXCEPTION " + e ) ; }
finally { WLXCap . Teardown ( ) ; WLXCap . Flush ( "WL814x_ITER2.txt" ) ; }
}
}
2026-09-12 16:53:53 +00:00
// ─────────────────────────────────────────────────────────────────────────────
// 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" ) ; }
}
}