diff --git a/unity/Assets/Light/Editor.meta b/unity/Assets/Light/Editor.meta
new file mode 100644
index 0000000..1876880
--- /dev/null
+++ b/unity/Assets/Light/Editor.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: WXMZvSurAH8plRNOSQQEewEIV5sZ3+2PTc3duA7S982mfLcu5BgVwNE=
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/unity/Assets/Light/Editor/GroundBuilderEditor.cs b/unity/Assets/Light/Editor/GroundBuilderEditor.cs
new file mode 100644
index 0000000..8c1307c
--- /dev/null
+++ b/unity/Assets/Light/Editor/GroundBuilderEditor.cs
@@ -0,0 +1,58 @@
+using UnityEditor;
+using UnityEngine;
+using IndianOceanAssets.Engine2_5D;
+
+namespace IndianOceanAssets.EditorTools
+{
+ [CustomEditor(typeof(GroundBuilder))]
+ public class GroundBuilderEditor : UnityEditor.Editor
+ {
+ public override void OnInspectorGUI()
+ {
+ DrawDefaultInspector();
+
+ var builder = (GroundBuilder)target;
+
+ EditorGUILayout.Space();
+ EditorGUILayout.LabelField("操作", EditorStyles.boldLabel);
+
+ if (GUILayout.Button("Build Ground", GUILayout.Height(32)))
+ {
+ Undo.RecordObject(builder, "Build Ground");
+ builder.Build();
+ EditorUtility.SetDirty(builder);
+ }
+
+ if (GUILayout.Button("Clear Generated", GUILayout.Height(24)))
+ {
+ Undo.RecordObject(builder, "Clear Generated");
+ builder.ClearGenerated();
+ EditorUtility.SetDirty(builder);
+ }
+
+ EditorGUILayout.Space();
+
+ // 显示边界信息
+ var paths = builder.GetBoundaryPaths();
+ if (paths.Count > 0)
+ {
+ EditorGUILayout.LabelField("边界信息", EditorStyles.boldLabel);
+ int total = 0;
+ for (int i = 0; i < paths.Count; i++)
+ {
+ int count = paths[i].Count;
+ total += count;
+ EditorGUILayout.LabelField($" 路径 {i}: {count} 个点");
+ }
+ EditorGUILayout.LabelField($" 合计: {paths.Count} 条路径, {total} 个点");
+ }
+ else
+ {
+ EditorGUILayout.HelpBox(
+ "未检测到边界数据。\n" +
+ "请指定 groundSprite(地面 SpriteRenderer)或 polygonCollider。",
+ MessageType.Warning);
+ }
+ }
+ }
+}
diff --git a/unity/Assets/Light/Editor/GroundBuilderEditor.cs.meta b/unity/Assets/Light/Editor/GroundBuilderEditor.cs.meta
new file mode 100644
index 0000000..987d95c
--- /dev/null
+++ b/unity/Assets/Light/Editor/GroundBuilderEditor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 5f4a22b278b94f918a6884f8dc26b9ed
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/unity/Assets/Light/scripts/CliffWallBuilder.cs b/unity/Assets/Light/scripts/CliffWallBuilder.cs
new file mode 100644
index 0000000..99aeb39
--- /dev/null
+++ b/unity/Assets/Light/scripts/CliffWallBuilder.cs
@@ -0,0 +1,236 @@
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace IndianOceanAssets.Engine2_5D
+{
+ ///
+ /// 悬崖崖壁生成器 —— 根据子物体(边界点)生成垂直崖壁 mesh + 边缘发光条带。
+ ///
+ /// 使用方式:
+ /// 1. 场景中创建空物体,挂上此脚本(放在地面高度,Y=0)
+ /// 2. 在此物体下创建若干子空物体作为边界角点
+ /// (至少 3 个,顺时针或逆时针排列,例如 4 个摆成矩形)
+ /// 3. Inspector 里指定 wallMaterial(建议 URP Unlit 暗色材质)
+ /// 4. 右键组件 > Build Cliff Walls 生成
+ /// 5. 修改边界点后再次 Build 会自动清除旧的重建
+ ///
+ /// 生成结果(在 "Generated_CliffWalls" 子物体下):
+ /// - CliffWall:垂直崖壁面,从边界点往下延伸 wallHeight
+ /// - EdgeGlow:顶部细条,使用 AbyssEdgeGlow shader 勾勒断边轮廓
+ ///
+ public class CliffWallBuilder : MonoBehaviour
+ {
+ [Header("崖壁参数")]
+ [Tooltip("崖壁往下延伸的高度(世界单位)")]
+ [SerializeField] private float wallHeight = 5f;
+
+ [Header("边缘发光线")]
+ [Tooltip("是否生成顶部发光条带")]
+ [SerializeField] private bool generateEdgeGlow = true;
+ [Tooltip("发光条带的高度(世界单位)")]
+ [SerializeField] private float edgeGlowHeight = 0.2f;
+ [Tooltip("发光颜色")]
+ [SerializeField] private Color edgeGlowColor = new Color(0.2f, 0.6f, 0.9f, 1f);
+ [Tooltip("发光亮度")]
+ [SerializeField, Range(0.1f, 5f)] private float edgeGlowIntensity = 1.5f;
+ [Tooltip("渐变:越大上边越亮下边越暗")]
+ [SerializeField, Range(0.1f, 3f)] private float edgeGlowGradient = 1f;
+
+ [Header("材质")]
+ [Tooltip("崖壁材质(建议 URP Unlit,深色,Render Face = Both)")]
+ [SerializeField] private Material wallMaterial;
+ [Tooltip("边缘发光材质(使用 AbyssEdgeGlow shader)。留空则自动创建。")]
+ [SerializeField] private Material edgeGlowMaterial;
+
+ private const string GeneratedName = "Generated_CliffWalls";
+
+ [ContextMenu("Build Cliff Walls")]
+ public void Build()
+ {
+ // 收集子物体作为边界点(跳过已生成的容器)
+ var points = new List();
+ for (int i = 0; i < transform.childCount; i++)
+ {
+ Transform child = transform.GetChild(i);
+ if (child.name == GeneratedName) continue;
+ points.Add(child.position);
+ }
+
+ if (points.Count < 2)
+ {
+ Debug.LogWarning("[CliffWallBuilder] 需要至少 2 个子物体作为边界点(不含生成物)。" +
+ "请在此物体下添加空物体作为角点。");
+ return;
+ }
+
+ // 清除旧生成物
+ Transform old = transform.Find(GeneratedName);
+ if (old != null)
+ {
+ if (Application.isPlaying)
+ Destroy(old.gameObject);
+ else
+ DestroyImmediate(old.gameObject);
+ }
+
+ // 创建容器
+ GameObject container = new GameObject(GeneratedName);
+ container.transform.SetParent(transform, false);
+
+ // ---- 崖壁 mesh ----
+ GameObject wallObj = new GameObject("CliffWall");
+ wallObj.transform.SetParent(container.transform, false);
+ MeshFilter wallMF = wallObj.AddComponent();
+ MeshRenderer wallMR = wallObj.AddComponent();
+ Mesh wallMesh = GenerateWallMesh(points);
+ wallMesh.name = "CliffWall_Mesh";
+ wallMF.sharedMesh = wallMesh;
+ wallMR.sharedMaterial = wallMaterial;
+
+ // ---- 边缘发光 mesh ----
+ if (generateEdgeGlow)
+ {
+ GameObject glowObj = new GameObject("EdgeGlow");
+ glowObj.transform.SetParent(container.transform, false);
+ MeshFilter glowMF = glowObj.AddComponent();
+ MeshRenderer glowMR = glowObj.AddComponent();
+ Mesh glowMesh = GenerateEdgeGlowMesh(points);
+ glowMesh.name = "EdgeGlow_Mesh";
+ glowMF.sharedMesh = glowMesh;
+
+ if (edgeGlowMaterial != null)
+ {
+ glowMR.sharedMaterial = edgeGlowMaterial;
+ }
+ else
+ {
+ // 尝试自动创建 AbyssEdgeGlow 材质
+ Shader edgeShader = Shader.Find("IndianOcean/AbyssEdgeGlow");
+ if (edgeShader != null)
+ {
+ Material mat = new Material(edgeShader);
+ mat.SetColor("_Color", edgeGlowColor);
+ mat.SetFloat("_Intensity", edgeGlowIntensity);
+ mat.SetFloat("_Gradient", edgeGlowGradient);
+ glowMR.sharedMaterial = mat;
+ }
+ else
+ {
+ Debug.LogWarning("[CliffWallBuilder] 找不到 IndianOcean/AbyssEdgeGlow shader。" +
+ "请手动创建材质并指定到 edgeGlowMaterial。");
+ }
+ }
+ }
+
+ Debug.Log($"[CliffWallBuilder] 已生成 {points.Count} 条崖壁边,高度 {wallHeight}");
+ }
+
+ ///
+ /// 生成崖壁 mesh:每条边一个垂直四边形(双面)
+ ///
+ private Mesh GenerateWallMesh(List points)
+ {
+ var verts = new List();
+ var tris = new List();
+ var uvs = new List();
+
+ int n = points.Count;
+ for (int i = 0; i < n; i++)
+ {
+ Vector3 p0 = transform.InverseTransformPoint(points[i]);
+ Vector3 p1 = transform.InverseTransformPoint(points[(i + 1) % n]);
+
+ Vector3 bl = p0 + new Vector3(0, -wallHeight, 0);
+ Vector3 br = p1 + new Vector3(0, -wallHeight, 0);
+ Vector3 tl = p0;
+ Vector3 tr = p1;
+
+ int b = verts.Count;
+ verts.Add(bl); verts.Add(br); verts.Add(tl); verts.Add(tr);
+ uvs.Add(new Vector2(0, 0));
+ uvs.Add(new Vector2(1, 0));
+ uvs.Add(new Vector2(0, 1));
+ uvs.Add(new Vector2(1, 1));
+
+ // 正面
+ tris.Add(b + 0); tris.Add(b + 2); tris.Add(b + 1);
+ tris.Add(b + 1); tris.Add(b + 2); tris.Add(b + 3);
+ // 背面(反绕,双面可见)
+ tris.Add(b + 0); tris.Add(b + 1); tris.Add(b + 2);
+ tris.Add(b + 1); tris.Add(b + 3); tris.Add(b + 2);
+ }
+
+ Mesh mesh = new Mesh { name = "CliffWall" };
+ mesh.SetVertices(verts);
+ mesh.SetUVs(0, uvs);
+ mesh.SetTriangles(tris, 0);
+ mesh.RecalculateNormals();
+ mesh.RecalculateBounds();
+ return mesh;
+ }
+
+ ///
+ /// 生成边缘发光 mesh:沿边界顶部的细条
+ /// UV.y: 1=顶部(亮), 0=底部(暗)
+ ///
+ private Mesh GenerateEdgeGlowMesh(List points)
+ {
+ var verts = new List();
+ var tris = new List();
+ var uvs = new List();
+
+ int n = points.Count;
+ for (int i = 0; i < n; i++)
+ {
+ Vector3 p0 = transform.InverseTransformPoint(points[i]);
+ Vector3 p1 = transform.InverseTransformPoint(points[(i + 1) % n]);
+
+ Vector3 tl = p0; // 顶(地面高度)
+ Vector3 tr = p1;
+ Vector3 bl = p0 + new Vector3(0, -edgeGlowHeight, 0); // 底(往下)
+ Vector3 br = p1 + new Vector3(0, -edgeGlowHeight, 0);
+
+ int b = verts.Count;
+ verts.Add(bl); verts.Add(br); verts.Add(tl); verts.Add(tr);
+ uvs.Add(new Vector2(0, 0)); // 底部暗
+ uvs.Add(new Vector2(1, 0));
+ uvs.Add(new Vector2(0, 1)); // 顶部亮
+ uvs.Add(new Vector2(1, 1));
+
+ tris.Add(b + 0); tris.Add(b + 2); tris.Add(b + 1);
+ tris.Add(b + 1); tris.Add(b + 2); tris.Add(b + 3);
+ tris.Add(b + 0); tris.Add(b + 1); tris.Add(b + 2);
+ tris.Add(b + 1); tris.Add(b + 3); tris.Add(b + 2);
+ }
+
+ Mesh mesh = new Mesh { name = "EdgeGlow" };
+ mesh.SetVertices(verts);
+ mesh.SetUVs(0, uvs);
+ mesh.SetTriangles(tris, 0);
+ mesh.RecalculateNormals();
+ mesh.RecalculateBounds();
+ return mesh;
+ }
+
+ private void OnDrawGizmosSelected()
+ {
+ // 画出边界点和连线,方便编辑
+ Gizmos.color = new Color(0.2f, 0.6f, 0.9f, 0.8f);
+ var points = new List();
+ for (int i = 0; i < transform.childCount; i++)
+ {
+ Transform child = transform.GetChild(i);
+ if (child.name == GeneratedName) continue;
+ points.Add(child.position);
+ Gizmos.DrawWireSphere(child.position, 0.3f);
+ }
+
+ if (points.Count < 2) return;
+ Gizmos.color = new Color(0.2f, 0.6f, 0.9f, 0.4f);
+ for (int i = 0; i < points.Count; i++)
+ {
+ Gizmos.DrawLine(points[i], points[(i + 1) % points.Count]);
+ }
+ }
+ }
+}
diff --git a/unity/Assets/Light/scripts/CliffWallBuilder.cs.meta b/unity/Assets/Light/scripts/CliffWallBuilder.cs.meta
new file mode 100644
index 0000000..311dd7f
--- /dev/null
+++ b/unity/Assets/Light/scripts/CliffWallBuilder.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: DHtOty3/AnIDq/NLNIqhP7Za7/hAPMKjZClwtxWX0yhSL3GrqzLPv3A=
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/unity/Assets/Light/scripts/GroundBuilder.cs b/unity/Assets/Light/scripts/GroundBuilder.cs
new file mode 100644
index 0000000..1c82ce9
--- /dev/null
+++ b/unity/Assets/Light/scripts/GroundBuilder.cs
@@ -0,0 +1,441 @@
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace IndianOceanAssets.Engine2_5D
+{
+ ///
+ /// 地面边界生成器 v2 —— 从 Sprite Physics Shape 或 PolygonCollider2D 自动提取边界,
+ /// 生成崖壁 Mesh + 边缘发光条带。
+ ///
+ /// 替代 CliffWallBuilder 的手动摆点方式。
+ /// 美术改图后只需重新 Build,无需手动调整边界点。
+ ///
+ /// 使用方式:
+ /// 1. 场景中创建空物体,挂上此脚本(放在地面高度 Y=0)
+ /// 2. 把地面 SpriteRenderer 拖到 groundSprite 字段
+ /// (或把 PolygonCollider2D 拖到 polygonCollider 字段)
+ /// 3. 指定 wallMaterial(URP Unlit 暗色)
+ /// 4. Inspector 点 "Build Ground" 按钮
+ ///
+ public class GroundBuilder : MonoBehaviour
+ {
+ [Header("数据源")]
+ [Tooltip("地面精灵渲染器。从此 Sprite 的 Physics Shape 自动提取边界轮廓。\n" +
+ "需要在 Sprite Editor 里 Generate Physics Shape(默认自动生成)。")]
+ [SerializeField] SpriteRenderer groundSprite;
+
+ [Tooltip("可选:直接用 PolygonCollider2D 的路径作为边界。\n" +
+ "留空则用 Sprite Physics Shape。")]
+ [SerializeField] PolygonCollider2D polygonCollider;
+
+ [Header("崖壁")]
+ [Tooltip("崖壁往下延伸的高度(世界单位)")]
+ [SerializeField] float wallHeight = 5f;
+
+ [Tooltip("崖壁材质(建议 URP Unlit,深色,Cull Off 双面)")]
+ [SerializeField] Material wallMaterial;
+
+ [Header("边缘发光")]
+ [Tooltip("是否生成顶部发光条带")]
+ [SerializeField] bool generateEdgeGlow = true;
+
+ [Tooltip("发光条带的高度(世界单位)")]
+ [SerializeField] float edgeGlowHeight = 0.3f;
+
+ [Tooltip("发光颜色")]
+ [SerializeField] Color edgeGlowColor = new Color(0.2f, 0.6f, 0.9f, 1f);
+
+ [Tooltip("发光亮度")]
+ [SerializeField, Range(0.1f, 5f)] float edgeGlowIntensity = 1.5f;
+
+ [Tooltip("渐变:越大上边越亮下边越暗")]
+ [SerializeField, Range(0.1f, 3f)] float edgeGlowGradient = 1f;
+
+ [Tooltip("边缘发光材质(使用 AbyssEdgeGlow shader)。留空则自动创建。")]
+ [SerializeField] Material edgeGlowMaterial;
+
+ [Header("优化")]
+ [Tooltip("Douglas-Peucker 简化容差(世界单位)。0 = 不简化。\n" +
+ "建议 0.05~0.2,减少顶点数同时保持轮廓形状。")]
+ [SerializeField] float simplifyTolerance = 0.1f;
+
+ [Tooltip("UV 平铺缩放(越大纹理越密)")]
+ [SerializeField] float uvScale = 1f;
+
+ private const string GENERATED = "Generated_Ground";
+
+ /// Editor 脚本读取用。
+ public float SimplifyTolerance => simplifyTolerance;
+
+ ///
+ /// 从数据源提取边界点(世界坐标)。
+ ///
+ public List> GetBoundaryPaths()
+ {
+ var paths = new List>();
+
+ if (polygonCollider != null)
+ {
+ // 从 PolygonCollider2D 读取所有路径
+ for (int i = 0; i < polygonCollider.pathCount; i++)
+ {
+ Vector2[] path = polygonCollider.GetPath(i);
+ var worldPath = new List();
+ foreach (var p in path)
+ {
+ Vector3 world = polygonCollider.transform.TransformPoint(p.x, p.y, 0f);
+ worldPath.Add(world);
+ }
+ if (worldPath.Count >= 3)
+ paths.Add(worldPath);
+ }
+ }
+ else if (groundSprite != null && groundSprite.sprite != null)
+ {
+ // 从 Sprite Physics Shape 读取所有轮廓
+ Sprite sprite = groundSprite.sprite;
+ int shapeCount = sprite.GetPhysicsShapeCount();
+
+ for (int i = 0; i < shapeCount; i++)
+ {
+ var shape = new List();
+ sprite.GetPhysicsShape(i, shape);
+
+ var worldPath = new List();
+ foreach (var p in shape)
+ {
+ // Sprite 局部坐标 → 世界坐标
+ // TransformPoint 自动处理旋转(地面精灵平铺时 X→X, Y→Z)
+ Vector3 world = groundSprite.transform.TransformPoint(p.x, p.y, 0f);
+ worldPath.Add(world);
+ }
+ if (worldPath.Count >= 3)
+ paths.Add(worldPath);
+ }
+ }
+
+ return paths;
+ }
+
+ ///
+ /// 生成崖壁 + 发光条带。
+ ///
+ [ContextMenu("Build Ground")]
+ public void Build()
+ {
+ var paths = GetBoundaryPaths();
+
+ if (paths.Count == 0)
+ {
+ Debug.LogWarning("[GroundBuilder] 没有找到边界数据。\n" +
+ "请指定 groundSprite(Sprite 需有 Physics Shape)或 polygonCollider。");
+ return;
+ }
+
+ // 简化
+ if (simplifyTolerance > 0f)
+ {
+ for (int i = 0; i < paths.Count; i++)
+ paths[i] = SimplifyClosedPath(paths[i], simplifyTolerance);
+ }
+
+ // 清除旧生成物
+ ClearGenerated();
+
+ // 创建容器
+ GameObject container = new GameObject(GENERATED);
+ container.transform.SetParent(transform, false);
+
+ int totalPoints = 0;
+
+ // 为每条路径生成崖壁和发光
+ for (int i = 0; i < paths.Count; i++)
+ {
+ var path = paths[i];
+ totalPoints += path.Count;
+
+ // ---- 崖壁 Mesh ----
+ GameObject wallObj = new GameObject($"CliffWall_{i}");
+ wallObj.transform.SetParent(container.transform, false);
+ MeshFilter wallMF = wallObj.AddComponent();
+ MeshRenderer wallMR = wallObj.AddComponent();
+ wallMF.sharedMesh = GenerateWallMesh(path);
+ wallMR.sharedMaterial = wallMaterial;
+
+ // ---- 边缘发光 Mesh ----
+ if (generateEdgeGlow)
+ {
+ GameObject glowObj = new GameObject($"EdgeGlow_{i}");
+ glowObj.transform.SetParent(container.transform, false);
+ MeshFilter glowMF = glowObj.AddComponent();
+ MeshRenderer glowMR = glowObj.AddComponent();
+ glowMF.sharedMesh = GenerateEdgeGlowMesh(path);
+
+ if (edgeGlowMaterial != null)
+ {
+ glowMR.sharedMaterial = edgeGlowMaterial;
+ }
+ else
+ {
+ Shader glowShader = Shader.Find("IndianOcean/AbyssEdgeGlow");
+ if (glowShader != null)
+ {
+ Material mat = new Material(glowShader);
+ mat.SetColor("_Color", edgeGlowColor);
+ mat.SetFloat("_Intensity", edgeGlowIntensity);
+ mat.SetFloat("_Gradient", edgeGlowGradient);
+ glowMR.sharedMaterial = mat;
+ }
+ else
+ {
+ Debug.LogWarning("[GroundBuilder] 找不到 IndianOcean/AbyssEdgeGlow shader。");
+ }
+ }
+ }
+ }
+
+ int totalEdges = 0;
+ foreach (var p in paths) totalEdges += p.Count;
+ Debug.Log($"[GroundBuilder] 生成完成:{paths.Count} 条路径,{totalEdges} 个边界点," +
+ $"崖壁高度 {wallHeight}。" +
+ (simplifyTolerance > 0 ? $" 已简化(容差 {simplifyTolerance})。" : ""));
+ }
+
+ ///
+ /// 清除所有生成物。
+ ///
+ [ContextMenu("Clear Generated")]
+ public void ClearGenerated()
+ {
+ Transform old = transform.Find(GENERATED);
+ if (old != null)
+ {
+ if (Application.isPlaying)
+ Destroy(old.gameObject);
+ else
+ DestroyImmediate(old.gameObject);
+ }
+ }
+
+ // ============================================================
+ // Mesh 生成(共享顶点 + 距离 UV + 手动法线)
+ // ============================================================
+
+ ///
+ /// 生成崖壁 Mesh:闭合路径,每条边一个垂直四边形(双面),共享顶点。
+ /// UV.x 按边长累积(纹理不拉伸),UV.y: 1=顶 0=底。
+ ///
+ Mesh GenerateWallMesh(List worldPoints)
+ {
+ int n = worldPoints.Count;
+
+ // 世界坐标 → 本地坐标
+ var local = new Vector3[n];
+ for (int i = 0; i < n; i++)
+ local[i] = transform.InverseTransformPoint(worldPoints[i]);
+
+ // 顶点:top ring (0..n-1) + bottom ring (n..2n-1)
+ Vector3[] verts = new Vector3[n * 2];
+ Vector2[] uvs = new Vector2[n * 2];
+ Vector3[] norms = new Vector3[n * 2];
+
+ float u = 0f;
+ for (int i = 0; i < n; i++)
+ {
+ int next = (i + 1) % n;
+ Vector3 top = local[i];
+ Vector3 bot = local[i] + new Vector3(0, -wallHeight, 0);
+
+ verts[i] = top;
+ verts[n + i] = bot;
+
+ // UV.x 按距离累积
+ float edgeLen = Vector3.Distance(local[i], local[next]);
+ uvs[i] = new Vector2(u * uvScale, 1f);
+ uvs[n + i] = new Vector2(u * uvScale, 0f);
+ u += edgeLen;
+
+ // 法线:边的垂直方向,朝外
+ Vector3 edge = local[next] - local[i];
+ Vector3 normal = Vector3.Cross(edge, Vector3.down).normalized;
+ norms[i] = normal;
+ norms[n + i] = normal;
+ }
+
+ // 三角形:双面
+ var tris = new List(n * 4);
+ for (int i = 0; i < n; i++)
+ {
+ int next = (i + 1) % n;
+ int ti = i, tj = next, bi = n + i, bj = n + next;
+
+ // 正面
+ tris.Add(ti); tris.Add(tj); tris.Add(bj);
+ tris.Add(ti); tris.Add(bj); tris.Add(bi);
+ // 背面(反绕)
+ tris.Add(ti); tris.Add(bj); tris.Add(tj);
+ tris.Add(ti); tris.Add(bi); tris.Add(bj);
+ }
+
+ Mesh mesh = new Mesh { name = "GroundWall" };
+ mesh.SetVertices(verts);
+ mesh.SetUVs(0, uvs);
+ mesh.SetNormals(norms);
+ mesh.SetTriangles(tris, 0);
+ mesh.RecalculateBounds();
+ return mesh;
+ }
+
+ ///
+ /// 生成边缘发光 Mesh:沿边界顶部的细条。
+ /// UV.y: 1=顶部(亮) 0=底部(暗),配合 AbyssEdgeGlow shader。
+ ///
+ Mesh GenerateEdgeGlowMesh(List worldPoints)
+ {
+ int n = worldPoints.Count;
+
+ var local = new Vector3[n];
+ for (int i = 0; i < n; i++)
+ local[i] = transform.InverseTransformPoint(worldPoints[i]);
+
+ Vector3[] verts = new Vector3[n * 2];
+ Vector2[] uvs = new Vector2[n * 2];
+
+ float u = 0f;
+ for (int i = 0; i < n; i++)
+ {
+ int next = (i + 1) % n;
+ verts[i] = local[i]; // 顶
+ verts[n + i] = local[i] + new Vector3(0, -edgeGlowHeight, 0); // 底
+
+ float edgeLen = Vector3.Distance(local[i], local[next]);
+ uvs[i] = new Vector2(u * uvScale, 1f);
+ uvs[n + i] = new Vector2(u * uvScale, 0f);
+ u += edgeLen;
+ }
+
+ var tris = new List(n * 4);
+ for (int i = 0; i < n; i++)
+ {
+ int next = (i + 1) % n;
+ int ti = i, tj = next, bi = n + i, bj = n + next;
+
+ tris.Add(ti); tris.Add(tj); tris.Add(bj);
+ tris.Add(ti); tris.Add(bj); tris.Add(bi);
+ tris.Add(ti); tris.Add(bj); tris.Add(tj);
+ tris.Add(ti); tris.Add(bi); tris.Add(bj);
+ }
+
+ Mesh mesh = new Mesh { name = "EdgeGlow" };
+ mesh.SetVertices(verts);
+ mesh.SetUVs(0, uvs);
+ mesh.SetTriangles(tris, 0);
+ mesh.RecalculateBounds();
+ return mesh;
+ }
+
+ // ============================================================
+ // Douglas-Peucker 路径简化
+ // ============================================================
+
+ ///
+ /// 对闭合路径做 Douglas-Peucker 简化。
+ /// 先找到离起点最远的点,把闭合路径拆成两条开放路径分别简化。
+ ///
+ List SimplifyClosedPath(List points, float tolerance)
+ {
+ if (points.Count < 4) return points;
+
+ // 找离 points[0] 最远的点
+ float maxDist = 0f;
+ int farIdx = 1;
+ for (int i = 1; i < points.Count; i++)
+ {
+ float d = Vector3.Distance(points[0], points[i]);
+ if (d > maxDist) { maxDist = d; farIdx = i; }
+ }
+
+ // 拆成两段开放路径
+ var arc1 = new List();
+ var arc2 = new List();
+ for (int i = 0; i <= farIdx; i++) arc1.Add(points[i]);
+ for (int i = farIdx; i < points.Count; i++) arc2.Add(points[i]);
+ arc2.Add(points[0]); // 闭合
+
+ arc1 = DouglasPeucker(arc1, tolerance);
+ arc2 = DouglasPeucker(arc2, tolerance);
+
+ // 合并:arc1 + arc2[1..count-2](跳过 arc2 首尾,它们和 arc1 重复)
+ var result = new List(arc1);
+ for (int i = 1; i < arc2.Count - 1; i++)
+ result.Add(arc2[i]);
+
+ return result;
+ }
+
+ List DouglasPeucker(List points, float tolerance)
+ {
+ if (points.Count < 3) return points;
+
+ // 找离首末连线最远的点
+ float maxDist = 0f;
+ int maxIdx = 0;
+ Vector3 a = points[0];
+ Vector3 b = points[points.Count - 1];
+
+ for (int i = 1; i < points.Count - 1; i++)
+ {
+ float d = PerpDistance(points[i], a, b);
+ if (d > maxDist) { maxDist = d; maxIdx = i; }
+ }
+
+ if (maxDist > tolerance)
+ {
+ var left = DouglasPeucker(points.GetRange(0, maxIdx + 1), tolerance);
+ var right = DouglasPeucker(points.GetRange(maxIdx, points.Count - maxIdx), tolerance);
+
+ var result = new List(left);
+ result.AddRange(right.GetRange(1, right.Count - 1));
+ return result;
+ }
+ else
+ {
+ return new List { a, b };
+ }
+ }
+
+ float PerpDistance(Vector3 p, Vector3 lineA, Vector3 lineB)
+ {
+ Vector3 ab = lineB - lineA;
+ float lenSq = ab.sqrMagnitude;
+ if (lenSq < 1e-8f) return Vector3.Distance(p, lineA);
+ Vector3 ap = p - lineA;
+ float t = Vector3.Dot(ap, ab) / lenSq;
+ t = Mathf.Clamp01(t);
+ Vector3 proj = lineA + t * ab;
+ return Vector3.Distance(p, proj);
+ }
+
+ // ============================================================
+ // Gizmos
+ // ============================================================
+
+ private void OnDrawGizmosSelected()
+ {
+ var paths = GetBoundaryPaths();
+ if (paths.Count == 0) return;
+
+ Gizmos.color = new Color(0.2f, 0.6f, 0.9f, 0.8f);
+ foreach (var path in paths)
+ {
+ for (int i = 0; i < path.Count; i++)
+ {
+ Vector3 cur = path[i];
+ Vector3 next = path[(i + 1) % path.Count];
+ Gizmos.DrawLine(cur, next);
+ Gizmos.DrawSphere(cur, 0.15f);
+ }
+ }
+ }
+ }
+}
diff --git a/unity/Assets/Light/scripts/GroundBuilder.cs.meta b/unity/Assets/Light/scripts/GroundBuilder.cs.meta
new file mode 100644
index 0000000..56768d9
--- /dev/null
+++ b/unity/Assets/Light/scripts/GroundBuilder.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: WnpNvHipV3PXXKeHcFsOVhPsBlAM73Y2J2p6cOGF0AxVywfqigIYWj0=
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant: