Project_WL/AgentScripts/WL788_Measure.cs

447 lines
22 KiB
C#
Raw Normal View History

WL 리뉴얼 세션 2026-09-06~07 (#760~#800) — 전투·배경·UI·타격감 (조직 PD 로그 #746~#800 · 대화로그 §14~§24) - 전투: Knight@Attack1~3_S 3콤보(4 컨트롤러·클래스별 서브클립) · 타겟팅 4규칙(정면 우선·재타겟·즉시 공격·어그로 금지 · WLTargetingSettings) · 대쉬 후 공격(Stander@Chase_Start · DashDriver · 5 m · 사정거리+0.8 · 근거리 적 우선) · 공격 이동 FrameTable(발 접지 실측 표 · AttackRootMotion) · 충돌 반경/투사체 0.3 배(WLCollisionTuning) · 무적(임시)·펫 금지(WLGameplaySettings) - 검기: NamuFX Slash_B 배리언트 Effect_WLSwingArc 원 피팅 정합 배치 + 캘리브레이션(SlashArcMeasure · SlashTrailSettings) · 찌르기 Effect_WLStab 대기 · 램프 리본(BladeTrail · WL_BladeRibbon.shader · T_WL_BladeRibbonRamp) 보존(drawRibbon 0) - 타격감: Assets/WL/Feel(WLHitFeel · 히트스톱 0.03 · 셰이크 0.10 m · 몹 펀치 1.12 · Actor.Get_Damage 훅 1줄 · 원본 RealCamera 셰이크 결함 대체) - 배경/맵: LMHPOLY Demo_01~10 → WL_Nature01~10(프리팹·씬·NavMesh·스포너·BattleMapConfig) · 물(ToonWaterU) · 포스트 블룸 0.9/0.3 · 잔디(BruteForce·드레싱) 제거 · 마젠타 머티리얼 URP 변환 - UI: 세로 HUD(WL_HUD · 하단 5메뉴 폭 전체 · 채팅/전투 패드 숨김 · WLIngameUiOverride) · Title/TitleInfo 1080×1920 Expand + 배경 높이 fit(WLBackgroundFit) · 로딩 SortOrder_5(WLRawImageAspectSync · 초점표 8장) · Loading1~8 ASTC 6×6 - 도구: AgentScripts/*(LightProbe · WL_MapSwitch · WL760~WL800 프로브/집행/검증 · 상단 사용법 주석) · 에디터 락 프로토콜 파일(staging) - 제외(별도 커밋 예정): Assets/LMHPOLY(703 MB) · Assets/Feel(422 MB) · Assets/Shinabro(300 MB) — 에셋 스토어/구 WL 팩 원본 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 23:24:25 +00:00
// WL788_Measure.cs — PD 지시 #788 크레센트 로컬 프레임 실측 (에디트 모드 · Assets 무수정)
// unity command run_script --file AgentScripts/WL788_Measure.cs --entry WL788_Measure.Dump --args '["Effect_Slash_10101_1"]'
// unity command run_script --file AgentScripts/WL788_Measure.cs --entry WL788_Measure.DumpMany --args '["Effect_Slash_10101_1,Effect_Slash_10501_1"]'
//
// 측정 방법 (구 방식 "메시 바운즈 최소축" 의 한계를 넘기 위한 정밀화):
// ① 프리팹을 프리뷰 씬에 인스턴스(현재 씬 무영향 · isDirty 유발 없음)
// ② 크레센트 이미터를 수명 30% 지점까지 Simulate → ParticleSystemRenderer.BakeMesh
// = 정렬(alignment) · startRotation · startSize · 스트레치가 모두 반영된 **실제 렌더 지오메트리**
// ③ 베이크 메시의 삼각형을 UV 로 리샘플하며 머티리얼 텍스처의 알파×휘도(잉크)를 가중치로 삼아
// 루트 로컬 공간의 "보이는 픽셀 구름" 을 만든다 (크레센트가 메시 모양이든 텍스처든 동일 처리)
// ④ 가중 PCA → 평면 법선 · 평면 투영 후 Kasa 원 적합 → 호 중심 · 내/외 반지름 · 각도 범위
// → 시위 방향(start→end) · 볼록 방향(중심→호 중점) 확정
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
public static class WL788_Measure
{
// ── 결과 구조 ────────────────────────────────────────────────────────────
public struct Frame
{
public string emitter;
public Vector3 normal; // 루트 로컬 — 크레센트 평면 법선
public Vector3 chord; // 루트 로컬 — 호 시작→끝 (시위) 방향, 정규화
public Vector3 convex; // 루트 로컬 — 호 중심→호 중점 (볼록) 방향, 정규화
public Vector3 center; // 루트 로컬 — 호의 원 중심
public float innerR, outerR, midR;
public float arcDeg; // 호가 덮는 각도
public float ink; // 잉크 총량(가중치 합)
public float planarity; // 평면성 = 1 - (최소분산/중간분산). 1 에 가까울수록 평평
public float circleRms; // 원 적합 잔차 RMS / midR
public int samples;
public string inkMode;
public bool oriented; // 방향 정보를 담고 있는가(카메라 정렬이 아닌가)
public string tile; // 스프라이트 시트에서 고른 타일
}
public static object DumpMany(string csv)
{
var sb = new StringBuilder();
foreach (var n in csv.Split(',')) { sb.AppendLine(Dump(n.Trim()) as string); }
return sb.ToString();
}
public static object Dump(string prefabName)
{
string path = FindPrefab(prefabName);
if (path == null) return "prefab not found: " + prefabName;
var src = AssetDatabase.LoadAssetAtPath<GameObject>(path);
var sb = new StringBuilder();
sb.AppendLine("== " + prefabName + " " + path);
sb.AppendLine(" rootScale=" + src.transform.localScale.ToString("F3") + " rootEuler=" + src.transform.localEulerAngles.ToString("F1"));
Scene preview = default(Scene);
GameObject inst = null;
GameObject camGo = null;
try
{
preview = EditorSceneManager.NewPreviewScene();
inst = Object.Instantiate(src);
inst.transform.SetPositionAndRotation(Vector3.zero, Quaternion.identity);
SceneManager.MoveGameObjectToScene(inst, preview);
camGo = new GameObject("wl788cam");
SceneManager.MoveGameObjectToScene(camGo, preview);
var cam = camGo.AddComponent<Camera>();
camGo.transform.SetPositionAndRotation(new Vector3(0f, 0f, -10f), Quaternion.identity);
cam.enabled = false;
var root = inst.transform;
foreach (var ps in inst.GetComponentsInChildren<ParticleSystem>(true))
{
var psr = ps.GetComponent<ParticleSystemRenderer>();
if (psr == null) continue;
var main = ps.main;
string mat = psr.sharedMaterial != null ? psr.sharedMaterial.name : "-";
var tex = GetTex(psr.sharedMaterial);
var tsa = ps.textureSheetAnimation;
sb.AppendLine(string.Format(
" -- {0,-16} on={1} render={2}{3} align={4} size={5:F2} life={6:F2} rot3D={7} rot={8:F0} sim={9} mat={10} tex={11}{12}",
ps.gameObject.name, psr.enabled, psr.renderMode,
psr.renderMode == ParticleSystemRenderMode.Mesh && psr.mesh != null ? "(" + psr.mesh.name + " v" + psr.mesh.vertexCount + ")" : "",
psr.alignment, main.startSize.constantMax, main.startLifetime.constantMax,
main.startRotation3D, main.startRotation.constantMax * Mathf.Rad2Deg, main.simulationSpace, mat,
tex != null ? tex.name + " " + tex.width + "x" + tex.height : "-",
tsa.enabled ? string.Format(" sheet={0}x{1}", tsa.numTilesX, tsa.numTilesY) : ""));
if (!psr.enabled) continue;
Frame f;
if (!MeasureEmitter(ps, psr, root, cam, out f)) { sb.AppendLine(" (측정 불가 — 잉크 없음)"); continue; }
sb.AppendLine(Format(f));
}
}
finally
{
if (camGo != null) Object.DestroyImmediate(camGo);
if (inst != null) Object.DestroyImmediate(inst);
if (preview.IsValid()) EditorSceneManager.ClosePreviewScene(preview);
}
return sb.ToString();
}
static string Format(Frame f)
{
return string.Format(
" FRAME n={0} chord={1} convex={2} center={3}\n" +
" innerR={4:F3} outerR={5:F3} midR={6:F3} arc={7:F1}deg ink={8:F0} planar={9:F3} circRms={10:F3} pts={11}",
f.normal.ToString("F3"), f.chord.ToString("F3"), f.convex.ToString("F3"), f.center.ToString("F3"),
f.innerR, f.outerR, f.midR, f.arcDeg, f.ink, f.planarity, f.circleRms, f.samples)
+ " " + f.inkMode + (string.IsNullOrEmpty(f.tile) ? "" : " tile=" + f.tile);
}
// ── 이미터 1개 실측 (결정론적) ───────────────────────────────────────────
// Simulate + BakeMesh 는 **여러 파티클의 합집합**이라 shape 산포·랜덤 시드 때문에
// 실행마다 호 중심·시위 방향이 달라진다(실측 2026-09-07: 같은 이미터 2회 측정에서
// 법선은 4° 이내로 같았지만 chord 가 평면 안에서 60° 이상 회전). 캘리브레이션 값은
// 재현 가능해야 하므로 **파티클 1개의 소스 지오메트리**를 결정론적으로 잰다.
// · Mesh 렌더 → psr.mesh 의 정점/UV 를 그대로 (메시 로컬)
// · Billboard/Local → 이미터 로컬 XY 평면의 단위 쿼드로 합성 (모양은 텍스처가 갖는다)
// · View 정렬 → 항상 카메라를 보므로 방향 정보 없음 → 측정 제외
// 그 뒤 이미터의 루트 기준 상대 회전·스케일·위치와 startSize·startRotation 을 곱한다.
public static bool MeasureEmitter(ParticleSystem ps, ParticleSystemRenderer psr, Transform root, Camera cam, out Frame f)
{
f = new Frame();
f.emitter = ps.gameObject.name;
var main = ps.main;
bool oriented = psr.alignment != ParticleSystemRenderSpace.View;
f.oriented = oriented;
if (!oriented) return false;
// ① 파티클 로컬 지오메트리 (정점 + UV + 삼각형)
Vector3[] verts; Vector2[] uvs; int[] tris;
if (psr.renderMode == ParticleSystemRenderMode.Mesh && psr.mesh != null)
{
verts = psr.mesh.vertices; uvs = psr.mesh.uv; tris = psr.mesh.triangles;
}
else
{
// 단위 쿼드 — 이미터 로컬 XY 평면, 법선 +Z
verts = new[] { new Vector3(-0.5f, -0.5f, 0f), new Vector3(0.5f, -0.5f, 0f), new Vector3(0.5f, 0.5f, 0f), new Vector3(-0.5f, 0.5f, 0f) };
uvs = new[] { new Vector2(0, 0), new Vector2(1, 0), new Vector2(1, 1), new Vector2(0, 1) };
tris = new[] { 0, 1, 2, 0, 2, 3 };
}
if (verts.Length < 3 || tris.Length < 3) return false;
if (uvs == null || uvs.Length != verts.Length) uvs = null;
// ② 텍스처 (스프라이트 시트면 잉크가 가장 많은 타일을 고른다)
Texture2D readable = MakeReadable(GetTex(psr.sharedMaterial));
Vector2 tileScale = Vector2.one, tileOffset = Vector2.zero;
var tsa = ps.textureSheetAnimation;
if (readable != null && tsa.enabled && tsa.numTilesX > 0 && tsa.numTilesY > 0)
{
tileScale = new Vector2(1f / tsa.numTilesX, 1f / tsa.numTilesY);
float best = -1f;
for (int ty = 0; ty < tsa.numTilesY; ty++)
for (int tx = 0; tx < tsa.numTilesX; tx++)
{
float ink = TileInk(readable, new Vector2(tx * tileScale.x, ty * tileScale.y), tileScale);
if (ink > best) { best = ink; tileOffset = new Vector2(tx * tileScale.x, ty * tileScale.y); }
}
f.tile = string.Format("{0}x{1}@{2:F2},{3:F2}", tsa.numTilesX, tsa.numTilesY, tileOffset.x, tileOffset.y);
}
try
{
// ③ 파티클 로컬 → 루트 로컬 변환
// 루트 기준 상대 회전 · 스케일. startSize 는 파티클 크기 배수.
Quaternion rel = Quaternion.Inverse(root.rotation) * ps.transform.rotation;
float rotZ = main.startRotation.constantMax * Mathf.Rad2Deg;
if (Mathf.Abs(rotZ) > 0.01f) rel = rel * Quaternion.Euler(0f, 0f, rotZ);
Vector3 relScale = InvScale(ps.transform.lossyScale, root.lossyScale) * Mathf.Max(main.startSize.constantMax, 1e-4f);
Vector3 origin = root.InverseTransformPoint(ps.transform.position);
float[] thresholds = { 0.05f, 0.01f, 0f }; // 마지막 0 = 지오메트리 전용 폴백
for (int ti2 = 0; ti2 < thresholds.Length; ti2++)
{
float thr = thresholds[ti2];
var pts = new List<Vector3>(8192);
var wts = new List<float>(8192);
const int GRID = 14;
int triCount = tris.Length / 3;
for (int ti = 0; ti < triCount; ti++)
{
Vector3 a = verts[tris[ti * 3]], b = verts[tris[ti * 3 + 1]], c = verts[tris[ti * 3 + 2]];
Vector2 ua = Vector2.zero, ub = Vector2.zero, uc = Vector2.zero;
if (uvs != null) { ua = uvs[tris[ti * 3]]; ub = uvs[tris[ti * 3 + 1]]; uc = uvs[tris[ti * 3 + 2]]; }
for (int i = 0; i <= GRID; i++)
for (int j = 0; i + j <= GRID; j++)
{
float w1 = (float)i / GRID, w2 = (float)j / GRID, w0 = 1f - w1 - w2;
float w = 1f;
if (readable != null && uvs != null && thr > 0f)
{
Vector2 uv = ua * w0 + ub * w1 + uc * w2;
uv = new Vector2(tileOffset.x + Frac(uv.x) * tileScale.x, tileOffset.y + Frac(uv.y) * tileScale.y);
var col = readable.GetPixelBilinear(uv.x, uv.y);
// 잉크 = 알파 × 휘도. 가산 합성 텍스처는 알파가 1 이라 휘도가 지배한다.
float lum = col.r * 0.299f + col.g * 0.587f + col.b * 0.114f;
w = col.a * Mathf.Max(lum, 0.0001f);
if (w < thr) continue;
}
Vector3 pLocal = a * w0 + b * w1 + c * w2;
pLocal = new Vector3(pLocal.x * relScale.x, pLocal.y * relScale.y, pLocal.z * relScale.z);
pts.Add(origin + rel * pLocal);
wts.Add(w);
}
}
if (pts.Count < 32) continue;
f.inkMode = thr > 0f ? "tex>" + thr.ToString("F2") : "geom";
bool ok = Solve(pts, wts, ref f);
f.emitter = ps.gameObject.name;
f.oriented = oriented;
return ok;
}
return false;
}
finally
{
if (readable != null) Object.DestroyImmediate(readable);
}
}
static float Frac(float v) { return v - Mathf.Floor(v); }
static Vector3 InvScale(Vector3 child, Vector3 parent)
{
return new Vector3(
Mathf.Abs(parent.x) > 1e-6f ? child.x / parent.x : child.x,
Mathf.Abs(parent.y) > 1e-6f ? child.y / parent.y : child.y,
Mathf.Abs(parent.z) > 1e-6f ? child.z / parent.z : child.z);
}
static float TileInk(Texture2D tex, Vector2 off, Vector2 scale)
{
float sum = 0f;
const int N = 24;
for (int y = 0; y < N; y++)
for (int x = 0; x < N; x++)
{
var c = tex.GetPixelBilinear(off.x + (x + 0.5f) / N * scale.x, off.y + (y + 0.5f) / N * scale.y);
sum += c.a * (c.r * 0.299f + c.g * 0.587f + c.b * 0.114f);
}
return sum;
}
// ── 가중 PCA + 원 적합 ───────────────────────────────────────────────────
static bool Solve(List<Vector3> pts, List<float> wts, ref Frame f)
{
int n = pts.Count;
float wsum = 0f; Vector3 mean = Vector3.zero;
for (int i = 0; i < n; i++) { mean += pts[i] * wts[i]; wsum += wts[i]; }
if (wsum <= 1e-6f) return false;
mean /= wsum;
// 3x3 공분산
double[,] C = new double[3, 3];
for (int i = 0; i < n; i++)
{
Vector3 d = pts[i] - mean; float w = wts[i];
C[0, 0] += w * d.x * d.x; C[0, 1] += w * d.x * d.y; C[0, 2] += w * d.x * d.z;
C[1, 1] += w * d.y * d.y; C[1, 2] += w * d.y * d.z; C[2, 2] += w * d.z * d.z;
}
C[1, 0] = C[0, 1]; C[2, 0] = C[0, 2]; C[2, 1] = C[1, 2];
for (int a = 0; a < 3; a++) for (int b = 0; b < 3; b++) C[a, b] /= wsum;
Vector3[] ev; float[] va;
Jacobi(C, out ev, out va);
// 오름차순 정렬 — va[0] 최소 = 법선
for (int i = 0; i < 2; i++)
for (int j = i + 1; j < 3; j++)
if (va[j] < va[i]) { var tv = va[i]; va[i] = va[j]; va[j] = tv; var te = ev[i]; ev[i] = ev[j]; ev[j] = te; }
Vector3 nrm = ev[0].normalized;
Vector3 e1 = ev[2].normalized; // 최대 분산 축(평면 내)
Vector3 e2 = Vector3.Cross(nrm, e1).normalized;
f.planarity = va[1] > 1e-12f ? 1f - Mathf.Sqrt(Mathf.Max(va[0], 0f) / va[1]) : 1f;
// 평면 2D 좌표
var x = new float[n]; var y = new float[n];
for (int i = 0; i < n; i++) { Vector3 d = pts[i] - mean; x[i] = Vector3.Dot(d, e1); y[i] = Vector3.Dot(d, e2); }
// Kasa 원 적합 (가중)
double Sx = 0, Sy = 0, Sxx = 0, Syy = 0, Sxy = 0, Sxz = 0, Syz = 0, Sz = 0, W = 0;
for (int i = 0; i < n; i++)
{
double w = wts[i], xi = x[i], yi = y[i], zi = xi * xi + yi * yi;
W += w; Sx += w * xi; Sy += w * yi; Sxx += w * xi * xi; Syy += w * yi * yi;
Sxy += w * xi * yi; Sxz += w * xi * zi; Syz += w * yi * zi; Sz += w * zi;
}
double a11 = Sxx - Sx * Sx / W, a12 = Sxy - Sx * Sy / W, a22 = Syy - Sy * Sy / W;
double b1 = 0.5 * (Sxz - Sx * Sz / W), b2 = 0.5 * (Syz - Sy * Sz / W);
double det = a11 * a22 - a12 * a12;
double cx, cy;
bool circleOk = System.Math.Abs(det) > 1e-14;
if (circleOk) { cx = (b1 * a22 - b2 * a12) / det; cy = (a11 * b2 - a12 * b1) / det; }
else { cx = 0; cy = 0; }
// 반지름 분포 · 각도 범위
var rad = new float[n]; var ang = new float[n];
for (int i = 0; i < n; i++)
{
double dx = x[i] - cx, dy = y[i] - cy;
rad[i] = (float)System.Math.Sqrt(dx * dx + dy * dy);
ang[i] = Mathf.Atan2((float)dy, (float)dx);
}
float rMid = WeightedPercentile(rad, wts, 0.5f);
f.innerR = WeightedPercentile(rad, wts, 0.05f);
f.outerR = WeightedPercentile(rad, wts, 0.95f);
f.midR = rMid;
double rms = 0;
for (int i = 0; i < n; i++) { double d = rad[i] - rMid; rms += wts[i] * d * d; }
f.circleRms = rMid > 1e-6f ? (float)System.Math.Sqrt(rms / W) / rMid : 999f;
// 각도 평균 = 원형 평균(±π 경계 안전)
double sa = 0, ca = 0;
for (int i = 0; i < n; i++) { sa += wts[i] * Mathf.Sin(ang[i]); ca += wts[i] * Mathf.Cos(ang[i]); }
float midAng = Mathf.Atan2((float)sa, (float)ca);
// 중점 기준 상대각으로 펴서 범위를 잰다
var rel = new float[n];
for (int i = 0; i < n; i++) rel[i] = Mathf.DeltaAngle(midAng * Mathf.Rad2Deg, ang[i] * Mathf.Rad2Deg);
float lo = WeightedPercentile(rel, wts, 0.03f), hi = WeightedPercentile(rel, wts, 0.97f);
f.arcDeg = hi - lo;
// 프레임 확정
Vector3 Dir(float degFromMid)
{
float t = midAng + degFromMid * Mathf.Deg2Rad;
return (e1 * Mathf.Cos(t) + e2 * Mathf.Sin(t)).normalized;
}
Vector3 convex = Dir(0f);
Vector3 start = Dir(lo), end = Dir(hi);
Vector3 chord = end - start; // 시위 = 호 시작점 → 끝점
if (chord.sqrMagnitude < 1e-8f) chord = Vector3.Cross(nrm, convex);
chord.Normalize();
// 법선 방향을 (chord × convex) 오른손 규약으로 고정한다 — 뒤집힘 없이 재현 가능
Vector3 nFixed = Vector3.Cross(chord, convex).normalized;
if (Vector3.Dot(nFixed, nrm) < 0f) nrm = -nrm;
nrm = nFixed;
f.normal = nrm;
f.chord = chord;
f.convex = convex;
f.center = mean + e1 * (float)cx + e2 * (float)cy;
f.ink = wsum;
f.samples = n;
return circleOk;
}
static float WeightedPercentile(float[] v, List<float> w, float p)
{
int n = v.Length;
var idx = Enumerable.Range(0, n).ToArray();
System.Array.Sort(idx, (a, b) => v[a].CompareTo(v[b]));
float total = 0f; for (int i = 0; i < n; i++) total += w[i];
float acc = 0f, target = total * p;
for (int i = 0; i < n; i++) { acc += w[idx[i]]; if (acc >= target) return v[idx[i]]; }
return v[idx[n - 1]];
}
// 대칭 3x3 야코비 고유분해
static void Jacobi(double[,] a, out Vector3[] vec, out float[] val)
{
double[,] v = new double[3, 3] { { 1, 0, 0 }, { 0, 1, 0 }, { 0, 0, 1 } };
double[,] m = (double[,])a.Clone();
for (int sweep = 0; sweep < 24; sweep++)
{
double off = m[0, 1] * m[0, 1] + m[0, 2] * m[0, 2] + m[1, 2] * m[1, 2];
if (off < 1e-20) break;
for (int p = 0; p < 2; p++)
for (int q = p + 1; q < 3; q++)
{
if (System.Math.Abs(m[p, q]) < 1e-18) continue;
double theta = (m[q, q] - m[p, p]) / (2 * m[p, q]);
double tt = System.Math.Sign(theta) / (System.Math.Abs(theta) + System.Math.Sqrt(theta * theta + 1));
if (theta == 0) tt = 1;
double c = 1 / System.Math.Sqrt(tt * tt + 1), s = tt * c;
for (int k = 0; k < 3; k++)
{
double mkp = m[k, p], mkq = m[k, q];
m[k, p] = c * mkp - s * mkq; m[k, q] = s * mkp + c * mkq;
}
for (int k = 0; k < 3; k++)
{
double mpk = m[p, k], mqk = m[q, k];
m[p, k] = c * mpk - s * mqk; m[q, k] = s * mpk + c * mqk;
}
for (int k = 0; k < 3; k++)
{
double vkp = v[k, p], vkq = v[k, q];
v[k, p] = c * vkp - s * vkq; v[k, q] = s * vkp + c * vkq;
}
}
}
vec = new Vector3[3]; val = new float[3];
for (int i = 0; i < 3; i++)
{
vec[i] = new Vector3((float)v[0, i], (float)v[1, i], (float)v[2, i]);
val[i] = (float)m[i, i];
}
}
// ── 유틸 ────────────────────────────────────────────────────────────────
public static string FindPrefab(string name)
{
var guids = AssetDatabase.FindAssets(name + " t:Prefab");
return guids.Select(AssetDatabase.GUIDToAssetPath)
.FirstOrDefault(p => System.IO.Path.GetFileNameWithoutExtension(p) == name);
}
public static Texture GetTex(Material m)
{
if (m == null) return null;
if (m.mainTexture != null) return m.mainTexture;
string[] names = { "_MainTex", "_BaseMap", "_BaseColorMap", "_Texture" };
foreach (var n in names) if (m.HasProperty(n) && m.GetTexture(n) != null) return m.GetTexture(n);
return null;
}
/// <summary>읽기 불가 텍스처도 RenderTexture 경유로 픽셀을 읽는다(임포터 무수정).</summary>
public static Texture2D MakeReadable(Texture src)
{
if (src == null) return null;
int w = Mathf.Min(src.width, 256), h = Mathf.Min(src.height, 256);
var rt = RenderTexture.GetTemporary(w, h, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Linear);
var prev = RenderTexture.active;
Graphics.Blit(src, rt);
RenderTexture.active = rt;
var tex = new Texture2D(w, h, TextureFormat.RGBA32, false, true);
tex.ReadPixels(new Rect(0, 0, w, h), 0, 0);
tex.Apply();
RenderTexture.active = prev;
RenderTexture.ReleaseTemporary(rt);
return tex;
}
}