Initial Dansori character workspace
This commit is contained in:
@@ -0,0 +1,418 @@
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$Source,
|
||||
|
||||
[string]$OutputDir = "03_Assets\Parts\Images",
|
||||
[int]$Width = 520,
|
||||
[int]$Height = 900
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
|
||||
$code = @"
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
|
||||
public static class HarukaRigBuilder
|
||||
{
|
||||
static readonly string[] PartNames = new string[] {
|
||||
"head", "neck", "chest", "pelvis",
|
||||
"upperarm_r", "forearm_r", "hand_r",
|
||||
"upperarm_l", "forearm_l", "hand_l",
|
||||
"thigh_r", "shin_r", "foot_r",
|
||||
"thigh_l", "shin_l", "foot_l"
|
||||
};
|
||||
|
||||
public static void Build(string sourcePath, string outputDir, int width, int height)
|
||||
{
|
||||
Directory.CreateDirectory(outputDir);
|
||||
using (var src = new Bitmap(sourcePath))
|
||||
using (var keyed = RemoveGreenKey(src))
|
||||
{
|
||||
Rectangle bbox = FindAlphaBounds(keyed, 8);
|
||||
using (var master = NormalizeToCanvas(keyed, bbox, width, height))
|
||||
{
|
||||
RemoveTinyComponents(master, 24);
|
||||
string masterPath = Path.Combine(outputDir, "haruka_part_master_apose.png");
|
||||
SavePng(master, masterPath);
|
||||
SliceParts(master, outputDir);
|
||||
Validate(outputDir, master, width, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static Bitmap RemoveGreenKey(Bitmap src)
|
||||
{
|
||||
var dst = new Bitmap(src.Width, src.Height, PixelFormat.Format32bppArgb);
|
||||
for (int y = 0; y < src.Height; y++)
|
||||
{
|
||||
for (int x = 0; x < src.Width; x++)
|
||||
{
|
||||
Color c = src.GetPixel(x, y);
|
||||
int r = c.R, g = c.G, b = c.B;
|
||||
int maxRB = Math.Max(r, b);
|
||||
int delta = g - maxRB;
|
||||
int alpha = 255;
|
||||
|
||||
if (g > 90 && delta > 35)
|
||||
{
|
||||
if (delta >= 120) alpha = 0;
|
||||
else alpha = Clamp((120 - delta) * 255 / 85, 0, 255);
|
||||
|
||||
if (alpha < 255)
|
||||
{
|
||||
int despill = Math.Max(r, b);
|
||||
g = Math.Min(g, despill + 8);
|
||||
}
|
||||
}
|
||||
|
||||
if (alpha <= 2)
|
||||
{
|
||||
dst.SetPixel(x, y, Color.FromArgb(0, 0, 0, 0));
|
||||
}
|
||||
else
|
||||
{
|
||||
dst.SetPixel(x, y, Color.FromArgb(alpha, r, g, b));
|
||||
}
|
||||
}
|
||||
}
|
||||
return dst;
|
||||
}
|
||||
|
||||
static Rectangle FindAlphaBounds(Bitmap bmp, int threshold)
|
||||
{
|
||||
int minX = bmp.Width, minY = bmp.Height, maxX = -1, maxY = -1;
|
||||
for (int y = 0; y < bmp.Height; y++)
|
||||
{
|
||||
for (int x = 0; x < bmp.Width; x++)
|
||||
{
|
||||
if (bmp.GetPixel(x, y).A > threshold)
|
||||
{
|
||||
if (x < minX) minX = x;
|
||||
if (x > maxX) maxX = x;
|
||||
if (y < minY) minY = y;
|
||||
if (y > maxY) maxY = y;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (maxX < minX || maxY < minY) return Rectangle.Empty;
|
||||
return Rectangle.FromLTRB(minX, minY, maxX + 1, maxY + 1);
|
||||
}
|
||||
|
||||
static Bitmap NormalizeToCanvas(Bitmap src, Rectangle bbox, int width, int height)
|
||||
{
|
||||
var dst = new Bitmap(width, height, PixelFormat.Format32bppArgb);
|
||||
using (var g = Graphics.FromImage(dst))
|
||||
{
|
||||
g.Clear(Color.Transparent);
|
||||
g.CompositingMode = CompositingMode.SourceOver;
|
||||
g.CompositingQuality = CompositingQuality.HighQuality;
|
||||
g.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
||||
g.SmoothingMode = SmoothingMode.HighQuality;
|
||||
g.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
||||
|
||||
float maxW = width - 40f;
|
||||
float maxH = height - 40f;
|
||||
float scale = Math.Min(maxW / bbox.Width, maxH / bbox.Height);
|
||||
int drawW = (int)Math.Round(bbox.Width * scale);
|
||||
int drawH = (int)Math.Round(bbox.Height * scale);
|
||||
int drawX = (width - drawW) / 2;
|
||||
int drawY = 20;
|
||||
if (drawY + drawH > height - 20) drawY = (height - drawH) / 2;
|
||||
g.DrawImage(src, new Rectangle(drawX, drawY, drawW, drawH), bbox, GraphicsUnit.Pixel);
|
||||
}
|
||||
HardClearGreen(dst);
|
||||
DespillGreenBias(dst);
|
||||
return dst;
|
||||
}
|
||||
|
||||
static void HardClearGreen(Bitmap bmp)
|
||||
{
|
||||
for (int y = 0; y < bmp.Height; y++)
|
||||
{
|
||||
for (int x = 0; x < bmp.Width; x++)
|
||||
{
|
||||
Color c = bmp.GetPixel(x, y);
|
||||
if (c.A == 0) continue;
|
||||
int maxRB = Math.Max(c.R, c.B);
|
||||
if (c.G > 70 && c.G - maxRB > 30)
|
||||
{
|
||||
int a = c.G - maxRB > 80 ? 0 : Math.Max(0, c.A - 140);
|
||||
int g = Math.Min(c.G, maxRB + 5);
|
||||
bmp.SetPixel(x, y, a == 0 ? Color.FromArgb(0, 0, 0, 0) : Color.FromArgb(a, c.R, g, c.B));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void DespillGreenBias(Bitmap bmp)
|
||||
{
|
||||
for (int y = 0; y < bmp.Height; y++)
|
||||
{
|
||||
for (int x = 0; x < bmp.Width; x++)
|
||||
{
|
||||
Color c = bmp.GetPixel(x, y);
|
||||
if (c.A == 0) continue;
|
||||
if (c.G > c.R + 6 && c.G > c.B + 10)
|
||||
{
|
||||
int g = Math.Min(c.G, Math.Max(c.R, c.B) + 4);
|
||||
int a = c.A;
|
||||
if (c.A < 235 && c.G > Math.Max(c.R, c.B) + 18) a = Math.Max(0, c.A - 35);
|
||||
bmp.SetPixel(x, y, Color.FromArgb(a, c.R, g, c.B));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void SliceParts(Bitmap master, string outputDir)
|
||||
{
|
||||
Rectangle bb = FindAlphaBounds(master, 8);
|
||||
var parts = new Dictionary<string, Bitmap>();
|
||||
foreach (string name in PartNames)
|
||||
parts[name] = new Bitmap(master.Width, master.Height, PixelFormat.Format32bppArgb);
|
||||
|
||||
for (int y = 0; y < master.Height; y++)
|
||||
{
|
||||
for (int x = 0; x < master.Width; x++)
|
||||
{
|
||||
Color c = master.GetPixel(x, y);
|
||||
if (c.A == 0) continue;
|
||||
string part = ClassifyPixel(c, x, y, bb);
|
||||
parts[part].SetPixel(x, y, c);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var kv in parts)
|
||||
{
|
||||
string path = Path.Combine(outputDir, "haruka_part_" + kv.Key + ".png");
|
||||
SavePng(kv.Value, path);
|
||||
kv.Value.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
static void RemoveTinyComponents(Bitmap bmp, int minPixels)
|
||||
{
|
||||
int w = bmp.Width, h = bmp.Height;
|
||||
bool[] seen = new bool[w * h];
|
||||
int[] stack = new int[w * h];
|
||||
int[] comp = new int[w * h];
|
||||
int[] dx = new int[] { 1, -1, 0, 0 };
|
||||
int[] dy = new int[] { 0, 0, 1, -1 };
|
||||
|
||||
for (int y = 0; y < h; y++)
|
||||
{
|
||||
for (int x = 0; x < w; x++)
|
||||
{
|
||||
int start = y * w + x;
|
||||
if (seen[start] || bmp.GetPixel(x, y).A == 0) continue;
|
||||
|
||||
int top = 0, count = 0;
|
||||
stack[top++] = start;
|
||||
seen[start] = true;
|
||||
|
||||
while (top > 0)
|
||||
{
|
||||
int id = stack[--top];
|
||||
comp[count++] = id;
|
||||
int cx = id % w;
|
||||
int cy = id / w;
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
int nx = cx + dx[i], ny = cy + dy[i];
|
||||
if (nx < 0 || nx >= w || ny < 0 || ny >= h) continue;
|
||||
int nid = ny * w + nx;
|
||||
if (seen[nid]) continue;
|
||||
seen[nid] = true;
|
||||
if (bmp.GetPixel(nx, ny).A > 0) stack[top++] = nid;
|
||||
}
|
||||
}
|
||||
|
||||
if (count < minPixels)
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
int id = comp[i];
|
||||
bmp.SetPixel(id % w, id / w, Color.FromArgb(0, 0, 0, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static string ClassifyPixel(Color c, int x, int y, Rectangle bb)
|
||||
{
|
||||
double nx = (x - bb.Left) / (double)Math.Max(1, bb.Width);
|
||||
double ny = (y - bb.Top) / (double)Math.Max(1, bb.Height);
|
||||
double cx = bb.Left + bb.Width * 0.5;
|
||||
bool screenLeft = x < cx;
|
||||
double dx = Math.Abs((x - cx) / Math.Max(1.0, bb.Width));
|
||||
|
||||
bool hair = IsHair(c);
|
||||
bool skin = IsSkin(c);
|
||||
|
||||
if (hair && ny < 0.54) return "head";
|
||||
if (ny < 0.255) return "head";
|
||||
if (hair && dx > 0.18 && ny < 0.58) return "head";
|
||||
|
||||
if (skin && dx < 0.09 && ny >= 0.235 && ny < 0.335) return "neck";
|
||||
|
||||
bool sideArmZone = dx > 0.18 && ny >= 0.28 && ny < 0.65;
|
||||
if (sideArmZone)
|
||||
{
|
||||
string side = screenLeft ? "_r" : "_l";
|
||||
bool farHand = (screenLeft && nx < 0.25 && ny >= 0.455) || (!screenLeft && nx > 0.75 && ny >= 0.455);
|
||||
if (skin && farHand) return "hand" + side;
|
||||
if (ny < 0.435) return "upperarm" + side;
|
||||
if (ny < 0.595) return "forearm" + side;
|
||||
return "hand" + side;
|
||||
}
|
||||
|
||||
if (ny < 0.33)
|
||||
{
|
||||
if (dx < 0.085) return "neck";
|
||||
return "head";
|
||||
}
|
||||
|
||||
if (ny < 0.505)
|
||||
{
|
||||
if (dx > 0.15)
|
||||
{
|
||||
string side = screenLeft ? "_r" : "_l";
|
||||
return ny < 0.43 ? "upperarm" + side : "forearm" + side;
|
||||
}
|
||||
return "chest";
|
||||
}
|
||||
|
||||
if (ny < 0.625)
|
||||
{
|
||||
if (dx > 0.24 && skin)
|
||||
{
|
||||
string side = screenLeft ? "_r" : "_l";
|
||||
return "hand" + side;
|
||||
}
|
||||
return "pelvis";
|
||||
}
|
||||
|
||||
if (ny < 0.755)
|
||||
return screenLeft ? "thigh_r" : "thigh_l";
|
||||
|
||||
if (ny < 0.895)
|
||||
return screenLeft ? "shin_r" : "shin_l";
|
||||
|
||||
return screenLeft ? "foot_r" : "foot_l";
|
||||
}
|
||||
|
||||
static bool IsHair(Color c)
|
||||
{
|
||||
return c.R > 95 && c.R > c.G + 22 && c.G > c.B - 8 && c.B < 155;
|
||||
}
|
||||
|
||||
static bool IsSkin(Color c)
|
||||
{
|
||||
return c.R > 190 && c.G > 125 && c.B > 95 && c.R >= c.G && c.G >= c.B - 18;
|
||||
}
|
||||
|
||||
static void Validate(string outputDir, Bitmap master, int width, int height)
|
||||
{
|
||||
string reportPath = Path.Combine(outputDir, "_validation.txt");
|
||||
using (var sw = new StreamWriter(reportPath, false))
|
||||
{
|
||||
sw.WriteLine("Haruka rig part build validation");
|
||||
sw.WriteLine("Canvas: " + width + "x" + height);
|
||||
sw.WriteLine("Master: haruka_part_master_apose.png");
|
||||
foreach (string name in PartNames)
|
||||
{
|
||||
string path = Path.Combine(outputDir, "haruka_part_" + name + ".png");
|
||||
using (var img = new Bitmap(path))
|
||||
{
|
||||
Rectangle b = FindAlphaBounds(img, 8);
|
||||
sw.WriteLine(Path.GetFileName(path) + " | " + img.Width + "x" + img.Height + " | " + img.PixelFormat + " | bbox=" + RectText(b));
|
||||
}
|
||||
}
|
||||
int missing, extra, differing, multiCovered;
|
||||
DirectCoverageCheck(outputDir, master, out missing, out extra, out differing, out multiCovered);
|
||||
sw.WriteLine("Direct coverage check | missing=" + missing + " | extra=" + extra + " | differing=" + differing + " | multi_covered=" + multiCovered);
|
||||
}
|
||||
}
|
||||
|
||||
static void DirectCoverageCheck(string outputDir, Bitmap master, out int missing, out int extra, out int differing, out int multiCovered)
|
||||
{
|
||||
missing = 0;
|
||||
extra = 0;
|
||||
differing = 0;
|
||||
multiCovered = 0;
|
||||
|
||||
var loaded = new List<Bitmap>();
|
||||
try
|
||||
{
|
||||
foreach (string name in PartNames)
|
||||
loaded.Add(new Bitmap(Path.Combine(outputDir, "haruka_part_" + name + ".png")));
|
||||
|
||||
for (int y = 0; y < master.Height; y++)
|
||||
{
|
||||
for (int x = 0; x < master.Width; x++)
|
||||
{
|
||||
Color m = master.GetPixel(x, y);
|
||||
int hits = 0;
|
||||
Color last = Color.Transparent;
|
||||
foreach (var part in loaded)
|
||||
{
|
||||
Color c = part.GetPixel(x, y);
|
||||
if (c.A > 0)
|
||||
{
|
||||
hits++;
|
||||
last = c;
|
||||
}
|
||||
}
|
||||
|
||||
if (m.A > 0 && hits == 0) missing++;
|
||||
if (m.A == 0 && hits > 0) extra++;
|
||||
if (hits > 1) multiCovered++;
|
||||
if (m.A > 0 && hits > 0)
|
||||
{
|
||||
int d = Math.Abs(m.A - last.A) + Math.Abs(m.R - last.R) + Math.Abs(m.G - last.G) + Math.Abs(m.B - last.B);
|
||||
if (d != 0) differing++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
foreach (var bmp in loaded) bmp.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
static string RectText(Rectangle r)
|
||||
{
|
||||
return r.Left + "," + r.Top + "," + r.Width + "," + r.Height;
|
||||
}
|
||||
|
||||
static void SavePng(Bitmap bmp, string path)
|
||||
{
|
||||
bmp.Save(path, ImageFormat.Png);
|
||||
}
|
||||
|
||||
static int Clamp(int v, int min, int max)
|
||||
{
|
||||
return v < min ? min : (v > max ? max : v);
|
||||
}
|
||||
}
|
||||
"@
|
||||
|
||||
Add-Type -TypeDefinition $code -ReferencedAssemblies "System.Drawing"
|
||||
|
||||
$resolvedSource = (Resolve-Path -LiteralPath $Source).Path
|
||||
$resolvedOut = if (Test-Path -LiteralPath $OutputDir) {
|
||||
(Resolve-Path -LiteralPath $OutputDir).Path
|
||||
} else {
|
||||
New-Item -ItemType Directory -Path $OutputDir -Force | Out-Null
|
||||
(Resolve-Path -LiteralPath $OutputDir).Path
|
||||
}
|
||||
|
||||
[HarukaRigBuilder]::Build($resolvedSource, $resolvedOut, $Width, $Height)
|
||||
Write-Output "Generated Haruka rig parts in $resolvedOut"
|
||||
Reference in New Issue
Block a user