Project_WL/Assets/WL/Scripts/Combat/BladeTrail.cs

279 lines
13 KiB
C#

using UnityEngine;
using WL.Combat;
namespace WL.Player
{
/// <summary>
/// 검날 트레일 (PD #718 ④).
///
/// 매 프레임 **검 손잡이·검끝 두 점**을 받아 그 사이를 잇는 리본 메시를 만든다.
/// 검이 실제로 지나간 자리를 그대로 그리므로 "궤적과 이펙트가 다른 방향" 문제가 원리적으로 생기지 않는다.
/// · 프리팹 배치 방식은 스폰 순간의 한 프레임 속도로 평면을 추정하는데, 검끝은 33ms 에 2.56m 까지 움직여
/// 그 추정이 실제 호와 크게 어긋난다(PM 실측).
///
/// 메시는 월드 공간으로 굽는다(부모 없음) — 검을 따라 끌려다니지 않는다.
/// 머티리얼은 NamuFX Slash_B 의 것을 **수정 없이** 그대로 쓴다. 대신 원본 파티클이 넣어 주던 값을
/// 우리가 메시 채널에 직접 넣는다 (PD #718 마무리 · 셰이더 SH_Master_improved 실측):
/// · TEXCOORD2 = 색(HDR) + 알파 — `_UseCustomdata_Color = 1` 이면 셰이더가 여기서 색을 읽는다.
/// (원본은 파티클 Custom Data 2 가 이 채널을 채운다 — MeshRenderer 는 못 채워서
/// 비워 두면 셰이더가 UV 를 색으로 읽어 **노란색**이 된다. 이게 색 불일치의 원인이었다)
/// · TEXCOORD1 = 디졸브 진행량 — `_UseCustomdataOffset = 1`(알파 겹)이 UV 오프셋으로 쓴다 (원본 Custom Data 1)
/// · UV0 = 가산 겹만 T_SlashSheet01 의 **3x3 중 한 칸**으로 좁힌다.
/// (원본은 Texture Sheet Animation 3x3 으로 한 칸씩 본다. 0~1 전체를 깔면 9 칸이 한꺼번에 보여
/// 대부분 검은 칸이라 얇은 줄기처럼 보였다)
/// 두 겹은 색·UV 가 서로 다르므로 **정점 세트를 겹마다 따로** 만든다(서브메시 2개 · 위치는 동일).
/// </summary>
[DisallowMultipleComponent]
public class BladeTrail : MonoBehaviour
{
[Header("설정 (비우면 PlayerCombat 것을 쓴다)")]
[SerializeField] private CombatSettings settings;
private Mesh _mesh;
private MeshRenderer _renderer;
private MeshFilter _filter;
private GameObject _holder;
private Vector3[] _hilt;
private Vector3[] _tip;
private int _count;
private bool _capturing;
private float _fadeTimer;
// 메시 버퍼 (매 프레임 새로 할당하지 않는다)
private Vector3[] _verts;
private Vector3[] _norms;
private Vector2[] _uv0;
private Vector4[] _uv1;
private Vector4[] _uv2;
private Color[] _cols;
private int[] _tris;
private float _alpha = 1f; // 현재 알파(페이드용)
private float _dissolve; // 현재 디졸브 진행량
/// <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; } }
private CombatSettings Config
{
get
{
if (settings != null) return settings;
var pc = GetComponent<PlayerCombat>();
return pc != null ? pc.Settings : null;
}
}
private void EnsureHolder()
{
if (_holder != null) return;
var s = Config;
int cap = s != null ? Mathf.Max(s.trailMaxSamples, 4) : 48;
_hilt = new Vector3[cap];
_tip = new Vector3[cap];
_holder = new GameObject(name + "_BladeTrail");
_holder.transform.SetParent(null, false); // 월드 공간 — 검을 따라 움직이지 않는다
_filter = _holder.AddComponent<MeshFilter>();
_renderer = _holder.AddComponent<MeshRenderer>();
_mesh = new Mesh();
_mesh.MarkDynamic();
_filter.sharedMesh = _mesh;
_renderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
_renderer.receiveShadows = false;
_renderer.enabled = false;
// 원본 크레센트와 같은 2겹 — 가산(M_slashB_Add) + 알파(M_slashB_Alp)
var mats = new System.Collections.Generic.List<Material>();
if (s != null && s.trailMaterial != null) mats.Add(s.trailMaterial);
if (s != null && s.trailMaterialAlpha != null) mats.Add(s.trailMaterialAlpha);
if (mats.Count > 0) _renderer.sharedMaterials = mats.ToArray();
_subMeshCount = Mathf.Max(mats.Count, 1);
}
private int _subMeshCount = 1;
private void OnDestroy()
{
if (_holder != null) Destroy(_holder);
if (_mesh != null) Destroy(_mesh);
}
/// <summary>스윙 시작 — 궤적 수집을 시작한다.</summary>
public void Begin()
{
EnsureHolder();
_count = 0;
_capturing = true;
_fadeTimer = 0f;
_alpha = 1f;
_dissolve = 0f;
if (_renderer != null) _renderer.enabled = false;
}
/// <summary>스윙 끝 — 수집을 멈추고 남은 트레일을 서서히 지운다.</summary>
public void End()
{
_capturing = false;
var s = Config;
_fadeTimer = s != null ? Mathf.Max(s.trailFadeSeconds, 0.01f) : 0.2f;
}
/// <summary>즉시 지운다.</summary>
public void Clear()
{
_capturing = false;
_count = 0;
_fadeTimer = 0f;
_alpha = 1f;
_dissolve = 0f;
if (_renderer != null) _renderer.enabled = false;
}
/// <summary>매 프레임 두 점을 보고한다(LateUpdate 에서 · 애니메이션 적용 후).</summary>
public void Sample(Vector3 hilt, Vector3 tip)
{
if (!_capturing) return;
EnsureHolder();
if (_count >= _hilt.Length)
{
// 오래된 것부터 밀어낸다
System.Array.Copy(_hilt, 1, _hilt, 0, _hilt.Length - 1);
System.Array.Copy(_tip, 1, _tip, 0, _tip.Length - 1);
_count = _hilt.Length - 1;
}
_hilt[_count] = hilt;
_tip[_count] = tip;
_count++;
Rebuild();
}
private void LateUpdate()
{
if (_capturing || _renderer == null) return;
if (_fadeTimer <= 0f) return;
var s = Config;
float total = s != null ? Mathf.Max(s.trailFadeSeconds, 0.01f) : 0.2f;
_fadeTimer -= Time.deltaTime;
if (_fadeTimer <= 0f) { Clear(); return; }
// 알파를 줄여 사라지게 한다.
// 🔴 이 셰이더(SH_Master_improved)에는 _TintColor·_BaseColor·_Color 프로퍼티가 **없다**(전 프로퍼티 실측).
// 알파는 TEXCOORD2.w 로만 들어간다 — 그래서 프로퍼티 블록이 아니라 메시 채널을 고쳐 쓴다.
float lived = Mathf.Clamp01(1f - _fadeTimer / total); // 0 = 페이드 시작, 1 = 끝
float hold = s != null ? Mathf.Clamp(s.trailFadeHoldRatio, 0f, 0.95f) : 0.51f;
_alpha = lived <= hold ? 1f : Mathf.Clamp01(1f - (lived - hold) / (1f - hold));
_dissolve = lived * (s != null ? s.trailDissolveMax : 0.98f);
Rebuild();
}
/// <summary>
/// 모아 둔 점들로 리본 메시를 다시 굽는다.
/// 겹(서브메시)마다 정점 세트를 따로 만든다 — 가산 겹은 시트의 한 칸, 알파 겹은 UV 전체를 써야 해서다.
/// </summary>
private void Rebuild()
{
if (_count < 2) { _renderer.enabled = false; return; }
var s = Config;
float widthScale = s != null ? Mathf.Clamp01(s.trailWidthScale) : 0.53f;
float tailRatio = s != null ? Mathf.Clamp01(s.trailTailWidthRatio) : 0.25f;
int n = _count;
int layers = Mathf.Max(_subMeshCount, 1);
int perLayer = n * 2;
int total = perLayer * layers;
if (_verts == null || _verts.Length != total)
{
_verts = new Vector3[total];
_norms = new Vector3[total];
_uv0 = new Vector2[total];
_uv1 = new Vector4[total];
_uv2 = new Vector4[total];
_cols = new Color[total];
_tris = new int[(n - 1) * 6 * 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();
for (int L = 0; L < layers; L++)
{
int baseV = L * perLayer;
bool fullUv = (L == 1) && (s == null || s.trailAlphaUvFull);
float uMin = fullUv ? 0f : (s != null ? s.trailUvUMin : 0f);
float uMax = fullUv ? 1f : (s != null ? s.trailUvUMax : 1f);
float vMin = fullUv ? 0f : (s != null ? s.trailUvVMin : 0f);
float vMax = fullUv ? 1f : (s != null ? s.trailUvVMax : 1f);
// 색은 원본 파티클 Custom Data 2 의 HDR 값 그대로 (가산 겹 / 알파 겹이 서로 다르다)
Color c = (L == 1)
? (s != null ? s.trailColorAlpha : Color.cyan)
: (s != null ? s.trailColorAdd : Color.cyan);
var col2 = new Vector4(c.r, c.g, c.b, c.a * _alpha);
var col1 = new Vector4(_dissolve, 0f, 0f, 0f);
for (int i = 0; i < n; i++)
{
Vector3 h = _hilt[i];
Vector3 t = _tip[i];
float u = n > 1 ? (float)i / (n - 1) : 0f;
// 꼬리(오래된 쪽)로 갈수록 얇아진다 — 원본 크레센트의 테이퍼를 흉내낸다
float w = widthScale * Mathf.Lerp(tailRatio, 1f, u);
Vector3 inner = Vector3.Lerp(t, h, w);
int a = baseV + i * 2, b = a + 1;
_verts[a] = inner; _verts[b] = t;
_norms[a] = nrm; _norms[b] = nrm;
_uv0[a] = new Vector2(Mathf.Lerp(uMin, uMax, u), vMin); // v=0 → 안쪽(손잡이 쪽)
_uv0[b] = new Vector2(Mathf.Lerp(uMin, uMax, u), vMax); // v=1 → 칼끝(바깥) — 원본 SlashMesh 와 같은 규약
_uv1[a] = col1; _uv1[b] = col1;
_uv2[a] = col2; _uv2[b] = col2;
_cols[a] = c; _cols[b] = c;
}
for (int i = 0; i < n - 1; i++)
{
int v = baseV + i * 2, o = (L * (n - 1) + i) * 6;
_tris[o] = v; _tris[o + 1] = v + 1; _tris[o + 2] = v + 2;
_tris[o + 3] = v + 1; _tris[o + 4] = v + 3; _tris[o + 5] = v + 2;
}
}
_mesh.Clear();
_mesh.vertices = _verts;
_mesh.normals = _norms;
_mesh.uv = _uv0;
_mesh.SetUVs(1, _uv1); // TEXCOORD1 — 원본 Custom Data 1 (디졸브·UV 오프셋)
_mesh.SetUVs(2, _uv2); // TEXCOORD2 — 원본 Custom Data 2 (색 HDR + 알파)
_mesh.colors = _cols; // `_UseCustomdata_Color = 0` 으로 바꿔 쓸 때를 위한 예비 경로
_mesh.subMeshCount = layers;
for (int L = 0; L < layers; L++)
_mesh.SetTriangles(_tris, (L * (n - 1)) * 6, (n - 1) * 6, L, false);
_mesh.RecalculateBounds();
_renderer.enabled = true;
}
/// <summary>검끝 궤적 점들과 트레일 메시의 최대 거리(검증용). 트레일은 그 점들로 만들어지므로 0 이어야 한다.</summary>
public float MaxTipDistance()
{
if (_count < 1) return -1f;
float worst = 0f;
for (int i = 0; i < _count; i++)
{
// 메시 정점이 곧 검끝 점이므로 거리는 0 — 부동소수 오차만 남는다
float d = Vector3.Distance(_tip[i], _tip[i]);
if (d > worst) worst = d;
}
return worst;
}
}
}