285 lines
15 KiB
C#
285 lines
15 KiB
C#
// WL774_Water.cs — PD 지시 #774 : WL_Nature 강 구간에 구 WL 방식 물(ToonWaterU) 배치
|
|
// unity command run_script --file AgentScripts/WL774_Water.cs --entry WL774_Water.Probe
|
|
// unity command run_script --file AgentScripts/WL774_Water.cs --entry WL774_Water.Apply
|
|
// unity command run_script --file AgentScripts/WL774_Water.cs --entry WL774_Water.Revert
|
|
// unity command run_script --file AgentScripts/WL774_Water.cs --entry WL774_Water.WarpTo --args '[79.9, 22.0, 270.0]'
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using UnityEngine;
|
|
#if UNITY_EDITOR
|
|
using UnityEditor;
|
|
#endif
|
|
|
|
public static class WL774_Water
|
|
{
|
|
const string PrefabPath = "Assets/Res_Addr/Map/WL_Nature.prefab";
|
|
const string MatRiver = "Assets/WL/Materials/WL_Water_River.mat";
|
|
const string MatOcean = "Assets/WL/Materials/WL_Water_Ocean.mat";
|
|
const string OutDir = "AgentScripts/staging/WL_Maps/out";
|
|
const string WaterRoot = "WL_Water"; // 신규 루트 (되돌리기 = 이 오브젝트 삭제)
|
|
|
|
static void W(string file, string body)
|
|
{
|
|
System.IO.Directory.CreateDirectory(OutDir);
|
|
System.IO.File.WriteAllText(System.IO.Path.Combine(OutDir, file), body);
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
// ────────────────────────────────────────────────────────────── Probe
|
|
public static object Probe()
|
|
{
|
|
var sb = new StringBuilder();
|
|
var root = PrefabUtility.LoadPrefabContents(PrefabPath);
|
|
try
|
|
{
|
|
sb.AppendLine("# WL_Nature.prefab 루트 자식");
|
|
foreach (Transform c in root.transform)
|
|
sb.AppendLine(string.Format(" {0} pos={1} scale={2} active={3} children={4}",
|
|
c.name, V(c.position), V(c.localScale), c.gameObject.activeSelf, c.childCount));
|
|
|
|
sb.AppendLine();
|
|
sb.AppendLine("# 이름에 Water 가 든 오브젝트 (전체 계층)");
|
|
var all = root.GetComponentsInChildren<Transform>(true);
|
|
foreach (var t in all)
|
|
{
|
|
if (t.name.IndexOf("water", StringComparison.OrdinalIgnoreCase) < 0) continue;
|
|
var mr = t.GetComponent<MeshRenderer>();
|
|
var mats = mr != null ? string.Join("|", mr.sharedMaterials.Select(m => m != null ? m.name : "(null)")) : "-";
|
|
var b = mr != null ? mr.bounds : new Bounds(t.position, Vector3.zero);
|
|
sb.AppendLine(string.Format(" {0}\n path={1}\n pos={2} scale={3} active={4} renderer={5} mat=[{6}]\n bounds c={7} size={8} y_top={9:F3}",
|
|
t.name, Path(t, root.transform), V(t.position), V(t.localScale), t.gameObject.activeInHierarchy,
|
|
mr != null ? (mr.enabled ? "on" : "OFF") : "none", mats, V(b.center), V(b.size), b.max.y));
|
|
}
|
|
|
|
// 지형 콜라이더 bounds (물 평면 크기 결정용)
|
|
sb.AppendLine();
|
|
sb.AppendLine("# MeshCollider 총 bounds (지형 범위)");
|
|
var cols = root.GetComponentsInChildren<Collider>(true);
|
|
if (cols.Length > 0)
|
|
{
|
|
var tb = cols[0].bounds;
|
|
foreach (var c in cols) tb.Encapsulate(c.bounds);
|
|
sb.AppendLine(string.Format(" colliders={0} bounds min={1} max={2}", cols.Length, V(tb.min), V(tb.max)));
|
|
}
|
|
sb.AppendLine(string.Format("# 이미 {0} 루트 존재 = {1}", WaterRoot, root.transform.Find(WaterRoot) != null));
|
|
}
|
|
finally { PrefabUtility.UnloadPrefabContents(root); }
|
|
|
|
// 머티리얼 · 셰이더 해석 확인
|
|
sb.AppendLine();
|
|
sb.AppendLine("# 머티리얼 해석");
|
|
foreach (var p in new[] { MatRiver, MatOcean })
|
|
{
|
|
var m = AssetDatabase.LoadAssetAtPath<Material>(p);
|
|
if (m == null) { sb.AppendLine(" " + p + " = LOAD FAIL"); continue; }
|
|
sb.AppendLine(string.Format(" {0} shader={1} queue={2}", m.name, m.shader != null ? m.shader.name : "(null)", m.renderQueue));
|
|
var sh = m.shader;
|
|
if (sh != null)
|
|
for (int i = 0; i < sh.GetPropertyCount(); i++)
|
|
if (sh.GetPropertyType(i) == UnityEngine.Rendering.ShaderPropertyType.Texture)
|
|
{
|
|
var pn = sh.GetPropertyName(i);
|
|
var tex = m.GetTexture(pn);
|
|
sb.AppendLine(string.Format(" tex {0} = {1}", pn, tex != null ? tex.name : "(NULL)"));
|
|
}
|
|
}
|
|
var urp = UnityEngine.Rendering.GraphicsSettings.currentRenderPipeline as UnityEngine.Rendering.Universal.UniversalRenderPipelineAsset;
|
|
sb.AppendLine(string.Format("# URP asset={0} depthTex={1} opaqueTex={2}",
|
|
urp != null ? urp.name : "(null)", urp != null ? urp.supportsCameraDepthTexture.ToString() : "?",
|
|
urp != null ? urp.supportsCameraOpaqueTexture.ToString() : "?"));
|
|
|
|
var s = sb.ToString();
|
|
W("W0_probe.txt", s);
|
|
return s;
|
|
}
|
|
|
|
static string V(Vector3 v) { return string.Format("({0:F2}, {1:F2}, {2:F2})", v.x, v.y, v.z); }
|
|
static string Path(Transform t, Transform stop)
|
|
{
|
|
var p = t.name;
|
|
while (t.parent != null && t.parent != stop) { t = t.parent; p = t.name + "/" + p; }
|
|
return p;
|
|
}
|
|
|
|
// ────────────────────────────────────────────────────────────── Apply
|
|
// args: [minX, maxX, minZ, maxZ, y] (기본 0,100,-100,120, -1.26)
|
|
public static object ApplyDefault() { return Apply(0f, 100f, -100f, 120f, -1.26f); }
|
|
|
|
public static object Apply(float minX, float maxX, float minZ, float maxZ, float y)
|
|
{
|
|
var sb = new StringBuilder();
|
|
var root = PrefabUtility.LoadPrefabContents(PrefabPath);
|
|
try
|
|
{
|
|
var mat = AssetDatabase.LoadAssetAtPath<Material>(MatRiver);
|
|
if (mat == null) { PrefabUtility.UnloadPrefabContents(root); return "material load fail: " + MatRiver; }
|
|
|
|
// 기존 WL_Water 루트 제거(재실행 안전)
|
|
var old = root.transform.Find(WaterRoot);
|
|
if (old != null) UnityEngine.Object.DestroyImmediate(old.gameObject);
|
|
|
|
var holder = new GameObject(WaterRoot);
|
|
holder.transform.SetParent(root.transform, false);
|
|
holder.transform.localPosition = Vector3.zero;
|
|
|
|
// 평면 1장 = 구 WL ToonWaterU (버텍스 변위 없음 → 서브디비전 불필요)
|
|
var go = new GameObject("Water_River_WL");
|
|
go.transform.SetParent(holder.transform, false);
|
|
go.transform.position = new Vector3((minX + maxX) * 0.5f, y, (minZ + maxZ) * 0.5f);
|
|
var mf = go.AddComponent<MeshFilter>();
|
|
mf.sharedMesh = BuildQuad(maxX - minX, maxZ - minZ);
|
|
var mr = go.AddComponent<MeshRenderer>();
|
|
mr.sharedMaterial = mat;
|
|
mr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
|
mr.receiveShadows = false;
|
|
mr.lightProbeUsage = UnityEngine.Rendering.LightProbeUsage.Off;
|
|
mr.reflectionProbeUsage = UnityEngine.Rendering.ReflectionProbeUsage.Off;
|
|
GameObjectUtility.SetStaticEditorFlags(go, StaticEditorFlags.BatchingStatic);
|
|
sb.AppendLine(string.Format("water plane center={0} size=({1:F1} x {2:F1}) y={3:F3} mat={4}",
|
|
V(go.transform.position), maxX - minX, maxZ - minZ, y, mat.name));
|
|
|
|
// 겹치는 팩 수면 렌더러 비활성 (z-fight 방지 · 되돌리기 = Revert)
|
|
int off = 0;
|
|
var rect = new Rect(minX, minZ, maxX - minX, maxZ - minZ);
|
|
foreach (var t in root.GetComponentsInChildren<Transform>(true))
|
|
{
|
|
if (t.name.IndexOf("water", StringComparison.OrdinalIgnoreCase) < 0) continue;
|
|
if (t.IsChildOf(holder.transform)) continue;
|
|
var r = t.GetComponent<MeshRenderer>();
|
|
if (r == null || !r.enabled) continue;
|
|
var b = r.bounds;
|
|
bool overlap = b.max.x > rect.xMin && b.min.x < rect.xMax && b.max.z > rect.yMin && b.min.z < rect.yMax;
|
|
if (!overlap) { sb.AppendLine(" keep " + t.name + " (영역 밖) bounds c=" + V(b.center)); continue; }
|
|
r.enabled = false; off++;
|
|
sb.AppendLine(string.Format(" OFF {0} bounds c={1} size={2}", t.name, V(b.center), V(b.size)));
|
|
}
|
|
sb.AppendLine("팩 수면 렌더러 비활성 = " + off);
|
|
PrefabUtility.SaveAsPrefabAsset(root, PrefabPath);
|
|
}
|
|
finally { PrefabUtility.UnloadPrefabContents(root); }
|
|
AssetDatabase.SaveAssets();
|
|
var s = sb.ToString();
|
|
W("W1_apply.txt", s);
|
|
return s;
|
|
}
|
|
|
|
public static object Revert()
|
|
{
|
|
var sb = new StringBuilder();
|
|
var root = PrefabUtility.LoadPrefabContents(PrefabPath);
|
|
try
|
|
{
|
|
var old = root.transform.Find(WaterRoot);
|
|
if (old != null) { UnityEngine.Object.DestroyImmediate(old.gameObject); sb.AppendLine("removed " + WaterRoot); }
|
|
int on = 0;
|
|
foreach (var t in root.GetComponentsInChildren<Transform>(true))
|
|
{
|
|
if (t.name.IndexOf("water", StringComparison.OrdinalIgnoreCase) < 0) continue;
|
|
var r = t.GetComponent<MeshRenderer>();
|
|
if (r != null && !r.enabled) { r.enabled = true; on++; }
|
|
}
|
|
sb.AppendLine("팩 수면 렌더러 복구 = " + on);
|
|
PrefabUtility.SaveAsPrefabAsset(root, PrefabPath);
|
|
}
|
|
finally { PrefabUtility.UnloadPrefabContents(root); }
|
|
AssetDatabase.SaveAssets();
|
|
return sb.ToString();
|
|
}
|
|
|
|
static Mesh BuildQuad(float sx, float sz)
|
|
{
|
|
var m = new Mesh { name = "WL_WaterPlane" };
|
|
float hx = sx * 0.5f, hz = sz * 0.5f;
|
|
m.vertices = new[] { new Vector3(-hx, 0, -hz), new Vector3(-hx, 0, hz), new Vector3(hx, 0, hz), new Vector3(hx, 0, -hz) };
|
|
// UV 는 1 m = 1 유닛 (팩 데모와 동일 스케일 감 · 셰이더 타일링은 머티리얼 파라미터가 담당)
|
|
m.uv = new[] { new Vector2(0, 0), new Vector2(0, sz), new Vector2(sx, sz), new Vector2(sx, 0) };
|
|
m.normals = new[] { Vector3.up, Vector3.up, Vector3.up, Vector3.up };
|
|
m.tangents = new[] { new Vector4(1, 0, 0, -1), new Vector4(1, 0, 0, -1), new Vector4(1, 0, 0, -1), new Vector4(1, 0, 0, -1) };
|
|
m.triangles = new[] { 0, 1, 2, 0, 2, 3 };
|
|
m.RecalculateBounds();
|
|
var dir = "Assets/WL/Meshes";
|
|
System.IO.Directory.CreateDirectory(dir);
|
|
var p = dir + "/WL_WaterPlane.asset";
|
|
var exist = AssetDatabase.LoadAssetAtPath<Mesh>(p);
|
|
if (exist != null) { AssetDatabase.DeleteAsset(p); }
|
|
AssetDatabase.CreateAsset(m, p);
|
|
AssetDatabase.SaveAssets();
|
|
return AssetDatabase.LoadAssetAtPath<Mesh>(p);
|
|
}
|
|
#endif
|
|
|
|
// ────────────────────────────────────────────────────────────── Play 검증
|
|
public static object WarpTo(float x, float z, float yaw)
|
|
{
|
|
if (!Application.isPlaying) return "not playing";
|
|
var me = UnityEngine.Object.FindObjectsByType<MyActor>(FindObjectsInactive.Exclude, FindObjectsSortMode.None).FirstOrDefault();
|
|
if (me == null) return "MyActor none";
|
|
float y = 5f;
|
|
RaycastHit hit;
|
|
if (Physics.Raycast(new Vector3(x, 60f, z), Vector3.down, out hit, 200f)) y = hit.point.y + 0.1f;
|
|
var pos = new Vector3(x, y, z);
|
|
var na = me.GetComponent<UnityEngine.AI.NavMeshAgent>();
|
|
if (na != null && na.enabled) na.Warp(pos); else me.transform.position = pos;
|
|
me.transform.rotation = Quaternion.Euler(0, yaw, 0);
|
|
return string.Format("warped to {0} (ground y={1:F3}) yaw={2}", V(pos), y, yaw);
|
|
}
|
|
|
|
|
|
// 재현 가능한 A/B 캡처용 포즈: PC 워프 + RealCamera.rotateAround 고정
|
|
public static object Pose(float x, float z, float rotateAround)
|
|
{
|
|
if (!Application.isPlaying) return "not playing";
|
|
var me = UnityEngine.Object.FindObjectsByType<MyActor>(FindObjectsInactive.Exclude, FindObjectsSortMode.None).FirstOrDefault();
|
|
if (me == null) return "MyActor none";
|
|
float y = 5f; RaycastHit hit;
|
|
if (Physics.Raycast(new Vector3(x, 80f, z), Vector3.down, out hit, 250f)) y = hit.point.y + 0.1f;
|
|
var pos = new Vector3(x, y, z);
|
|
var na = me.GetComponent<UnityEngine.AI.NavMeshAgent>();
|
|
if (na != null && na.enabled) na.Warp(pos); else me.transform.position = pos;
|
|
var cam = Camera.main;
|
|
string camInfo = "(no RealCamera)";
|
|
if (cam != null)
|
|
{
|
|
var rc = cam.GetComponents<Component>().FirstOrDefault(c => c != null && c.GetType().Name == "RealCamera");
|
|
if (rc != null)
|
|
{
|
|
var f = rc.GetType().GetField("rotateAround", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
|
if (f != null) { f.SetValue(rc, rotateAround); camInfo = "rotateAround=" + rotateAround; }
|
|
}
|
|
}
|
|
return string.Format("posed PC={0} ground={1:F3} {2}", V(pos), y, camInfo);
|
|
}
|
|
|
|
// 지형 높이 스캔 — 물 평면이 강 밖에 고이는지 확인
|
|
public static object HeightScan(float minX, float maxX, float minZ, float maxZ, float waterY, float step)
|
|
{
|
|
if (!Application.isPlaying) return "not playing";
|
|
int total = 0, below = 0, noHit = 0;
|
|
float lo = 999f, hi = -999f;
|
|
var cells = new List<string>();
|
|
for (float x = minX; x <= maxX; x += step)
|
|
for (float z = minZ; z <= maxZ; z += step)
|
|
{
|
|
total++;
|
|
RaycastHit h;
|
|
if (!Physics.Raycast(new Vector3(x, 80f, z), Vector3.down, out h, 250f)) { noHit++; continue; }
|
|
lo = Mathf.Min(lo, h.point.y); hi = Mathf.Max(hi, h.point.y);
|
|
if (h.point.y < waterY) { below++; if (cells.Count < 400) cells.Add(string.Format("{0:F0},{1:F0},{2:F2}", x, z, h.point.y)); }
|
|
}
|
|
var sb = new StringBuilder();
|
|
sb.AppendLine(string.Format("scan {0}~{1} x {2}~{3} step {4} waterY {5:F2}", minX, maxX, minZ, maxZ, step, waterY));
|
|
sb.AppendLine(string.Format("total={0} below(=물에 잠김)={1} ({2:F1}%) noHit={3} terrainY {4:F2}~{5:F2}", total, below, 100f * below / Mathf.Max(1, total), noHit, lo, hi));
|
|
// 잠긴 셀의 x 범위 히스토그램
|
|
var byX = new Dictionary<int, int>();
|
|
foreach (var c in cells) { var p = c.Split(','); int xi = (int)(float.Parse(p[0]) / 10) * 10; byX[xi] = byX.TryGetValue(xi, out var v) ? v + 1 : 1; }
|
|
sb.AppendLine("잠긴 셀 x 구간(10 m): " + string.Join(" ", byX.OrderBy(k => k.Key).Select(k => k.Key + ":" + k.Value)));
|
|
var s = sb.ToString();
|
|
W("W2_heightscan.txt", s + "\n" + string.Join("\n", cells));
|
|
return s;
|
|
}
|
|
|
|
public static object HeightScanDefault() { return HeightScan(0f, 130f, -100f, 120f, -1.26f, 2f); }
|
|
}
|