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: