342 lines
13 KiB
C#
342 lines
13 KiB
C#
using System;
|
||
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using Dreamteck.Splines;
|
||
using Ichni.RhythmGame.Beatmap;
|
||
using Sirenix.OdinInspector;
|
||
using Unity.VisualScripting;
|
||
using UnityEngine;
|
||
using UnityEngine.Splines;
|
||
using Spline = Dreamteck.Splines.Spline;
|
||
|
||
namespace Ichni.RhythmGame
|
||
{
|
||
using System.Collections.Generic;
|
||
using Unity.Mathematics;
|
||
using UnityEngine;
|
||
|
||
// 引入官方的 Splines 命名空间
|
||
using UnitySpline = UnityEngine.Splines.Spline;
|
||
using UnitySplineContainer = UnityEngine.Splines.SplineContainer;
|
||
|
||
public partial class TrackPathSubmodule : TrackSubmodule
|
||
{
|
||
public Dreamteck.Splines.SplineComputer path;
|
||
public List<PathNode> pathNodeList;
|
||
|
||
public Track.TrackSpaceType trackSpaceType;
|
||
public Track.TrackSamplingType trackSamplingType;
|
||
public bool isClosed;
|
||
|
||
public bool refreshedThisFrame = false;
|
||
|
||
public bool isShowingDisplay;
|
||
|
||
public TrackPathSubmodule(Track track, Track.TrackSpaceType trackSpaceType,
|
||
Track.TrackSamplingType trackSamplingType, bool isClosed, bool isShowingDisplay) : base(track)
|
||
{
|
||
this.path = track.AddComponent<SplineComputer>();
|
||
|
||
this.pathNodeList = new List<PathNode>();
|
||
this.trackSpaceType = trackSpaceType;
|
||
this.trackSamplingType = trackSamplingType;
|
||
this.isClosed = isClosed;
|
||
|
||
this.path.sampleRate = 8;
|
||
this.path.updateMode = SplineComputer.UpdateMode.LateUpdate;
|
||
SetUpSplineComputer(this.trackSpaceType, this.trackSamplingType);
|
||
//闭合路径在PathNode生成时执行,在初始化的情况下,PathNode数量为0,不会执行闭合操作
|
||
|
||
this.isShowingDisplay = isShowingDisplay;
|
||
|
||
if (!HaveSameSubmodule)
|
||
{
|
||
this.track.trackPathSubmodule = this;
|
||
}
|
||
|
||
|
||
this.container = track.AddComponent<UnitySplineContainer>();
|
||
|
||
}
|
||
}
|
||
|
||
public partial class TrackPathSubmodule
|
||
{
|
||
private void SetUpSplineComputer(Track.TrackSpaceType trackSpaceType, Track.TrackSamplingType trackSamplingType)
|
||
{
|
||
path.type = (Spline.Type)trackSpaceType;
|
||
path.sampleMode = (SplineComputer.SampleMode)(int)trackSamplingType;
|
||
path.space = SplineComputer.Space.Local;
|
||
}
|
||
|
||
public void ClosePath()
|
||
{
|
||
if (isClosed)
|
||
{
|
||
path.Close();
|
||
}
|
||
else
|
||
{
|
||
path.Break();
|
||
}
|
||
}
|
||
|
||
public void SetTrackSpaceType(int spaceType)
|
||
{
|
||
int SpaceType = spaceType;
|
||
if (spaceType == 2) SpaceType++;
|
||
trackSpaceType = (Track.TrackSpaceType)SpaceType;
|
||
path.type = (Spline.Type)SpaceType;
|
||
}
|
||
|
||
public void SetPathNode(PathNode point)
|
||
{
|
||
path.SetPoint(point.index, point.node, SplineComputer.Space.Local);
|
||
}
|
||
|
||
public override void Refresh()
|
||
{
|
||
if(refreshedThisFrame) return;
|
||
refreshedThisFrame = true;
|
||
|
||
SetTrackSpaceType((int)trackSpaceType);
|
||
SetUpSplineComputer(trackSpaceType, trackSamplingType);
|
||
foreach (var pathNode in pathNodeList)
|
||
{
|
||
SetPathNode(pathNode);
|
||
}
|
||
ClosePath();
|
||
path.RebuildImmediate(true, true);
|
||
}
|
||
}
|
||
|
||
public partial class TrackPathSubmodule
|
||
{
|
||
public override void SaveBM()
|
||
{
|
||
matchedBM = new TrackPathSubmodule_BM(attachedGameElement, this);
|
||
}
|
||
}
|
||
|
||
public partial class TrackPathSubmodule
|
||
{
|
||
public UnitySplineContainer container;
|
||
public bool isSplineDirty = false; // 这个标记可以在外部被设置为 true 来触发表现层更新
|
||
|
||
public void GenerateCatmullRomSpline()
|
||
{
|
||
UnitySpline spline = new UnitySpline();
|
||
int count = pathNodeList.Count;
|
||
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
//使用Auto Knot模式,Unity会自动计算切线以实现Catmull-Rom样条的特性
|
||
BezierKnot knot = new BezierKnot();
|
||
knot.Position = pathNodeList[i].transformSubmodule.currentPosition;
|
||
knot.Rotation = quaternion.identity; //初始旋转,后续可以根据需要
|
||
spline.Add(knot);
|
||
}
|
||
|
||
//必须设置为Auto Knot模式,让Unity自动计算切线以实现Catmull-Rom样条的特性
|
||
spline.SetTangentMode(TangentMode.AutoSmooth);
|
||
container.Splines = new UnitySpline[] { spline };
|
||
}
|
||
|
||
public void GenerateLinearSpline()
|
||
{
|
||
UnitySpline spline = new UnitySpline();
|
||
int count = pathNodeList.Count;
|
||
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
BezierKnot knot = new BezierKnot();
|
||
knot.Position = pathNodeList[i].transformSubmodule.currentPosition;
|
||
knot.Rotation = quaternion.identity; //初始旋转,后续可以根据需要
|
||
spline.Add(knot);
|
||
}
|
||
|
||
//必须设置为Linear模式,让Unity不计算切线,保持线性插值
|
||
spline.SetTangentMode(TangentMode.Linear);
|
||
container.Splines = new UnitySpline[] { spline };
|
||
}
|
||
|
||
[Button]
|
||
public void GenerateBSpline()
|
||
{
|
||
//if(pathNodeList.Count < 3) return;
|
||
|
||
UnitySpline spline = new UnitySpline();
|
||
int count = pathNodeList.Count;
|
||
|
||
for (int i = 0; i < count; i++)
|
||
{
|
||
spline.Add(CalculateBSplineKnot(i));
|
||
}
|
||
|
||
// 必须设置为 Broken 模式,因为我们已经手动用数学算出了完美平滑的 Tangent,不需要 Unity 再去插手
|
||
spline.SetTangentMode(TangentMode.Broken);
|
||
container.Splines = new UnitySpline[] { spline };
|
||
}
|
||
|
||
private BezierKnot CalculateBSplineKnot(int i)
|
||
{
|
||
UnitySpline spline = new UnitySpline();
|
||
int count = pathNodeList.Count;
|
||
|
||
Vector3 pPrev, pCurr, pNext;
|
||
pCurr = pathNodeList[i].transformSubmodule.currentPosition;
|
||
|
||
// 1. 获取前后相邻点 (处理封闭与开放的拓扑逻辑)
|
||
if (isClosed)
|
||
{
|
||
// 使用取模运算,让索引在首尾循环缠绕 (加 count 是为了防止负数)
|
||
pPrev = pathNodeList[(i - 1 + count) % count].transformSubmodule.currentPosition;
|
||
pNext = pathNodeList[(i + 1) % count].transformSubmodule.currentPosition;
|
||
}
|
||
else
|
||
{
|
||
// 开放状态下,首尾节点重复使用端点坐标
|
||
pPrev = pathNodeList[Mathf.Max(0, i - 1)].transformSubmodule.currentPosition;
|
||
pNext = pathNodeList[Mathf.Min(count - 1, i + 1)].transformSubmodule.currentPosition;
|
||
}
|
||
|
||
BezierKnot knot = new BezierKnot();
|
||
|
||
// 2. 计算 B-Spline 等价的 Bezier 节点位置与切线
|
||
if (!isClosed && (i == 0 || i == count - 1))
|
||
{
|
||
// 开放曲线的硬性端点锚定
|
||
knot.Position = pCurr;
|
||
if (i == 0)
|
||
{
|
||
knot.TangentIn = float3.zero;
|
||
knot.TangentOut = (pNext - pCurr) / 3f;
|
||
}
|
||
else
|
||
{
|
||
knot.TangentIn = (pPrev - pCurr) / 3f;
|
||
knot.TangentOut = float3.zero;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 标准 1/6 B-Spline 平滑换算公式 (封闭曲线全程走这里)
|
||
knot.Position = (pPrev + 4f * pCurr + pNext) / 6f;
|
||
knot.TangentOut = (pNext - pPrev) / 6f;
|
||
knot.TangentIn = (pPrev - pNext) / 6f;
|
||
}
|
||
|
||
knot.Rotation = Quaternion.Euler(pathNodeList[i].transformSubmodule.currentEulerAngles); // 直接使用节点的欧拉角作为旋转,保持与节点编辑器一致
|
||
return knot;
|
||
}
|
||
|
||
public void UpdateSplineFromPathNode(int index)
|
||
{
|
||
if (container == null || container.Splines.Count == 0) return;
|
||
UnitySpline spline = new UnitySpline();
|
||
int count = pathNodeList.Count;
|
||
|
||
if (index < 0 || index >= spline.Count)
|
||
{
|
||
Debug.LogError($"节点索引 {index} 越界!");
|
||
return;
|
||
}
|
||
|
||
if (trackSpaceType is Track.TrackSpaceType.Linear or Track.TrackSpaceType.CatmullRom)
|
||
{
|
||
BezierKnot knot = spline[index];
|
||
Vector3 newPosition = pathNodeList[index].transformSubmodule.currentPosition;
|
||
|
||
// 1. 提取当前节点 (使用 spline[index] 读取可以保留该节点原本的切线数据)
|
||
// 这一步对于 Catmull-Rom 非常重要,因为它的切线是由 Unity 自动维护的
|
||
// 转换到本地坐标系 (如果传入的是世界坐标)
|
||
knot.Position = container.transform.InverseTransformPoint(newPosition);
|
||
|
||
// 2. 更新旋转 (如果需要的话)
|
||
Quaternion newRotation = Quaternion.Euler(pathNodeList[index].transformSubmodule.currentEulerAngles);
|
||
knot.Rotation = newRotation; // 直接使用节点的欧拉角作为旋转,保持与节点编辑器一致
|
||
|
||
// 3. 将修改后的 Knot 写回 Spline
|
||
// 此时 Unity 底层会自动触发它自己的 Dirty 标记
|
||
spline.SetKnot(index, knot);
|
||
}
|
||
else if (trackSpaceType == Track.TrackSpaceType.BSpline)
|
||
{
|
||
if (isClosed)
|
||
{
|
||
// 封闭曲线使用取模处理环绕越界
|
||
int i0 = (index - 1 + count) % count;
|
||
int i1 = (index + 1) % count;
|
||
spline.SetKnot(i0, CalculateBSplineKnot(i0));
|
||
spline.SetKnot(index, CalculateBSplineKnot(index));
|
||
spline.SetKnot(i1, CalculateBSplineKnot(i1));
|
||
}
|
||
else
|
||
{
|
||
// 开放曲线需进行安全边界检查
|
||
if (index - 1 >= 0) spline.SetKnot(index - 1, CalculateBSplineKnot(index - 1));
|
||
spline.SetKnot(index, CalculateBSplineKnot(index));
|
||
if (index + 1 < count) spline.SetKnot(index + 1, CalculateBSplineKnot(index + 1));
|
||
}
|
||
}
|
||
|
||
// 4. 触发我们自己的表现层脏标记
|
||
isSplineDirty = true;
|
||
}
|
||
|
||
public void UpdatePoint(Transform pointTransform, float progress)
|
||
{
|
||
UnitySpline spline = container.Splines[0];
|
||
progress = isClosed ? Mathf.Repeat(progress, 1f) : Mathf.Clamp01(progress);
|
||
float mathT = progress;
|
||
if (trackSamplingType == Track.TrackSamplingType.DistanceDistributed)
|
||
{
|
||
float targetDistance = progress * spline.GetLength();
|
||
mathT = spline.ConvertIndexUnit(targetDistance, PathIndexUnit.Distance, PathIndexUnit.Normalized);
|
||
}
|
||
container.Evaluate(0, mathT, out float3 pos, out float3 tangent, out float3 upVector);
|
||
pointTransform.position = pos;
|
||
|
||
if (math.lengthsq(tangent) > 0)
|
||
{
|
||
Vector3 worldTangent = container.transform.TransformDirection(tangent);
|
||
Vector3 worldUp = container.transform.TransformDirection(upVector);
|
||
pointTransform.rotation = quaternion.LookRotationSafe(worldTangent, worldUp);
|
||
}
|
||
}
|
||
}
|
||
|
||
namespace Beatmap
|
||
{
|
||
public class TrackPathSubmodule_BM : Submodule_BM
|
||
{
|
||
public Track.TrackSpaceType trackSpaceType;
|
||
public Track.TrackSamplingType trackSamplingType;
|
||
public bool isClosed;
|
||
public bool isShowingDisplay;
|
||
|
||
public TrackPathSubmodule_BM()
|
||
{
|
||
|
||
}
|
||
|
||
|
||
public TrackPathSubmodule_BM(GameElement attachedElement, TrackPathSubmodule trackPathSubmodule) : base(
|
||
attachedElement)
|
||
{
|
||
this.trackSpaceType = trackPathSubmodule.trackSpaceType;
|
||
this.trackSamplingType = trackPathSubmodule.trackSamplingType;
|
||
this.isClosed = trackPathSubmodule.isClosed;
|
||
this.isShowingDisplay = trackPathSubmodule.isShowingDisplay;
|
||
}
|
||
|
||
public override void ExecuteBM()
|
||
{
|
||
attachedElement = GameElement_BM.GetElement(attachedElementGuid);
|
||
Track track = attachedElement as Track;
|
||
track.trackPathSubmodule = new TrackPathSubmodule(track, trackSpaceType, trackSamplingType, isClosed, isShowingDisplay);
|
||
}
|
||
}
|
||
|
||
}
|
||
} |