404 lines
22 KiB
C#
404 lines
22 KiB
C#
// WL814u_Crawl.cs — 「3D 를 유지한 채 픽셀 떨림을 줄이는 대책」 실측 (에디터 전용 · 발주서 WL-814u 범위변경 ⓓ)
|
||
// 🔴 씬 저장 0 · 에셋 수정 0 · ProjectSettings 0.
|
||
//
|
||
// 떨림 수치 정의 (근거를 남긴다)
|
||
// 두 연속 프레임의 캐릭터 실루엣을 각자의 bbox 왼쪽아래로 맞춘 뒤(= 이동분을 뺀 뒤) 비교한다.
|
||
// shapeFlip % = XOR(실루엣A, 실루엣B) / 두 면적 평균 ← 「모양이 몇 % 바뀌었나」
|
||
// colorΔ % = 둘 다 덮인 픽셀 중 색 차이>24 인 비율 ← 「색이 몇 % 흔들렸나」
|
||
// 이동분을 빼기 때문에 「캐릭터가 지나간다」는 정상 움직임은 0 이 되고, 떨림만 남는다.
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Text;
|
||
using UnityEditor;
|
||
using UnityEditor.SceneManagement;
|
||
using UnityEngine;
|
||
using UnityEngine.Rendering.Universal;
|
||
|
||
public static class WL814u_Crawl
|
||
{
|
||
const string SCENE = "Assets/WL/Look/Arena/Scenes/WL_ArenaProto.unity";
|
||
const string SHOT = "Screenshots_WL/WL814u/";
|
||
const string OUT = "AgentScripts/WL814u_CRAWL.txt";
|
||
const int LAYER = 31;
|
||
static readonly StringBuilder sb = new StringBuilder();
|
||
static void L(string s) { sb.AppendLine(s); Console.WriteLine("[814uX] " + s); }
|
||
|
||
class Shot { public Color32[] px; public int w, h; }
|
||
class Frame { public bool[] m; public Shot c; public int x0, y0, x1, y1, area; }
|
||
|
||
static GameObject root; static SkinnedMeshRenderer[] smrs; static Camera scam;
|
||
static Camera cam, mcam;
|
||
|
||
static Shot Render(Camera c, int w, int h)
|
||
{
|
||
var rt = new RenderTexture(w, h, 24, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB);
|
||
rt.antiAliasing = 1; rt.filterMode = FilterMode.Point;
|
||
var pA = RenderTexture.active; c.targetTexture = rt; c.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();
|
||
c.targetTexture = null; RenderTexture.active = pA;
|
||
var s = new Shot { px = t.GetPixels32(), w = w, h = h };
|
||
UnityEngine.Object.DestroyImmediate(t); rt.Release(); UnityEngine.Object.DestroyImmediate(rt);
|
||
return s;
|
||
}
|
||
|
||
static Frame Grab(int w, int h)
|
||
{
|
||
mcam.backgroundColor = Color.black; var a = Render(mcam, w, h);
|
||
mcam.backgroundColor = Color.white; var b = Render(mcam, w, h);
|
||
var f = new Frame { m = new bool[w * h], c = a, x0 = w, y0 = h, x1 = -1, y1 = -1 };
|
||
for (int i = 0; i < f.m.Length; i++)
|
||
{
|
||
int d = Mathf.Max(Mathf.Abs(a.px[i].r - b.px[i].r), Mathf.Max(Mathf.Abs(a.px[i].g - b.px[i].g), Mathf.Abs(a.px[i].b - b.px[i].b)));
|
||
if (d < 128) { f.m[i] = true; f.area++; int x = i % w, y = i / w; if (x < f.x0) f.x0 = x; if (x > f.x1) f.x1 = x; if (y < f.y0) f.y0 = y; if (y > f.y1) f.y1 = y; }
|
||
}
|
||
return f;
|
||
}
|
||
|
||
// bbox 왼쪽아래 기준으로 맞춘 뒤 비교
|
||
static void Compare(Frame A, Frame B, int w, int h, out float shapeFlip, out float colorD)
|
||
{
|
||
int dx = B.x0 - A.x0, dy = B.y0 - A.y0;
|
||
int xor = 0, both = 0, chg = 0;
|
||
for (int y = A.y0; y <= A.y1; y++)
|
||
for (int x = A.x0; x <= A.x1; x++)
|
||
{
|
||
int bx = x + dx, by = y + dy;
|
||
bool ma = A.m[y * w + x];
|
||
bool mb = (bx >= 0 && by >= 0 && bx < w && by < h) && B.m[by * w + bx];
|
||
if (ma != mb) xor++;
|
||
else if (ma)
|
||
{
|
||
both++;
|
||
var ca = A.c.px[y * w + x]; var cb = B.c.px[by * w + bx];
|
||
if (Mathf.Abs(ca.r - cb.r) + Mathf.Abs(ca.g - cb.g) + Mathf.Abs(ca.b - cb.b) > 24) chg++;
|
||
}
|
||
}
|
||
shapeFlip = 200f * xor / Mathf.Max(1, A.area + B.area);
|
||
colorD = 100f * chg / Mathf.Max(1, both);
|
||
}
|
||
|
||
static bool[] Dilate(bool[] m, int w, int h, int n)
|
||
{
|
||
var cur = (bool[])m.Clone();
|
||
for (int k = 0; k < n; k++)
|
||
{
|
||
var nx = (bool[])cur.Clone();
|
||
for (int y = 0; y < h; y++)
|
||
for (int x = 0; x < w; x++)
|
||
if (cur[y * w + x])
|
||
{
|
||
if (x > 0) nx[y * w + x - 1] = true; if (x < w - 1) nx[y * w + x + 1] = true;
|
||
if (y > 0) nx[(y - 1) * w + x] = true; if (y < h - 1) nx[(y + 1) * w + x] = true;
|
||
}
|
||
cur = nx;
|
||
}
|
||
return cur;
|
||
}
|
||
|
||
static void Setup(float ortho, int w, int h)
|
||
{
|
||
if (cam != null) UnityEngine.Object.DestroyImmediate(cam.gameObject);
|
||
if (mcam != null) UnityEngine.Object.DestroyImmediate(mcam.gameObject);
|
||
var b = new Bounds(root.transform.position, Vector3.zero); foreach (var r in smrs) b.Encapsulate(r.bounds);
|
||
cam = new GameObject("WL814u_X").AddComponent<Camera>(); cam.CopyFrom(scam);
|
||
cam.orthographic = true; cam.orthographicSize = ortho; cam.aspect = (float)w / h; cam.targetTexture = null;
|
||
cam.transform.SetPositionAndRotation(b.center - scam.transform.forward * 50f, scam.transform.rotation);
|
||
var d1 = cam.GetUniversalAdditionalCameraData(); if (d1 != null) { d1.renderPostProcessing = false; d1.SetRenderer(0); }
|
||
mcam = new GameObject("WL814u_XM").AddComponent<Camera>(); mcam.CopyFrom(cam);
|
||
mcam.cullingMask = 1 << LAYER; mcam.clearFlags = CameraClearFlags.SolidColor; mcam.aspect = (float)w / h;
|
||
var d2 = mcam.GetUniversalAdditionalCameraData(); if (d2 != null) { d2.renderPostProcessing = false; d2.SetRenderer(0); }
|
||
for (int i = 0; i < 3; i++) Render(mcam, w, h); // 워밍업(첫 렌더는 텍스처 미로드)
|
||
}
|
||
|
||
// PixelCameraManager.PositionToGrid 와 같은 식: 카메라 축 기준 픽셀 격자에 반올림
|
||
static Vector3 ToGrid(Vector3 p, float pixelWorld)
|
||
{
|
||
var t = cam.transform;
|
||
float rx = Vector3.Dot(p, t.right), ry = Vector3.Dot(p, t.up), rz = Vector3.Dot(p, t.forward);
|
||
rx = Mathf.Round(rx / pixelWorld) * pixelWorld; ry = Mathf.Round(ry / pixelWorld) * pixelWorld; rz = Mathf.Round(rz / pixelWorld) * pixelWorld;
|
||
return rx * t.right + ry * t.up + rz * t.forward;
|
||
}
|
||
|
||
public static void Run()
|
||
{
|
||
try { Body(); }
|
||
catch (Exception e) { L("EXCEPTION " + e); }
|
||
finally { Directory.CreateDirectory(Path.GetDirectoryName(OUT)); File.WriteAllText(OUT, sb.ToString(), new UTF8Encoding(true)); }
|
||
}
|
||
|
||
// ⓓ-2 재측정 — 1차는 8° 만 돌려서 16방향이 경계를 한 번도 안 넘었다(0.00 % 는 측정 구간 탓).
|
||
// 이번엔 90° 를 돌려 모든 양자화가 경계를 여러 번 넘게 한다.
|
||
public static void Run2()
|
||
{
|
||
try
|
||
{
|
||
EditorSceneManager.OpenScene(SCENE, OpenSceneMode.Single);
|
||
foreach (var go in UnityEngine.SceneManagement.SceneManager.GetActiveScene().GetRootGameObjects())
|
||
if (go.name.Contains("LH_M05")) root = go;
|
||
smrs = root.GetComponentsInChildren<SkinnedMeshRenderer>(true).Where(r => r.enabled).ToArray();
|
||
scam = Camera.main ?? UnityEngine.Object.FindObjectsByType<Camera>(FindObjectsInactive.Exclude, FindObjectsSortMode.None).First(c => c.orthographic);
|
||
var allT = root.GetComponentsInChildren<Transform>(true);
|
||
var orig = new Dictionary<Transform, int>(); foreach (var t in allT) { orig[t] = t.gameObject.layer; t.gameObject.layer = LAYER; }
|
||
var baseRot = root.transform.rotation;
|
||
float yaw0 = scam.transform.eulerAngles.y + 180f;
|
||
Setup(3.4f, 135, 240);
|
||
const int N = 91; const float STEP = 1.0f; // 1°/프레임 × 90 = 90° 회전
|
||
L("");
|
||
L("== ⓓ-2(재측정) 회전 계단화 · 90° 회전 (1°/프레임 × 90) · 도트A 240줄 ==");
|
||
L(" | 방향 수 | 각도 간격 | 떨림 0 인 프레임 | 모양 변화 평균 %/프레임 | 계단 순간 최대 % | 계단 횟수/90° |");
|
||
foreach (int dirs in new[] { 0, 32, 16, 8 })
|
||
{
|
||
float q = dirs == 0 ? 0f : 360f / dirs;
|
||
Frame prev = null; var sf = new List<float>(); int zero = 0, steps = 0; float mx = 0;
|
||
for (int i = 0; i < N; i++)
|
||
{
|
||
float y = yaw0 + i * STEP; if (q > 0f) y = Mathf.Round(y / q) * q;
|
||
root.transform.rotation = Quaternion.Euler(0, y, 0);
|
||
var f = Grab(135, 240);
|
||
if (prev != null)
|
||
{
|
||
Compare(prev, f, 135, 240, out float s, out float c);
|
||
sf.Add(s); if (s < 0.01f) zero++; if (s > 0.5f) steps++; if (s > mx) mx = s;
|
||
}
|
||
prev = f;
|
||
}
|
||
L(string.Format(" | {0} | {1} | **{2}/90** | **{3:F2} %** | {4:F1} % | {5} |",
|
||
dirs == 0 ? "양자화 없음" : dirs.ToString(), dirs == 0 ? "-" : q.ToString("F1") + "°",
|
||
zero, sf.Average(), mx, steps));
|
||
}
|
||
root.transform.rotation = baseRot;
|
||
foreach (var kv in orig) if (kv.Key != null) kv.Key.gameObject.layer = kv.Value;
|
||
if (cam != null) UnityEngine.Object.DestroyImmediate(cam.gameObject);
|
||
if (mcam != null) UnityEngine.Object.DestroyImmediate(mcam.gameObject);
|
||
}
|
||
catch (Exception e) { L("EXCEPTION " + e); }
|
||
finally { File.AppendAllText(OUT, sb.ToString(), new UTF8Encoding(false)); }
|
||
}
|
||
|
||
static void Body()
|
||
{
|
||
EditorSceneManager.OpenScene(SCENE, OpenSceneMode.Single);
|
||
foreach (var go in UnityEngine.SceneManagement.SceneManager.GetActiveScene().GetRootGameObjects())
|
||
if (go.name.Contains("LH_M05")) root = go;
|
||
smrs = root.GetComponentsInChildren<SkinnedMeshRenderer>(true).Where(r => r.enabled).ToArray();
|
||
scam = Camera.main ?? UnityEngine.Object.FindObjectsByType<Camera>(FindObjectsInactive.Exclude, FindObjectsSortMode.None).First(c => c.orthographic);
|
||
var all = root.GetComponentsInChildren<Transform>(true);
|
||
var orig = new Dictionary<Transform, int>(); foreach (var t in all) { orig[t] = t.gameObject.layer; t.gameObject.layer = LAYER; }
|
||
float camYaw = scam.transform.eulerAngles.y;
|
||
var basePos = root.transform.position;
|
||
var baseRot = root.transform.rotation;
|
||
root.transform.rotation = Quaternion.Euler(0, camYaw + 180f, 0);
|
||
float yaw0 = camYaw + 180f;
|
||
|
||
const int N = 33; // 33 프레임 = 32 비교
|
||
const float STEP = 0.25f; // 프레임당 0.25° 회전 (걷다가 방향 트는 속도 수준)
|
||
|
||
try
|
||
{
|
||
// ── 1) 해상도별 회전 떨림 ────────────────────────────────────
|
||
L("== ⓓ-1 해상도별 회전 떨림 (캐릭터 yaw 0.25°/프레임 × 32회 · 이동분 제거 후 비교) ==");
|
||
L(" | 설정 | 캐릭터 키 | 모양 변화 %/프레임 | 색 변화 %/프레임 | 1° 환산 모양 % |");
|
||
var res = new (string name, float ortho, int w, int h)[] {
|
||
("도트A 240줄 (ortho 3.4)", 3.4f, 135, 240),
|
||
("360줄 (ortho 3.4)", 3.4f, 203, 360),
|
||
("480줄 (ortho 3.4)", 3.4f, 270, 480),
|
||
("현재 1920줄 (ortho 10)", 10f, 1080, 1920),
|
||
};
|
||
foreach (var r in res)
|
||
{
|
||
Setup(r.ortho, r.w, r.h);
|
||
var (sf, cd, hgt) = RotSweep(r.w, r.h, yaw0, STEP, N, 0f);
|
||
L(string.Format(" | {0} | {1} px | **{2:F2} %** | **{3:F1} %** | {4:F1} % |", r.name, hgt, sf, cd, sf / STEP));
|
||
}
|
||
|
||
// ── 2) 회전 양자화 ──────────────────────────────────────────
|
||
L("");
|
||
L("== ⓓ-2 회전 계단화 (yaw 를 N방향으로 반올림 · 도트A 240줄) ==");
|
||
L(" | 방향 수 | 각도 간격 | 모양 변화 평균 %/프레임 | 그중 최대(계단 순간) % | 계단이 생긴 프레임 수/32 |");
|
||
Setup(3.4f, 135, 240);
|
||
foreach (int dirs in new[] { 0, 32, 16, 8 })
|
||
{
|
||
float q = dirs == 0 ? 0f : 360f / dirs;
|
||
var (sf, cd, _) = RotSweep(135, 240, yaw0, STEP, N, q, out float mx, out int steps);
|
||
L(string.Format(" | {0} | {1} | **{2:F2} %** | {3:F2} % | {4} |",
|
||
dirs == 0 ? "양자화 없음(연속)" : dirs.ToString(), dirs == 0 ? "-" : (q.ToString("F1") + "°"), sf, mx, steps));
|
||
}
|
||
|
||
// ── 3) 이동 + 픽셀 격자 스냅 ────────────────────────────────
|
||
L("");
|
||
L("== ⓓ-3 이동 시 떨림 · 픽셀 격자 스냅(VoxelGridAdjuster 방식) 유/무 (도트A 240줄) ==");
|
||
Setup(3.4f, 135, 240);
|
||
float pixelWorld = 2f * 3.4f / 240f;
|
||
L(string.Format(" 픽셀 1개 = {0:F5} m · 프레임당 이동 0.37 px (일부러 격자에 안 맞는 값)", pixelWorld));
|
||
L(" | 스냅 | 모양 변화 평균 %/프레임 | 최대 % |");
|
||
foreach (bool snap in new[] { false, true })
|
||
{
|
||
root.transform.rotation = Quaternion.Euler(0, yaw0, 0);
|
||
Frame prev = null; var sfs = new List<float>();
|
||
for (int i = 0; i < N; i++)
|
||
{
|
||
var p = basePos + cam.transform.right * (0.37f * pixelWorld * i);
|
||
root.transform.position = snap ? ToGrid(p, pixelWorld) : p;
|
||
var f = Grab(135, 240);
|
||
if (prev != null) { Compare(prev, f, 135, 240, out float s, out float c); sfs.Add(s); }
|
||
prev = f;
|
||
}
|
||
root.transform.position = basePos;
|
||
L(string.Format(" | {0} | **{1:F2} %** | {2:F2} % |", snap ? "켬(격자 스냅)" : "끔", sfs.Average(), sfs.Max()));
|
||
}
|
||
|
||
// ── 4) 외곽선 두께와 떨림 ───────────────────────────────────
|
||
L("");
|
||
L("== ⓓ-4 외곽선 두께 vs 떨림 (회전 중 · 도트A 240줄 · 외곽선 = 실루엣을 N px 부풀린 띠) ==");
|
||
Setup(3.4f, 135, 240);
|
||
L(" | 외곽선 두께 | 띠 면적(px) | 띠가 프레임마다 바뀌는 비율 % | 캐릭터 대비 띠 비중 % |");
|
||
var frames = new List<Frame>();
|
||
for (int i = 0; i < 17; i++) { root.transform.rotation = Quaternion.Euler(0, yaw0 + i * STEP, 0); frames.Add(Grab(135, 240)); }
|
||
foreach (int th in new[] { 1, 2, 3 })
|
||
{
|
||
var bandFlip = new List<float>(); var bandArea = new List<float>(); var ratio = new List<float>();
|
||
for (int i = 1; i < frames.Count; i++)
|
||
{
|
||
var A = frames[i - 1]; var B = frames[i];
|
||
var dA = Dilate(A.m, 135, 240, th); var dB = Dilate(B.m, 135, 240, th);
|
||
int dx = B.x0 - A.x0, dy = B.y0 - A.y0;
|
||
int xor = 0, ar = 0;
|
||
for (int y = 1; y < 239; y++)
|
||
for (int x = 1; x < 134; x++)
|
||
{
|
||
bool ba = dA[y * 135 + x] && !A.m[y * 135 + x];
|
||
int bxx = x + dx, byy = y + dy;
|
||
bool bb = (bxx > 0 && byy > 0 && bxx < 134 && byy < 239) && dB[byy * 135 + bxx] && !B.m[byy * 135 + bxx];
|
||
if (ba) ar++;
|
||
if (ba != bb) xor++;
|
||
}
|
||
bandFlip.Add(100f * xor / Mathf.Max(1, ar)); bandArea.Add(ar); ratio.Add(100f * ar / A.area);
|
||
}
|
||
L(string.Format(" | {0} px | {1:F0} | **{2:F1} %** | {3:F0} % |", th, bandArea.Average(), bandFlip.Average(), ratio.Average()));
|
||
}
|
||
|
||
// ── 5) 연속 프레임 비교 이미지 ──────────────────────────────
|
||
L("");
|
||
L("== 캡처 ==");
|
||
Strip("u_6_crawl_rotation.png", 3.4f, 135, 240, yaw0, STEP, 0f, 16f, "회전 8프레임: 위=양자화 없음 / 아래=16방향 계단화");
|
||
StripMove("u_7_crawl_move_snap.png", 3.4f, 135, 240, yaw0, basePos);
|
||
}
|
||
finally
|
||
{
|
||
root.transform.rotation = baseRot; root.transform.position = basePos;
|
||
foreach (var kv in orig) if (kv.Key != null) kv.Key.gameObject.layer = kv.Value;
|
||
if (cam != null) UnityEngine.Object.DestroyImmediate(cam.gameObject);
|
||
if (mcam != null) UnityEngine.Object.DestroyImmediate(mcam.gameObject);
|
||
}
|
||
}
|
||
|
||
static (float, float, int) RotSweep(int w, int h, float yaw0, float step, int n, float q)
|
||
{ return RotSweep(w, h, yaw0, step, n, q, out _, out _); }
|
||
|
||
static (float, float, int) RotSweep(int w, int h, float yaw0, float step, int n, float q, out float maxFlip, out int stepCount)
|
||
{
|
||
Frame prev = null; var sf = new List<float>(); var cd = new List<float>(); int hgt = 0;
|
||
maxFlip = 0; stepCount = 0;
|
||
for (int i = 0; i < n; i++)
|
||
{
|
||
float y = yaw0 + i * step;
|
||
if (q > 0f) y = Mathf.Round(y / q) * q;
|
||
root.transform.rotation = Quaternion.Euler(0, y, 0);
|
||
var f = Grab(w, h);
|
||
hgt = f.y1 - f.y0 + 1;
|
||
if (prev != null)
|
||
{
|
||
Compare(prev, f, w, h, out float s, out float c);
|
||
sf.Add(s); cd.Add(c);
|
||
if (s > maxFlip) maxFlip = s;
|
||
if (s > 0.5f) stepCount++;
|
||
}
|
||
prev = f;
|
||
}
|
||
return (sf.Average(), cd.Average(), hgt);
|
||
}
|
||
|
||
// 회전 8프레임 × 2행(양자화 없음 / 양자화)
|
||
static void Strip(string file, float ortho, int w, int h, float yaw0, float step, float q1, float dirs2, string note)
|
||
{
|
||
Setup(ortho, w, h);
|
||
int zoom = 6, crop = 48;
|
||
var rows = new List<Shot[]>();
|
||
foreach (float dirs in new[] { 0f, dirs2 })
|
||
{
|
||
float q = dirs == 0f ? 0f : 360f / dirs;
|
||
var row = new List<Shot>();
|
||
for (int i = 0; i < 8; i++)
|
||
{
|
||
float y = yaw0 + i * step * 4f; // 8프레임에 8° 회전
|
||
if (q > 0f) y = Mathf.Round(y / q) * q;
|
||
root.transform.rotation = Quaternion.Euler(0, y, 0);
|
||
row.Add(Render(cam, w, h));
|
||
}
|
||
rows.Add(row.ToArray());
|
||
}
|
||
// 합성
|
||
int cw = crop, chh = crop, gap = 4;
|
||
int ow = (cw * zoom + gap) * 8 - gap, oh = (chh * zoom + gap) * rows.Count - gap;
|
||
var o = new Shot { w = ow, h = oh, px = new Color32[ow * oh] };
|
||
for (int i = 0; i < o.px.Length; i++) o.px[i] = new Color32(24, 24, 28, 255);
|
||
int cx = w / 2 - crop / 2, cy = h / 2 - crop / 2;
|
||
for (int r = 0; r < rows.Count; r++)
|
||
for (int k = 0; k < 8; k++)
|
||
for (int y = 0; y < chh * zoom; y++)
|
||
for (int x = 0; x < cw * zoom; x++)
|
||
{
|
||
int sx = cx + x / zoom, sy = cy + y / zoom;
|
||
if (sx < 0 || sy < 0 || sx >= w || sy >= h) continue;
|
||
int dxp = k * (cw * zoom + gap) + x;
|
||
int dyp = (rows.Count - 1 - r) * (chh * zoom + gap) + y;
|
||
o.px[dyp * ow + dxp] = rows[r][k].px[sy * w + sx];
|
||
}
|
||
SaveShot(o, file); L(" " + file + " — " + note);
|
||
}
|
||
|
||
static void StripMove(string file, float ortho, int w, int h, float yaw0, Vector3 basePos)
|
||
{
|
||
Setup(ortho, w, h);
|
||
root.transform.rotation = Quaternion.Euler(0, yaw0, 0);
|
||
float pixelWorld = 2f * ortho / h;
|
||
int zoom = 6, crop = 48, gap = 4;
|
||
var rows = new List<Shot[]>();
|
||
foreach (bool snap in new[] { false, true })
|
||
{
|
||
var row = new List<Shot>();
|
||
for (int i = 0; i < 8; i++)
|
||
{
|
||
var p = basePos + cam.transform.right * (0.37f * pixelWorld * i);
|
||
root.transform.position = snap ? ToGrid(p, pixelWorld) : p;
|
||
row.Add(Render(cam, w, h));
|
||
}
|
||
rows.Add(row.ToArray());
|
||
}
|
||
root.transform.position = basePos;
|
||
int ow = (crop * zoom + gap) * 8 - gap, oh = (crop * zoom + gap) * 2 - gap;
|
||
var o = new Shot { w = ow, h = oh, px = new Color32[ow * oh] };
|
||
for (int i = 0; i < o.px.Length; i++) o.px[i] = new Color32(24, 24, 28, 255);
|
||
int cx = w / 2 - crop / 2, cy = h / 2 - crop / 2;
|
||
for (int r = 0; r < 2; r++)
|
||
for (int k = 0; k < 8; k++)
|
||
for (int y = 0; y < crop * zoom; y++)
|
||
for (int x = 0; x < crop * zoom; x++)
|
||
{
|
||
int sx = cx + x / zoom, sy = cy + y / zoom;
|
||
if (sx < 0 || sy < 0 || sx >= w || sy >= h) continue;
|
||
o.px[((1 - r) * (crop * zoom + gap) + y) * ow + k * (crop * zoom + gap) + x] = rows[r][k].px[sy * w + sx];
|
||
}
|
||
SaveShot(o, file); L(" " + file + " — 이동 8프레임: 위=스냅 없음 / 아래=픽셀 격자 스냅");
|
||
}
|
||
|
||
static void SaveShot(Shot s, string file)
|
||
{
|
||
var t = new Texture2D(s.w, s.h, TextureFormat.RGBA32, false); t.SetPixels32(s.px); t.Apply();
|
||
Directory.CreateDirectory(SHOT); File.WriteAllBytes(SHOT + file, t.EncodeToPNG());
|
||
UnityEngine.Object.DestroyImmediate(t);
|
||
}
|
||
}
|