Project_WL/AgentScripts/WL814s_Posterize.cs

256 lines
12 KiB
C#
Raw Normal View History

// ─────────────────────────────────────────────────────────────────────────────
// WL814s_Posterize.cs — 캐릭터 텍스처 복사 + 색 평탄화(양자화) · 에디터 전용
//
// PD 지시 #814 · 발주서 WL-814s §2 / §3
//
// 🔴 원본 png(`Assets/Suriyun/**`)는 **바이트 하나도 건드리지 않는다**.
// · 복사본을 `Assets/WL/Look/Character/Textures/` 에 만들고
// · 축소는 **복사본의 임포트 설정 maxTextureSize** 로만 한다(픽셀 리샘플 0 = 되돌리기 쉬움)
// · 평탄화는 복사본을 읽어 **새 png** 로 저장한다(원본 덮어쓰기 0)
//
// 평탄화 = 채널당 색 단계를 levels 개로 반올림. 알파는 **손대지 않는다**
// (얼굴 face02 · 머리 가장자리가 깨지지 않게 · 발주 §3).
// ─────────────────────────────────────────────────────────────────────────────
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEditor;
using UnityEngine;
public static class WL814s_Posterize
{
public const string DST = "Assets/WL/Look/Character/Textures/";
// 원본 4장(+검은 참고). name → 원본 경로
public static readonly string[,] SRC = new string[,]
{
{ "M05", "Assets/Suriyun/Characters/RedKnight/Texture/M05/M05.png" },
{ "Skin", "Assets/Suriyun/Characters/RedKnight/Texture/Skin&Hair&Face/Face/Skin.png" },
{ "Hair05", "Assets/Suriyun/Characters/RedKnight/Texture/M05/Hair05.png" },
{ "face02", "Assets/Suriyun/Characters/RedKnight/Texture/Skin&Hair&Face/Face/face02.png" },
};
static void Ensure(string folder)
{
folder = folder.TrimEnd('/');
if (AssetDatabase.IsValidFolder(folder)) return;
int i = folder.LastIndexOf('/');
Ensure(folder.Substring(0, i));
AssetDatabase.CreateFolder(folder.Substring(0, i), folder.Substring(i + 1));
}
static Texture2D LoadRaw(string path, out int w, out int h)
{
byte[] bytes = File.ReadAllBytes(path);
var t = new Texture2D(2, 2, TextureFormat.RGBA32, false);
t.LoadImage(bytes);
w = t.width; h = t.height;
return t;
}
// ═════════════════════════════════════════════════════════════════════
// ① 복사본 만들기 (바이트 그대로) + 임포트 설정
// ═════════════════════════════════════════════════════════════════════
public static string CopyAll()
{
var sb = new StringBuilder();
Ensure(DST);
for (int i = 0; i < SRC.GetLength(0); i++)
{
string name = SRC[i, 0], src = SRC[i, 1];
string dst = DST + name + "_WL.png";
File.Copy(src, dst, true);
sb.AppendLine("COPY " + src + " -> " + dst + " (" + new FileInfo(dst).Length + " bytes · 원본과 동일 = " +
(new FileInfo(src).Length == new FileInfo(dst).Length) + ")");
}
AssetDatabase.Refresh();
// 원본 임포터 설정을 복사본에 이식
for (int i = 0; i < SRC.GetLength(0); i++)
{
string name = SRC[i, 0], src = SRC[i, 1];
string dst = DST + name + "_WL.png";
var si = AssetImporter.GetAtPath(src) as TextureImporter;
var di = AssetImporter.GetAtPath(dst) as TextureImporter;
if (si == null || di == null) { sb.AppendLine("IMPORTER MISS " + name); continue; }
di.textureType = si.textureType;
di.sRGBTexture = si.sRGBTexture;
di.alphaSource = si.alphaSource;
di.alphaIsTransparency = si.alphaIsTransparency;
di.mipmapEnabled = si.mipmapEnabled;
di.wrapMode = si.wrapMode;
di.filterMode = si.filterMode;
di.anisoLevel = si.anisoLevel;
di.npotScale = si.npotScale;
di.maxTextureSize = si.maxTextureSize;
di.textureCompression = si.textureCompression;
di.SaveAndReimport();
sb.AppendLine("IMPORT " + name + "_WL type=" + di.textureType + " sRGB=" + di.sRGBTexture +
" alphaSrc=" + di.alphaSource + " alphaIsTransparency=" + di.alphaIsTransparency +
" mip=" + di.mipmapEnabled + " max=" + di.maxTextureSize);
}
AssetDatabase.SaveAssets();
return sb.ToString();
}
// ═════════════════════════════════════════════════════════════════════
// ② 평탄화 — 복사본을 읽어 levels 단계로 양자화한 새 png
// ═════════════════════════════════════════════════════════════════════
public static string MakeFlat(int levels)
{
var sb = new StringBuilder();
Ensure(DST);
for (int i = 0; i < SRC.GetLength(0); i++)
{
string name = SRC[i, 0];
string src = DST + name + "_WL.png";
if (!File.Exists(src)) { sb.AppendLine("SKIP(no copy) " + src); continue; }
int w, h;
var t = LoadRaw(src, out w, out h);
var px = t.GetPixels32();
float step = 255f / (levels - 1);
int uniqBefore = CountUnique(px);
int alphaChanged = 0;
for (int p = 0; p < px.Length; p++)
{
var c = px[p];
c.r = (byte)Mathf.Clamp(Mathf.RoundToInt(Mathf.Round(c.r / step) * step), 0, 255);
c.g = (byte)Mathf.Clamp(Mathf.RoundToInt(Mathf.Round(c.g / step) * step), 0, 255);
c.b = (byte)Mathf.Clamp(Mathf.RoundToInt(Mathf.Round(c.b / step) * step), 0, 255);
// 알파는 그대로 (얼굴·머리 가장자리 보존)
px[p] = c;
}
var outT = new Texture2D(w, h, TextureFormat.RGBA32, false);
outT.SetPixels32(px);
outT.Apply(false);
string dst = DST + name + "_WL_Q" + levels + ".png";
File.WriteAllBytes(dst, outT.EncodeToPNG());
int uniqAfter = CountUnique(px);
sb.AppendLine("FLAT " + name + " levels=" + levels + " " + w + "x" + h +
" 고유색 " + uniqBefore + " -> " + uniqAfter + " · 알파변경 " + alphaChanged + " · " + dst);
UnityEngine.Object.DestroyImmediate(t);
UnityEngine.Object.DestroyImmediate(outT);
}
AssetDatabase.Refresh();
// 임포트 설정을 복사본과 같게
for (int i = 0; i < SRC.GetLength(0); i++)
{
string name = SRC[i, 0];
var si = AssetImporter.GetAtPath(DST + name + "_WL.png") as TextureImporter;
var di = AssetImporter.GetAtPath(DST + name + "_WL_Q" + levels + ".png") as TextureImporter;
if (si == null || di == null) continue;
di.textureType = si.textureType;
di.sRGBTexture = si.sRGBTexture;
di.alphaSource = si.alphaSource;
di.alphaIsTransparency = si.alphaIsTransparency;
di.mipmapEnabled = si.mipmapEnabled;
di.wrapMode = si.wrapMode;
di.filterMode = si.filterMode;
di.npotScale = si.npotScale;
di.maxTextureSize = si.maxTextureSize;
di.textureCompression = si.textureCompression;
di.SaveAndReimport();
}
AssetDatabase.SaveAssets();
return sb.ToString();
}
static int CountUnique(Color32[] px)
{
var set = new HashSet<int>();
int stride = px.Length > 1048576 ? 4 : 1; // 4M 픽셀 이상이면 1/4 샘플링(메모리)
for (int i = 0; i < px.Length; i += stride)
{
var c = px[i];
if (c.a == 0) continue;
set.Add((c.r << 16) | (c.g << 8) | c.b);
}
return set.Count;
}
// ═════════════════════════════════════════════════════════════════════
// ③ 축소 — 복사본/평탄화본의 maxTextureSize 만 바꾼다
// ═════════════════════════════════════════════════════════════════════
public static string SetMaxSize(string suffix, int maxSize)
{
var sb = new StringBuilder();
for (int i = 0; i < SRC.GetLength(0); i++)
{
string p = DST + SRC[i, 0] + suffix + ".png";
var im = AssetImporter.GetAtPath(p) as TextureImporter;
if (im == null) { sb.AppendLine("MISS " + p); continue; }
im.maxTextureSize = maxSize;
im.SaveAndReimport();
var t = AssetDatabase.LoadAssetAtPath<Texture2D>(p);
long mem = t != null ? UnityEngine.Profiling.Profiler.GetRuntimeMemorySizeLong(t) : 0;
sb.AppendLine("MAXSIZE " + p + " -> " + maxSize + " · 실제 " + (t != null ? t.width + "x" + t.height : "?") +
" · " + (t != null ? t.format.ToString() : "?") + " · " + (mem / 1024f / 1024f).ToString("F3") + " MB");
}
AssetDatabase.SaveAssets();
return sb.ToString();
}
/// <summary>원본 4장의 런타임 메모리(현재 임포트 상태)를 잰다.</summary>
public static string MeasureOriginals()
{
var sb = new StringBuilder();
float tot = 0;
for (int i = 0; i < SRC.GetLength(0); i++)
{
var t = AssetDatabase.LoadAssetAtPath<Texture2D>(SRC[i, 1]);
if (t == null) { sb.AppendLine("MISS " + SRC[i, 1]); continue; }
long mem = UnityEngine.Profiling.Profiler.GetRuntimeMemorySizeLong(t);
tot += mem / 1024f / 1024f;
sb.AppendLine("ORIG " + SRC[i, 0] + " " + t.width + "x" + t.height + " " + t.format + " mip=" + (t.mipmapCount > 1) +
" · " + (mem / 1024f / 1024f).ToString("F3") + " MB");
}
sb.AppendLine("ORIG TOTAL " + tot.ToString("F3") + " MB");
return sb.ToString();
}
public static string MeasureSet(string suffix)
{
var sb = new StringBuilder();
float tot = 0;
for (int i = 0; i < SRC.GetLength(0); i++)
{
string p = DST + SRC[i, 0] + suffix + ".png";
var t = AssetDatabase.LoadAssetAtPath<Texture2D>(p);
if (t == null) { sb.AppendLine("MISS " + p); continue; }
long mem = UnityEngine.Profiling.Profiler.GetRuntimeMemorySizeLong(t);
tot += mem / 1024f / 1024f;
sb.AppendLine("SET" + suffix + " " + SRC[i, 0] + " " + t.width + "x" + t.height + " " + t.format +
" · " + (mem / 1024f / 1024f).ToString("F3") + " MB");
}
sb.AppendLine("SET" + suffix + " TOTAL " + tot.ToString("F3") + " MB");
return sb.ToString();
}
public static void Run()
{
var sb = new StringBuilder();
sb.Append(MeasureOriginals());
sb.Append(CopyAll());
Debug.Log("[WL814s posterize]\n" + sb);
}
// 디스패처 — unity command run_script --entry WL814s_Posterize.Cmd --args "<cmd> …"
public static string Cmd(string[] args)
{
if (args == null || args.Length == 0) return MeasureOriginals();
string c = args[0];
try
{
if (c == "measure") return MeasureOriginals();
if (c == "copy") return MeasureOriginals() + CopyAll();
if (c == "flat") return MakeFlat(int.Parse(args[1]));
if (c == "maxsize") return SetMaxSize(args[1] == "-" ? "_WL" : args[1], int.Parse(args[2]));
if (c == "set") return MeasureSet(args[1] == "-" ? "_WL" : args[1]);
return "UNKNOWN " + c;
}
catch (Exception e) { return "EXCEPTION " + e.GetType().Name + " " + e.Message + "\n" + e.StackTrace; }
}
}