Files
ichni_Official/Assets/Scripts/Game/GameElements/Track/TrackSubmodules/TrackPathSubmodule.cs
SoulliesOfficial a635ec4221 GPU优化
2026-02-27 08:21:00 -05:00

342 lines
13 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;
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);
}
}
}
}