Project_WL/Assets/WL/Combat/BladeTrail.cs

541 lines
27 KiB
C#

// ─────────────────────────────────────────────────────────────────────────────
// BladeTrail.cs — 검 손잡이·검끝 두 점을 잇는 월드 공간 리본 메시
//
// PD 지시 #747 (2026-09-05) 최초 이식 → **PD 지시 #785 (2026-09-06) 전면 개편**
//
// 왜 리본인가 (구 WL #718 교훈 · 그대로 유효):
// ① 궤적 이펙트를 **무기 본의 자식으로 붙이면 안 된다** — 검과 함께 회전해
// 궤적 위에 놓이지 않는다. 그래서 메시는 부모 없는 월드 공간에 굽는다.
// ② **강체 호(크레센트) 프리팹으로 임의 스윙을 맞출 수 없다** — 스폰 순간의
// 한 프레임 속도로 평면을 추정하는데, 검끝은 33ms 에 2.56m 까지 움직여
// 그 추정이 실제 호와 크게 어긋난다(구 WL PM 실측 · #761/#777 에서 재확인).
// ③ 정답 = 매 프레임 손잡이·검끝 2점을 모아 그 사이를 리본으로 잇는다.
// 검이 실제로 지나간 자리를 그대로 그리므로 "공격 방향에 따라 궤적이 어긋나는"
// 문제가 원리적으로 생기지 않는다(#785 지시의 핵심).
//
// ── #785 개편점 (왜 바꿨는가) ────────────────────────────────────────────────
// ⓐ **나이 기반 롤링 버퍼**. 이전 구조는 Begin() 이 버퍼를 통째로 비웠다.
// 3콤보처럼 스윙이 연달아 들어오면 1타 리본이 페이드 도중 잘려 사라진다.
// 이제 샘플마다 기록 시각을 들고, 수명(fadeSeconds)이 지난 것부터 앞에서
// 빠져나간다. Begin() 은 "여기서부터 새 띠" 라는 **끊김 표시**만 남긴다.
// → 메시는 여전히 **1개 · 드로우 1회**인데 앞 스윙의 잔상이 자연스럽게 녹는다.
// ⓑ **그라디언트 2축**. 참고 이미지의 색 배치(안쪽 주황/노랑 → 바깥·꼬리 파랑/보라)를
// Gradient 필드 2개(잔상축 u · 칼날축 v)로 굽는다. 색은 전부 CPU 정점색이라
// 셰이더 분기가 0 이고 텍스처도 필요 없다.
// ⓒ **서브스텝 보간**. 한 프레임에 검끝이 크게 움직이면 리본이 각져 보인다.
// 직전 샘플과의 거리로 중간 샘플을 최대 maxSubSteps 개 끼운다. 손잡이는 선형,
// 칼날 벡터는 **Slerp** 라 직선이 아니라 실제 호를 복원한다.
// ⓓ **폭 = 손잡이~검끝 전체**(widthScale 1). PD 지시 "검 끝~손잡이 폭".
//
// 🔴 어셈블리 주의: Assets/WL/Combat/ 에 .asmdef 를 만들지 말 것
// (Actor/PCActor 가 Assembly-CSharp 에 있다).
// ─────────────────────────────────────────────────────────────────────────────
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
namespace WL.Combat
{
/// <summary>
/// 스윙 동안 모은 (손잡이, 검끝) 쌍으로 월드 공간 리본 메시를 굽는다.
/// 자기 자신이 부모 없는 월드 오브젝트다 — 무기를 따라 끌려다니지 않는다.
/// 생성은 <see cref="Create"/> 로만 한다.
/// </summary>
// 실행 순서를 뒤로 밀어 **WeaponTrailDriver.LateUpdate(샘플 수집)가 끝난 뒤** 굽는다.
// 두 LateUpdate 의 순서는 기본값(0)끼리는 보장되지 않아, 반대로 돌면 리본 머리가
// 한 프레임 뒤처져 칼날과 벌어져 보인다(#777 에서 PD 가 지적한 그 증상과 같은 계열).
[DefaultExecutionOrder(1000)]
[DisallowMultipleComponent]
[RequireComponent(typeof(MeshFilter))]
[RequireComponent(typeof(MeshRenderer))]
public sealed class BladeTrail : MonoBehaviour
{
private SlashTrailSettings _settings;
private Mesh _mesh;
private MeshFilter _filter;
private MeshRenderer _renderer;
private MaterialPropertyBlock _mpb;
// ── 샘플 링 (나이 기반) ──────────────────────────────────────────────────
private Vector3[] _hilt;
private Vector3[] _tip;
private float[] _stamp; // 기록 시각(Time.time). 나이 = now - stamp
private bool[] _newStrip; // true = 이 샘플부터 새 띠(앞 샘플과 잇지 않는다)
private int _count;
private bool _capturing;
private bool _pendingBreak; // 다음 Sample 을 새 띠의 첫 점으로 찍는다
private int _layerCount = 1;
// 겹별 색(그라디언트를 끈 경우에만 쓰는 단색 경로).
private Color _colorAdditive = Color.white;
private Color _colorAlpha = Color.white;
private float _widthScale = 1f;
// 메시 버퍼 — 매 프레임 새로 할당하지 않는다.
private Vector3[] _verts;
private Vector3[] _norms;
private Vector2[] _uv0;
private Color[] _cols;
private int[] _tris;
private int[] _triCount; // 겹별 실제 인덱스 수(끊김 때문에 최대치보다 작다)
private readonly List<Vector4> _uv1 = new List<Vector4>();
private readonly List<Vector4> _uv2 = new List<Vector4>();
// ── 검증용 공개 프로퍼티 (PM CSV 샘플러가 읽는다) ──────────────────────
/// <summary>지금 궤적을 모으는 중인가.</summary>
public bool IsCapturing { get { return _capturing; } }
/// <summary>살아 있는 샘플 수.</summary>
public int SampleCount { get { return _count; } }
/// <summary>트레일 메시가 실제로 보이는가.</summary>
public bool IsVisible { get { return _renderer != null && _renderer.enabled; } }
/// <summary>i 번째 검끝 샘플(월드).</summary>
public Vector3 GetTip(int i) { return (i >= 0 && i < _count) ? _tip[i] : Vector3.zero; }
/// <summary>i 번째 손잡이 샘플(월드).</summary>
public Vector3 GetHilt(int i) { return (i >= 0 && i < _count) ? _hilt[i] : Vector3.zero; }
/// <summary>i 번째 샘플의 나이(초).</summary>
public float GetAge(int i) { return (i >= 0 && i < _count) ? (Time.time - _stamp[i]) : -1f; }
/// <summary>마지막 검끝 샘플(월드). 샘플이 없으면 Vector3.zero.</summary>
public Vector3 LastTip { get { return _count > 0 ? _tip[_count - 1] : Vector3.zero; } }
/// <summary>마지막 손잡이 샘플(월드). 샘플이 없으면 Vector3.zero.</summary>
public Vector3 LastHilt { get { return _count > 0 ? _hilt[_count - 1] : Vector3.zero; } }
/// <summary>이번 프레임에 그린 삼각형 수(예산 검증용).</summary>
public int TriangleCount { get; private set; }
/// <summary>서브스텝 보간으로 끼워 넣은 누적 샘플 수(검증용).</summary>
public int InterpolatedSamples { get; private set; }
/// <summary>
/// 월드 공간 트레일 오브젝트를 새로 만든다.
/// 부모를 두지 않는다 — 무기 본에 붙이면 궤적 위에 놓이지 않는다(구 WL #718 ①).
/// </summary>
public static BladeTrail Create(SlashTrailSettings settings, string objectName)
{
var go = new GameObject(string.IsNullOrEmpty(objectName) ? "BladeTrail" : objectName);
go.transform.SetParent(null, false);
go.hideFlags = HideFlags.DontSave;
var trail = go.AddComponent<BladeTrail>();
trail.Configure(settings);
return trail;
}
/// <summary>설정을 주입하고 버퍼·머티리얼을 준비한다. 색/폭 오버라이드는 <see cref="SetLook"/> 로.</summary>
public void Configure(SlashTrailSettings settings)
{
_settings = settings;
_filter = GetComponent<MeshFilter>();
_renderer = GetComponent<MeshRenderer>();
int cap = _settings != null ? Mathf.Clamp(_settings.maxSamples, 4, 256) : 64;
if (_hilt == null || _hilt.Length != cap)
{
_hilt = new Vector3[cap];
_tip = new Vector3[cap];
_stamp = new float[cap];
_newStrip = new bool[cap];
_count = 0;
}
if (_mesh == null)
{
_mesh = new Mesh();
_mesh.name = "BladeTrailMesh";
_mesh.MarkDynamic();
_filter.sharedMesh = _mesh;
}
_renderer.shadowCastingMode = ShadowCastingMode.Off;
_renderer.receiveShadows = false;
_renderer.lightProbeUsage = LightProbeUsage.Off;
_renderer.reflectionProbeUsage = ReflectionProbeUsage.Off;
_renderer.motionVectorGenerationMode = MotionVectorGenerationMode.ForceNoMotion;
_renderer.allowOcclusionWhenDynamic = false;
_renderer.enabled = false;
// 겹 = 머티리얼 수. #785 기본은 리본 전용 셰이더 1장 = 드로우 1회.
var mats = new List<Material>(2);
if (_settings != null && _settings.materialAdditive != null) mats.Add(_settings.materialAdditive);
if (_settings != null && _settings.materialAlpha != null) mats.Add(_settings.materialAlpha);
if (mats.Count > 0) _renderer.sharedMaterials = mats.ToArray();
_layerCount = Mathf.Max(mats.Count, 1);
// 정점 버퍼는 **최대 크기로 1회만** 잡는다. 샘플이 늘 때마다 다시 잡으면
// 스윙 중 매 프레임 GC 할당이 생긴다(판정 기준 = 프레임당 alloc 0).
int maxVerts = cap * 2 * _layerCount;
int maxTris = (cap - 1) * 6 * _layerCount;
if (_verts == null || _verts.Length != maxVerts)
{
_verts = new Vector3[maxVerts];
_norms = new Vector3[maxVerts];
_uv0 = new Vector2[maxVerts];
_cols = new Color[maxVerts];
}
if (_tris == null || _tris.Length != maxTris) _tris = new int[maxTris];
if (_triCount == null || _triCount.Length != _layerCount) _triCount = new int[_layerCount];
_uv1.Capacity = maxVerts;
_uv2.Capacity = maxVerts;
if (_settings != null)
{
_colorAdditive = _settings.colorAdditive;
_colorAlpha = _settings.colorAlpha;
_widthScale = _settings.widthScale;
}
}
/// <summary>클래스 오버라이드로 색·폭을 바꾼다(SlashTrailClassOverride.overrideLook).</summary>
public void SetLook(Color additive, Color alpha, float widthScale)
{
_colorAdditive = additive;
_colorAlpha = alpha;
_widthScale = Mathf.Clamp01(widthScale);
}
/// <summary>연결된 머티리얼이 하나라도 있는가. 없으면 그려도 보이지 않는다.</summary>
public bool HasMaterial
{
get
{
return _settings != null &&
(_settings.materialAdditive != null || _settings.materialAlpha != null);
}
}
private void OnDestroy()
{
if (_mesh != null)
{
if (Application.isPlaying) Destroy(_mesh);
else DestroyImmediate(_mesh);
_mesh = null;
}
}
// ── 수집 제어 ────────────────────────────────────────────────────────────
/// <summary>
/// 스윙 시작. #785 이후로 **버퍼를 비우지 않는다** — 앞 스윙의 잔상이 수명대로
/// 계속 녹도록 두고, 다음 샘플에 "새 띠 시작" 표시만 남긴다(콤보 연타 대응).
/// </summary>
public void Begin()
{
_capturing = true;
_pendingBreak = true;
InterpolatedSamples = 0;
}
/// <summary>스윙 끝 — 수집만 멈춘다. 남은 리본은 샘플 나이대로 알아서 사라진다.</summary>
public void End()
{
_capturing = false;
_pendingBreak = false;
}
/// <summary>즉시 지운다(콤보 취소·피격·사망).</summary>
public void Clear()
{
_capturing = false;
_pendingBreak = false;
_count = 0;
TriangleCount = 0;
if (_renderer != null) _renderer.enabled = false;
}
/// <summary>샘플 수명(초). 이 시간이 지난 샘플은 리본에서 빠진다.</summary>
private float LifeSeconds
{
get { return _settings != null ? Mathf.Max(_settings.fadeSeconds, 0.02f) : 0.2f; }
}
/// <summary>
/// 한 프레임의 두 점을 보고한다. 반드시 LateUpdate(애니메이션 적용 후)에서 부른다.
/// 직전 샘플과 검끝 거리가 minSampleDistance 미만이면 버린다 — 정지 중 정점이
/// 겹쳐 면이 뒤집히는 것을 막는다. 거리가 크면 서브스텝을 끼워 호를 복원한다.
/// </summary>
public void Sample(Vector3 hilt, Vector3 tip)
{
if (!_capturing || _hilt == null) return;
float now = Time.time;
if (_count > 0 && !_pendingBreak)
{
float minDist = _settings != null ? Mathf.Max(_settings.minSampleDistance, 0f) : 0.004f;
if (minDist > 0f && (tip - _tip[_count - 1]).sqrMagnitude < minDist * minDist)
return;
// ── 서브스텝 보간 ────────────────────────────────────────────────
// 손잡이는 선형, 칼날 벡터는 Slerp — 직선 보간이면 호가 다각형이 된다.
float maxSeg = _settings != null ? _settings.maxSegmentDistance : 0f;
int maxSub = _settings != null ? _settings.maxSubSteps : 0;
if (maxSeg > 0f && maxSub > 0)
{
Vector3 pHilt = _hilt[_count - 1];
Vector3 pTip = _tip[_count - 1];
float pStamp = _stamp[_count - 1];
float moved = (tip - pTip).magnitude;
int extra = Mathf.Min(Mathf.CeilToInt(moved / maxSeg) - 1, maxSub);
if (extra > 0)
{
Vector3 dPrev = pTip - pHilt;
Vector3 dNow = tip - hilt;
for (int k = 1; k <= extra; k++)
{
float t = (float)k / (extra + 1);
Vector3 h = Vector3.Lerp(pHilt, hilt, t);
Vector3 d = Vector3.Slerp(dPrev, dNow, t);
Push(h, h + d, Mathf.Lerp(pStamp, now, t), false);
InterpolatedSamples++;
}
}
}
}
Push(hilt, tip, now, _pendingBreak);
_pendingBreak = false;
// 메시는 여기서 굽지 않는다 — LateUpdate(실행순서 1000)에서 프레임당 1회만 굽는다.
}
/// <summary>샘플 1개를 링에 밀어 넣는다. 가득 차면 가장 오래된 것을 버린다.</summary>
private void Push(Vector3 hilt, Vector3 tip, float stamp, bool newStrip)
{
if (_count >= _hilt.Length) DropOldest(1);
_hilt[_count] = hilt;
_tip[_count] = tip;
_stamp[_count] = stamp;
_newStrip[_count] = newStrip || _count == 0;
_count++;
}
/// <summary>앞에서 n 개를 버린다. 새 선두는 자동으로 띠의 시작이 된다.</summary>
private void DropOldest(int n)
{
if (n <= 0) return;
if (n >= _count) { _count = 0; return; }
int keep = _count - n;
System.Array.Copy(_hilt, n, _hilt, 0, keep);
System.Array.Copy(_tip, n, _tip, 0, keep);
System.Array.Copy(_stamp, n, _stamp, 0, keep);
System.Array.Copy(_newStrip, n, _newStrip, 0, keep);
_count = keep;
if (_count > 0) _newStrip[0] = true;
}
private void LateUpdate()
{
if (_count == 0) return;
// 수명이 다한 샘플을 앞에서 걷어낸다 — 꼬리부터 자연스럽게 녹는다.
float life = LifeSeconds;
float now = Time.time;
int expired = 0;
while (expired < _count && (now - _stamp[expired]) > life) expired++;
if (expired > 0) DropOldest(expired);
Rebuild();
}
// ── 메시 생성 ────────────────────────────────────────────────────────────
/// <summary>
/// 모아 둔 점들로 리본 메시를 다시 굽는다.
/// 겹(서브메시)마다 정점 세트를 따로 만든다 — 겹마다 색·UV 가 다르기 때문.
/// </summary>
private void Rebuild()
{
if (_renderer == null || _mesh == null) return;
// 리본 OFF 스위치(C8 롤백). 궤적 샘플링은 계속하고 그리기만 멈춘다 —
// PrefabArc 레거시 경로가 이 샘플로 평면을 구하므로 수집 자체를 끄면 안 된다.
if (_settings != null && !_settings.drawRibbon) { _renderer.enabled = false; TriangleCount = 0; return; }
if (_count < 2) { _renderer.enabled = false; TriangleCount = 0; return; }
bool useGrad = _settings == null || _settings.useGradients;
float tailRatio = _settings != null ? Mathf.Clamp01(_settings.tailWidthRatio) : 1f;
float tipExtend = _settings != null ? Mathf.Clamp(_settings.tipExtendRatio, 0f, 0.5f) : 0f;
bool writeCustom = _settings != null && _settings.writeCustomDataChannels;
float hold = _settings != null ? Mathf.Clamp(_settings.fadeHoldRatio, 0f, 0.95f) : 0.35f;
float intensity = _settings != null ? Mathf.Max(_settings.colorIntensity, 0f) : 1f;
float bladeW = _settings != null ? Mathf.Clamp01(_settings.bladeGradientWeight) : 0.5f;
float life = LifeSeconds;
float now = Time.time;
// 칼날축 그라디언트는 v = 0 / 1 두 점에서만 쓰므로 프레임당 2회만 평가한다.
Color bladeInner = Color.white, bladeOuter = Color.white;
if (useGrad && _settings != null && _settings.gradientAlongBlade != null)
{
bladeInner = _settings.gradientAlongBlade.Evaluate(0f);
bladeOuter = _settings.gradientAlongBlade.Evaluate(1f);
}
int n = _count;
int layers = Mathf.Max(_layerCount, 1);
int perLayer = n * 2;
int totalVerts = perLayer * layers;
int maxTrisPerLayer = (n - 1) * 6;
// 버퍼는 Configure 에서 최대 크기로 한 번만 잡았다(스윙 중 GC alloc 0).
// 방어적으로 모자랄 때만 다시 잡는다 — 정상 경로에서는 절대 타지 않는다.
if (_verts == null || _verts.Length < totalVerts)
{
_verts = new Vector3[totalVerts];
_norms = new Vector3[totalVerts];
_uv0 = new Vector2[totalVerts];
_cols = new Color[totalVerts];
}
if (_tris == null || _tris.Length < maxTrisPerLayer * layers)
_tris = new int[maxTrisPerLayer * layers];
if (_triCount == null || _triCount.Length < layers) _triCount = new int[layers];
// 리본 평면의 법선 — 언릿이라 셰이딩엔 안 쓰지만 정점 스트림을 비워 두지 않는다.
Vector3 nrm = Vector3.Cross(_tip[n - 1] - _tip[0], _hilt[n / 2] - _tip[n / 2]);
if (nrm.sqrMagnitude < 1e-8f) nrm = Vector3.up; else nrm.Normalize();
if (writeCustom) { _uv1.Clear(); _uv2.Clear(); }
for (int L = 0; L < layers; L++)
{
int baseV = L * perLayer;
bool isAlphaLayer = (L == 1);
bool fullUv = isAlphaLayer && (_settings == null || _settings.alphaLayerUvFull);
float vMin = fullUv ? 0f : (_settings != null ? _settings.uvVMin : 0f);
float vMax = fullUv ? 1f : (_settings != null ? _settings.uvVMax : 1f);
Color flat = isAlphaLayer ? _colorAlpha : _colorAdditive;
for (int i = 0; i < n; i++)
{
Vector3 h = _hilt[i];
Vector3 t = _tip[i];
// u = 잔상 수명. 1 = 방금(검의 현재 위치) · 0 = 소멸 직전.
float aged = Mathf.Clamp01((now - _stamp[i]) / life);
float u = 1f - aged;
// 나이 알파 — hold 구간까지는 원색, 그 뒤 선형 소멸.
float ageAlpha = aged <= hold ? 1f : 1f - (aged - hold) / (1f - hold);
// 꼬리로 갈수록 얇아진다(tailWidthRatio 1 이면 폭 일정 = PD 지시 기본값).
float w = _widthScale * Mathf.Lerp(tailRatio, 1f, u);
Vector3 inner = Vector3.Lerp(t, h, w);
Vector3 outer = t;
if (tipExtend > 0f) outer = t + (t - h) * tipExtend;
Color cIn, cOut;
if (useGrad)
{
// 잔상축(u) 색과 칼날축(v) 색을 bladeGradientWeight 로 섞는다.
// 곱이 아니라 Lerp 인 이유 — 두 색을 곱하면 채도가 죽고 어두워진다.
Color cU = (_settings != null && _settings.gradientAlongSwing != null)
? _settings.gradientAlongSwing.Evaluate(u) : Color.white;
cIn = Color.Lerp(cU, bladeInner, bladeW);
cOut = Color.Lerp(cU, bladeOuter, bladeW);
float baseA = cU.a * ageAlpha;
cIn.r *= intensity; cIn.g *= intensity; cIn.b *= intensity;
cOut.r *= intensity; cOut.g *= intensity; cOut.b *= intensity;
cIn.a = baseA * bladeInner.a; // 칼날축 알파로 안/바깥 세기를 따로 준다
cOut.a = baseA * bladeOuter.a;
}
else
{
cIn = cOut = new Color(flat.r, flat.g, flat.b, flat.a * ageAlpha);
}
int a0 = baseV + i * 2, b0 = a0 + 1;
_verts[a0] = inner; _verts[b0] = outer;
_norms[a0] = nrm; _norms[b0] = nrm;
// v = 0 → 안쪽(손잡이) · v = 1 → 칼끝(바깥). 셰이더가 양 끝을 페이드한다.
_uv0[a0] = new Vector2(u, vMin);
_uv0[b0] = new Vector2(u, vMax);
_cols[a0] = cIn; _cols[b0] = cOut;
if (writeCustom)
{
var col2 = new Vector4(cIn.r, cIn.g, cIn.b, cIn.a);
var col1 = new Vector4(aged, 0f, 0f, 0f);
_uv1.Add(col1); _uv1.Add(col1);
_uv2.Add(col2); _uv2.Add(col2);
}
}
// 삼각형 — 끊김(새 띠 시작) 구간은 잇지 않는다.
int o = L * maxTrisPerLayer;
int written = 0;
for (int i = 0; i < n - 1; i++)
{
if (_newStrip[i + 1]) continue;
int v = baseV + i * 2;
_tris[o + written] = v; _tris[o + written + 1] = v + 1; _tris[o + written + 2] = v + 2;
_tris[o + written + 3] = v + 1; _tris[o + written + 4] = v + 3; _tris[o + written + 5] = v + 2;
written += 6;
}
_triCount[L] = written;
}
int totalIndices = 0;
for (int L = 0; L < layers; L++) totalIndices += _triCount[L];
TriangleCount = totalIndices / 3;
if (totalIndices == 0) { _renderer.enabled = false; return; }
_mesh.Clear();
// 배열 전체가 아니라 **이번 프레임에 실제로 쓴 구간만** 넘긴다(버퍼 재할당 없음 = GC alloc 0).
_mesh.SetVertices(_verts, 0, totalVerts);
_mesh.SetNormals(_norms, 0, totalVerts);
_mesh.SetUVs(0, _uv0, 0, totalVerts);
_mesh.SetColors(_cols, 0, totalVerts); // 리본 셰이더가 정점색을 곱한다
if (writeCustom)
{
_mesh.SetUVs(1, _uv1); // 구 WL NamuFX Custom Data 1 (디졸브)
_mesh.SetUVs(2, _uv2); // 구 WL NamuFX Custom Data 2 (색 HDR + 알파)
}
_mesh.subMeshCount = layers;
for (int L = 0; L < layers; L++)
_mesh.SetTriangles(_tris, L * maxTrisPerLayer, _triCount[L], L, false);
_mesh.RecalculateBounds();
if (_settings != null && _settings.alsoApplyColorViaPropertyBlock)
ApplyColorPropertyBlock();
_renderer.enabled = true;
}
/// <summary>정점색이 먹지 않는 셰이더용 예비 경로. _BaseColor 에 대표 색을 넣는다.</summary>
private void ApplyColorPropertyBlock()
{
if (_mpb == null) _mpb = new MaterialPropertyBlock();
int layers = Mathf.Max(_layerCount, 1);
for (int L = 0; L < layers; L++)
{
Color c = (L == 1) ? _colorAlpha : _colorAdditive;
_mpb.Clear();
_mpb.SetColor(ShaderIds.BaseColor, c);
_renderer.SetPropertyBlock(_mpb, L);
}
}
/// <summary>셰이더 프로퍼티 ID 캐시(매 프레임 문자열 해싱을 피한다).</summary>
private static class ShaderIds
{
public static readonly int BaseColor = Shader.PropertyToID("_BaseColor");
}
#if UNITY_EDITOR
private void OnDrawGizmos()
{
if (_settings == null || !_settings.drawGizmos || _count < 2) return;
Gizmos.color = Color.cyan;
for (int i = 1; i < _count; i++) if (!_newStrip[i]) Gizmos.DrawLine(_tip[i - 1], _tip[i]);
Gizmos.color = Color.yellow;
for (int i = 1; i < _count; i++) if (!_newStrip[i]) Gizmos.DrawLine(_hilt[i - 1], _hilt[i]);
}
#endif
}
}