445 lines
17 KiB
C#
445 lines
17 KiB
C#
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
|
||
namespace IndianOceanAssets.Engine2_5D
|
||
{
|
||
/// <summary>
|
||
/// 地面边界生成器 v2 —— 从 Sprite Physics Shape 或 PolygonCollider2D 自动提取边界,
|
||
/// 生成崖壁 Mesh + 边缘发光条带。
|
||
///
|
||
/// 替代 CliffWallBuilder 的手动摆点方式。
|
||
/// 美术改图后只需重新 Build,无需手动调整边界点。
|
||
///
|
||
/// 使用方式:
|
||
/// 1. 场景中创建空物体,挂上此脚本(放在地面高度 Y=0)
|
||
/// 2. 把地面 SpriteRenderer 拖到 groundSprite 字段
|
||
/// (或把 PolygonCollider2D 拖到 polygonCollider 字段)
|
||
/// 3. 指定 wallMaterial(URP Unlit 暗色)
|
||
/// 4. Inspector 点 "Build Ground" 按钮
|
||
/// </summary>
|
||
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;
|
||
|
||
[Tooltip("边缘发光着色器(留空则尝试 Shader.Find 兜底;建议拖入 Assets/Light/shaders/AbyssEdgeGlow.shader 以避免构建裁剪)")]
|
||
[SerializeField] private Shader edgeGlowShader;
|
||
|
||
[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";
|
||
|
||
/// <summary>Editor 脚本读取用。</summary>
|
||
public float SimplifyTolerance => simplifyTolerance;
|
||
|
||
/// <summary>
|
||
/// 从数据源提取边界点(世界坐标)。
|
||
/// </summary>
|
||
public List<List<Vector3>> GetBoundaryPaths()
|
||
{
|
||
var paths = new List<List<Vector3>>();
|
||
|
||
if (polygonCollider != null)
|
||
{
|
||
// 从 PolygonCollider2D 读取所有路径
|
||
for (int i = 0; i < polygonCollider.pathCount; i++)
|
||
{
|
||
Vector2[] path = polygonCollider.GetPath(i);
|
||
var worldPath = new List<Vector3>();
|
||
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<Vector2>();
|
||
sprite.GetPhysicsShape(i, shape);
|
||
|
||
var worldPath = new List<Vector3>();
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成崖壁 + 发光条带。
|
||
/// </summary>
|
||
[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<MeshFilter>();
|
||
MeshRenderer wallMR = wallObj.AddComponent<MeshRenderer>();
|
||
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<MeshFilter>();
|
||
MeshRenderer glowMR = glowObj.AddComponent<MeshRenderer>();
|
||
glowMF.sharedMesh = GenerateEdgeGlowMesh(path);
|
||
|
||
if (edgeGlowMaterial != null)
|
||
{
|
||
glowMR.sharedMaterial = edgeGlowMaterial;
|
||
}
|
||
else
|
||
{
|
||
Shader glowShader = edgeGlowShader != null ? edgeGlowShader : 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})。" : ""));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清除所有生成物。
|
||
/// </summary>
|
||
[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 + 手动法线)
|
||
// ============================================================
|
||
|
||
/// <summary>
|
||
/// 生成崖壁 Mesh:闭合路径,每条边一个垂直四边形(双面),共享顶点。
|
||
/// UV.x 按边长累积(纹理不拉伸),UV.y: 1=顶 0=底。
|
||
/// </summary>
|
||
Mesh GenerateWallMesh(List<Vector3> 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<int>(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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成边缘发光 Mesh:沿边界顶部的细条。
|
||
/// UV.y: 1=顶部(亮) 0=底部(暗),配合 AbyssEdgeGlow shader。
|
||
/// </summary>
|
||
Mesh GenerateEdgeGlowMesh(List<Vector3> 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<int>(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 路径简化
|
||
// ============================================================
|
||
|
||
/// <summary>
|
||
/// 对闭合路径做 Douglas-Peucker 简化。
|
||
/// 先找到离起点最远的点,把闭合路径拆成两条开放路径分别简化。
|
||
/// </summary>
|
||
List<Vector3> SimplifyClosedPath(List<Vector3> 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<Vector3>();
|
||
var arc2 = new List<Vector3>();
|
||
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<Vector3>(arc1);
|
||
for (int i = 1; i < arc2.Count - 1; i++)
|
||
result.Add(arc2[i]);
|
||
|
||
return result;
|
||
}
|
||
|
||
List<Vector3> DouglasPeucker(List<Vector3> 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<Vector3>(left);
|
||
result.AddRange(right.GetRange(1, right.Count - 1));
|
||
return result;
|
||
}
|
||
else
|
||
{
|
||
return new List<Vector3> { 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);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|