Fix : TwoBoneIKSolver 커스텀 솔버로 교체 — 180° 무릎 덜컥거림 해결 및 역관절 지원
- FinalIK IKSolverTrigonometric 의존성 제거, 자체 솔버 구현 - cosine law 대신 소스 무릎 위치를 비율 스케일하여 타겟 무릎 직접 배치 - 180° 특이점 없이 정상↔역관절 자연스러운 전환 - FromToRotation 기반 본 회전으로 twist 보존 - 팔/다리 모두 소스 본 참조 설정, 소스 없으면 cosine law fallback Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
67c6f3b634
commit
0a7624dab6
@ -1188,6 +1188,26 @@ namespace KindRetargeting
|
|||||||
|
|
||||||
// TwoBoneIKSolver 본 캐싱 초기화
|
// TwoBoneIKSolver 본 캐싱 초기화
|
||||||
ikSolver.Initialize(targetAnimator);
|
ikSolver.Initialize(targetAnimator);
|
||||||
|
|
||||||
|
// IK에 소스 본 참조 설정 (소스 관절 위치 기반 IK용)
|
||||||
|
if (optitrackSource != null)
|
||||||
|
{
|
||||||
|
ikSolver.leftLeg.sourceUpper = GetSourceBoneTransform(HumanBodyBones.LeftUpperLeg);
|
||||||
|
ikSolver.leftLeg.sourceLower = GetSourceBoneTransform(HumanBodyBones.LeftLowerLeg);
|
||||||
|
ikSolver.leftLeg.sourceEnd = GetSourceBoneTransform(HumanBodyBones.LeftFoot);
|
||||||
|
|
||||||
|
ikSolver.rightLeg.sourceUpper = GetSourceBoneTransform(HumanBodyBones.RightUpperLeg);
|
||||||
|
ikSolver.rightLeg.sourceLower = GetSourceBoneTransform(HumanBodyBones.RightLowerLeg);
|
||||||
|
ikSolver.rightLeg.sourceEnd = GetSourceBoneTransform(HumanBodyBones.RightFoot);
|
||||||
|
|
||||||
|
ikSolver.leftArm.sourceUpper = GetSourceBoneTransform(HumanBodyBones.LeftUpperArm);
|
||||||
|
ikSolver.leftArm.sourceLower = GetSourceBoneTransform(HumanBodyBones.LeftLowerArm);
|
||||||
|
ikSolver.leftArm.sourceEnd = GetSourceBoneTransform(HumanBodyBones.LeftHand);
|
||||||
|
|
||||||
|
ikSolver.rightArm.sourceUpper = GetSourceBoneTransform(HumanBodyBones.RightUpperArm);
|
||||||
|
ikSolver.rightArm.sourceLower = GetSourceBoneTransform(HumanBodyBones.RightLowerArm);
|
||||||
|
ikSolver.rightArm.sourceEnd = GetSourceBoneTransform(HumanBodyBones.RightHand);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -1233,6 +1253,7 @@ namespace KindRetargeting
|
|||||||
updatejointTarget(ikSolver.rightArm.bendGoal, HumanBodyBones.RightLowerArm);
|
updatejointTarget(ikSolver.rightArm.bendGoal, HumanBodyBones.RightLowerArm);
|
||||||
updatejointTarget(ikSolver.leftLeg.bendGoal, HumanBodyBones.LeftLowerLeg);
|
updatejointTarget(ikSolver.leftLeg.bendGoal, HumanBodyBones.LeftLowerLeg);
|
||||||
updatejointTarget(ikSolver.rightLeg.bendGoal, HumanBodyBones.RightLowerLeg);
|
updatejointTarget(ikSolver.rightLeg.bendGoal, HumanBodyBones.RightLowerLeg);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using RootMotion.FinalIK;
|
|
||||||
|
|
||||||
namespace KindRetargeting
|
namespace KindRetargeting
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// FinalIK IKSolverTrigonometric.Solve()를 사용하는 IK 래퍼.
|
/// 커스텀 Two-Bone IK 솔버.
|
||||||
/// 4개 사지(양팔, 양다리)에 대해 FinalIK의 검증된 코사인 법칙 솔버를 호출합니다.
|
/// 소스 무릎 위치를 비율 스케일하여 타겟 무릎을 직접 배치합니다.
|
||||||
|
/// cosine law 없이 동작하므로 180° 특이점이 없고 역관절도 자연스럽게 지원합니다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[System.Serializable]
|
[System.Serializable]
|
||||||
public class TwoBoneIKSolver
|
public class TwoBoneIKSolver
|
||||||
@ -28,6 +28,11 @@ namespace KindRetargeting
|
|||||||
[HideInInspector] public float lowerLength;
|
[HideInInspector] public float lowerLength;
|
||||||
|
|
||||||
[HideInInspector] public Vector3 localBendNormal;
|
[HideInInspector] public Vector3 localBendNormal;
|
||||||
|
|
||||||
|
// 소스 본 참조 (소스 무릎 위치 기반 IK용)
|
||||||
|
[HideInInspector] public Transform sourceUpper;
|
||||||
|
[HideInInspector] public Transform sourceLower;
|
||||||
|
[HideInInspector] public Transform sourceEnd;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HideInInspector] public Animator animator;
|
[HideInInspector] public Animator animator;
|
||||||
@ -96,28 +101,33 @@ namespace KindRetargeting
|
|||||||
if (!limb.enabled || limb.target == null || limb.positionWeight < 0.001f) return;
|
if (!limb.enabled || limb.target == null || limb.positionWeight < 0.001f) return;
|
||||||
if (limb.upper == null || limb.lower == null || limb.end == null) return;
|
if (limb.upper == null || limb.lower == null || limb.end == null) return;
|
||||||
|
|
||||||
Vector3 bendNormal = GetBendNormal(limb);
|
float upperLen = Vector3.Distance(limb.upper.position, limb.lower.position);
|
||||||
|
float lowerLen = Vector3.Distance(limb.lower.position, limb.end.position);
|
||||||
|
|
||||||
if (limb.bendGoal != null && limb.bendGoalWeight > 0.001f)
|
Vector3 targetPos = Vector3.Lerp(limb.end.position, limb.target.position, limb.positionWeight);
|
||||||
|
|
||||||
|
// --- 무릎 위치 결정 ---
|
||||||
|
Vector3 kneePos;
|
||||||
|
|
||||||
|
if (limb.sourceUpper != null && limb.sourceLower != null && limb.sourceEnd != null)
|
||||||
{
|
{
|
||||||
Vector3 goalNormal = Vector3.Cross(
|
// 소스 무릎 위치 기반: 소스의 무릎 위치를 타겟 비율로 스케일
|
||||||
limb.bendGoal.position - limb.upper.position,
|
kneePos = ComputeKneePosFromSource(limb, upperLen, lowerLen, targetPos);
|
||||||
limb.target.position - limb.upper.position
|
}
|
||||||
);
|
else
|
||||||
if (goalNormal.sqrMagnitude > 0.0001f)
|
{
|
||||||
{
|
// 소스 참조 없음: 기존 bendGoal 기반 fallback (팔 등)
|
||||||
bendNormal = Vector3.Lerp(bendNormal, goalNormal.normalized, limb.bendGoalWeight);
|
kneePos = ComputeKneePosFromBendGoal(limb, upperLen, lowerLen, targetPos);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IKSolverTrigonometric.Solve(
|
// --- 본 회전 적용 ---
|
||||||
limb.upper,
|
Vector3 currentUpperDir = (limb.lower.position - limb.upper.position).normalized;
|
||||||
limb.lower,
|
Vector3 desiredUpperDir = (kneePos - limb.upper.position).normalized;
|
||||||
limb.end,
|
limb.upper.rotation = Quaternion.FromToRotation(currentUpperDir, desiredUpperDir) * limb.upper.rotation;
|
||||||
limb.target.position,
|
|
||||||
bendNormal,
|
Vector3 currentLowerDir = (limb.end.position - limb.lower.position).normalized;
|
||||||
limb.positionWeight
|
Vector3 desiredLowerDir = (targetPos - limb.lower.position).normalized;
|
||||||
);
|
limb.lower.rotation = Quaternion.FromToRotation(currentLowerDir, desiredLowerDir) * limb.lower.rotation;
|
||||||
|
|
||||||
if (limb.rotationWeight > 0.001f)
|
if (limb.rotationWeight > 0.001f)
|
||||||
{
|
{
|
||||||
@ -129,9 +139,89 @@ namespace KindRetargeting
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Vector3 GetBendNormal(LimbIK limb)
|
/// <summary>
|
||||||
|
/// 소스 무릎 위치를 hip→foot 직선 기준으로 분해(투영+수직)한 뒤
|
||||||
|
/// 타겟 비율로 스케일하여 타겟 무릎 위치를 결정합니다.
|
||||||
|
/// 소스가 역관절이면 수직 성분이 반대쪽 → 타겟도 자연스럽게 역관절.
|
||||||
|
/// </summary>
|
||||||
|
private Vector3 ComputeKneePosFromSource(LimbIK limb, float upperLen, float lowerLen, Vector3 targetPos)
|
||||||
{
|
{
|
||||||
return limb.upper.rotation * limb.localBendNormal;
|
// 소스 체인 길이
|
||||||
|
float sourceUpperLen = Vector3.Distance(limb.sourceUpper.position, limb.sourceLower.position);
|
||||||
|
float sourceLowerLen = Vector3.Distance(limb.sourceLower.position, limb.sourceEnd.position);
|
||||||
|
float sourceChain = sourceUpperLen + sourceLowerLen;
|
||||||
|
float targetChain = upperLen + lowerLen;
|
||||||
|
|
||||||
|
if (sourceChain < 0.001f || targetChain < 0.001f)
|
||||||
|
return limb.lower.position;
|
||||||
|
|
||||||
|
// 소스 hip → foot 방향
|
||||||
|
Vector3 sourceHipToFoot = limb.sourceEnd.position - limb.sourceUpper.position;
|
||||||
|
float sourceHipToFootMag = sourceHipToFoot.magnitude;
|
||||||
|
if (sourceHipToFootMag < 0.001f)
|
||||||
|
return limb.lower.position;
|
||||||
|
Vector3 sourceHipToFootDir = sourceHipToFoot / sourceHipToFootMag;
|
||||||
|
|
||||||
|
// 소스 hip → knee 벡터를 투영(직선 성분)과 수직(오프셋 성분)으로 분해
|
||||||
|
Vector3 sourceHipToKnee = limb.sourceLower.position - limb.sourceUpper.position;
|
||||||
|
float projection = Vector3.Dot(sourceHipToKnee, sourceHipToFootDir);
|
||||||
|
Vector3 rejection = sourceHipToKnee - projection * sourceHipToFootDir;
|
||||||
|
|
||||||
|
// 소스 체인 길이 대비 비율로 정규화 → 타겟 체인 길이로 스케일
|
||||||
|
float scale = targetChain / sourceChain;
|
||||||
|
float scaledProjection = projection * scale;
|
||||||
|
Vector3 scaledRejection = rejection * scale;
|
||||||
|
|
||||||
|
// 타겟 hip → foot 방향
|
||||||
|
Vector3 targetHipToFoot = targetPos - limb.upper.position;
|
||||||
|
float targetHipToFootMag = targetHipToFoot.magnitude;
|
||||||
|
if (targetHipToFootMag < 0.001f)
|
||||||
|
return limb.lower.position;
|
||||||
|
Vector3 targetHipToFootDir = targetHipToFoot / targetHipToFootMag;
|
||||||
|
|
||||||
|
// 타겟 무릎 위치: 타겟 hip에서 투영 + 수직 성분 적용
|
||||||
|
Vector3 kneePos = limb.upper.position
|
||||||
|
+ targetHipToFootDir * scaledProjection
|
||||||
|
+ scaledRejection;
|
||||||
|
|
||||||
|
return kneePos;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// bendGoal 기반 fallback (소스 참조가 없는 사지용).
|
||||||
|
/// 기존 cosine law + bendNormal 방식.
|
||||||
|
/// </summary>
|
||||||
|
private Vector3 ComputeKneePosFromBendGoal(LimbIK limb, float upperLen, float lowerLen, Vector3 targetPos)
|
||||||
|
{
|
||||||
|
float chainLength = upperLen + lowerLen;
|
||||||
|
Vector3 toTarget = targetPos - limb.upper.position;
|
||||||
|
float targetDist = toTarget.magnitude;
|
||||||
|
if (targetDist < 0.001f) return limb.lower.position;
|
||||||
|
Vector3 toTargetDir = toTarget / targetDist;
|
||||||
|
|
||||||
|
Vector3 bendNormal = limb.upper.rotation * limb.localBendNormal;
|
||||||
|
|
||||||
|
if (limb.bendGoal != null && limb.bendGoalWeight > 0.001f)
|
||||||
|
{
|
||||||
|
Vector3 goalNormal = Vector3.Cross(
|
||||||
|
limb.bendGoal.position - limb.upper.position,
|
||||||
|
targetPos - limb.upper.position
|
||||||
|
);
|
||||||
|
if (goalNormal.sqrMagnitude > 0.01f)
|
||||||
|
{
|
||||||
|
bendNormal = Vector3.Lerp(bendNormal, goalNormal.normalized, limb.bendGoalWeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bendNormal.Normalize();
|
||||||
|
|
||||||
|
float clampedDist = Mathf.Clamp(targetDist, Mathf.Abs(upperLen - lowerLen) + 0.001f, chainLength - 0.001f);
|
||||||
|
float cosUpper = (clampedDist * clampedDist + upperLen * upperLen - lowerLen * lowerLen)
|
||||||
|
/ (2f * clampedDist * upperLen);
|
||||||
|
cosUpper = Mathf.Clamp(cosUpper, -1f, 1f);
|
||||||
|
float upperAngleDeg = Mathf.Acos(cosUpper) * Mathf.Rad2Deg;
|
||||||
|
|
||||||
|
Vector3 kneeDir = Quaternion.AngleAxis(-upperAngleDeg, bendNormal) * toTargetDir;
|
||||||
|
return limb.upper.position + kneeDir * upperLen;
|
||||||
}
|
}
|
||||||
|
|
||||||
public float CalculateAutoFloorHeight(float comfortRatio = 0.98f)
|
public float CalculateAutoFloorHeight(float comfortRatio = 0.98f)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user