Refactor : FootGroundingController를 Serializable 모듈로 전환
- FootGroundingController: MonoBehaviour → [Serializable] 클래스 - Start() → Initialize(TwoBoneIKSolver, Animator) - Update()/LateUpdate() → OnUpdate()/OnLateUpdate() - CRS에서 footGrounding 필드로 소유, Update/LateUpdate에서 호출 - CustomRetargetingScriptEditor: groundingSO 제거, serializedObject 경로로 접근 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
64a2069b69
commit
62a5a9bbb5
@ -98,6 +98,9 @@ namespace KindRetargeting
|
|||||||
[Header("어깨 보정")]
|
[Header("어깨 보정")]
|
||||||
[SerializeField] public ShoulderCorrectionFunction shoulderCorrection = new ShoulderCorrectionFunction();
|
[SerializeField] public ShoulderCorrectionFunction shoulderCorrection = new ShoulderCorrectionFunction();
|
||||||
|
|
||||||
|
[Header("발 접지")]
|
||||||
|
[SerializeField] public FootGroundingController footGrounding = new FootGroundingController();
|
||||||
|
|
||||||
[Header("프랍 부착")]
|
[Header("프랍 부착")]
|
||||||
[SerializeField] public PropLocationController propLocation = new PropLocationController();
|
[SerializeField] public PropLocationController propLocation = new PropLocationController();
|
||||||
|
|
||||||
@ -328,6 +331,9 @@ namespace KindRetargeting
|
|||||||
if (targetAnimator != null)
|
if (targetAnimator != null)
|
||||||
shoulderCorrection.Initialize(targetAnimator);
|
shoulderCorrection.Initialize(targetAnimator);
|
||||||
|
|
||||||
|
// 발 접지 모듈 초기화
|
||||||
|
footGrounding.Initialize(ikSolver, targetAnimator);
|
||||||
|
|
||||||
// 프랍 부착 모듈 초기화
|
// 프랍 부착 모듈 초기화
|
||||||
if (targetAnimator != null)
|
if (targetAnimator != null)
|
||||||
propLocation.Initialize(targetAnimator);
|
propLocation.Initialize(targetAnimator);
|
||||||
@ -831,6 +837,9 @@ namespace KindRetargeting
|
|||||||
// 어깨 보정 (기존 ExecutionOrder 3)
|
// 어깨 보정 (기존 ExecutionOrder 3)
|
||||||
shoulderCorrection.OnUpdate();
|
shoulderCorrection.OnUpdate();
|
||||||
|
|
||||||
|
// 발 접지 Pre-IK (기존 ExecutionOrder 5)
|
||||||
|
footGrounding.OnUpdate();
|
||||||
|
|
||||||
// IK 솔버 (기존 ExecutionOrder 6)
|
// IK 솔버 (기존 ExecutionOrder 6)
|
||||||
ikSolver.OnUpdate();
|
ikSolver.OnUpdate();
|
||||||
|
|
||||||
@ -847,6 +856,9 @@ namespace KindRetargeting
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
void LateUpdate()
|
void LateUpdate()
|
||||||
{
|
{
|
||||||
|
// 발 접지 Post-IK (기존 FootGroundingController LateUpdate)
|
||||||
|
footGrounding.OnLateUpdate();
|
||||||
|
|
||||||
ApplyHeadRotationOffset();
|
ApplyHeadRotationOffset();
|
||||||
ApplyHeadScale();
|
ApplyHeadScale();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,8 +10,6 @@ namespace KindRetargeting
|
|||||||
{
|
{
|
||||||
private const string CommonUssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/StreamingleCommon.uss";
|
private const string CommonUssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/StreamingleCommon.uss";
|
||||||
|
|
||||||
private SerializedObject groundingSO;
|
|
||||||
|
|
||||||
// SerializedProperty
|
// SerializedProperty
|
||||||
private SerializedProperty sourceAnimatorProp;
|
private SerializedProperty sourceAnimatorProp;
|
||||||
private SerializedProperty targetAnimatorProp;
|
private SerializedProperty targetAnimatorProp;
|
||||||
@ -33,7 +31,7 @@ namespace KindRetargeting
|
|||||||
protected override void OnDisable()
|
protected override void OnDisable()
|
||||||
{
|
{
|
||||||
base.OnDisable();
|
base.OnDisable();
|
||||||
groundingSO = null;
|
// groundingSO 삭제됨 — footGrounding은 CRS 내부 모듈
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnEnable()
|
protected override void OnEnable()
|
||||||
@ -197,36 +195,27 @@ namespace KindRetargeting
|
|||||||
"• Toe Pivot: 발끝 고정 + 발목 회전 자동 감지",
|
"• Toe Pivot: 발끝 고정 + 발목 회전 자동 감지",
|
||||||
HelpBoxMessageType.Info));
|
HelpBoxMessageType.Info));
|
||||||
|
|
||||||
// FootGroundingController의 SerializedObject를 직접 바인딩
|
// FootGroundingController는 CRS 내부 모듈 — serializedObject의 프로퍼티 경로로 접근
|
||||||
var script = (CustomRetargetingScript)target;
|
var groundHeightField = new PropertyField(serializedObject.FindProperty("footGrounding.groundHeight"), "바닥 높이");
|
||||||
var grounding = script.GetComponent<FootGroundingController>();
|
foldout.Add(groundHeightField);
|
||||||
if (grounding != null)
|
|
||||||
{
|
|
||||||
groundingSO = new SerializedObject(grounding);
|
|
||||||
|
|
||||||
var groundHeightField = new PropertyField(groundingSO.FindProperty("groundHeight"), "바닥 높이");
|
var weightSlider = new Slider("접지 강도", 0f, 1f) { showInputField = true };
|
||||||
foldout.Add(groundHeightField);
|
weightSlider.BindProperty(serializedObject.FindProperty("footGrounding.groundingWeight"));
|
||||||
|
foldout.Add(weightSlider);
|
||||||
|
|
||||||
var weightSlider = new Slider("접지 강도", 0f, 1f) { showInputField = true };
|
var activationField = new PropertyField(serializedObject.FindProperty("footGrounding.activationHeight"), "활성화 높이");
|
||||||
weightSlider.BindProperty(groundingSO.FindProperty("groundingWeight"));
|
activationField.tooltip = "발목이 이 높이 이상이면 접지 보정 비활성화 (점프 등)";
|
||||||
foldout.Add(weightSlider);
|
foldout.Add(activationField);
|
||||||
|
|
||||||
var activationField = new PropertyField(groundingSO.FindProperty("activationHeight"), "활성화 높이");
|
var thresholdField = new PropertyField(serializedObject.FindProperty("footGrounding.plantThreshold"), "접지 판정 범위");
|
||||||
activationField.tooltip = "발목이 이 높이 이상이면 접지 보정 비활성화 (점프 등)";
|
thresholdField.tooltip = "Toes가 이 범위 안이면 접지 중으로 판정";
|
||||||
foldout.Add(activationField);
|
foldout.Add(thresholdField);
|
||||||
|
|
||||||
var thresholdField = new PropertyField(groundingSO.FindProperty("plantThreshold"), "접지 판정 범위");
|
var smoothField = new PropertyField(serializedObject.FindProperty("footGrounding.smoothSpeed"), "보정 스무딩 속도");
|
||||||
thresholdField.tooltip = "Toes가 이 범위 안이면 접지 중으로 판정";
|
smoothField.tooltip = "보정량 변화 속도 (높을수록 빠른 반응, 낮으면 부드러운 전환)";
|
||||||
foldout.Add(thresholdField);
|
foldout.Add(smoothField);
|
||||||
|
|
||||||
var smoothField = new PropertyField(groundingSO.FindProperty("smoothSpeed"), "보정 스무딩 속도");
|
foldout.Add(new HelpBox("힙 높이 보정은 '바닥 높이 조정' 섹션의 floorHeight로 제어합니다.", HelpBoxMessageType.Info));
|
||||||
smoothField.tooltip = "보정량 변화 속도 (높을수록 빠른 반응, 낮으면 부드러운 전환)";
|
|
||||||
foldout.Add(smoothField);
|
|
||||||
|
|
||||||
foldout.Add(new HelpBox("힙 높이 보정은 '바닥 높이 조정' 섹션의 floorHeight로 제어합니다.", HelpBoxMessageType.Info));
|
|
||||||
|
|
||||||
foldout.TrackSerializedObjectValue(groundingSO, so => so.ApplyModifiedProperties());
|
|
||||||
}
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
foldout.Add(new HelpBox("FootGroundingController 컴포넌트를 찾을 수 없습니다.", HelpBoxMessageType.Warning));
|
foldout.Add(new HelpBox("FootGroundingController 컴포넌트를 찾을 수 없습니다.", HelpBoxMessageType.Warning));
|
||||||
|
|||||||
@ -5,19 +5,14 @@ namespace KindRetargeting
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// HIK 스타일 2-Pass 접지 시스템.
|
/// HIK 스타일 2-Pass 접지 시스템.
|
||||||
///
|
///
|
||||||
/// Pass 1 (Update, Order 5 → IK 전):
|
/// Pass 1 (OnUpdate, Order 5 → IK 전):
|
||||||
/// IK 타겟 위치를 수정하여 Toes가 바닥을 뚫지 않도록 보정.
|
/// IK 타겟 위치를 수정하여 Toes가 바닥을 뚫지 않도록 보정.
|
||||||
/// Toe Pivot 감지: 발끝이 바닥에 있고 발목이 올라가면
|
|
||||||
/// 발목 타겟을 역산하여 Toes가 groundHeight에 고정.
|
|
||||||
///
|
///
|
||||||
/// Pass 2 (LateUpdate → IK 후):
|
/// Pass 2 (OnLateUpdate → IK 후):
|
||||||
/// IK 결과의 잔차를 Foot 회전으로 미세 보정.
|
/// IK 결과의 잔차를 Foot 회전으로 미세 보정.
|
||||||
/// 위치 변경 없음 — 본 길이 보존.
|
|
||||||
///
|
|
||||||
/// 힙 높이 보정은 CRS의 floorHeight가 담당합니다 (이중 보정 방지).
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DefaultExecutionOrder(5)]
|
[System.Serializable]
|
||||||
public class FootGroundingController : MonoBehaviour
|
public class FootGroundingController
|
||||||
{
|
{
|
||||||
[Header("접지 설정")]
|
[Header("접지 설정")]
|
||||||
[Tooltip("바닥 Y 좌표 (월드 공간)")]
|
[Tooltip("바닥 Y 좌표 (월드 공간)")]
|
||||||
@ -41,38 +36,31 @@ namespace KindRetargeting
|
|||||||
private TwoBoneIKSolver ikSolver;
|
private TwoBoneIKSolver ikSolver;
|
||||||
private Animator animator;
|
private Animator animator;
|
||||||
|
|
||||||
// 타겟 아바타 캐싱
|
|
||||||
private Transform leftFoot;
|
private Transform leftFoot;
|
||||||
private Transform rightFoot;
|
private Transform rightFoot;
|
||||||
private Transform leftToes;
|
private Transform leftToes;
|
||||||
private Transform rightToes;
|
private Transform rightToes;
|
||||||
|
|
||||||
// Toes의 Foot 로컬 오프셋 (T-pose에서 캐싱)
|
|
||||||
private Vector3 leftLocalToesOffset;
|
private Vector3 leftLocalToesOffset;
|
||||||
private Vector3 rightLocalToesOffset;
|
private Vector3 rightLocalToesOffset;
|
||||||
|
|
||||||
// flat 상태에서 발목 최소 높이 (Foot.y - Toes.y)
|
|
||||||
private float leftFootHeight;
|
private float leftFootHeight;
|
||||||
private float rightFootHeight;
|
private float rightFootHeight;
|
||||||
|
|
||||||
// Toes 본 존재 여부
|
|
||||||
private bool leftHasToes;
|
private bool leftHasToes;
|
||||||
private bool rightHasToes;
|
private bool rightHasToes;
|
||||||
|
|
||||||
// 스무딩용: 이전 프레임 보정량
|
|
||||||
private float leftPrevAdj;
|
private float leftPrevAdj;
|
||||||
private float rightPrevAdj;
|
private float rightPrevAdj;
|
||||||
|
|
||||||
private bool isInitialized;
|
private bool isInitialized;
|
||||||
|
|
||||||
private void Start()
|
public void Initialize(TwoBoneIKSolver ikSolver, Animator animator)
|
||||||
{
|
{
|
||||||
var crs = GetComponent<CustomRetargetingScript>();
|
this.ikSolver = ikSolver;
|
||||||
if (crs != null) ikSolver = crs.ikSolver;
|
this.animator = animator;
|
||||||
animator = GetComponent<Animator>();
|
|
||||||
|
|
||||||
if (animator == null || !animator.isHuman || ikSolver == null) return;
|
if (animator == null || !animator.isHuman || ikSolver == null) return;
|
||||||
if (leftFoot == null && rightFoot == null) return;
|
|
||||||
|
|
||||||
leftFoot = animator.GetBoneTransform(HumanBodyBones.LeftFoot);
|
leftFoot = animator.GetBoneTransform(HumanBodyBones.LeftFoot);
|
||||||
rightFoot = animator.GetBoneTransform(HumanBodyBones.RightFoot);
|
rightFoot = animator.GetBoneTransform(HumanBodyBones.RightFoot);
|
||||||
@ -81,7 +69,6 @@ namespace KindRetargeting
|
|||||||
|
|
||||||
if (leftFoot == null || rightFoot == null) return;
|
if (leftFoot == null || rightFoot == null) return;
|
||||||
|
|
||||||
// Toes 존재 여부 + 캐싱
|
|
||||||
leftHasToes = leftToes != null;
|
leftHasToes = leftToes != null;
|
||||||
rightHasToes = rightToes != null;
|
rightHasToes = rightToes != null;
|
||||||
|
|
||||||
@ -92,7 +79,7 @@ namespace KindRetargeting
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
leftFootHeight = 0.05f; // Toes 없을 때 기본 발목 높이
|
leftFootHeight = 0.05f;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rightHasToes)
|
if (rightHasToes)
|
||||||
@ -111,7 +98,7 @@ namespace KindRetargeting
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Pass 1: Pre-IK — IK 타겟 위치를 수정합니다.
|
/// Pass 1: Pre-IK — IK 타겟 위치를 수정합니다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void Update()
|
public void OnUpdate()
|
||||||
{
|
{
|
||||||
if (!isInitialized || groundingWeight < 0.001f) return;
|
if (!isInitialized || groundingWeight < 0.001f) return;
|
||||||
|
|
||||||
@ -122,16 +109,11 @@ namespace KindRetargeting
|
|||||||
ikSolver.rightLeg, rightLocalToesOffset, rightFootHeight,
|
ikSolver.rightLeg, rightLocalToesOffset, rightFootHeight,
|
||||||
rightHasToes, ikSolver.rightLeg.positionWeight);
|
rightHasToes, ikSolver.rightLeg.positionWeight);
|
||||||
|
|
||||||
// 스무딩: 보정량 급변 방지
|
|
||||||
float dt = Time.deltaTime * smoothSpeed;
|
float dt = Time.deltaTime * smoothSpeed;
|
||||||
leftPrevAdj = Mathf.Lerp(leftPrevAdj, leftAdj, Mathf.Clamp01(dt));
|
leftPrevAdj = Mathf.Lerp(leftPrevAdj, leftAdj, Mathf.Clamp01(dt));
|
||||||
rightPrevAdj = Mathf.Lerp(rightPrevAdj, rightAdj, Mathf.Clamp01(dt));
|
rightPrevAdj = Mathf.Lerp(rightPrevAdj, rightAdj, Mathf.Clamp01(dt));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 발 IK 타겟을 접지 모드에 따라 보정합니다.
|
|
||||||
/// Toes가 없는 아바타는 발목 Y 클램프만 수행합니다.
|
|
||||||
/// </summary>
|
|
||||||
private float AdjustFootTarget(TwoBoneIKSolver.LimbIK limb, Vector3 localToesOffset,
|
private float AdjustFootTarget(TwoBoneIKSolver.LimbIK limb, Vector3 localToesOffset,
|
||||||
float footHeight, bool hasToes, float ikWeight)
|
float footHeight, bool hasToes, float ikWeight)
|
||||||
{
|
{
|
||||||
@ -141,12 +123,10 @@ namespace KindRetargeting
|
|||||||
Vector3 anklePos = ankleTarget.position;
|
Vector3 anklePos = ankleTarget.position;
|
||||||
float ankleY = anklePos.y;
|
float ankleY = anklePos.y;
|
||||||
|
|
||||||
// AIRBORNE 체크
|
|
||||||
if (ankleY - groundHeight > activationHeight) return 0f;
|
if (ankleY - groundHeight > activationHeight) return 0f;
|
||||||
|
|
||||||
float weight = groundingWeight * ikWeight;
|
float weight = groundingWeight * ikWeight;
|
||||||
|
|
||||||
// === Toes 없는 아바타: 단순 Y 클램프 ===
|
|
||||||
if (!hasToes)
|
if (!hasToes)
|
||||||
{
|
{
|
||||||
float minAnkleY = groundHeight + footHeight;
|
float minAnkleY = groundHeight + footHeight;
|
||||||
@ -160,7 +140,6 @@ namespace KindRetargeting
|
|||||||
return 0f;
|
return 0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Toes 있는 아바타: 예측 기반 보정 ===
|
|
||||||
Vector3 predictedToesWorld = anklePos + ankleTarget.rotation * localToesOffset;
|
Vector3 predictedToesWorld = anklePos + ankleTarget.rotation * localToesOffset;
|
||||||
float predictedToesY = predictedToesWorld.y;
|
float predictedToesY = predictedToesWorld.y;
|
||||||
|
|
||||||
@ -172,7 +151,6 @@ namespace KindRetargeting
|
|||||||
|
|
||||||
if (ankleY < groundHeight + footHeight + plantThreshold)
|
if (ankleY < groundHeight + footHeight + plantThreshold)
|
||||||
{
|
{
|
||||||
// PLANTED: 발 전체가 바닥 근처
|
|
||||||
float minAnkleY = groundHeight + footHeight;
|
float minAnkleY = groundHeight + footHeight;
|
||||||
if (ankleY < minAnkleY)
|
if (ankleY < minAnkleY)
|
||||||
{
|
{
|
||||||
@ -189,7 +167,6 @@ namespace KindRetargeting
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// TOE_PIVOT: 발끝 고정, 발목 올라감
|
|
||||||
if (toesError > 0f)
|
if (toesError > 0f)
|
||||||
{
|
{
|
||||||
adjustment = toesError * weight;
|
adjustment = toesError * weight;
|
||||||
@ -201,7 +178,6 @@ namespace KindRetargeting
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Toes 충분히 위 → 발목만 바닥 아래 방지
|
|
||||||
float minAnkleY = groundHeight + footHeight;
|
float minAnkleY = groundHeight + footHeight;
|
||||||
if (ankleY < minAnkleY)
|
if (ankleY < minAnkleY)
|
||||||
{
|
{
|
||||||
@ -217,7 +193,7 @@ namespace KindRetargeting
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Pass 2: Post-IK — 잔차를 Foot 회전으로 미세 보정합니다.
|
/// Pass 2: Post-IK — 잔차를 Foot 회전으로 미세 보정합니다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void LateUpdate()
|
public void OnLateUpdate()
|
||||||
{
|
{
|
||||||
if (!isInitialized || groundingWeight < 0.001f) return;
|
if (!isInitialized || groundingWeight < 0.001f) return;
|
||||||
|
|
||||||
@ -227,10 +203,6 @@ namespace KindRetargeting
|
|||||||
AlignFootToGround(rightFoot, rightToes, ikSolver.rightLeg.positionWeight);
|
AlignFootToGround(rightFoot, rightToes, ikSolver.rightLeg.positionWeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// IK 후 실제 Toes 위치를 확인하고, Foot 본을 pitch 회전하여 잔차 보정.
|
|
||||||
/// 바닥 아래로 뚫린 경우만 보정합니다.
|
|
||||||
/// </summary>
|
|
||||||
private void AlignFootToGround(Transform foot, Transform toes, float ikWeight)
|
private void AlignFootToGround(Transform foot, Transform toes, float ikWeight)
|
||||||
{
|
{
|
||||||
if (foot == null || toes == null) return;
|
if (foot == null || toes == null) return;
|
||||||
@ -245,7 +217,6 @@ namespace KindRetargeting
|
|||||||
|
|
||||||
if (Mathf.Abs(error) < 0.001f) return;
|
if (Mathf.Abs(error) < 0.001f) return;
|
||||||
|
|
||||||
// 바닥 아래로 뚫린 경우만 보정
|
|
||||||
if (error > plantThreshold) return;
|
if (error > plantThreshold) return;
|
||||||
|
|
||||||
Vector3 footToToes = toes.position - foot.position;
|
Vector3 footToToes = toes.position - foot.position;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user