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); } } } } }