2026-09-10 14:53:47 +00:00
|
|
|
|
// WL-814c 프로브 + 룩 캡처 6장 (게임 코드 아님 · 에디트 모드 전용 · 임시 씬 저장 0 · 커밋 0)
|
|
|
|
|
|
// run: unity command run_script --file AgentScripts/WL814c_Probe.cs --entry WL814c_Probe.RunAll
|
|
|
|
|
|
// cap: unity command run_script --file AgentScripts/WL814c_Probe.cs --entry WL814c_Probe.Capture
|
|
|
|
|
|
// 🔴 원본 .mat / 프리팹 / 씬 수정 0 · AssetDatabase.SaveAssets 호출 0(URP 에셋 재저장 방지).
|
|
|
|
|
|
// 🔴 Critter 의 전역 네임스페이스 `Environment` 는 System.Environment 와 충돌 → global:: 로 명시.
|
|
|
|
|
|
|
|
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using System.IO;
|
|
|
|
|
|
using System.Reflection;
|
|
|
|
|
|
using System.Text;
|
|
|
|
|
|
using UnityEditor;
|
|
|
|
|
|
using UnityEditor.SceneManagement;
|
|
|
|
|
|
using UnityEngine;
|
|
|
|
|
|
using UnityEngine.Profiling;
|
|
|
|
|
|
using UnityEngine.Rendering;
|
|
|
|
|
|
using UnityEngine.Rendering.Universal;
|
|
|
|
|
|
using UnityEngine.SceneManagement;
|
|
|
|
|
|
using WL.Look.Env;
|
|
|
|
|
|
|
|
|
|
|
|
public static class WL814c_Probe
|
|
|
|
|
|
{
|
|
|
|
|
|
const string kOutDir = @"E:\NerdNavis\nn_himminji\Screenshots_WL\WL814c";
|
|
|
|
|
|
const string kTxt = "AgentScripts/WL814c_PROBE.txt";
|
|
|
|
|
|
const string kTxtCap = "AgentScripts/WL814c_CAPTURE.txt";
|
|
|
|
|
|
const string kMapPrefab = "Assets/Res_Addr/Map/WL_Nature.prefab";
|
|
|
|
|
|
const string kRendererPath = "Assets/Settings/URP-Balanced-Renderer.asset";
|
|
|
|
|
|
|
|
|
|
|
|
const int kLowW = 270, kLowH = 480, kHiW = 1080, kHiH = 1920;
|
|
|
|
|
|
|
|
|
|
|
|
static StringBuilder s_sb;
|
|
|
|
|
|
static int s_pass, s_fail;
|
|
|
|
|
|
static void L(string s) { s_sb.AppendLine(s); Debug.Log("[WL814c] " + s); }
|
|
|
|
|
|
static void Chk(string n, bool ok, string d) { if (ok) s_pass++; else s_fail++; L((ok ? " PASS " : " FAIL ") + n + " — " + d); }
|
|
|
|
|
|
static void Write(string p) { File.WriteAllText(p, s_sb.ToString(), new UTF8Encoding(false)); Debug.Log("[WL814c] → " + p); }
|
|
|
|
|
|
|
|
|
|
|
|
// ═════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// RunAll
|
|
|
|
|
|
// ═════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
public static void RunAll()
|
|
|
|
|
|
{
|
|
|
|
|
|
s_sb = new StringBuilder(); s_pass = 0; s_fail = 0;
|
|
|
|
|
|
L("WL-814c 프로브 · " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + " · Unity " + Application.unityVersion);
|
|
|
|
|
|
Scene temp = default(Scene);
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
A_Settings();
|
|
|
|
|
|
B_Feature();
|
|
|
|
|
|
temp = EditorSceneManager.NewScene(NewSceneSetup.DefaultGameObjects, NewSceneMode.Single);
|
|
|
|
|
|
var map = Inst(kMapPrefab, Vector3.zero);
|
|
|
|
|
|
C_SwapRestore(map);
|
|
|
|
|
|
D_Grass(map);
|
|
|
|
|
|
E_Water(map);
|
|
|
|
|
|
F_Gc();
|
|
|
|
|
|
G_C8(map);
|
2026-09-10 15:11:11 +00:00
|
|
|
|
H_ApplyNow(map);
|
2026-09-10 14:53:47 +00:00
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex) { s_fail++; L("EXCEPTION " + ex); }
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
try { EnvLook.RestoreNow("probe-final"); } catch { }
|
|
|
|
|
|
WLEnvLookSettings.RuntimeDisabled = false;
|
|
|
|
|
|
try { EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single); } catch { }
|
|
|
|
|
|
}
|
|
|
|
|
|
L("");
|
|
|
|
|
|
L("RESULT " + (s_fail == 0 ? "PASS" : "FAIL") + " · " + s_pass + "/" + (s_pass + s_fail) + " · FAIL " + s_fail);
|
|
|
|
|
|
Write(kTxt);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── A. 설정 ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
static void A_Settings()
|
|
|
|
|
|
{
|
|
|
|
|
|
L("");
|
|
|
|
|
|
L("## A. 설정 SO · 스왑 표");
|
|
|
|
|
|
var c = WLEnvLookSettings.Instance;
|
|
|
|
|
|
Chk("A1 SO 로드", c != null, c != null ? AssetDatabase.GetAssetPath(c) : "null");
|
|
|
|
|
|
if (c == null) return;
|
|
|
|
|
|
int pairs = c.originals != null ? c.originals.Length : 0;
|
|
|
|
|
|
int nullPairs = 0;
|
|
|
|
|
|
var dstDirs = new HashSet<string>();
|
|
|
|
|
|
for (int i = 0; i < pairs; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (c.originals[i] == null || c.toons == null || i >= c.toons.Length || c.toons[i] == null) { nullPairs++; continue; }
|
|
|
|
|
|
dstDirs.Add(System.IO.Path.GetDirectoryName(AssetDatabase.GetAssetPath(c.toons[i])).Replace('\\', '/'));
|
|
|
|
|
|
}
|
|
|
|
|
|
Chk("A2 스왑 표", pairs > 0 && nullPairs == 0, pairs + "쌍 · null " + nullPairs);
|
|
|
|
|
|
Chk("A3 생성물 위치", dstDirs.Count == 1 && dstDirs.Contains("Assets/WL/Look/Env/Materials"), string.Join(" ", new List<string>(dstDirs).ToArray()));
|
|
|
|
|
|
|
|
|
|
|
|
// 원본 .mat 무수정 확인 = 원본은 여전히 원래 셰이더다
|
|
|
|
|
|
int origToon = 0;
|
|
|
|
|
|
for (int i = 0; i < pairs; i++)
|
|
|
|
|
|
if (c.originals[i] != null && c.originals[i].shader != null && c.originals[i].shader.name.Contains("Shader Graphs/Toon")) origToon++;
|
|
|
|
|
|
Chk("A4 원본 머티리얼 무수정(셰이더 유지)", origToon == 0, "원본이 Toon 이 된 것 " + origToon + "개");
|
|
|
|
|
|
|
|
|
|
|
|
int toonShader = 0, withMap = 0, cloudsOn = 0, outlineOn = 0, shadesOk = 0;
|
|
|
|
|
|
for (int i = 0; i < pairs; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var t = c.toons[i]; if (t == null) continue;
|
|
|
|
|
|
if (t.shader != null && t.shader.name.Contains("Toon")) toonShader++;
|
|
|
|
|
|
if (t.GetTexture("_BaseMap") != null) withMap++;
|
|
|
|
|
|
if (t.GetFloat("_Cloud_Strength") > 0f) cloudsOn++; // 🔴 _CLOUDSENABLED 는 이 셰이더에 없는 구버전 잔재
|
|
|
|
|
|
if (t.IsKeywordEnabled("_OUTLINESENABLED") && t.GetFloat("_OUTLINESENABLED") > 0f) outlineOn++;
|
|
|
|
|
|
if (Mathf.Approximately(t.GetFloat("_Shades"), c.shades)) shadesOk++;
|
|
|
|
|
|
}
|
|
|
|
|
|
Chk("A5 생성 머티리얼 셰이더", toonShader == pairs, toonShader + "/" + pairs + " Toon");
|
2026-09-10 15:04:56 +00:00
|
|
|
|
Chk("A6 구름(_Cloud_Strength>0)·외곽선(_OUTLINESENABLED)", cloudsOn == pairs && outlineOn > 0,
|
|
|
|
|
|
"clouds " + cloudsOn + "/" + pairs + " · outlines " + outlineOn + "/" + pairs +
|
|
|
|
|
|
" (캐릭터는 SO characterOutlines=" + c.characterOutlines + " 라 " + (c.characterOutlines ? "켜짐" : "꺼짐") + ")");
|
2026-09-10 14:53:47 +00:00
|
|
|
|
Chk("A6b 툰 램프 Shades = SO", shadesOk == pairs, shadesOk + "/" + pairs + " (SO " + c.shades + ")");
|
|
|
|
|
|
L(" BaseMap 이관된 머티리얼 " + withMap + "/" + pairs + " (나머지는 원본에 텍스처가 없어 색만 이관)");
|
|
|
|
|
|
Chk("A7 잔디·물 에셋 참조", c.grassMesh != null && c.grassMaterial != null && c.flowerMesh != null && c.flowerMaterial != null && c.waterMaterial != null,
|
|
|
|
|
|
"grass " + (c.grassMesh != null) + " · flower " + (c.flowerMesh != null) + " · water " + (c.waterMaterial != null));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── B. 렌더러 피처 ───────────────────────────────────────────────────────
|
|
|
|
|
|
static void B_Feature()
|
|
|
|
|
|
{
|
|
|
|
|
|
L("");
|
|
|
|
|
|
L("## B. 픽셀 외곽선 렌더러 피처");
|
|
|
|
|
|
var rd = AssetDatabase.LoadAssetAtPath<ScriptableRendererData>(kRendererPath);
|
|
|
|
|
|
Chk("B1 렌더러 에셋", rd != null, kRendererPath);
|
|
|
|
|
|
if (rd == null) return;
|
|
|
|
|
|
ScriptableRendererFeature outline = null;
|
|
|
|
|
|
foreach (var f in rd.rendererFeatures)
|
|
|
|
|
|
{
|
|
|
|
|
|
L(" - " + (f == null ? "(null)" : f.name + " [" + f.GetType().Name + "] active=" + f.isActive));
|
|
|
|
|
|
if (f is global::Environment.PixelOutlineSetupFeature) outline = f;
|
|
|
|
|
|
}
|
|
|
|
|
|
Chk("B2 피처 존재", outline != null, outline != null ? outline.name : "없음");
|
|
|
|
|
|
Chk("B3 null 피처 0", !rd.rendererFeatures.Contains(null), rd.rendererFeatures.Count + "개");
|
|
|
|
|
|
if (outline == null) return;
|
|
|
|
|
|
|
|
|
|
|
|
bool before = outline.isActive;
|
|
|
|
|
|
EnvLook.SetFeature(true);
|
|
|
|
|
|
bool during = outline.isActive;
|
|
|
|
|
|
EnvLook.SetFeature(false);
|
|
|
|
|
|
bool after = outline.isActive;
|
|
|
|
|
|
Chk("B4 디스크 기본값 off(타이틀·로비 비용 0)", before == false, "isActive=" + before);
|
|
|
|
|
|
Chk("B5 SetActive 전/후", during == true && after == before, before + " → " + during + " → " + after);
|
|
|
|
|
|
Chk("B6 에셋 dirty 0", !EditorUtility.IsDirty(rd) && !EditorUtility.IsDirty(outline), "rd=" + EditorUtility.IsDirty(rd) + " feature=" + EditorUtility.IsDirty(outline));
|
|
|
|
|
|
|
|
|
|
|
|
// 디스크 원문이 그대로인가(내가 손으로 넣은 최소 diff 유지 · 버전 업그레이드 0)
|
|
|
|
|
|
string text = File.ReadAllText(kRendererPath);
|
|
|
|
|
|
Chk("B7 디스크 m_AssetVersion 유지", text.Contains("m_AssetVersion: 2"), text.Contains("m_AssetVersion: 3") ? "3 으로 업그레이드됨" : "2");
|
|
|
|
|
|
Chk("B8 디스크 피처 항목", text.Contains("- {fileID: 3640211299048422604}") && text.Contains("m_Name: PixelOutlineSetup"), "OK");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── C. 스왑 / 복원 ───────────────────────────────────────────────────────
|
|
|
|
|
|
static void C_SwapRestore(GameObject map)
|
|
|
|
|
|
{
|
|
|
|
|
|
L("");
|
|
|
|
|
|
L("## C. 런타임 스왑 / 원복");
|
|
|
|
|
|
Chk("C1 맵 배치", map != null, kMapPrefab);
|
|
|
|
|
|
if (map == null) return;
|
|
|
|
|
|
|
|
|
|
|
|
var before = Snapshot(map);
|
|
|
|
|
|
EnvLook.ResetForProbe();
|
|
|
|
|
|
EnvLook.BeginForProbe();
|
|
|
|
|
|
int swapped = EnvLook.SweepRoot(map.transform);
|
|
|
|
|
|
var after = Snapshot(map);
|
|
|
|
|
|
|
|
|
|
|
|
Chk("C2 스왑된 렌더러", swapped > 0, swapped + "개 · 슬롯 " + EnvLook.SwappedSlots);
|
|
|
|
|
|
int toonSlots = 0, keptSlots = 0;
|
|
|
|
|
|
foreach (var kv in after) { if (kv.Value.Contains("Toon")) toonSlots++; else keptSlots++; }
|
|
|
|
|
|
Chk("C3 Toon 으로 바뀐 슬롯", toonSlots > 0, "Toon " + toonSlots + " · 유지 " + keptSlots + "(파티클·투명·물)");
|
|
|
|
|
|
|
|
|
|
|
|
int restored = 0, mismatch = 0;
|
|
|
|
|
|
EnvLook.RestoreNow("probe-c");
|
|
|
|
|
|
var back = Snapshot(map);
|
|
|
|
|
|
foreach (var kv in before)
|
|
|
|
|
|
{
|
|
|
|
|
|
string now;
|
|
|
|
|
|
if (!back.TryGetValue(kv.Key, out now)) continue;
|
|
|
|
|
|
if (now == kv.Value) restored++; else mismatch++;
|
|
|
|
|
|
}
|
|
|
|
|
|
Chk("C4 원복 == 원본 참조 동일", mismatch == 0, "동일 " + restored + " · 불일치 " + mismatch);
|
|
|
|
|
|
Chk("C5 이중 복원 무해", EnvLook.RestoreNow("probe-c2") == false, "두 번째 복원 = 무동작");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static Dictionary<int, string> Snapshot(GameObject root)
|
|
|
|
|
|
{
|
|
|
|
|
|
var d = new Dictionary<int, string>();
|
|
|
|
|
|
foreach (var r in root.GetComponentsInChildren<Renderer>(true))
|
|
|
|
|
|
{
|
|
|
|
|
|
var sb = new StringBuilder();
|
|
|
|
|
|
foreach (var m in r.sharedMaterials) sb.Append(m != null ? m.name : "(null)").Append('|');
|
|
|
|
|
|
d[r.GetInstanceID()] = sb.ToString();
|
|
|
|
|
|
}
|
|
|
|
|
|
return d;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── D. 잔디 ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
static void D_Grass(GameObject map)
|
|
|
|
|
|
{
|
|
|
|
|
|
L("");
|
|
|
|
|
|
L("## D. 잔디 인스턴싱");
|
|
|
|
|
|
var c = WLEnvLookSettings.Instance;
|
|
|
|
|
|
if (map == null || c == null) return;
|
|
|
|
|
|
|
|
|
|
|
|
EnvLook.ResetForProbe();
|
|
|
|
|
|
EnvLook.BeginForProbe();
|
|
|
|
|
|
int made = EnvLook.AttachGrass(map.transform, c);
|
|
|
|
|
|
Chk("D1 지면 부착", made > 0, made + "개 지면 · 인스턴스(계산) " + EnvLook.GrassInstances + " · 드로우콜 +" + EnvLook.GrassDrawCalls);
|
|
|
|
|
|
Chk("D2 상한 준수", EnvLook.GrassInstances <= c.maxInstances, EnvLook.GrassInstances + " ≤ " + c.maxInstances);
|
|
|
|
|
|
|
|
|
|
|
|
L(" | 지면 | 메시 면적(m²) | Density | 인스턴스 | 설정 |");
|
|
|
|
|
|
L(" |---|---|---|---|---|");
|
|
|
|
|
|
int comps = 0, navChanged = 0;
|
|
|
|
|
|
foreach (var mib in map.GetComponentsInChildren<global::Environment.Instancing.MeshInstancesBehaviour>(true))
|
|
|
|
|
|
{
|
|
|
|
|
|
comps++;
|
|
|
|
|
|
var mf = mib.GetComponent<MeshFilter>();
|
|
|
|
|
|
float area = mf != null && mf.sharedMesh != null ? global::Environment.Utilities.MeshUtilities.GetMeshArea(mf.sharedMesh) : 0f;
|
|
|
|
|
|
int n = Mathf.CeilToInt(area / Mathf.Max(0.01f, mib.Density));
|
|
|
|
|
|
string st = "";
|
|
|
|
|
|
foreach (var s in mib.InstancingSettings) st += (s.Mesh != null ? s.Mesh.name : "-") + "/" + (s.Material != null ? s.Material.name : "-") + "(p" + s.Probability + " s" + s.Scale + ") ";
|
|
|
|
|
|
L(" | " + mib.name + " | " + area.ToString("0") + " | " + mib.Density.ToString("0.00") + " | " + n + " | " + st.Trim() + " |");
|
|
|
|
|
|
var mr = mib.GetComponent<MeshRenderer>();
|
|
|
|
|
|
if (mr != null && mr.enabled) navChanged++;
|
|
|
|
|
|
}
|
|
|
|
|
|
Chk("D3 컴포넌트 수", comps == made, comps + " / " + made);
|
|
|
|
|
|
Chk("D4 대리 렌더러는 그리지 않는다(지면 이중 렌더 0)", navChanged == 0, "enabled=true 인 대리 렌더러 " + navChanged + "개");
|
|
|
|
|
|
int colliders = 0;
|
|
|
|
|
|
foreach (var mib in map.GetComponentsInChildren<global::Environment.Instancing.MeshInstancesBehaviour>(true))
|
|
|
|
|
|
if (mib.GetComponent<Collider>() != null) colliders++;
|
|
|
|
|
|
Chk("D5 렌더 전용(콜라이더·NavMesh 무관)", colliders == 0, "잔디 오브젝트의 콜라이더 " + colliders + "개");
|
|
|
|
|
|
|
|
|
|
|
|
EnvLook.RestoreNow("probe-d");
|
|
|
|
|
|
int left = map.GetComponentsInChildren<global::Environment.Instancing.MeshInstancesBehaviour>(true).Length;
|
|
|
|
|
|
Chk("D6 이탈 시 파괴", left == 0, "남은 컴포넌트 " + left);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── E. 물 ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
static void E_Water(GameObject map)
|
|
|
|
|
|
{
|
|
|
|
|
|
L("");
|
|
|
|
|
|
L("## E. 물 스왑");
|
|
|
|
|
|
var c = WLEnvLookSettings.Instance;
|
|
|
|
|
|
if (map == null || c == null) return;
|
|
|
|
|
|
EnvLook.ResetForProbe();
|
|
|
|
|
|
EnvLook.BeginForProbe();
|
|
|
|
|
|
EnvLook.SweepRoot(map.transform);
|
|
|
|
|
|
int waterBefore = 0;
|
|
|
|
|
|
foreach (var r in map.GetComponentsInChildren<Renderer>(true))
|
|
|
|
|
|
if (r.sharedMaterial != null && r.sharedMaterial.name == "Water") waterBefore++;
|
|
|
|
|
|
// SweepRoot 만으로는 물이 안 바뀐다 → ApplyNow 경로의 SwapWater 를 쓰는지 확인
|
|
|
|
|
|
Chk("E1 SweepRoot 는 물을 건드리지 않는다", waterBefore == 0, "Water 로 바뀐 렌더러 " + waterBefore);
|
|
|
|
|
|
EnvLook.RestoreNow("probe-e0");
|
|
|
|
|
|
|
|
|
|
|
|
EnvLook.ResetForProbe();
|
|
|
|
|
|
EnvLook.BeginForProbe();
|
|
|
|
|
|
var mi = typeof(EnvLook).GetMethod("SwapWater", BindingFlags.NonPublic | BindingFlags.Static);
|
|
|
|
|
|
if (mi != null) mi.Invoke(null, new object[] { map.transform, c });
|
|
|
|
|
|
int waterAfter = 0;
|
|
|
|
|
|
var reflCams = 0;
|
|
|
|
|
|
foreach (var r in map.GetComponentsInChildren<Renderer>(true))
|
|
|
|
|
|
{
|
|
|
|
|
|
if (r.sharedMaterial != null && r.sharedMaterial.name == "Water") waterAfter++;
|
|
|
|
|
|
if (r.GetComponent<global::Environment.PixelWater.WaterReflectionCamera>() != null) reflCams++;
|
|
|
|
|
|
}
|
|
|
|
|
|
Chk("E2 물 스왑", waterAfter > 0, waterAfter + "개 렌더러 → Critter Water · 카운터 " + EnvLook.WaterSwapped);
|
|
|
|
|
|
Chk("E3 반사 카메라 미부착(성능)", reflCams == 0, "WaterReflectionCamera " + reflCams + "개");
|
|
|
|
|
|
EnvLook.RestoreNow("probe-e");
|
|
|
|
|
|
int back = 0;
|
|
|
|
|
|
foreach (var r in map.GetComponentsInChildren<Renderer>(true))
|
|
|
|
|
|
if (r.sharedMaterial != null && r.sharedMaterial.name == "Water") back++;
|
|
|
|
|
|
Chk("E4 물 원복", back == 0, "남은 Water " + back);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── F. GC ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
static void F_Gc()
|
|
|
|
|
|
{
|
|
|
|
|
|
L("");
|
|
|
|
|
|
L("## F. GC 0");
|
|
|
|
|
|
EnvLook.ResetForProbe();
|
|
|
|
|
|
Gc("EnvLook.Tick(비적용 · 폴링 전)", () => EnvLook.TickForProbe(), 200000);
|
|
|
|
|
|
EnvLook.BeginForProbe();
|
|
|
|
|
|
Gc("EnvLook.SweepNewSpawns(캐시 히트)", () => EnvLook.SweepNewSpawns(), 200000);
|
|
|
|
|
|
EnvLook.RestoreNow("probe-f");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void Gc(string name, Action a, int n)
|
|
|
|
|
|
{
|
|
|
|
|
|
a(); a();
|
|
|
|
|
|
GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect();
|
|
|
|
|
|
long m0 = Profiler.GetMonoUsedSizeLong(); long t0 = GC.GetTotalMemory(false); int g0 = GC.CollectionCount(0);
|
|
|
|
|
|
for (int i = 0; i < n; i++) a();
|
|
|
|
|
|
long m1 = Profiler.GetMonoUsedSizeLong(); long t1 = GC.GetTotalMemory(false); int g1 = GC.CollectionCount(0);
|
|
|
|
|
|
Chk("F " + name + " ×" + n, (m1 - m0) == 0 && (t1 - t0) == 0 && (g1 - g0) == 0,
|
|
|
|
|
|
"monoΔ " + (m1 - m0) + " B · totalΔ " + (t1 - t0) + " B · gen0 " + (g1 - g0));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── G. C8 ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
static void G_C8(GameObject map)
|
|
|
|
|
|
{
|
|
|
|
|
|
L("");
|
|
|
|
|
|
L("## G. C8 롤백 (enabled = 0)");
|
|
|
|
|
|
var c = WLEnvLookSettings.Instance;
|
|
|
|
|
|
if (map == null || c == null) return;
|
|
|
|
|
|
EnvLook.ResetForProbe();
|
|
|
|
|
|
WLEnvLookSettings.RuntimeDisabled = true;
|
|
|
|
|
|
|
|
|
|
|
|
bool applied = EnvLook.ApplyNow("c8");
|
|
|
|
|
|
int swapped = EnvLook.SweepRoot(map.transform);
|
|
|
|
|
|
int grass = map.GetComponentsInChildren<global::Environment.Instancing.MeshInstancesBehaviour>(true).Length;
|
|
|
|
|
|
var rd = AssetDatabase.LoadAssetAtPath<ScriptableRendererData>(kRendererPath);
|
|
|
|
|
|
bool feature = false;
|
|
|
|
|
|
foreach (var f in rd.rendererFeatures) if (f is global::Environment.PixelOutlineSetupFeature) feature = f.isActive;
|
|
|
|
|
|
|
|
|
|
|
|
Chk("G1 enabled=0 → 적용 0", !applied, "ApplyNow=" + applied + " · Enabled=" + WLEnvLookSettings.Enabled);
|
|
|
|
|
|
Chk("G2 enabled=0 → 스왑 0", swapped == 0, "스왑 렌더러 " + swapped);
|
|
|
|
|
|
Chk("G3 enabled=0 → 잔디 0", grass == 0, "잔디 컴포넌트 " + grass);
|
|
|
|
|
|
Chk("G4 enabled=0 → 피처 off", !feature, "isActive=" + feature);
|
|
|
|
|
|
EnvLook.EnsureRunnerForProbe();
|
|
|
|
|
|
int runners = 0;
|
|
|
|
|
|
foreach (var go in UnityEngine.Object.FindObjectsByType<GameObject>(FindObjectsInactive.Include, FindObjectsSortMode.None))
|
|
|
|
|
|
if (go.name.StartsWith("[WL814c]")) runners++;
|
|
|
|
|
|
Chk("G5 에디트 모드 러너 오브젝트 0", runners == 0, runners + "개");
|
|
|
|
|
|
WLEnvLookSettings.RuntimeDisabled = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-10 15:11:11 +00:00
|
|
|
|
// ── H. 실제 진입점(ApplyNow) 왕복 ───────────────────────────────────────
|
|
|
|
|
|
// 🔴 InGameInfo.Ins 는 자동 생성되지 않는 순수 static 필드라(실측) 에디트 모드에서 null 이고,
|
|
|
|
|
|
// MyValue.MyPC 도 null 이라 ApplyNow 는 맵·물·잔디·피처 경로만 타고 캐릭터 경로는 건너뛴다.
|
|
|
|
|
|
static void H_ApplyNow(GameObject map)
|
|
|
|
|
|
{
|
|
|
|
|
|
L("");
|
|
|
|
|
|
L("## H. ApplyNow / RestoreNow 왕복 (게임이 실제로 부르는 진입점)");
|
|
|
|
|
|
if (map == null) return;
|
|
|
|
|
|
var before = Snapshot(map);
|
|
|
|
|
|
EnvLook.ResetForProbe();
|
|
|
|
|
|
|
|
|
|
|
|
bool ok = EnvLook.ApplyNow("probe-h");
|
|
|
|
|
|
var rd = AssetDatabase.LoadAssetAtPath<ScriptableRendererData>(kRendererPath);
|
|
|
|
|
|
bool feature = false;
|
|
|
|
|
|
foreach (var f in rd.rendererFeatures) if (f is global::Environment.PixelOutlineSetupFeature) feature = f.isActive;
|
|
|
|
|
|
int grass = map.GetComponentsInChildren<global::Environment.Instancing.MeshInstancesBehaviour>(true).Length;
|
|
|
|
|
|
|
|
|
|
|
|
Chk("H1 ApplyNow", ok && EnvLook.SwappedRenderers > 0 && EnvLook.WaterSwapped > 0 && EnvLook.GrassObjects > 0,
|
|
|
|
|
|
"렌더러 " + EnvLook.SwappedRenderers + " · 슬롯 " + EnvLook.SwappedSlots +
|
|
|
|
|
|
" · 물 " + EnvLook.WaterSwapped + " · 잔디 " + EnvLook.GrassObjects + "(" + EnvLook.GrassInstances + "개 · +" + EnvLook.GrassDrawCalls + " 드로우콜)");
|
|
|
|
|
|
Chk("H2 피처 활성", feature, "isActive=" + feature);
|
|
|
|
|
|
Chk("H3 잔디 컴포넌트", grass == EnvLook.GrassObjects && grass > 0, grass + "개");
|
|
|
|
|
|
Chk("H4 이중 적용 무해", EnvLook.ApplyNow("probe-h2") == false, "두 번째 ApplyNow = 무동작");
|
|
|
|
|
|
|
|
|
|
|
|
EnvLook.RestoreNow("probe-h");
|
|
|
|
|
|
var back = Snapshot(map);
|
|
|
|
|
|
int mismatch = 0;
|
|
|
|
|
|
foreach (var kv in before) { string now; if (back.TryGetValue(kv.Key, out now) && now != kv.Value) mismatch++; }
|
|
|
|
|
|
bool feature2 = false;
|
|
|
|
|
|
foreach (var f in rd.rendererFeatures) if (f is global::Environment.PixelOutlineSetupFeature) feature2 = f.isActive;
|
|
|
|
|
|
int grass2 = map.GetComponentsInChildren<global::Environment.Instancing.MeshInstancesBehaviour>(true).Length;
|
|
|
|
|
|
Chk("H5 원복 == 원본", mismatch == 0, "불일치 " + mismatch + " / 렌더러 " + before.Count);
|
|
|
|
|
|
Chk("H6 피처 원복", !feature2, "isActive=" + feature2);
|
|
|
|
|
|
Chk("H7 잔디 파괴", grass2 == 0, grass2 + "개");
|
|
|
|
|
|
Chk("H8 에셋 dirty 0", !EditorUtility.IsDirty(rd), "renderer dirty=" + EditorUtility.IsDirty(rd));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-10 14:53:47 +00:00
|
|
|
|
// ═════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// Capture — 임시 씬(저장 0) · 직교 · 270×480 렌더 → 1080×1920 확대 · 6장
|
|
|
|
|
|
// ═════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
|
|
|
|
|
|
static readonly List<global::Environment.Instancing.InstancingConfiguration> s_cfgs =
|
|
|
|
|
|
new List<global::Environment.Instancing.InstancingConfiguration>();
|
|
|
|
|
|
static Bounds s_grassBounds;
|
|
|
|
|
|
|
|
|
|
|
|
public static void Capture()
|
|
|
|
|
|
{
|
|
|
|
|
|
s_sb = new StringBuilder(); s_pass = 0; s_fail = 0;
|
|
|
|
|
|
L("WL-814c 룩 캡처(직교 픽셀 카메라 근사 " + kLowW + "×" + kLowH + " → " + kHiW + "×" + kHiH + ") · " + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
|
|
|
|
|
|
|
|
|
|
|
|
var c = WLEnvLookSettings.Instance;
|
|
|
|
|
|
Scene temp = default(Scene);
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
Directory.CreateDirectory(kOutDir);
|
|
|
|
|
|
temp = EditorSceneManager.NewScene(NewSceneSetup.DefaultGameObjects, NewSceneMode.Single);
|
|
|
|
|
|
foreach (var go in temp.GetRootGameObjects())
|
|
|
|
|
|
if (go.GetComponent<Camera>() != null) UnityEngine.Object.DestroyImmediate(go);
|
|
|
|
|
|
|
|
|
|
|
|
var map = Inst(kMapPrefab, Vector3.zero);
|
|
|
|
|
|
Chk("E0 맵", map != null, "WL_Nature");
|
|
|
|
|
|
if (map == null) return;
|
|
|
|
|
|
|
2026-09-10 15:04:56 +00:00
|
|
|
|
// 🔴 직교 카메라(814b 대신 임시) · 811o 회전(h 76 · yaw 233) · size 3.8
|
|
|
|
|
|
// far 를 160 m 로 좁힌다 = 깊이 외곽선 임계(SO depthThreshold)가 실제 단차(m)에 대응하게 하려고.
|
|
|
|
|
|
// 🔴 camBack 는 25 m — URP-Balanced 의 그림자 거리가 **50 m**(814a 실측)라 카메라가 더 멀면
|
|
|
|
|
|
// 피사체가 그림자 범위 밖으로 나가 그림자가 통째로 사라진다(직교라 거리는 구도에 영향 없음).
|
|
|
|
|
|
const float h = 76f, yaw = 233f, dUp = 2.4f, ortho = 3.8f, camBack = 25f, farClip = 120f;
|
|
|
|
|
|
Vector3 dir = (Quaternion.Euler(h, yaw, 0f) * Vector3.one).normalized;
|
|
|
|
|
|
Vector3 fwd0 = new Vector3(dir.x, 0f, dir.z).normalized;
|
|
|
|
|
|
|
|
|
|
|
|
// 기준점 = 「나무·수풀이 가장 촘촘하고 물이 가까운 곳」(배경 아트를 보여주는 것이 이 캡처의 목적)
|
|
|
|
|
|
string aname;
|
|
|
|
|
|
Vector3 scenic = ScenicSpot(map, out aname);
|
|
|
|
|
|
Vector3 pcPos = scenic - fwd0 * 11f; // 나무 군락이 PC 뒤 11 m(화면 위쪽 배경)에 오도록
|
2026-09-10 14:53:47 +00:00
|
|
|
|
RaycastHit hit;
|
2026-09-10 15:04:56 +00:00
|
|
|
|
if (Physics.Raycast(pcPos + Vector3.up * 300f, Vector3.down, out hit, 700f)) pcPos = hit.point;
|
|
|
|
|
|
L(" 기준점(" + aname + ") " + scenic.ToString("0.0") + " → PC " + pcPos.ToString("0.0"));
|
2026-09-10 14:53:47 +00:00
|
|
|
|
|
|
|
|
|
|
var camGo = new GameObject("[WL814c] OrthoCam");
|
|
|
|
|
|
var cam = camGo.AddComponent<Camera>();
|
|
|
|
|
|
var acd = camGo.AddComponent<UniversalAdditionalCameraData>();
|
|
|
|
|
|
acd.renderShadows = true;
|
|
|
|
|
|
acd.renderPostProcessing = false;
|
|
|
|
|
|
acd.antialiasing = AntialiasingMode.None;
|
|
|
|
|
|
cam.orthographic = true;
|
|
|
|
|
|
cam.orthographicSize = ortho;
|
2026-09-10 15:04:56 +00:00
|
|
|
|
cam.nearClipPlane = 0.1f;
|
|
|
|
|
|
cam.farClipPlane = farClip;
|
2026-09-10 14:53:47 +00:00
|
|
|
|
cam.clearFlags = CameraClearFlags.Skybox;
|
2026-09-10 15:04:56 +00:00
|
|
|
|
Vector3 lookAt = pcPos + new Vector3(0f, 1.06f, 0f) + fwd0 * 2f; // 배경 쪽으로 살짝 치우친 구도
|
|
|
|
|
|
cam.transform.position = pcPos + Vector3.up * dUp - dir * camBack;
|
2026-09-10 14:53:47 +00:00
|
|
|
|
cam.transform.LookAt(lookAt);
|
|
|
|
|
|
|
|
|
|
|
|
Vector3 fwd = pcPos - cam.transform.position; fwd.y = 0f; fwd.Normalize();
|
|
|
|
|
|
Vector3 right = Vector3.Cross(Vector3.up, fwd);
|
2026-09-10 15:04:56 +00:00
|
|
|
|
L(" 카메라 직교 size " + ortho + " · pos " + cam.transform.position.ToString("0.0") + " · look " + lookAt.ToString("0.0") +
|
|
|
|
|
|
" · 811o(h 76 · yaw 233) · near 0.1 / far " + farClip +
|
|
|
|
|
|
" → 깊이 임계 " + (c != null ? c.depthThreshold : 0f) + " = 단차 " + ((c != null ? c.depthThreshold : 0f) * (farClip - 0.1f)).ToString("0.00") + " m");
|
2026-09-10 14:53:47 +00:00
|
|
|
|
|
|
|
|
|
|
var pc = Inst("Assets/Res_Addr/PC/Ai01.prefab", pcPos);
|
|
|
|
|
|
if (pc != null) pc.transform.rotation = Quaternion.LookRotation(fwd, Vector3.up);
|
|
|
|
|
|
Chk("E1 PC", pc != null, "Ai01");
|
|
|
|
|
|
|
|
|
|
|
|
string[] mobs = { "Batty_A", "Batty_B", "Batty_C" };
|
|
|
|
|
|
Vector2[] off = { new Vector2(2.4f, 1.2f), new Vector2(3.4f, -1.5f), new Vector2(4.4f, 0.5f) };
|
|
|
|
|
|
var mobGos = new List<GameObject>();
|
|
|
|
|
|
for (int i = 0; i < mobs.Length; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
Vector3 p = pcPos + fwd * off[i].x + right * off[i].y;
|
|
|
|
|
|
if (Physics.Raycast(p + Vector3.up * 50f, Vector3.down, out hit, 200f)) p = hit.point;
|
|
|
|
|
|
var m = Inst("Assets/Res_Addr/Mobs/Mob/" + mobs[i] + ".prefab", p);
|
|
|
|
|
|
if (m != null) { m.transform.localScale = Vector3.one * 1.5f; m.transform.rotation = Quaternion.LookRotation(-fwd, Vector3.up); mobGos.Add(m); }
|
|
|
|
|
|
}
|
|
|
|
|
|
Chk("E2 잡몹", mobGos.Count == 3, mobGos.Count + "/3");
|
|
|
|
|
|
Pose(pc); foreach (var m in mobGos) Pose(m);
|
|
|
|
|
|
|
|
|
|
|
|
EnvLook.ResetForProbe();
|
|
|
|
|
|
|
|
|
|
|
|
// ─ ⓐ 원본 (Lit · 같은 직교 · 외곽선 0 · 잔디 0) ───────────────────
|
2026-09-10 15:04:56 +00:00
|
|
|
|
Feature(false);
|
2026-09-10 14:53:47 +00:00
|
|
|
|
Shot(cam, kOutDir + @"\look_1_original.png", "ⓐ 원본(URP/Lit)", false);
|
|
|
|
|
|
|
|
|
|
|
|
// ─ ⓑ Toon + 1px 외곽선 (구름 off) ────────────────────────────────
|
|
|
|
|
|
EnvLook.BeginForProbe();
|
|
|
|
|
|
int swMap = EnvLook.SweepRoot(map.transform);
|
|
|
|
|
|
Clouds(c, false);
|
2026-09-10 15:04:56 +00:00
|
|
|
|
Feature(true);
|
2026-09-10 14:53:47 +00:00
|
|
|
|
Shot(cam, kOutDir + @"\look_2_toon_outline.png", "ⓑ Toon 램프 + 1px 픽셀 외곽선(맵 " + swMap + " 렌더러 · 구름 off)", false);
|
|
|
|
|
|
|
|
|
|
|
|
// ─ ⓒ + 잔디 ──────────────────────────────────────────────────────
|
|
|
|
|
|
int made = EnvLook.AttachGrass(map.transform, c);
|
|
|
|
|
|
BuildGrassDraw(map, c);
|
|
|
|
|
|
Shot(cam, kOutDir + @"\look_3_grass.png", "ⓒ + 인스턴싱 잔디(지면 " + made + " · 인스턴스 " + EnvLook.GrassInstances + ")", true);
|
|
|
|
|
|
|
|
|
|
|
|
// ─ ⓓ + 구름 그림자 ───────────────────────────────────────────────
|
|
|
|
|
|
Clouds(c, true);
|
|
|
|
|
|
Shot(cam, kOutDir + @"\look_4_clouds.png", "ⓓ + 구름 그림자", true);
|
|
|
|
|
|
|
|
|
|
|
|
// ─ ⓔ/ⓕ 캐릭터 전 / 후 (클로즈업) ─────────────────────────────────
|
|
|
|
|
|
float keepSize = cam.orthographicSize;
|
|
|
|
|
|
Vector3 keepPos = cam.transform.position;
|
|
|
|
|
|
cam.orthographicSize = 1.5f;
|
|
|
|
|
|
Vector3 focus = pcPos + Vector3.up * 0.9f;
|
|
|
|
|
|
cam.transform.position = focus + Vector3.up * 0.8f - dir * 60f;
|
|
|
|
|
|
cam.transform.LookAt(focus);
|
|
|
|
|
|
Shot(cam, kOutDir + @"\look_5_char_before.png", "ⓔ 캐릭터 원본(MK/Toon) · 배경만 Critter Toon", true);
|
|
|
|
|
|
|
|
|
|
|
|
int swPc = pc != null ? EnvLook.SweepRoot(pc.transform) : 0;
|
|
|
|
|
|
int swMob = 0; foreach (var m in mobGos) swMob += EnvLook.SweepRoot(m.transform);
|
|
|
|
|
|
Shot(cam, kOutDir + @"\look_6_char_after.png", "ⓕ 캐릭터 Critter Toon(PC " + swPc + " · 몹 " + swMob + " 렌더러)", true);
|
|
|
|
|
|
cam.orthographicSize = keepSize; cam.transform.position = keepPos;
|
|
|
|
|
|
|
|
|
|
|
|
Chk("E9 캡처 6장", Directory.GetFiles(kOutDir, "look_*.png").Length >= 6, Directory.GetFiles(kOutDir, "look_*.png").Length + "장");
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex) { s_fail++; L("EXCEPTION " + ex); }
|
|
|
|
|
|
finally
|
|
|
|
|
|
{
|
|
|
|
|
|
FreeGrassDraw();
|
|
|
|
|
|
try { Clouds(WLEnvLookSettings.Instance, true); } catch { }
|
2026-09-10 15:04:56 +00:00
|
|
|
|
try { Feature(false); } catch { }
|
2026-09-10 14:53:47 +00:00
|
|
|
|
try { EnvLook.RestoreNow("capture-final"); } catch { }
|
|
|
|
|
|
try { EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single); } catch { }
|
|
|
|
|
|
}
|
|
|
|
|
|
L("");
|
|
|
|
|
|
L("RESULT " + (s_fail == 0 ? "PASS" : "FAIL") + " · " + s_pass + "/" + (s_pass + s_fail) + " · FAIL " + s_fail);
|
|
|
|
|
|
Write(kTxtCap);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-10 15:04:56 +00:00
|
|
|
|
/// <summary>피처 on/off + 렌더러 재생성 강제(SetDirty = isInvalidated · 에셋 파일에는 안 쓴다).</summary>
|
|
|
|
|
|
static void Feature(bool on)
|
|
|
|
|
|
{
|
|
|
|
|
|
EnvLook.SetFeature(on);
|
|
|
|
|
|
var rd = AssetDatabase.LoadAssetAtPath<ScriptableRendererData>(kRendererPath);
|
|
|
|
|
|
if (rd != null) rd.SetDirty();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>「나무·수풀이 촘촘하고 물이 가까운」 곳 = 배경 아트를 보여줄 자리(실측 좌표를 보고서에 남긴다).</summary>
|
|
|
|
|
|
static Vector3 ScenicSpot(GameObject map, out string why)
|
|
|
|
|
|
{
|
|
|
|
|
|
var pts = new List<Vector3>();
|
|
|
|
|
|
var waters = new List<Bounds>();
|
|
|
|
|
|
foreach (var r in map.GetComponentsInChildren<Renderer>(true))
|
|
|
|
|
|
{
|
|
|
|
|
|
if (r == null || r is ParticleSystemRenderer) continue;
|
|
|
|
|
|
var m = r.sharedMaterial;
|
|
|
|
|
|
if (m == null) continue;
|
|
|
|
|
|
if (m.name == "Trees" || m.name == "Vegetation") { if (pts.Count % 1 == 0) pts.Add(r.bounds.center); }
|
|
|
|
|
|
else if (m.name.IndexOf("Water", StringComparison.OrdinalIgnoreCase) >= 0) waters.Add(r.bounds);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (pts.Count == 0) { why = "(나무 0 · 원점)"; return Vector3.zero; }
|
|
|
|
|
|
|
|
|
|
|
|
// 25 m 이웃 수 + 물 근접 보너스
|
|
|
|
|
|
int step = Mathf.Max(1, pts.Count / 300);
|
|
|
|
|
|
Vector3 best = pts[0]; float bestScore = -1f; int bestN = 0; float bestWater = 9999f;
|
|
|
|
|
|
for (int i = 0; i < pts.Count; i += step)
|
|
|
|
|
|
{
|
|
|
|
|
|
var p = pts[i];
|
|
|
|
|
|
int n = 0;
|
|
|
|
|
|
for (int j = 0; j < pts.Count; j += step)
|
|
|
|
|
|
if ((pts[j] - p).sqrMagnitude < 25f * 25f) n++;
|
|
|
|
|
|
float wd = 9999f;
|
|
|
|
|
|
for (int w = 0; w < waters.Count; w++) wd = Mathf.Min(wd, Vector3.Distance(waters[w].ClosestPoint(p), p));
|
|
|
|
|
|
float score = n + (wd < 40f ? 12f : 0f) + (wd < 20f ? 12f : 0f);
|
|
|
|
|
|
if (score > bestScore) { bestScore = score; best = p; bestN = n; bestWater = wd; }
|
|
|
|
|
|
}
|
|
|
|
|
|
best.y = 0f;
|
|
|
|
|
|
RaycastHit hit;
|
|
|
|
|
|
if (Physics.Raycast(new Vector3(best.x, 400f, best.z), Vector3.down, out hit, 900f)) best = hit.point;
|
|
|
|
|
|
why = "나무·수풀 군락 " + bestN + "그루/25 m · 물까지 " + (bestWater > 9000f ? "없음" : bestWater.ToString("0") + " m");
|
|
|
|
|
|
return best;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-10 14:53:47 +00:00
|
|
|
|
/// <summary>생성한 Toon 머티리얼의 구름 그림자만 토글(_Cloud_Strength · 원본 .mat 무관 · SaveAssets 0).</summary>
|
|
|
|
|
|
static void Clouds(WLEnvLookSettings c, bool on)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (c == null || c.toons == null) return;
|
|
|
|
|
|
foreach (var m in c.toons)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (m == null) continue;
|
|
|
|
|
|
m.SetFloat("_Cloud_Strength", on ? c.cloudStrength : 0f);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── 잔디를 에디트 모드에서 그리기 ────────────────────────────────────────
|
|
|
|
|
|
// MeshInstancesBehaviour 의 OnEnable/Update 는 [ExecuteAlways] 가 없어 에디트 모드에서 돌지 않는다.
|
|
|
|
|
|
// 그래서 **같은 공개 API** 로 같은 데이터를 만들어 Camera.Render 직전에 직접 그린다.
|
|
|
|
|
|
static void BuildGrassDraw(GameObject map, WLEnvLookSettings c)
|
|
|
|
|
|
{
|
|
|
|
|
|
FreeGrassDraw();
|
|
|
|
|
|
bool first = true;
|
|
|
|
|
|
foreach (var mib in map.GetComponentsInChildren<global::Environment.Instancing.MeshInstancesBehaviour>(true))
|
|
|
|
|
|
{
|
|
|
|
|
|
var mf = mib.GetComponent<MeshFilter>();
|
|
|
|
|
|
if (mf == null || mf.sharedMesh == null || mib.InstancingSettings == null) continue;
|
|
|
|
|
|
var data = global::Environment.Instancing.MeshInstancesBehaviour.RandomMeshInstanceData(mf.sharedMesh, mib.Density, mib.InstancingSettings);
|
|
|
|
|
|
if (data == null) continue;
|
|
|
|
|
|
var divided = global::Environment.Instancing.InstancesBehaviour.DivideInstanceData(data, mib.InstancingSettings);
|
|
|
|
|
|
if (divided == null) continue;
|
|
|
|
|
|
var mr = mib.GetComponent<MeshRenderer>();
|
|
|
|
|
|
var lw = mib.transform.localToWorldMatrix; lw.m03 = 0; lw.m13 = 0; lw.m23 = 0;
|
|
|
|
|
|
foreach (var kv in divided)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (kv.Value.Count == 0) continue;
|
|
|
|
|
|
var cfg = new global::Environment.Instancing.InstancingConfiguration(kv.Key, kv.Value, "_InstanceData");
|
|
|
|
|
|
cfg.MaterialPropertyBlock.SetMatrix("_LocalToWorld", lw);
|
|
|
|
|
|
s_cfgs.Add(cfg);
|
|
|
|
|
|
}
|
|
|
|
|
|
var b = mr != null ? mr.bounds : new Bounds(mib.transform.position, Vector3.one * 100f);
|
|
|
|
|
|
var diff = mib.transform.position - b.center;
|
|
|
|
|
|
var abs = new Vector3(Mathf.Abs(diff.x), Mathf.Abs(diff.y), Mathf.Abs(diff.z));
|
|
|
|
|
|
var bb = new Bounds(mib.transform.position, (b.extents + abs) * 2f);
|
|
|
|
|
|
if (first) { s_grassBounds = bb; first = false; } else s_grassBounds.Encapsulate(bb);
|
|
|
|
|
|
}
|
|
|
|
|
|
L(" 잔디 드로우 구성 " + s_cfgs.Count + "개(지면×{잔디,꽃}) · bounds " + s_grassBounds.size.ToString("0"));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void DrawGrass()
|
|
|
|
|
|
{
|
|
|
|
|
|
for (int i = 0; i < s_cfgs.Count; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var cfg = s_cfgs[i];
|
|
|
|
|
|
Graphics.DrawMeshInstancedIndirect(cfg.Mesh, 0, cfg.Material, s_grassBounds, cfg.CommandBuffer, 0,
|
|
|
|
|
|
cfg.MaterialPropertyBlock, ShadowCastingMode.Off, false, 0);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void FreeGrassDraw()
|
|
|
|
|
|
{
|
|
|
|
|
|
for (int i = 0; i < s_cfgs.Count; i++) s_cfgs[i].FreeMemory();
|
|
|
|
|
|
s_cfgs.Clear();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── 렌더 ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
static void Shot(Camera cam, string file, string label, bool grass)
|
|
|
|
|
|
{
|
|
|
|
|
|
var low = new RenderTexture(kLowW, kLowH, 24, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB);
|
|
|
|
|
|
low.filterMode = FilterMode.Point;
|
|
|
|
|
|
low.antiAliasing = 1;
|
|
|
|
|
|
low.Create();
|
|
|
|
|
|
var hi = new RenderTexture(kHiW, kHiH, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB);
|
|
|
|
|
|
hi.filterMode = FilterMode.Point;
|
|
|
|
|
|
hi.Create();
|
|
|
|
|
|
|
|
|
|
|
|
var prev = cam.targetTexture;
|
|
|
|
|
|
cam.targetTexture = low;
|
|
|
|
|
|
if (grass) DrawGrass();
|
|
|
|
|
|
cam.Render();
|
|
|
|
|
|
cam.targetTexture = prev;
|
|
|
|
|
|
|
|
|
|
|
|
Graphics.Blit(low, hi); // Point 확대 = 픽셀 카메라 근사
|
|
|
|
|
|
|
|
|
|
|
|
var active = RenderTexture.active;
|
|
|
|
|
|
RenderTexture.active = hi;
|
|
|
|
|
|
var tex = new Texture2D(kHiW, kHiH, TextureFormat.RGB24, false);
|
|
|
|
|
|
tex.ReadPixels(new Rect(0, 0, kHiW, kHiH), 0, 0);
|
|
|
|
|
|
tex.Apply();
|
|
|
|
|
|
RenderTexture.active = active;
|
|
|
|
|
|
File.WriteAllBytes(file, tex.EncodeToPNG());
|
|
|
|
|
|
|
|
|
|
|
|
var px = tex.GetPixels32();
|
|
|
|
|
|
var seen = new HashSet<int>();
|
|
|
|
|
|
long dark = 0;
|
|
|
|
|
|
for (int i = 0; i < px.Length; i += 331)
|
|
|
|
|
|
{
|
|
|
|
|
|
seen.Add((px[i].r << 16) | (px[i].g << 8) | px[i].b);
|
|
|
|
|
|
if (px[i].r < 24 && px[i].g < 24 && px[i].b < 24) dark++;
|
|
|
|
|
|
}
|
|
|
|
|
|
int distinct = seen.Count;
|
|
|
|
|
|
int dc = UnityEditor.UnityStats.drawCalls, batches = UnityEditor.UnityStats.batches, tri = UnityEditor.UnityStats.triangles;
|
|
|
|
|
|
|
|
|
|
|
|
UnityEngine.Object.DestroyImmediate(tex);
|
|
|
|
|
|
low.Release(); UnityEngine.Object.DestroyImmediate(low);
|
|
|
|
|
|
hi.Release(); UnityEngine.Object.DestroyImmediate(hi);
|
|
|
|
|
|
RenderTexture.active = null;
|
|
|
|
|
|
|
|
|
|
|
|
var fi = new FileInfo(file);
|
|
|
|
|
|
Chk("E " + System.IO.Path.GetFileName(file), fi.Exists && fi.Length > 4096 && distinct > 4,
|
|
|
|
|
|
label + " · " + fi.Length + " B · 색 " + distinct + "종 · 진한픽셀 " + dark +
|
|
|
|
|
|
" · drawCalls " + dc + " · batches " + batches + " · tri " + tri);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-10 15:04:56 +00:00
|
|
|
|
// ═════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
// Diag — 외곽선 파라미터 스윕(임시 · 커밋 대상 아님 · 어떤 값에서 외곽선이 실제로 나오는가)
|
|
|
|
|
|
// ═════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
public static void Diag()
|
|
|
|
|
|
{
|
|
|
|
|
|
s_sb = new StringBuilder(); s_pass = 0; s_fail = 0;
|
|
|
|
|
|
L("WL-814c 외곽선 진단 · " + DateTime.Now.ToString("HH:mm:ss"));
|
|
|
|
|
|
var c = WLEnvLookSettings.Instance;
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var temp = EditorSceneManager.NewScene(NewSceneSetup.DefaultGameObjects, NewSceneMode.Single);
|
|
|
|
|
|
foreach (var go in temp.GetRootGameObjects())
|
|
|
|
|
|
if (go.GetComponent<Camera>() != null) UnityEngine.Object.DestroyImmediate(go);
|
|
|
|
|
|
var map = Inst(kMapPrefab, Vector3.zero);
|
|
|
|
|
|
|
|
|
|
|
|
Vector3 dir = (Quaternion.Euler(76f, 233f, 0f) * Vector3.one).normalized;
|
|
|
|
|
|
Vector3 fwd0 = new Vector3(dir.x, 0f, dir.z).normalized;
|
|
|
|
|
|
string aname;
|
|
|
|
|
|
Vector3 pcPos = ScenicSpot(map, out aname) - fwd0 * 11f;
|
|
|
|
|
|
RaycastHit hit;
|
|
|
|
|
|
if (Physics.Raycast(pcPos + Vector3.up * 300f, Vector3.down, out hit, 700f)) pcPos = hit.point;
|
|
|
|
|
|
|
|
|
|
|
|
var camGo = new GameObject("[WL814c] Diag");
|
|
|
|
|
|
var cam = camGo.AddComponent<Camera>();
|
|
|
|
|
|
camGo.AddComponent<UniversalAdditionalCameraData>();
|
|
|
|
|
|
cam.orthographic = true; cam.orthographicSize = 3.8f;
|
|
|
|
|
|
cam.transform.position = pcPos + Vector3.up * 2.4f - dir * 55f;
|
|
|
|
|
|
cam.transform.LookAt(pcPos + new Vector3(0f, 1.06f, 0f) + fwd0 * 2f);
|
|
|
|
|
|
|
|
|
|
|
|
var pc = Inst("Assets/Res_Addr/PC/Ai01.prefab", pcPos);
|
|
|
|
|
|
Pose(pc);
|
|
|
|
|
|
|
|
|
|
|
|
EnvLook.ResetForProbe(); EnvLook.BeginForProbe();
|
|
|
|
|
|
EnvLook.SweepRoot(map.transform);
|
|
|
|
|
|
if (pc != null) EnvLook.SweepRoot(pc.transform);
|
|
|
|
|
|
Clouds(c, false);
|
|
|
|
|
|
|
|
|
|
|
|
var rd = AssetDatabase.LoadAssetAtPath<ScriptableRendererData>(kRendererPath);
|
|
|
|
|
|
|
|
|
|
|
|
EnvLook.SetFeature(true);
|
|
|
|
|
|
if (rd != null) rd.SetDirty();
|
|
|
|
|
|
cam.nearClipPlane = 0.1f; cam.farClipPlane = 160f;
|
|
|
|
|
|
|
|
|
|
|
|
// (Shades, Brightness, MinimumDarkness, AmbientStrength)
|
|
|
|
|
|
float[][] sets = {
|
|
|
|
|
|
new float[]{5, 0.25f, 0.20f, 0.10f}, // 현재(데모 Brightness/MinDark)
|
|
|
|
|
|
new float[]{5, 0.10f, 0.35f, 0.10f},
|
|
|
|
|
|
new float[]{4, 0.05f, 0.45f, 0.05f},
|
|
|
|
|
|
new float[]{6, 0.00f, 0.55f, 0.05f},
|
|
|
|
|
|
new float[]{4, -0.10f,0.55f, 0.02f},
|
|
|
|
|
|
};
|
|
|
|
|
|
for (int i = 0; i < sets.Length; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var s = sets[i];
|
|
|
|
|
|
foreach (var m in c.toons)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (m == null) continue;
|
|
|
|
|
|
m.SetFloat("_Shades", s[0]); m.SetFloat("_Brightness", s[1]);
|
|
|
|
|
|
m.SetFloat("_MinimumDarkness", s[2]);
|
|
|
|
|
|
if (m.HasProperty("_AmbientStrength")) m.SetFloat("_AmbientStrength", s[3]);
|
|
|
|
|
|
}
|
|
|
|
|
|
float dark = ShotStat(cam, kOutDir + @"\diag_" + i + ".png");
|
|
|
|
|
|
L(" [" + i + "] Shades=" + s[0] + " Brightness=" + s[1] + " MinDark=" + s[2] + " Ambient=" + s[3] +
|
|
|
|
|
|
" → 어두운픽셀 " + (dark * 100f).ToString("0.000") + "%");
|
|
|
|
|
|
}
|
|
|
|
|
|
EnvLook.SetFeature(false);
|
|
|
|
|
|
foreach (var m in c.toons)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (m == null) continue;
|
|
|
|
|
|
m.SetFloat("_Shades", c.shades); m.SetFloat("_Brightness", c.brightness);
|
|
|
|
|
|
m.SetFloat("_MinimumDarkness", c.minimumDarkness);
|
|
|
|
|
|
if (m.HasProperty("_AmbientStrength")) m.SetFloat("_AmbientStrength", c.ambientStrength);
|
|
|
|
|
|
}
|
|
|
|
|
|
Clouds(c, true);
|
|
|
|
|
|
EnvLook.RestoreNow("diag");
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex) { L("EXCEPTION " + ex); }
|
|
|
|
|
|
finally { try { EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single); } catch { } }
|
|
|
|
|
|
Write("AgentScripts/WL814c_DIAG.txt");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static float ShotStat(Camera cam, string file)
|
|
|
|
|
|
{
|
|
|
|
|
|
var low = new RenderTexture(kLowW, kLowH, 24, RenderTextureFormat.ARGB32, RenderTextureReadWrite.sRGB);
|
|
|
|
|
|
low.filterMode = FilterMode.Point; low.antiAliasing = 1; low.Create();
|
|
|
|
|
|
cam.targetTexture = low; cam.Render(); cam.targetTexture = null;
|
|
|
|
|
|
var active = RenderTexture.active; RenderTexture.active = low;
|
|
|
|
|
|
var tex = new Texture2D(kLowW, kLowH, TextureFormat.RGB24, false);
|
|
|
|
|
|
tex.ReadPixels(new Rect(0, 0, kLowW, kLowH), 0, 0); tex.Apply();
|
|
|
|
|
|
RenderTexture.active = active;
|
|
|
|
|
|
File.WriteAllBytes(file, tex.EncodeToPNG());
|
|
|
|
|
|
var px = tex.GetPixels32();
|
|
|
|
|
|
int dark = 0;
|
|
|
|
|
|
for (int i = 0; i < px.Length; i++) if (px[i].r < 40 && px[i].g < 40 && px[i].b < 40) dark++;
|
|
|
|
|
|
UnityEngine.Object.DestroyImmediate(tex);
|
|
|
|
|
|
low.Release(); UnityEngine.Object.DestroyImmediate(low);
|
|
|
|
|
|
RenderTexture.active = null;
|
|
|
|
|
|
return dark / (float)px.Length;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-10 14:53:47 +00:00
|
|
|
|
// ── 헬퍼 ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
static GameObject Inst(string path, Vector3 pos)
|
|
|
|
|
|
{
|
|
|
|
|
|
var src = AssetDatabase.LoadAssetAtPath<GameObject>(path);
|
|
|
|
|
|
if (src == null) { L(" 프리팹 없음 " + path); return null; }
|
|
|
|
|
|
var go = (GameObject)PrefabUtility.InstantiatePrefab(src);
|
|
|
|
|
|
go.transform.position = pos;
|
|
|
|
|
|
return go;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static void Pose(GameObject go)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (go == null) return;
|
|
|
|
|
|
foreach (var an in go.GetComponentsInChildren<Animator>(true))
|
|
|
|
|
|
{
|
|
|
|
|
|
if (an.runtimeAnimatorController == null) continue;
|
|
|
|
|
|
AnimationClip idle = null;
|
|
|
|
|
|
foreach (var cl in an.runtimeAnimatorController.animationClips)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (cl == null) continue;
|
|
|
|
|
|
var n = cl.name.ToLowerInvariant();
|
|
|
|
|
|
if (n.Contains("idle")) { idle = cl; break; }
|
|
|
|
|
|
if (idle == null) idle = cl;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (idle != null) idle.SampleAnimation(an.gameObject, 0.5f);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|