Project_WL/Assets/WL/Camera/CameraMoveLead.cs

261 lines
14 KiB
C#
Raw Normal View History

WL 리뉴얼 세션 2026-09-06~07 (#760~#800) — 전투·배경·UI·타격감 (조직 PD 로그 #746~#800 · 대화로그 §14~§24) - 전투: Knight@Attack1~3_S 3콤보(4 컨트롤러·클래스별 서브클립) · 타겟팅 4규칙(정면 우선·재타겟·즉시 공격·어그로 금지 · WLTargetingSettings) · 대쉬 후 공격(Stander@Chase_Start · DashDriver · 5 m · 사정거리+0.8 · 근거리 적 우선) · 공격 이동 FrameTable(발 접지 실측 표 · AttackRootMotion) · 충돌 반경/투사체 0.3 배(WLCollisionTuning) · 무적(임시)·펫 금지(WLGameplaySettings) - 검기: NamuFX Slash_B 배리언트 Effect_WLSwingArc 원 피팅 정합 배치 + 캘리브레이션(SlashArcMeasure · SlashTrailSettings) · 찌르기 Effect_WLStab 대기 · 램프 리본(BladeTrail · WL_BladeRibbon.shader · T_WL_BladeRibbonRamp) 보존(drawRibbon 0) - 타격감: Assets/WL/Feel(WLHitFeel · 히트스톱 0.03 · 셰이크 0.10 m · 몹 펀치 1.12 · Actor.Get_Damage 훅 1줄 · 원본 RealCamera 셰이크 결함 대체) - 배경/맵: LMHPOLY Demo_01~10 → WL_Nature01~10(프리팹·씬·NavMesh·스포너·BattleMapConfig) · 물(ToonWaterU) · 포스트 블룸 0.9/0.3 · 잔디(BruteForce·드레싱) 제거 · 마젠타 머티리얼 URP 변환 - UI: 세로 HUD(WL_HUD · 하단 5메뉴 폭 전체 · 채팅/전투 패드 숨김 · WLIngameUiOverride) · Title/TitleInfo 1080×1920 Expand + 배경 높이 fit(WLBackgroundFit) · 로딩 SortOrder_5(WLRawImageAspectSync · 초점표 8장) · Loading1~8 ASTC 6×6 - 도구: AgentScripts/*(LightProbe · WL_MapSwitch · WL760~WL800 프로브/집행/검증 · 상단 사용법 주석) · 에디터 락 프로토콜 파일(staging) - 제외(별도 커밋 예정): Assets/LMHPOLY(703 MB) · Assets/Feel(422 MB) · Assets/Shinabro(300 MB) — 에셋 스토어/구 WL 팩 원본 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 23:24:25 +00:00
using UnityEngine;
/// <summary>
/// 플레이어 이동 방향에 따른 카메라 리드(전진/후진)·피치 가산(후진) 계산기.
///
/// ■ 무엇을 하나
/// 전진(카메라에서 멀어지는 성분) → 궤도 중심과 주시점을 진행 방향으로 leadDistance 만큼 함께 민다.
/// 둘을 같은 값으로 밀기 때문에 카메라는 회전 없이 통째로 평행 이동하고,
/// 캐릭터는 화면에서 아래로 내려가 진행 방향이 더 보인다.
/// 후진(카메라 쪽으로 오는 성분) → ⓐ 궤도 파라미터 cameraHeight 를 올려 더 위에서 내려다보게 하고
/// ⓑ 주시점을 camFwd 축으로 backwardLeadDistance(음수) 만큼 밀어
/// 캐릭터를 화면 위로 올린다(구 WL 앵커 전환과 같은 방향 · PM 결정 ①).
/// 정지·측면 → 전부 0 으로 복귀(SmoothDamp).
///
/// ■ 어디에 붙나 — MonoBehaviour 가 아니다.
/// RealCamera 가 private 필드로 1개 들고 Update_Cam() 안에서 Evaluate() 를 부른다.
/// · Update_Cam() 은 CamStatus == 1(평소)에서만 호출된다 → 인트로(0)·스킬 팔로우(2)·탑뷰(3)·
/// 보스 연출은 **호출 자체가 안 되므로** 별도의 상태 검사 없이 자동 배제된다.
/// · 프리팹(Main Camera.prefab) 을 건드리지 않는다 — 컴포넌트 추가·직렬화 필드 추가가 없다.
/// · 값은 전부 Resources/WL/CameraLeadSettings.asset(ScriptableObject · C45)에서 읽는다.
/// 에셋이 없거나 enabled = false 면 모든 출력이 0 → RealCamera 는 100% 기존 동작.
///
/// ■ 왜 "RealCamera 가 쓴 transform.position 에 오프셋을 더하는 후처리 컴포넌트" 가 아닌가 (기각 근거)
/// RealCamera 는 transform.position = Vector3.Lerp(transform.position, camPosition, dt*smooth) 로
/// **자기 출력을 다시 입력으로 읽는** 저역통과 필터다. 여기에 매 프레임 오프셋 e 를 사후 가산하면
/// 평형점이 p = camPosition + ((2a1)/a)·e (a = dt*smooth) 가 된다.
/// 실측 a = (1/60)×4 = 0.0667 → 계수 13.0. 즉 의도한 오프셋의 13배가 **반대 방향**으로 나오고
/// 프레임률에 따라 계수가 또 변한다. 사후 가산 방식은 성립하지 않는다.
///
/// ■ 이동 방향 = 입력이 아니라 위치 차분
/// 조이스틱(PCActor.Run_byJoystick → NavMeshAgent.Move)과 자동 이동(NavMeshAgent 경로)을 모두 포함한다.
/// Update_Cam 은 LateUpdate 에서 호출되므로 그 프레임의 이동이 끝난 뒤의 위치다.
/// </summary>
// [System.Serializable] 을 일부러 붙이지 않는다 — 붙이면 RealCamera 의 필드가 직렬화되어
// Main Camera.prefab 이 더러워진다. 값의 SOT 는 ScriptableObject 한 곳이다.
public class CameraMoveLead
{
const string SettingsPath = "WL/CameraLeadSettings"; // Assets/WL/Settings/Resources/WL/CameraLeadSettings.asset
static CameraLeadSettings s_Settings;
static bool s_Loaded;
/// <summary>설정 에셋. 없으면 null — 그 경우 이 기능은 통째로 비활성이다.</summary>
public static CameraLeadSettings Settings
{
get
{
if (!s_Loaded)
{
s_Loaded = true;
s_Settings = Resources.Load<CameraLeadSettings>(SettingsPath);
}
return s_Settings;
}
}
/// <summary>에디터/진단에서 에셋을 갈아끼울 때.</summary>
public static void ReloadSettings() { s_Loaded = false; s_Settings = null; }
/// <summary>
/// WL #811d — 런타임 리드 배율(전진·후진·측면 리드 전부에 곱한다 · 기본 1). 3타 ComboStage(isLast) 에
/// CombatCameraDirector 가 잠시 1.3 으로 올렸다가 되돌린다(값은 WLCombatCameraSettings). 1 이면 기존 동작과 동일.
/// </summary>
public static float LeadMultiplier = 1f;
WL 리뉴얼 세션 2026-09-06~07 (#760~#800) — 전투·배경·UI·타격감 (조직 PD 로그 #746~#800 · 대화로그 §14~§24) - 전투: Knight@Attack1~3_S 3콤보(4 컨트롤러·클래스별 서브클립) · 타겟팅 4규칙(정면 우선·재타겟·즉시 공격·어그로 금지 · WLTargetingSettings) · 대쉬 후 공격(Stander@Chase_Start · DashDriver · 5 m · 사정거리+0.8 · 근거리 적 우선) · 공격 이동 FrameTable(발 접지 실측 표 · AttackRootMotion) · 충돌 반경/투사체 0.3 배(WLCollisionTuning) · 무적(임시)·펫 금지(WLGameplaySettings) - 검기: NamuFX Slash_B 배리언트 Effect_WLSwingArc 원 피팅 정합 배치 + 캘리브레이션(SlashArcMeasure · SlashTrailSettings) · 찌르기 Effect_WLStab 대기 · 램프 리본(BladeTrail · WL_BladeRibbon.shader · T_WL_BladeRibbonRamp) 보존(drawRibbon 0) - 타격감: Assets/WL/Feel(WLHitFeel · 히트스톱 0.03 · 셰이크 0.10 m · 몹 펀치 1.12 · Actor.Get_Damage 훅 1줄 · 원본 RealCamera 셰이크 결함 대체) - 배경/맵: LMHPOLY Demo_01~10 → WL_Nature01~10(프리팹·씬·NavMesh·스포너·BattleMapConfig) · 물(ToonWaterU) · 포스트 블룸 0.9/0.3 · 잔디(BruteForce·드레싱) 제거 · 마젠타 머티리얼 URP 변환 - UI: 세로 HUD(WL_HUD · 하단 5메뉴 폭 전체 · 채팅/전투 패드 숨김 · WLIngameUiOverride) · Title/TitleInfo 1080×1920 Expand + 배경 높이 fit(WLBackgroundFit) · 로딩 SortOrder_5(WLRawImageAspectSync · 초점표 8장) · Loading1~8 ASTC 6×6 - 도구: AgentScripts/*(LightProbe · WL_MapSwitch · WL760~WL800 프로브/집행/검증 · 상단 사용법 주석) · 에디터 락 프로토콜 파일(staging) - 제외(별도 커밋 예정): Assets/LMHPOLY(703 MB) · Assets/Feel(422 MB) · Assets/Shinabro(300 MB) — 에셋 스토어/구 WL 팩 원본 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 23:24:25 +00:00
// ── 상태
Transform _bound; // 현재 추적 중인 타겟(바뀌면 리셋)
Vector3 _prevPos;
float _bindDistance; // 바인드 시점의 DistanceAway — 리드 거리비의 기준
bool _hasPrev;
Vector3 _lead, _leadVel;
Vector3 _leadDir = Vector3.forward, _leadDirVel;
float _pitch, _pitchVel;
// ── 진단 (settings.recordSample 이 켜져 있을 때만 갱신)
public struct Sample
{
public float time, speed, fwd01, back01, speed01, leadLen, pitchAdd, yawAdd, camYaw, camPitch;
public Vector3 leadDir, camPos, lookPoint;
}
public Sample LastSample;
public Vector3 LeadOffset { get { return _lead; } }
public float PitchAdd { get { return _pitch; } }
/// <summary>리드·피치를 즉시 0 으로 되돌린다(순간이동·타겟 교체·비활성).</summary>
public void Snap()
{
_lead = Vector3.zero; _leadVel = Vector3.zero;
_leadDirVel = Vector3.zero;
_pitch = 0f; _pitchVel = 0f;
}
/// <summary>
/// RealCamera.Update_Cam() 에서 매 프레임 1회 호출한다.
/// </summary>
/// <param name="target">추적 대상(플레이어) Transform — RealCamera.target</param>
/// <param name="actor">추적 대상 Actor — RealCamera 의 m_Target. 기준 속도(Get_MoveSpeed)를 읽는다. null 허용</param>
/// <param name="camTf">카메라 Transform — 수평 전방으로 전/후진을 판정한다</param>
/// <param name="distanceAway">RealCamera.DistanceAway — 리드 거리비 스케일용</param>
/// <param name="cameraHeight">RealCamera.cameraHeight — 피치 상한·요 상쇄 계산의 기준</param>
/// <param name="lead">궤도 중심과 주시점에 **함께** 더할 수평 오프셋(월드)</param>
/// <param name="pitchAdd">cameraHeight 에 더할 각(도)</param>
/// <param name="yawAdd">rotateAround 에 더할 각(도) — 피치 가산의 수평 방위 커플링 상쇄분</param>
public void Evaluate(Transform target, Actor actor, Transform camTf,
float distanceAway, float cameraHeight,
out Vector3 lead, out float pitchAdd, out float yawAdd)
{
lead = Vector3.zero; pitchAdd = 0f; yawAdd = 0f;
var st = Settings;
if (st == null || !st.enabled || target == null || camTf == null)
{
if (_lead != Vector3.zero || _pitch != 0f) Snap();
_hasPrev = false;
return;
}
float dt = Time.deltaTime;
// 타겟 교체(맵 이동·부활·PC 교체) → 차분을 버리고 리셋
if (!ReferenceEquals(_bound, target))
{
_bound = target;
_prevPos = target.position;
_bindDistance = distanceAway;
_hasPrev = true;
Snap();
return;
}
if (dt <= 0f) { lead = _lead; pitchAdd = _pitch; yawAdd = YawComp(st, cameraHeight, _pitch); return; }
Vector3 delta = target.position - _prevPos;
_prevPos = target.position;
if (!_hasPrev) { _hasPrev = true; return; }
delta.y = 0f;
float dist = delta.magnitude;
// 순간이동(포탈·워프·리스폰)
if (dist > st.teleportDistance) { Snap(); return; }
float speed = dist / dt;
// 카메라 수평 전방 — +방향으로 이동 = 카메라에서 멀어짐 = 전진
Vector3 camFwd = camTf.forward; camFwd.y = 0f;
if (camFwd.sqrMagnitude < 1e-6f) { lead = _lead; pitchAdd = _pitch; yawAdd = YawComp(st, cameraHeight, _pitch); return; }
camFwd.Normalize();
// 기준 속도 — 0 이면 캐릭터의 실제 이동속도(테이블·버프 반영)를 쓴다 (C45)
float refSpeed = st.speedReference;
if (refSpeed <= 0.0001f && actor != null)
{
// 2026-09-15 PD 「에러 나는 부분 수정」 — 섬(비전투 맵)에서는 PC 스탯 사전에 MOVSPD_Up 키가 없어
// Get_MoveSpeed 가 매 프레임 KeyNotFoundException 을 던졌다 → 실패하면 기준 속도 0(= 아래 임계값 분기)으로 처리.
try { refSpeed = actor.Get_MoveSpeed(); }
catch (System.Exception) { refSpeed = 0f; }
}
WL 리뉴얼 세션 2026-09-06~07 (#760~#800) — 전투·배경·UI·타격감 (조직 PD 로그 #746~#800 · 대화로그 §14~§24) - 전투: Knight@Attack1~3_S 3콤보(4 컨트롤러·클래스별 서브클립) · 타겟팅 4규칙(정면 우선·재타겟·즉시 공격·어그로 금지 · WLTargetingSettings) · 대쉬 후 공격(Stander@Chase_Start · DashDriver · 5 m · 사정거리+0.8 · 근거리 적 우선) · 공격 이동 FrameTable(발 접지 실측 표 · AttackRootMotion) · 충돌 반경/투사체 0.3 배(WLCollisionTuning) · 무적(임시)·펫 금지(WLGameplaySettings) - 검기: NamuFX Slash_B 배리언트 Effect_WLSwingArc 원 피팅 정합 배치 + 캘리브레이션(SlashArcMeasure · SlashTrailSettings) · 찌르기 Effect_WLStab 대기 · 램프 리본(BladeTrail · WL_BladeRibbon.shader · T_WL_BladeRibbonRamp) 보존(drawRibbon 0) - 타격감: Assets/WL/Feel(WLHitFeel · 히트스톱 0.03 · 셰이크 0.10 m · 몹 펀치 1.12 · Actor.Get_Damage 훅 1줄 · 원본 RealCamera 셰이크 결함 대체) - 배경/맵: LMHPOLY Demo_01~10 → WL_Nature01~10(프리팹·씬·NavMesh·스포너·BattleMapConfig) · 물(ToonWaterU) · 포스트 블룸 0.9/0.3 · 잔디(BruteForce·드레싱) 제거 · 마젠타 머티리얼 URP 변환 - UI: 세로 HUD(WL_HUD · 하단 5메뉴 폭 전체 · 채팅/전투 패드 숨김 · WLIngameUiOverride) · Title/TitleInfo 1080×1920 Expand + 배경 높이 fit(WLBackgroundFit) · 로딩 SortOrder_5(WLRawImageAspectSync · 초점표 8장) · Loading1~8 ASTC 6×6 - 도구: AgentScripts/*(LightProbe · WL_MapSwitch · WL760~WL800 프로브/집행/검증 · 상단 사용법 주석) · 에디터 락 프로토콜 파일(staging) - 제외(별도 커밋 예정): Assets/LMHPOLY(703 MB) · Assets/Feel(422 MB) · Assets/Shinabro(300 MB) — 에셋 스토어/구 WL 팩 원본 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 23:24:25 +00:00
float lo = st.moveSpeedThreshold;
float speed01 = (refSpeed > lo + 0.0001f) ? Mathf.Clamp01((speed - lo) / (refSpeed - lo)) : (speed > lo ? 1f : 0f);
float fwd01 = 0f, back01 = 0f;
float side01 = 0f, sideSign = 0f; // #759 좌/우 성분 (camRight 축)
Vector3 camRight = Vector3.Cross(Vector3.up, camFwd); camRight.Normalize();
if (speed > lo && dist > 1e-6f)
{
Vector3 dir = delta / dist;
float dotR = Vector3.Dot(dir, camRight);
side01 = Mathf.Clamp01(Mathf.Abs(dotR));
sideSign = dotR >= 0f ? 1f : -1f;
// 측면 무반응 반각: |각| 이 (90° sideDeadAngle) 를 넘으면 0
float dot = Vector3.Dot(dir, camFwd);
float gate = Mathf.Cos((90f - Mathf.Clamp(st.sideDeadAngle, 0f, 45f)) * Mathf.Deg2Rad);
float denom = Mathf.Max(1f - gate, 1e-4f);
fwd01 = Mathf.Clamp01((dot - gate) / denom);
back01 = Mathf.Clamp01((-dot - gate) / denom);
// 리드가 향하는 방향 자체도 감쇠 — 급반전 때 오프셋이 화면을 가로지르지 않게 (구 WL #720)
if (st.leadDirSmoothTime > 0.001f)
_leadDir = Vector3.SmoothDamp(_leadDir, dir, ref _leadDirVel, st.leadDirSmoothTime, Mathf.Infinity, dt);
else _leadDir = dir;
if (_leadDir.sqrMagnitude > 1e-6f) _leadDir.Normalize(); else _leadDir = dir;
}
// ── 리드 거리비 스케일 (실내·보스에서 카메라가 가까우면 리드도 함께 줄어든다)
float leadScale = LeadMultiplier; // #811d 3타 리드 배율(기본 1)
WL 리뉴얼 세션 2026-09-06~07 (#760~#800) — 전투·배경·UI·타격감 (조직 PD 로그 #746~#800 · 대화로그 §14~§24) - 전투: Knight@Attack1~3_S 3콤보(4 컨트롤러·클래스별 서브클립) · 타겟팅 4규칙(정면 우선·재타겟·즉시 공격·어그로 금지 · WLTargetingSettings) · 대쉬 후 공격(Stander@Chase_Start · DashDriver · 5 m · 사정거리+0.8 · 근거리 적 우선) · 공격 이동 FrameTable(발 접지 실측 표 · AttackRootMotion) · 충돌 반경/투사체 0.3 배(WLCollisionTuning) · 무적(임시)·펫 금지(WLGameplaySettings) - 검기: NamuFX Slash_B 배리언트 Effect_WLSwingArc 원 피팅 정합 배치 + 캘리브레이션(SlashArcMeasure · SlashTrailSettings) · 찌르기 Effect_WLStab 대기 · 램프 리본(BladeTrail · WL_BladeRibbon.shader · T_WL_BladeRibbonRamp) 보존(drawRibbon 0) - 타격감: Assets/WL/Feel(WLHitFeel · 히트스톱 0.03 · 셰이크 0.10 m · 몹 펀치 1.12 · Actor.Get_Damage 훅 1줄 · 원본 RealCamera 셰이크 결함 대체) - 배경/맵: LMHPOLY Demo_01~10 → WL_Nature01~10(프리팹·씬·NavMesh·스포너·BattleMapConfig) · 물(ToonWaterU) · 포스트 블룸 0.9/0.3 · 잔디(BruteForce·드레싱) 제거 · 마젠타 머티리얼 URP 변환 - UI: 세로 HUD(WL_HUD · 하단 5메뉴 폭 전체 · 채팅/전투 패드 숨김 · WLIngameUiOverride) · Title/TitleInfo 1080×1920 Expand + 배경 높이 fit(WLBackgroundFit) · 로딩 SortOrder_5(WLRawImageAspectSync · 초점표 8장) · Loading1~8 ASTC 6×6 - 도구: AgentScripts/*(LightProbe · WL_MapSwitch · WL760~WL800 프로브/집행/검증 · 상단 사용법 주석) · 에디터 락 프로토콜 파일(staging) - 제외(별도 커밋 예정): Assets/LMHPOLY(703 MB) · Assets/Feel(422 MB) · Assets/Shinabro(300 MB) — 에셋 스토어/구 WL 팩 원본 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 23:24:25 +00:00
if (st.scaleLeadByDistance)
{
float refDist = st.leadReferenceDistance > 0.0001f ? st.leadReferenceDistance : _bindDistance;
if (refDist > 0.0001f) leadScale *= distanceAway / refDist;
WL 리뉴얼 세션 2026-09-06~07 (#760~#800) — 전투·배경·UI·타격감 (조직 PD 로그 #746~#800 · 대화로그 §14~§24) - 전투: Knight@Attack1~3_S 3콤보(4 컨트롤러·클래스별 서브클립) · 타겟팅 4규칙(정면 우선·재타겟·즉시 공격·어그로 금지 · WLTargetingSettings) · 대쉬 후 공격(Stander@Chase_Start · DashDriver · 5 m · 사정거리+0.8 · 근거리 적 우선) · 공격 이동 FrameTable(발 접지 실측 표 · AttackRootMotion) · 충돌 반경/투사체 0.3 배(WLCollisionTuning) · 무적(임시)·펫 금지(WLGameplaySettings) - 검기: NamuFX Slash_B 배리언트 Effect_WLSwingArc 원 피팅 정합 배치 + 캘리브레이션(SlashArcMeasure · SlashTrailSettings) · 찌르기 Effect_WLStab 대기 · 램프 리본(BladeTrail · WL_BladeRibbon.shader · T_WL_BladeRibbonRamp) 보존(drawRibbon 0) - 타격감: Assets/WL/Feel(WLHitFeel · 히트스톱 0.03 · 셰이크 0.10 m · 몹 펀치 1.12 · Actor.Get_Damage 훅 1줄 · 원본 RealCamera 셰이크 결함 대체) - 배경/맵: LMHPOLY Demo_01~10 → WL_Nature01~10(프리팹·씬·NavMesh·스포너·BattleMapConfig) · 물(ToonWaterU) · 포스트 블룸 0.9/0.3 · 잔디(BruteForce·드레싱) 제거 · 마젠타 머티리얼 URP 변환 - UI: 세로 HUD(WL_HUD · 하단 5메뉴 폭 전체 · 채팅/전투 패드 숨김 · WLIngameUiOverride) · Title/TitleInfo 1080×1920 Expand + 배경 높이 fit(WLBackgroundFit) · 로딩 SortOrder_5(WLRawImageAspectSync · 초점표 8장) · Loading1~8 ASTC 6×6 - 도구: AgentScripts/*(LightProbe · WL_MapSwitch · WL760~WL800 프로브/집행/검증 · 상단 사용법 주석) · 에디터 락 프로토콜 파일(staging) - 제외(별도 커밋 예정): Assets/LMHPOLY(703 MB) · Assets/Feel(422 MB) · Assets/Shinabro(300 MB) — 에셋 스토어/구 WL 팩 원본 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-06 23:24:25 +00:00
}
// ── 전진 리드(진행 방향) + 후진 리드(camFwd 축 · backwardLeadDistance 음수면 카메라 쪽으로 당김)
Vector3 wantedLead = _leadDir * (st.leadDistance * leadScale * fwd01 * speed01)
+ camFwd * (st.backwardLeadDistance * leadScale * back01 * speed01)
+ camRight * (st.lateralLeadDistance * leadScale * side01 * sideSign * speed01); // #759 측면 리드
// 시선 방향 성분 제거(선택) — 수평 리드는 34.93° 카메라에서 82 % 가 줌인으로 소모된다.
// blend = 1 이면 시선에 수직인 성분만 남아 깊이 변화 0(구 WL screenAnchorY/FocusShift 와 같은 방향).
if (st.leadPerpendicularBlend > 0.0001f && wantedLead.sqrMagnitude > 1e-8f)
wantedLead -= Vector3.Project(wantedLead, camTf.forward) * Mathf.Clamp01(st.leadPerpendicularBlend);
if (st.leadSmoothTime > 0.001f)
_lead = Vector3.SmoothDamp(_lead, wantedLead, ref _leadVel, st.leadSmoothTime, Mathf.Infinity, dt);
else { _lead = wantedLead; _leadVel = Vector3.zero; }
// ── 후진 피치 (총합이 pitchTotalMax 를 넘지 않게 입력 단계에서 한 번만 제한)
float wantedPitch = Mathf.Max(0f, Mathf.Min(st.backwardPitchAdd * back01 * speed01,
st.pitchTotalMax - cameraHeight));
float maxSpd = st.pitchMaxSpeed > 0.0001f ? st.pitchMaxSpeed : Mathf.Infinity;
if (st.pitchSmoothTime > 0.001f)
_pitch = Mathf.SmoothDamp(_pitch, wantedPitch, ref _pitchVel, st.pitchSmoothTime, maxSpd, dt);
else { _pitch = wantedPitch; _pitchVel = 0f; }
lead = _lead;
pitchAdd = _pitch;
yawAdd = YawComp(st, cameraHeight, _pitch);
if (st.recordSample)
{
LastSample.time = Time.time;
LastSample.speed = speed; LastSample.fwd01 = fwd01; LastSample.back01 = back01; LastSample.speed01 = speed01;
LastSample.leadLen = _lead.magnitude; LastSample.leadDir = _leadDir;
LastSample.pitchAdd = _pitch; LastSample.yawAdd = yawAdd;
LastSample.camPos = camTf.position;
LastSample.camYaw = camTf.eulerAngles.y; LastSample.camPitch = camTf.eulerAngles.x;
LastSample.lookPoint = target.position + _lead;
}
}
/// <summary>
/// 피치 가산이 만드는 수평 방위 회전을 정확히 상쇄한다.
///
/// RealCamera 의 궤도 벡터는 Quaternion.Euler(h, r, 0) * Vector3.one 이다.
/// 이 벡터의 수평 방위각은 r + atan2(1, sin h + cos h) 이므로, h 를 올리면 r 이 그대로여도
/// 카메라가 옆으로 돈다(실측: h = 78° 에서 파라미터 +1° 당 +0.32°, +6° 면 +2.2° · 반지름 4.65 m 에서 약 18 cm).
/// 따라서 r 에 그 차이만큼을 빼 주면 방위가 고정된다.
/// </summary>
static float YawComp(CameraLeadSettings st, float baseHeight, float add)
{
if (st == null || !st.compensatePitchYawCoupling || Mathf.Abs(add) < 1e-4f) return 0f;
return -(YawOffsetDeg(baseHeight + add) - YawOffsetDeg(baseHeight));
}
static float YawOffsetDeg(float h)
{
float r = h * Mathf.Deg2Rad;
return Mathf.Atan2(1f, Mathf.Sin(r) + Mathf.Cos(r)) * Mathf.Rad2Deg;
}
/// <summary>진단용 — 파라미터 cameraHeight 에 대응하는 실제 궤도 고도각(도).</summary>
public static float RealElevationDeg(float h)
{
float r = h * Mathf.Deg2Rad;
return Mathf.Asin(Mathf.Clamp((Mathf.Sin(r) - Mathf.Cos(r)) / Mathf.Sqrt(3f), -1f, 1f)) * Mathf.Rad2Deg;
}
}