461 lines
18 KiB
C#
461 lines
18 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using XericLibrary.Runtime.UIGraph;
|
|
|
|
namespace XericUI.XChart.Pseudo3D
|
|
{
|
|
/// <summary>
|
|
/// 伪 3D 立方体背景工具。
|
|
///
|
|
/// 使用 UILineRendererV2 绘制:
|
|
/// 1. 立方体 12 条棱的线框(前向棱使用实色,后向棱可选半透明)
|
|
/// 2. 三个可见面上的轴标线网格
|
|
///
|
|
/// 3D 坐标系统:
|
|
/// 立方体中心在原点,半边长 1(即 [-1, 1] 范围)。
|
|
/// 通过 pitch/yaw 旋转后进行正交投影(丢弃 Z 轴)得到 2D UI 坐标。
|
|
/// 最终通过 scale + offset 映射到 RectTransform 的像素空间。
|
|
///
|
|
/// 子节点结构:
|
|
/// [ChartTool_CubeBg]
|
|
/// └─ CubeRenderer (UILineRendererV2)
|
|
/// </summary>
|
|
[AddComponentMenu("Xeric/XChart/Pseudo3D/Cube Background Tool")]
|
|
public class ChartToolCubeBg : XChartToolBase
|
|
{
|
|
// ── 外观配置 ──────────────────────────────────────────────
|
|
|
|
/// <summary>立方体半边长(UI 像素)</summary>
|
|
public float CubeHalfSize { get; set; } = 150f;
|
|
|
|
/// <summary>俯仰角(绕 X 轴旋转,度数)</summary>
|
|
public float Pitch { get; set; } = 25f;
|
|
|
|
/// <summary>偏航角(绕 Y 轴旋转,度数)</summary>
|
|
public float Yaw { get; set; } = -30f;
|
|
|
|
/// <summary>前向棱颜色</summary>
|
|
public Color FrontEdgeColor { get; set; } = new Color(0.7f, 0.7f, 0.7f, 1f);
|
|
|
|
/// <summary>后向棱颜色</summary>
|
|
public Color BackEdgeColor { get; set; } = new Color(0.4f, 0.4f, 0.4f, 0.4f);
|
|
|
|
/// <summary>棱线宽度</summary>
|
|
public float EdgeThickness { get; set; } = 2f;
|
|
|
|
/// <summary>网格线颜色</summary>
|
|
public Color GridLineColor { get; set; } = new Color(0.5f, 0.5f, 0.5f, 0.3f);
|
|
|
|
/// <summary>网格线宽度</summary>
|
|
public float GridLineThickness { get; set; } = 1f;
|
|
|
|
/// <summary>每轴网格分割数</summary>
|
|
public int GridDivisions { get; set; } = 5;
|
|
|
|
/// <summary>是否显示后向棱</summary>
|
|
public bool ShowBackEdges { get; set; } = true;
|
|
|
|
/// <summary>是否显示网格标线</summary>
|
|
public bool ShowGridLines { get; set; } = true;
|
|
|
|
/// <summary>立方体中心偏移</summary>
|
|
public Vector2 CenterOffset { get; set; } = Vector2.zero;
|
|
|
|
// ── 内部组件 ──────────────────────────────────────────────
|
|
|
|
/// <summary>棱线渲染器</summary>
|
|
public UILineRendererV2 EdgeRenderer { get; private set; }
|
|
|
|
/// <summary>网格线渲染器</summary>
|
|
public UILineRendererV2 GridRenderer { get; private set; }
|
|
|
|
// ── 3D 管线状态 ──────────────────────────────────────────
|
|
|
|
// 8 个立方体顶点(中心在原点,半边长 1)
|
|
private static readonly Vector3[] s_CubeVertices = new Vector3[]
|
|
{
|
|
new Vector3(-1, -1, -1), // 0: 后下左
|
|
new Vector3( 1, -1, -1), // 1: 后下右
|
|
new Vector3( 1, 1, -1), // 2: 后上右
|
|
new Vector3(-1, 1, -1), // 3: 后上左
|
|
new Vector3(-1, -1, 1), // 4: 前下左
|
|
new Vector3( 1, -1, 1), // 5: 前下右
|
|
new Vector3( 1, 1, 1), // 6: 前上右
|
|
new Vector3(-1, 1, 1), // 7: 前上左
|
|
};
|
|
|
|
// 12 条棱(顶点索引对)
|
|
private static readonly int[,] s_CubeEdges = new int[12, 2]
|
|
{
|
|
{0,1}, {1,2}, {2,3}, {3,0}, // 后面
|
|
{4,5}, {5,6}, {6,7}, {7,4}, // 前面
|
|
{0,4}, {1,5}, {2,6}, {3,7}, // 连接棱
|
|
};
|
|
|
|
// 6 个面的顶点索引 + 法线
|
|
private struct FaceDef { public int[] verts; public Vector3 normal; }
|
|
private static readonly FaceDef[] s_Faces = new FaceDef[]
|
|
{
|
|
new FaceDef { verts = new int[] {4,5,6,7}, normal = new Vector3(0, 0, 1) }, // 前
|
|
new FaceDef { verts = new int[] {0,1,2,3}, normal = new Vector3(0, 0,-1) }, // 后
|
|
new FaceDef { verts = new int[] {1,5,6,2}, normal = new Vector3(1, 0, 0) }, // 右
|
|
new FaceDef { verts = new int[] {0,4,7,3}, normal = new Vector3(-1,0, 0) }, // 左
|
|
new FaceDef { verts = new int[] {2,6,7,3}, normal = new Vector3(0, 1, 0) }, // 上
|
|
new FaceDef { verts = new int[] {0,1,5,4}, normal = new Vector3(0,-1, 0) }, // 下
|
|
};
|
|
|
|
// 每个面由两个轴定义(用于网格线方向)
|
|
private static readonly Vector3[] s_FaceAxisA = new Vector3[]
|
|
{
|
|
new Vector3(1,0,0), new Vector3(1,0,0), // 前/后: X 轴
|
|
new Vector3(0,1,0), new Vector3(0,1,0), // 右/左: Y 轴
|
|
new Vector3(1,0,0), new Vector3(1,0,0), // 上/下: X 轴
|
|
};
|
|
private static readonly Vector3[] s_FaceAxisB = new Vector3[]
|
|
{
|
|
new Vector3(0,1,0), new Vector3(0,1,0), // 前/后: Y 轴
|
|
new Vector3(0,0,1), new Vector3(0,0,1), // 右/左: Z 轴
|
|
new Vector3(0,0,1), new Vector3(0,0,1), // 上/下: Z 轴
|
|
};
|
|
|
|
// ── 投影状态 ──────────────────────────────────────────────
|
|
|
|
private Vector3[] m_RotatedVerts = new Vector3[8]; // 旋转后的 3D 顶点
|
|
private Vector2[] m_ProjectedVerts = new Vector2[8]; // 投影后的 2D 顶点
|
|
private bool m_ProjectionDirty = true;
|
|
|
|
/// <summary>投影版本号(每次投影矩阵变化时递增,供外部检测变化)</summary>
|
|
public int ProjectionVersion { get; private set; } = 0;
|
|
|
|
private Matrix4x4 m_RotationMatrix;
|
|
private float m_Scale;
|
|
private Vector2 m_Offset;
|
|
|
|
// ── 初始化 ────────────────────────────────────────────────
|
|
|
|
protected override void OnInitialize()
|
|
{
|
|
// 创建棱线渲染器
|
|
EdgeRenderer = CreateChildComponent<UILineRendererV2>("CubeEdgeRenderer");
|
|
if (EdgeRenderer != null)
|
|
{
|
|
EdgeRenderer.defaultColor = FrontEdgeColor;
|
|
EdgeRenderer.defaultThickness = EdgeThickness;
|
|
var rt = EdgeRenderer.rectTransform;
|
|
rt.anchorMin = Vector2.zero;
|
|
rt.anchorMax = Vector2.one;
|
|
rt.offsetMin = Vector2.zero;
|
|
rt.offsetMax = Vector2.zero;
|
|
}
|
|
|
|
// 创建网格线渲染器
|
|
GridRenderer = CreateChildComponent<UILineRendererV2>("CubeGridRenderer");
|
|
if (GridRenderer != null)
|
|
{
|
|
GridRenderer.defaultColor = GridLineColor;
|
|
GridRenderer.defaultThickness = GridLineThickness;
|
|
var rt = GridRenderer.rectTransform;
|
|
rt.anchorMin = Vector2.zero;
|
|
rt.anchorMax = Vector2.one;
|
|
rt.offsetMin = Vector2.zero;
|
|
rt.offsetMax = Vector2.zero;
|
|
}
|
|
|
|
SetVisible(true);
|
|
}
|
|
|
|
// ── 更新投影矩阵 ──────────────────────────────────────────
|
|
|
|
private void UpdateProjection()
|
|
{
|
|
if (!m_ProjectionDirty) return;
|
|
m_ProjectionDirty = false;
|
|
|
|
// 1. 构建旋转矩阵:先绕 X 轴(pitch),再绕 Y 轴(yaw)
|
|
float pitchRad = Pitch * Mathf.Deg2Rad;
|
|
float yawRad = Yaw * Mathf.Deg2Rad;
|
|
m_RotationMatrix = Matrix4x4.Rotate(
|
|
Quaternion.Euler(pitchRad * Mathf.Rad2Deg, yawRad * Mathf.Rad2Deg, 0));
|
|
|
|
// 2. 旋转所有顶点
|
|
for (int i = 0; i < 8; i++)
|
|
m_RotatedVerts[i] = m_RotationMatrix.MultiplyPoint3x4(s_CubeVertices[i]);
|
|
|
|
// 3. 计算投影后的 2D 范围(丢弃 Z)
|
|
float minX = float.MaxValue, maxX = float.MinValue;
|
|
float minY = float.MaxValue, maxY = float.MinValue;
|
|
for (int i = 0; i < 8; i++)
|
|
{
|
|
float px = m_RotatedVerts[i].x;
|
|
float py = m_RotatedVerts[i].y;
|
|
if (px < minX) minX = px;
|
|
if (px > maxX) maxX = px;
|
|
if (py < minY) minY = py;
|
|
if (py > maxY) maxY = py;
|
|
}
|
|
|
|
// 4. 计算缩放和偏移,使立方体适配 CubeHalfSize
|
|
float rangeX = maxX - minX;
|
|
float rangeY = maxY - minY;
|
|
float maxRange = Mathf.Max(rangeX, rangeY);
|
|
m_Scale = (maxRange > 0.001f) ? (CubeHalfSize * 2f / maxRange) : 1f;
|
|
|
|
// 中心对齐到 CenterOffset
|
|
float centerX = (minX + maxX) * 0.5f;
|
|
float centerY = (minY + maxY) * 0.5f;
|
|
m_Offset = new Vector2(
|
|
CenterOffset.x - centerX * m_Scale,
|
|
CenterOffset.y - centerY * m_Scale);
|
|
|
|
// 5. 计算最终投影坐标
|
|
for (int i = 0; i < 8; i++)
|
|
{
|
|
m_ProjectedVerts[i] = new Vector2(
|
|
m_RotatedVerts[i].x * m_Scale + m_Offset.x,
|
|
m_RotatedVerts[i].y * m_Scale + m_Offset.y);
|
|
}
|
|
}
|
|
|
|
/// <summary>标记投影需要重新计算</summary>
|
|
public void MarkProjectionDirty()
|
|
{
|
|
m_ProjectionDirty = true;
|
|
ProjectionVersion++;
|
|
}
|
|
|
|
// ── 3D 投影辅助 ──────────────────────────────────────────
|
|
|
|
/// <summary>将 3D 点投影到 2D</summary>
|
|
public Vector2 ProjectPoint3D(Vector3 point3d)
|
|
{
|
|
UpdateProjection();
|
|
Vector3 rotated = m_RotationMatrix.MultiplyPoint3x4(point3d);
|
|
return new Vector2(rotated.x * m_Scale + m_Offset.x,
|
|
rotated.y * m_Scale + m_Offset.y);
|
|
}
|
|
|
|
/// <summary>批量投影 3D 点到 2D</summary>
|
|
public Vector2[] ProjectPoints3D(Vector3[] points3d)
|
|
{
|
|
UpdateProjection();
|
|
var result = new Vector2[points3d.Length];
|
|
for (int i = 0; i < points3d.Length; i++)
|
|
{
|
|
Vector3 rotated = m_RotationMatrix.MultiplyPoint3x4(points3d[i]);
|
|
result[i] = new Vector2(rotated.x * m_Scale + m_Offset.x,
|
|
rotated.y * m_Scale + m_Offset.y);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// <summary>获取当前旋转矩阵</summary>
|
|
public Matrix4x4 RotationMatrix => m_RotationMatrix;
|
|
|
|
/// <summary>获取当前缩放</summary>
|
|
public float Scale => m_Scale;
|
|
|
|
/// <summary>获取当前偏移</summary>
|
|
public Vector2 Offset => m_Offset;
|
|
|
|
// ── 渲染 ──────────────────────────────────────────────────
|
|
|
|
public override void OnRender()
|
|
{
|
|
base.OnRender();
|
|
if (EdgeRenderer == null) return;
|
|
|
|
UpdateProjection();
|
|
|
|
// 绘制棱线
|
|
DrawEdges();
|
|
|
|
// 绘制网格标线
|
|
if (GridRenderer != null)
|
|
{
|
|
if (ShowGridLines)
|
|
{
|
|
DrawGridLines();
|
|
}
|
|
else
|
|
{
|
|
GridRenderer.ClearAll();
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── 棱线绘制 ──────────────────────────────────────────────
|
|
|
|
private void DrawEdges()
|
|
{
|
|
EdgeRenderer.ClearAll();
|
|
|
|
// 确定前向棱:棱的两个顶点中,Z 值较大的为前向
|
|
// 更准确:棱所属的面中,法线 Z > 0 的面为可见面
|
|
// 简单方法:棱的两个顶点的 rotated Z 平均值 > 0 为前向
|
|
var frontVerts = new List<LineVertexData>();
|
|
var backVerts = new List<LineVertexData>();
|
|
|
|
for (int e = 0; e < 12; e++)
|
|
{
|
|
int v0 = s_CubeEdges[e, 0];
|
|
int v1 = s_CubeEdges[e, 1];
|
|
float avgZ = (m_RotatedVerts[v0].z + m_RotatedVerts[v1].z) * 0.5f;
|
|
bool isFront = avgZ >= 0;
|
|
|
|
var list = isFront ? frontVerts : backVerts;
|
|
int startIdx = list.Count;
|
|
|
|
list.Add(new LineVertexData
|
|
{
|
|
position = m_ProjectedVerts[v0],
|
|
color = isFront ? FrontEdgeColor : BackEdgeColor,
|
|
thickness = EdgeThickness,
|
|
userData = Vector4.zero,
|
|
});
|
|
list.Add(new LineVertexData
|
|
{
|
|
position = m_ProjectedVerts[v1],
|
|
color = isFront ? FrontEdgeColor : BackEdgeColor,
|
|
thickness = EdgeThickness,
|
|
userData = Vector4.zero,
|
|
});
|
|
}
|
|
|
|
// 先画后向棱(在底层),再画前向棱(在上层)
|
|
if (ShowBackEdges && backVerts.Count > 0)
|
|
{
|
|
// 每两个顶点构成一条线段
|
|
for (int i = 0; i < backVerts.Count; i += 2)
|
|
{
|
|
EdgeRenderer.DrawLine(
|
|
backVerts[i].position,
|
|
backVerts[i + 1].position,
|
|
backVerts[i].color,
|
|
backVerts[i].thickness);
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < frontVerts.Count; i += 2)
|
|
{
|
|
EdgeRenderer.DrawLine(
|
|
frontVerts[i].position,
|
|
frontVerts[i + 1].position,
|
|
frontVerts[i].color,
|
|
frontVerts[i].thickness);
|
|
}
|
|
}
|
|
|
|
// ── 网格标线绘制 ──────────────────────────────────────────
|
|
|
|
private void DrawGridLines()
|
|
{
|
|
GridRenderer.ClearAll();
|
|
|
|
// 获取 3 个可见面(法线 Z 值最大的 3 个面)
|
|
var visibleFaces = GetVisibleFaces();
|
|
if (visibleFaces.Count == 0) return;
|
|
|
|
foreach (int faceIdx in visibleFaces)
|
|
{
|
|
DrawGridLinesOnFace(faceIdx);
|
|
}
|
|
}
|
|
|
|
/// <summary>获取可见面索引列表(按可见度排序,最多 3 个)</summary>
|
|
private List<int> GetVisibleFaces()
|
|
{
|
|
var result = new List<int>(3);
|
|
var faceScores = new (int index, float score)[6];
|
|
|
|
for (int i = 0; i < 6; i++)
|
|
{
|
|
// 将面法线旋转到当前视角
|
|
Vector3 rotatedNormal = m_RotationMatrix.MultiplyVector(s_Faces[i].normal);
|
|
// Z 分量越大,越面向用户
|
|
faceScores[i] = (i, rotatedNormal.z);
|
|
}
|
|
|
|
// 按 Z 值降序排列(Z 越大越面向用户)
|
|
System.Array.Sort(faceScores, (a, b) => b.score.CompareTo(a.score));
|
|
|
|
// 取前 3 个且 Z > 0 的面
|
|
for (int i = 0; i < 3 && faceScores[i].score > 0; i++)
|
|
result.Add(faceScores[i].index);
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>在指定面上绘制网格线</summary>
|
|
private void DrawGridLinesOnFace(int faceIdx)
|
|
{
|
|
var face = s_Faces[faceIdx];
|
|
Vector3 axisA = s_FaceAxisA[faceIdx];
|
|
Vector3 axisB = s_FaceAxisB[faceIdx];
|
|
|
|
// 面的四个顶点(在立方体局部坐标中)
|
|
Vector3 v0 = s_CubeVertices[face.verts[0]];
|
|
Vector3 v1 = s_CubeVertices[face.verts[1]];
|
|
Vector3 v2 = s_CubeVertices[face.verts[2]];
|
|
Vector3 v3 = s_CubeVertices[face.verts[3]];
|
|
|
|
// 面的范围
|
|
float minA = float.MaxValue, maxA = float.MinValue;
|
|
float minB = float.MaxValue, maxB = float.MinValue;
|
|
|
|
// 计算顶点在轴上的投影
|
|
float GetAxisValue(Vector3 v, Vector3 axis) => Vector3.Dot(v, axis);
|
|
|
|
for (int i = 0; i < 4; i++)
|
|
{
|
|
var v = s_CubeVertices[face.verts[i]];
|
|
float va = GetAxisValue(v, axisA);
|
|
float vb = GetAxisValue(v, axisB);
|
|
if (va < minA) minA = va;
|
|
if (va > maxA) maxA = va;
|
|
if (vb < minB) minB = vb;
|
|
if (vb > maxB) maxB = vb;
|
|
}
|
|
|
|
// 沿 A 轴画线(平行于 B 轴)
|
|
float stepA = (maxA - minA) / GridDivisions;
|
|
float stepB = (maxB - minB) / GridDivisions;
|
|
|
|
// 沿 A 轴方向的网格线
|
|
for (int i = 0; i <= GridDivisions; i++)
|
|
{
|
|
float aVal = minA + i * stepA;
|
|
// 线的两个端点
|
|
Vector3 p1 = face.normal + axisA * aVal + axisB * minB;
|
|
Vector3 p2 = face.normal + axisA * aVal + axisB * maxB;
|
|
|
|
Vector2 pp1 = ProjectPoint3D(p1);
|
|
Vector2 pp2 = ProjectPoint3D(p2);
|
|
|
|
// 只绘制落在面范围内的线
|
|
GridRenderer.DrawLine(pp1, pp2, GridLineColor, GridLineThickness);
|
|
}
|
|
|
|
// 沿 B 轴方向的网格线
|
|
for (int i = 0; i <= GridDivisions; i++)
|
|
{
|
|
float bVal = minB + i * stepB;
|
|
Vector3 p1 = face.normal + axisA * minA + axisB * bVal;
|
|
Vector3 p2 = face.normal + axisA * maxA + axisB * bVal;
|
|
|
|
Vector2 pp1 = ProjectPoint3D(p1);
|
|
Vector2 pp2 = ProjectPoint3D(p2);
|
|
|
|
GridRenderer.DrawLine(pp1, pp2, GridLineColor, GridLineThickness);
|
|
}
|
|
}
|
|
|
|
// ── 清理 ──────────────────────────────────────────────────
|
|
|
|
protected override void OnDispose()
|
|
{
|
|
EdgeRenderer = null;
|
|
GridRenderer = null;
|
|
}
|
|
}
|
|
}
|