256 lines
12 KiB
C#
256 lines
12 KiB
C#
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
|
// 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; }
|
||
|
|
}
|
||
|
|
}
|