Files
aibis-dream/Assets/Scripts/FixSystemNew/BodyModule/CutLine.cs
T
2025-05-02 16:06:06 +08:00

311 lines
10 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace AibisDream
{
public class CutLine : MonoBehaviour
{
#region Shader参数
private static readonly int RangeCount = Shader.PropertyToID("_RangeCount");
#endregion
private LineRenderer _lineRenderer;
private Material _lineMaterial;
private Vector2[] _linePoints;
private float[] _normalizedLinePoints;
private float _totalLength;
private Bounds _bounds;
private List<Vector2> _recordedRanges = new();
private static readonly int Ranges = Shader.PropertyToID("_Ranges");
private static readonly int TotalLength = Shader.PropertyToID("_TotalLength");
public event Action OnCuttingDown;
#region 初始化
public void Init(List<Vector3> shapePoints, float clickRadius)
{
if (!_lineRenderer)
{
_lineRenderer = GetComponent<LineRenderer>();
_lineMaterial = _lineRenderer.materials[0];
}
_lineRenderer.gameObject.SetActive(true);
// 清除旧数据
ClearOldData();
// 生成归一化点
NormalizePoints(shapePoints);
// 按照新点位重绘线条
RedrawLine(shapePoints);
// 生成基本的包围盒
GeneBoundingBox(clickRadius);
ClearRecordedRanges();
}
private void ClearOldData()
{
_recordedRanges.Clear();
_totalLength = 0;
}
private void RedrawLine(List<Vector3> shapePoints)
{
// 画线
var linePoints = shapePoints.ToArray();
_lineRenderer.positionCount = linePoints.Length;
_lineRenderer.SetPositions(linePoints);
// 设置Shader
_lineMaterial.SetFloat(TotalLength, _totalLength);
_lineMaterial.SetInt(RangeCount, 1);
_lineMaterial.SetVectorArray(Ranges, new List<Vector4> { Vector4.zero });
}
private void UpdateMaterial()
{
if (_recordedRanges.Count <= 0)
{
return;
}
// 将Range组装成Shader可以读取的格式
var ranges = new List<Vector4>();
for (int i = 0; i < _recordedRanges.Count; i += 2)
{
var v1 = _recordedRanges[i];
var v2 = i + 1 < _recordedRanges.Count ? _recordedRanges[i + 1] : Vector2.zero;
var combined = new Vector4(v1.x, v1.y, v2.x, v2.y);
ranges.Add(combined);
}
// 更新线条
_lineMaterial.SetInt(RangeCount, ranges.Count);
_lineMaterial.SetVectorArray(Ranges, ranges);
}
private void NormalizePoints(List<Vector3> shapePoints)
{
// 先生成首尾相连的点,默认Loop
shapePoints.Add(shapePoints[0]);
_linePoints = shapePoints
.Select(item => new Vector2(item.x, item.y))
.ToArray();
var segmentLengths = new List<float>();
// 计算总长度和各段长度
for (var i = 1; i < _linePoints.Length; i++)
{
var segmentLength = Vector2.Distance(_linePoints[i - 1], _linePoints[i]);
segmentLengths.Add(segmentLength);
_totalLength += segmentLength;
}
// 计算每个点的归一化位置
var normalizedLinePoints = new float[segmentLengths.Count + 1];
normalizedLinePoints[0] = 0f;
var accumulatedLength = 0f;
for (var i = 0; i < segmentLengths.Count; i++)
{
accumulatedLength += segmentLengths[i];
normalizedLinePoints[i + 1] = accumulatedLength / _totalLength;
}
_normalizedLinePoints = normalizedLinePoints;
}
private void GeneBoundingBox(float clickRadius)
{
_bounds = new Bounds();
foreach (var point in _linePoints)
{
_bounds.Encapsulate(point);
}
_bounds.Expand(clickRadius * 2);
}
#endregion
#region 切割检测
public void DetectLineClick(Vector2 clickPosition, float clickRadius)
{
// 先进行包围盒检测,不在包围盒直接返回
if (!_bounds.Contains(clickPosition))
{
return;
}
List<Vector2> newRanges = new List<Vector2>();
// 检查每条线段
for (int i = 1; i < _linePoints.Length; i++)
{
Vector2 start = _linePoints[i - 1];
Vector2 end = _linePoints[i];
// 计算点覆盖线段的范围
if (!TryGetCoverRange(start, end, clickPosition, clickRadius, out var rawRange))
{
// 如果未覆盖直接计算下一段
continue;
}
// 将线段范围转成整体范围
var normalizedStart = _normalizedLinePoints[i - 1];
var normalizedEnd = _normalizedLinePoints[i];
var normalizedDistance = normalizedEnd - normalizedStart;
var range = rawRange * normalizedDistance + new Vector2(normalizedStart, normalizedStart);
newRanges.Add(range);
}
// 合并新检测到的区间
foreach (Vector2 range in newRanges)
{
_recordedRanges.Add(range);
}
// 合并所有重叠区间
MergeRanges();
// 判断当前切割有没有割到95%
if (CalcTotalRange() >= 0.95)
{
Debug.Log("cutting All");
OnCuttingDown?.Invoke();
}
// 打印当前所有区间(调试用)
Debug.Log($"rangesCount:{_recordedRanges.Count}");
string log = "Ranges: ";
foreach (Vector2 range in _recordedRanges)
{
log += $"{range.x:F2}-{range.y:F2}; ";
}
Debug.Log(log);
UpdateMaterial();
}
private bool TryGetCoverRange(Vector2 lineStart, Vector2 lineEnd, Vector2 point, float radius,
out Vector2 rawRange)
{
// 计算线段方向向量和长度
Vector2 lineDir = lineEnd - lineStart;
float lineLength = lineDir.magnitude;
// 如果线段长度为零(两个端点重合)
if (Mathf.Approximately(lineLength, 0f))
{
// 检查点是否覆盖了这个点
var distance = Vector2.Distance(lineStart, point);
rawRange = distance <= radius ? new Vector2(0f, 1f) : default;
return distance <= radius;
}
// 计算点到线段所在直线距离
Vector2 lineDirNormalized = lineDir / lineLength; // 归一化方向向量
Vector2 pointToStart = point - lineStart; // 计算点到线段起点的向量
float projection = Vector2.Dot(pointToStart, lineDirNormalized); // 计算投影长度(点在线段上的投影位置)
float closestT = Mathf.Clamp01(projection / lineLength); // 计算实际最近点在线段上的比例位置
Vector2 closestPoint = lineStart + closestT * lineDir; // 计算最近点坐标
float distanceToLine = Vector2.Distance(point, closestPoint); // 计算点到最近点的距离
// 如果距离大于半径,完全没有覆盖
if (distanceToLine > radius)
{
rawRange = default;
return false;
}
// 计算覆盖范围
float halfCoveredLength =
Mathf.Sqrt(radius * radius - distanceToLine * distanceToLine); // 计算实际覆盖的线段长度(勾股定理)
float startCoverage = (projection - halfCoveredLength) / lineLength;
float endCoverage = (projection + halfCoveredLength) / lineLength; // 计算覆盖的起点和终点比例
startCoverage = Mathf.Clamp01(startCoverage);
endCoverage = Mathf.Clamp01(endCoverage); // 限制在0-1范围内
// 如果起点和终点相同,表示只覆盖了一个点
if (Mathf.Approximately(startCoverage, endCoverage))
{
rawRange = default;
return false;
}
rawRange = new Vector2(startCoverage, endCoverage);
return true;
}
private void MergeRanges()
{
if (_recordedRanges.Count <= 1) return;
// 先按起始位置排序
_recordedRanges.Sort((a, b) => a.x.CompareTo(b.x));
List<Vector2> mergedRanges = new List<Vector2>();
Vector2 currentRange = _recordedRanges[0];
for (int i = 1; i < _recordedRanges.Count; i++)
{
Vector2 nextRange = _recordedRanges[i];
// 检查是否有重叠或相邻
if (nextRange.x - currentRange.y <= 0.05f)
{
// 合并区间
currentRange.y = Mathf.Max(currentRange.y, nextRange.y);
}
else
{
mergedRanges.Add(currentRange);
currentRange = nextRange;
}
}
mergedRanges.Add(currentRange);
_recordedRanges = mergedRanges;
}
private float CalcTotalRange()
{
return _recordedRanges.Select(item => item.y - item.x).Sum();
}
#endregion
public void HideLine()
{
gameObject.SetActive(false);
}
// 获取当前所有记录的范围(0-1之间的值)
public List<Vector2> GetRecordedRanges()
{
return new List<Vector2>(_recordedRanges);
}
// 清除所有记录的范围
public void ClearRecordedRanges()
{
_recordedRanges.Clear();
}
}
}