Files
gold_dolphin/unity/Assets/Light/scripts/GroundBuilder.cs
T
2026-07-04 02:19:54 +08:00

442 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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. 指定 wallMaterialURP 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;
[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" +
"请指定 groundSpriteSprite 需有 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 = 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);
}
}
}
}
}