Fix : 리타겟팅 시스템 업데이트 대부분의 아바타 업데이트
This commit is contained in:
parent
8954204bb2
commit
db9e968499
BIN
Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.prefab
(Stored with Git LFS)
BIN
Assets/ResourcesData/Character/00.R&D/TestVRM/Zonko_VRM.prefab
(Stored with Git LFS)
Binary file not shown.
@ -1,7 +1,9 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 21faec3b318252e4d8bf08c0fd7cb57a
|
||||
labels:
|
||||
- NiloToonBakeSmoothNormalTSIntoUV8
|
||||
ModelImporter:
|
||||
serializedVersion: 22200
|
||||
serializedVersion: 24200
|
||||
internalIDToNameTable: []
|
||||
externalObjects:
|
||||
- first:
|
||||
@ -41,8 +43,6 @@ ModelImporter:
|
||||
optimizeGameObjects: 0
|
||||
removeConstantScaleCurves: 0
|
||||
motionNodeName:
|
||||
rigImportErrors:
|
||||
rigImportWarnings:
|
||||
animationImportErrors:
|
||||
animationImportWarnings:
|
||||
animationRetargetingWarnings:
|
||||
@ -83,6 +83,9 @@ ModelImporter:
|
||||
maxBonesPerVertex: 4
|
||||
minBoneWeight: 0.001
|
||||
optimizeBones: 1
|
||||
generateMeshLods: 0
|
||||
meshLodGenerationFlags: 0
|
||||
maximumMeshLod: -1
|
||||
meshOptimizationFlags: -1
|
||||
indexFormat: 0
|
||||
secondaryUVAngleDistortion: 8
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -22,23 +22,12 @@ namespace KindRetargeting
|
||||
// IK 컴포넌트 참조
|
||||
[SerializeField] public TwoBoneIKSolver ikSolver = new TwoBoneIKSolver();
|
||||
|
||||
[Header("힙 위치 보정 (로컬 좌표계 기반)")]
|
||||
[SerializeField, Range(-1, 1)]
|
||||
private float hipsOffsetX = 0f; // 캐릭터 기준 좌우 (항상 Right 방향)
|
||||
[SerializeField, Range(-1, 1)]
|
||||
private float hipsOffsetY = 0f; // 캐릭터 기준 상하 (항상 Up 방향)
|
||||
[SerializeField, Range(-1, 1)]
|
||||
private float hipsOffsetZ = 0f; // 캐릭터 기준 앞뒤 (항상 Forward 방향)
|
||||
|
||||
[HideInInspector] public float HipsWeightOffset = 1f;
|
||||
[HideInInspector] public float ChairSeatHeightOffset = 0f; // 의자 좌석 높이 오프셋 (월드 Y 기준)
|
||||
|
||||
// 축 매핑: 월드 방향(Right/Up/Forward)을 담당하는 로컬 축을 저장
|
||||
// 예: localAxisForWorldRight = (0, 0, 1) 이면 로컬 Z축이 월드 Right 방향을 담당
|
||||
// 부호는 방향을 나타냄: (0, 0, -1)이면 로컬 -Z가 월드 Right를 담당
|
||||
private Vector3 localAxisForWorldRight = Vector3.right; // 월드 좌우(Right)를 담당하는 로컬 축
|
||||
private Vector3 localAxisForWorldUp = Vector3.up; // 월드 상하(Up)를 담당하는 로컬 축
|
||||
private Vector3 localAxisForWorldForward = Vector3.forward; // 월드 앞뒤(Forward)를 담당하는 로컬 축
|
||||
// 캐릭터 기준 "위(Up)" 방향을 담당하는 로컬 축 (자동 힙 보정에서 사용)
|
||||
// 예: (0,1,0) 이면 로컬 Y축이 월드 Up. 부호는 방향(0,-1,0이면 로컬 -Y가 Up)
|
||||
private Vector3 localAxisForWorldUp = Vector3.up;
|
||||
|
||||
[Header("축 정규화 정보 (읽기 전용)")]
|
||||
[SerializeField]
|
||||
@ -73,15 +62,8 @@ namespace KindRetargeting
|
||||
// 본별 회전 오프셋을 저장하는 딕셔너리
|
||||
private Dictionary<HumanBodyBones, Quaternion> rotationOffsets = new Dictionary<HumanBodyBones, Quaternion>();
|
||||
|
||||
// HumanBodyBones.LastBone을 이용한 본 순회 범위
|
||||
private int lastBoneIndex = 55; // 0~54: 몸체 + 손가락 전부
|
||||
|
||||
[Header("무릎 안/밖 조정")]
|
||||
[SerializeField, Range(-1f, 1f)]
|
||||
private float kneeInOutWeight = 0f; // 무릎 안/밖 위치 조정 가중치
|
||||
[Header("무릎 앞/뒤 조정")]
|
||||
[SerializeField, Range(-1f, 1f)]
|
||||
private float kneeFrontBackWeight = 0.4f; // 무릎 앞/뒤 위치 조정 가중치
|
||||
// HumanBodyBones 본 순회 범위 (0~54: 몸체 + 손가락 전부)
|
||||
private const int lastBoneIndex = 55;
|
||||
|
||||
[Header("발 IK 위치 조정")]
|
||||
[SerializeField, Range(-1f, 1f)]
|
||||
@ -153,11 +135,6 @@ namespace KindRetargeting
|
||||
[System.Serializable]
|
||||
private class RetargetingSettings
|
||||
{
|
||||
public float hipsOffsetX; // 변경: hipsWeight → hipsOffsetX/Y/Z
|
||||
public float hipsOffsetY;
|
||||
public float hipsOffsetZ;
|
||||
public float kneeInOutWeight;
|
||||
public float kneeFrontBackWeight;
|
||||
public float footFrontBackOffset; // 발 앞뒤 오프셋
|
||||
public float footInOutOffset; // 발 안쪽/바깥쪽 오프셋
|
||||
public float floorHeight;
|
||||
@ -175,11 +152,9 @@ namespace KindRetargeting
|
||||
}
|
||||
|
||||
|
||||
// IK 조인트 싱을 위한 구조체
|
||||
// IK 조인트 캐시 구조체 (팔꿈치 bendGoal 위치 결정용)
|
||||
private struct IKJoints
|
||||
{
|
||||
public Transform leftLowerLeg;
|
||||
public Transform rightLowerLeg;
|
||||
public Transform leftLowerArm;
|
||||
public Transform rightLowerArm;
|
||||
}
|
||||
@ -281,7 +256,7 @@ namespace KindRetargeting
|
||||
/// - 아바타 B: 로컬 Z가 월드 Up을 가리킴 → localAxisForWorldUp = (0, 0, 1)
|
||||
/// - 아바타 C: 로컬 -Z가 월드 Up을 가리킴 → localAxisForWorldUp = (0, 0, -1)
|
||||
///
|
||||
/// 이를 통해 hipsOffsetY는 항상 "위/아래" 방향으로 작동합니다.
|
||||
/// 이를 통해 자동 힙 보정은 항상 캐릭터 기준 "위/아래" 방향으로 작동합니다.
|
||||
/// </summary>
|
||||
private void CalculateAxisNormalizer()
|
||||
{
|
||||
@ -295,19 +270,12 @@ namespace KindRetargeting
|
||||
Vector3 localYInWorld = hips.TransformDirection(Vector3.up).normalized;
|
||||
Vector3 localZInWorld = hips.TransformDirection(Vector3.forward).normalized;
|
||||
|
||||
// 월드 Right(X)에 가장 가까운 로컬 축 찾기
|
||||
localAxisForWorldRight = FindBestLocalAxisForWorld(localXInWorld, localYInWorld, localZInWorld, Vector3.right, out string rightAxisName);
|
||||
|
||||
// 월드 Up(Y)에 가장 가까운 로컬 축 찾기
|
||||
// 월드 Right/Up/Forward 에 가장 가까운 로컬 축을 분석 (디버그 표시 + Up 매핑용)
|
||||
Vector3 localAxisForWorldRight = FindBestLocalAxisForWorld(localXInWorld, localYInWorld, localZInWorld, Vector3.right, out string rightAxisName);
|
||||
localAxisForWorldUp = FindBestLocalAxisForWorld(localXInWorld, localYInWorld, localZInWorld, Vector3.up, out string upAxisName);
|
||||
Vector3 localAxisForWorldForward = FindBestLocalAxisForWorld(localXInWorld, localYInWorld, localZInWorld, Vector3.forward, out string forwardAxisName);
|
||||
|
||||
// 월드 Forward(Z)에 가장 가까운 로컬 축 찾기
|
||||
localAxisForWorldForward = FindBestLocalAxisForWorld(localXInWorld, localYInWorld, localZInWorld, Vector3.forward, out string forwardAxisName);
|
||||
|
||||
// 디버그용: 각 오프셋이 어느 로컬 축에 매핑되는지 표시
|
||||
// X: 좌우 오프셋이 사용하는 로컬 축 (1=X, 2=Y, 3=Z, 부호는 방향)
|
||||
// Y: 상하 오프셋이 사용하는 로컬 축
|
||||
// Z: 앞뒤 오프셋이 사용하는 로컬 축
|
||||
// 디버그용: X=좌우, Y=상하, Z=앞뒤 매핑 (1=X, 2=Y, 3=Z, 부호=방향)
|
||||
debugAxisNormalizer = new Vector3(
|
||||
GetAxisIndex(localAxisForWorldRight),
|
||||
GetAxisIndex(localAxisForWorldUp),
|
||||
@ -454,11 +422,6 @@ namespace KindRetargeting
|
||||
|
||||
var settings = new RetargetingSettings
|
||||
{
|
||||
hipsOffsetX = hipsOffsetX,
|
||||
hipsOffsetY = hipsOffsetY,
|
||||
hipsOffsetZ = hipsOffsetZ,
|
||||
kneeInOutWeight = kneeInOutWeight,
|
||||
kneeFrontBackWeight = kneeFrontBackWeight,
|
||||
footFrontBackOffset = footFrontBackOffset,
|
||||
footInOutOffset = footInOutOffset,
|
||||
floorHeight = floorHeight,
|
||||
@ -512,11 +475,6 @@ namespace KindRetargeting
|
||||
var settings = JsonUtility.FromJson<RetargetingSettings>(json);
|
||||
|
||||
// 설정 적용
|
||||
hipsOffsetX = settings.hipsOffsetX;
|
||||
hipsOffsetY = settings.hipsOffsetY;
|
||||
hipsOffsetZ = settings.hipsOffsetZ;
|
||||
kneeInOutWeight = settings.kneeInOutWeight;
|
||||
kneeFrontBackWeight = settings.kneeFrontBackWeight;
|
||||
footFrontBackOffset = settings.footFrontBackOffset;
|
||||
footInOutOffset = settings.footInOutOffset;
|
||||
floorHeight = settings.floorHeight;
|
||||
@ -702,11 +660,9 @@ namespace KindRetargeting
|
||||
return;
|
||||
}
|
||||
|
||||
// IK 조인트들 캐싱
|
||||
// IK 조인트 캐싱 (팔꿈치 bendGoal 용)
|
||||
sourceIKJoints = new IKJoints
|
||||
{
|
||||
leftLowerLeg = sourceIKRoot.Find("LeftLowerLeg"),
|
||||
rightLowerLeg = sourceIKRoot.Find("RightLowerLeg"),
|
||||
leftLowerArm = sourceIKRoot.Find("LeftLowerArm"),
|
||||
rightLowerArm = sourceIKRoot.Find("RightLowerArm")
|
||||
};
|
||||
@ -933,30 +889,12 @@ namespace KindRetargeting
|
||||
targetHips.rotation = finalHipsRotation;
|
||||
}
|
||||
|
||||
// 2. 캐릭터 기준 로컬 오프셋 계산 (축 정규화 적용)
|
||||
//
|
||||
// 문제: 아바타마다 힙의 로컬 축 방향이 다름
|
||||
// - 아바타 A: 로컬 Y가 "위", 로컬 Z가 "앞"
|
||||
// - 아바타 B: 로컬 Z가 "위", 로컬 X가 "앞"
|
||||
//
|
||||
// 해결: T-포즈에서 계산한 축 매핑을 사용
|
||||
// - localAxisForWorldRight: 실제로 "오른쪽"을 가리키는 로컬 축
|
||||
// - localAxisForWorldUp: 실제로 "위"를 가리키는 로컬 축
|
||||
// - localAxisForWorldForward: 실제로 "앞"을 가리키는 로컬 축
|
||||
//
|
||||
// 이렇게 하면 모든 아바타에서 동일하게 작동합니다.
|
||||
|
||||
// 힙의 현재 회전을 기준으로, 정규화된 방향 벡터 계산
|
||||
Vector3 characterRight = finalHipsRotation * localAxisForWorldRight;
|
||||
// 2. 다리 길이 차 + Hips↔UpperLeg 갭으로 힙 상하 자동 보정 (캐릭터 Up 방향)
|
||||
Vector3 characterUp = finalHipsRotation * localAxisForWorldUp;
|
||||
Vector3 characterForward = finalHipsRotation * localAxisForWorldForward;
|
||||
float autoHipsOffsetY = ComputeAutoHipsOffsetY();
|
||||
Vector3 characterOffset = characterUp * (autoHipsOffsetY * HipsWeightOffset);
|
||||
|
||||
Vector3 characterOffset =
|
||||
characterRight * (hipsOffsetX * HipsWeightOffset) + // 캐릭터 기준 좌우
|
||||
characterUp * (hipsOffsetY * HipsWeightOffset) + // 캐릭터 기준 상하
|
||||
characterForward * (hipsOffsetZ * HipsWeightOffset); // 캐릭터 기준 앞뒤
|
||||
|
||||
// 3. 힙 위치 동기화 + 캐릭터 기준 오프셋 적용
|
||||
// 3. 힙 위치 동기화 + 자동 보정 오프셋 적용
|
||||
Vector3 adjustedPosition = sourceHips.position + characterOffset;
|
||||
|
||||
// 4. 바닥 높이 추가 (월드 Y축 - 바닥은 항상 월드 기준)
|
||||
@ -975,6 +913,37 @@ namespace KindRetargeting
|
||||
SyncBoneRotations(skipBone: HumanBodyBones.Hips);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 매 프레임 자동으로 적용되는 힙 상하 보정값.
|
||||
/// = (타겟 다리길이 - 소스 다리길이) + (타겟 Hips↔UpperLeg 갭) × avatarScale
|
||||
///
|
||||
/// 첫 항: 다리 길이 차이로 발이 뜨거나 묻히는 현상 보정.
|
||||
/// 둘째 항: Hips 본이 UpperLeg보다 위에 있는 아바타 추가 보정.
|
||||
/// </summary>
|
||||
private float ComputeAutoHipsOffsetY()
|
||||
{
|
||||
if (optitrackSource == null || targetAnimator == null) return 0f;
|
||||
|
||||
Transform sUp = optitrackSource.GetBoneTransform(HumanBodyBones.LeftUpperLeg);
|
||||
Transform sLo = optitrackSource.GetBoneTransform(HumanBodyBones.LeftLowerLeg);
|
||||
Transform sFt = optitrackSource.GetBoneTransform(HumanBodyBones.LeftFoot);
|
||||
Transform tUp = targetAnimator.GetBoneTransform(HumanBodyBones.LeftUpperLeg);
|
||||
Transform tLo = targetAnimator.GetBoneTransform(HumanBodyBones.LeftLowerLeg);
|
||||
Transform tFt = targetAnimator.GetBoneTransform(HumanBodyBones.LeftFoot);
|
||||
Transform tHi = targetAnimator.GetBoneTransform(HumanBodyBones.Hips);
|
||||
|
||||
if (sUp == null || sLo == null || sFt == null
|
||||
|| tUp == null || tLo == null || tFt == null || tHi == null)
|
||||
return 0f;
|
||||
|
||||
float sourceLeg = Vector3.Distance(sUp.position, sLo.position) + Vector3.Distance(sLo.position, sFt.position);
|
||||
float targetLeg = Vector3.Distance(tUp.position, tLo.position) + Vector3.Distance(tLo.position, tFt.position);
|
||||
if (sourceLeg < 0.01f || targetLeg < 0.01f) return 0f;
|
||||
|
||||
float hipsToLegGap = tHi.position.y - tUp.position.y;
|
||||
return (targetLeg - sourceLeg) + hipsToLegGap * avatarScale;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 힙을 제외한 모든 본의 회전을 오프셋을 적용하여 동기화합니다.
|
||||
/// </summary>
|
||||
@ -1236,11 +1205,9 @@ namespace KindRetargeting
|
||||
UpdateEndTarget(ikSolver.leftLeg.target, HumanBodyBones.LeftFoot);
|
||||
UpdateEndTarget(ikSolver.rightLeg.target, HumanBodyBones.RightFoot);
|
||||
|
||||
// 팔 bendGoal만 갱신 (다리는 ComputeKneePosFromSource가 소스 무릎으로 풀이하므로 bendGoal 사용 안 함)
|
||||
updatejointTarget(ikSolver.leftArm.bendGoal, HumanBodyBones.LeftLowerArm);
|
||||
updatejointTarget(ikSolver.rightArm.bendGoal, HumanBodyBones.RightLowerArm);
|
||||
updatejointTarget(ikSolver.leftLeg.bendGoal, HumanBodyBones.LeftLowerLeg);
|
||||
updatejointTarget(ikSolver.rightLeg.bendGoal, HumanBodyBones.RightLowerLeg);
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -1305,21 +1272,10 @@ namespace KindRetargeting
|
||||
Vector3 targetBone = targetAnimator.GetBoneTransform(bone).position;
|
||||
Vector3 sourceIKpoint;
|
||||
float zOffset = 0f;
|
||||
float xOffset = 0f;
|
||||
float yOffset = 0f;
|
||||
|
||||
// bone의 이름에 따라 적절한 IK 조인트 오프셋 선택
|
||||
switch (bone)
|
||||
{
|
||||
case HumanBodyBones.LeftLowerLeg:
|
||||
case HumanBodyBones.RightLowerLeg:
|
||||
sourceIKpoint = bone == HumanBodyBones.LeftLowerLeg ?
|
||||
sourceIKJoints.leftLowerLeg.position :
|
||||
sourceIKJoints.rightLowerLeg.position;
|
||||
zOffset = kneeFrontBackWeight; // 무릎 앞/뒤 조정
|
||||
yOffset = floorHeight;
|
||||
xOffset = kneeInOutWeight * (bone == HumanBodyBones.LeftLowerLeg ? -1f : 1f); // 무릎 안/밖 조정
|
||||
break;
|
||||
case HumanBodyBones.LeftLowerArm:
|
||||
case HumanBodyBones.RightLowerArm:
|
||||
sourceIKpoint = bone == HumanBodyBones.LeftLowerArm ?
|
||||
@ -1337,11 +1293,8 @@ namespace KindRetargeting
|
||||
target.position = targetBone;
|
||||
target.rotation = LookatIK;
|
||||
|
||||
// LookatIK 기준으로 프셋 적용
|
||||
Vector3 offset = LookatIK * new Vector3(xOffset, 0, zOffset);
|
||||
Vector3 offset2 = new Vector3(0, yOffset, 0);
|
||||
target.position += offset;
|
||||
target.position += offset2;
|
||||
// LookatIK 기준으로 z축 오프셋 적용 (팔꿈치 뒤쪽)
|
||||
target.position += LookatIK * new Vector3(0f, 0f, zOffset);
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -1427,18 +1380,6 @@ namespace KindRetargeting
|
||||
return File.Exists(filePath);
|
||||
}
|
||||
|
||||
// 무릎 앞/뒤뒤 위치 조정을 위한 public 메서드 추가
|
||||
public void SetKneeFrontBackOffset(float offset)
|
||||
{
|
||||
kneeFrontBackWeight = offset;
|
||||
}
|
||||
|
||||
// 무릎 조정을 위한 public 메서드들
|
||||
public void SetKneeInOutOffset(float offset)
|
||||
{
|
||||
kneeInOutWeight = offset;
|
||||
}
|
||||
|
||||
public void ResetPoseAndCache()
|
||||
{
|
||||
// 캐시 파일 삭제
|
||||
@ -1552,12 +1493,6 @@ namespace KindRetargeting
|
||||
avatarScale = Mathf.Clamp(scale, 0.1f, 3f);
|
||||
}
|
||||
|
||||
// 현재 스케일을 가져오는 메서드
|
||||
public float GetAvatarScale()
|
||||
{
|
||||
return avatarScale;
|
||||
}
|
||||
|
||||
// 외부에서 머리 크기를 설정할 수 있는 public 메서드
|
||||
public void SetHeadScale(float scale)
|
||||
{
|
||||
|
||||
@ -34,16 +34,6 @@ namespace KindRetargeting
|
||||
// ── 힙 위치 보정 ──
|
||||
root.Add(BuildHipsSection());
|
||||
|
||||
// ── 무릎 위치 조정 ──
|
||||
var kneeFoldout = new Foldout { text = "무릎 위치 조정", value = false };
|
||||
var kneeFB = new Slider("무릎 앞/뒤", -1f, 1f) { showInputField = true };
|
||||
kneeFB.BindProperty(serializedObject.FindProperty("kneeFrontBackWeight"));
|
||||
kneeFoldout.Add(kneeFB);
|
||||
var kneeIO = new Slider("무릎 안/밖", -1f, 1f) { showInputField = true };
|
||||
kneeIO.BindProperty(serializedObject.FindProperty("kneeInOutWeight"));
|
||||
kneeFoldout.Add(kneeIO);
|
||||
root.Add(kneeFoldout);
|
||||
|
||||
// ── 발 IK 위치 조정 ──
|
||||
var footFoldout = new Foldout { text = "발 IK 위치 조정", value = false };
|
||||
var footFB = new Slider("발 앞/뒤", -1f, 1f) { showInputField = true };
|
||||
@ -84,54 +74,17 @@ namespace KindRetargeting
|
||||
}
|
||||
|
||||
// ========== 힙 위치 보정 ==========
|
||||
// 힙 상하 보정은 매 프레임 자동 계산됨 (CustomRetargetingScript.ComputeAutoHipsOffsetY).
|
||||
// 의자 앉기 높이만 수동 조정 가능.
|
||||
|
||||
private VisualElement BuildHipsSection()
|
||||
{
|
||||
var foldout = new Foldout { text = "힙 위치 보정 (로컬 좌표계)", value = true };
|
||||
var foldout = new Foldout { text = "힙 위치 보정", value = true };
|
||||
|
||||
var axisInfo = new HelpBox("플레이 모드에서 축 매핑 정보가 표시됩니다.", HelpBoxMessageType.Info);
|
||||
foldout.Add(axisInfo);
|
||||
|
||||
foldout.schedule.Execute(() =>
|
||||
{
|
||||
if (target == null) return;
|
||||
serializedObject.Update();
|
||||
var axisProp = serializedObject.FindProperty("debugAxisNormalizer");
|
||||
if (axisProp == null || !Application.isPlaying) { axisInfo.text = "플레이 모드에서 축 매핑 정보가 표시됩니다."; return; }
|
||||
Vector3 m = axisProp.vector3Value;
|
||||
if (m == Vector3.one) { axisInfo.text = "플레이 모드에서 축 매핑 정보가 표시됩니다."; return; }
|
||||
string A(float v) => Mathf.RoundToInt(Mathf.Abs(v)) switch { 1 => (v > 0 ? "+X" : "-X"), 2 => (v > 0 ? "+Y" : "-Y"), 3 => (v > 0 ? "+Z" : "-Z"), _ => "?" };
|
||||
axisInfo.text = $"축 매핑: 좌우→{A(m.x)} 상하→{A(m.y)} 앞뒤→{A(m.z)}";
|
||||
}).Every(500);
|
||||
|
||||
var hx = new Slider("← 좌우 →", -1f, 1f) { showInputField = true };
|
||||
hx.BindProperty(serializedObject.FindProperty("hipsOffsetX"));
|
||||
foldout.Add(hx);
|
||||
var hy = new Slider("↓ 상하 ↑", -1f, 1f) { showInputField = true };
|
||||
hy.BindProperty(serializedObject.FindProperty("hipsOffsetY"));
|
||||
foldout.Add(hy);
|
||||
var hz = new Slider("← 앞뒤 →", -1f, 1f) { showInputField = true };
|
||||
hz.BindProperty(serializedObject.FindProperty("hipsOffsetZ"));
|
||||
foldout.Add(hz);
|
||||
|
||||
// 의자 앉기 높이
|
||||
var chairSlider = new Slider("의자 앉기 높이", -1f, 1f) { showInputField = true, tooltip = "의자에 앉을 때 엉덩이 높이 조정" };
|
||||
chairSlider.BindProperty(serializedObject.FindProperty("limbWeight.chairSeatHeightOffset"));
|
||||
foldout.Add(chairSlider);
|
||||
|
||||
// 다리 길이 자동 보정 버튼
|
||||
var autoHipsBtn = new Button(() =>
|
||||
{
|
||||
if (!Application.isPlaying) { Debug.LogWarning("플레이 모드에서만 사용 가능합니다."); return; }
|
||||
var script = (CustomRetargetingScript)target;
|
||||
float offset = CalculateHipsOffsetFromLegDifference(script);
|
||||
serializedObject.FindProperty("hipsOffsetY").floatValue = offset;
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
script.SaveSettings();
|
||||
}) { text = "다리 길이 자동 보정", tooltip = "소스/타겟 다리 길이 차이로 힙 상하 오프셋을 자동 계산합니다." };
|
||||
autoHipsBtn.style.marginTop = 4; autoHipsBtn.style.height = 25;
|
||||
foldout.Add(autoHipsBtn);
|
||||
|
||||
return foldout;
|
||||
}
|
||||
|
||||
@ -419,7 +372,6 @@ namespace KindRetargeting
|
||||
|
||||
script.ResetScale();
|
||||
so.FindProperty("avatarScale").floatValue = 1f;
|
||||
so.FindProperty("hipsOffsetY").floatValue = CalculateHipsOffsetFromLegDifference(script);
|
||||
so.ApplyModifiedProperties();
|
||||
|
||||
EditorApplication.delayCall += () =>
|
||||
@ -440,7 +392,6 @@ namespace KindRetargeting
|
||||
{
|
||||
if (script == null) return;
|
||||
var so3 = new SerializedObject(script);
|
||||
so3.FindProperty("hipsOffsetY").floatValue = CalculateHipsOffsetFromLegDifference(script);
|
||||
|
||||
var xP = so3.FindProperty("headRotationOffsetX");
|
||||
var yP = so3.FindProperty("headRotationOffsetY");
|
||||
@ -457,37 +408,6 @@ namespace KindRetargeting
|
||||
|
||||
// ========== 유틸리티 ==========
|
||||
|
||||
private float CalculateHipsOffsetFromLegDifference(CustomRetargetingScript script)
|
||||
{
|
||||
var source = script.optitrackSource;
|
||||
Animator targetAnim = script.targetAnimator;
|
||||
if (source == null || targetAnim == null) return 0f;
|
||||
|
||||
float sourceLeg = GetSourceLegLength(source);
|
||||
float targetLeg = GetLegLength(targetAnim);
|
||||
if (sourceLeg < 0.01f || targetLeg < 0.01f) return 0f;
|
||||
|
||||
return targetLeg - sourceLeg;
|
||||
}
|
||||
|
||||
private float GetLegLength(Animator animator)
|
||||
{
|
||||
Transform upper = animator.GetBoneTransform(HumanBodyBones.LeftUpperLeg);
|
||||
Transform lower = animator.GetBoneTransform(HumanBodyBones.LeftLowerLeg);
|
||||
Transform foot = animator.GetBoneTransform(HumanBodyBones.LeftFoot);
|
||||
if (upper == null || lower == null || foot == null) return 0f;
|
||||
return Vector3.Distance(upper.position, lower.position) + Vector3.Distance(lower.position, foot.position);
|
||||
}
|
||||
|
||||
private float GetSourceLegLength(OptitrackSkeletonAnimator_Mingle source)
|
||||
{
|
||||
Transform upper = source.GetBoneTransform(HumanBodyBones.LeftUpperLeg);
|
||||
Transform lower = source.GetBoneTransform(HumanBodyBones.LeftLowerLeg);
|
||||
Transform foot = source.GetBoneTransform(HumanBodyBones.LeftFoot);
|
||||
if (upper == null || lower == null || foot == null) return 0f;
|
||||
return Vector3.Distance(upper.position, lower.position) + Vector3.Distance(lower.position, foot.position);
|
||||
}
|
||||
|
||||
private void CalibrateHeadToForward(SerializedObject so, SerializedProperty xProp, SerializedProperty yProp, SerializedProperty zProp)
|
||||
{
|
||||
CustomRetargetingScript script = so.targetObject as CustomRetargetingScript;
|
||||
|
||||
@ -198,19 +198,6 @@ public class RetargetingControlWindow : EditorWindow
|
||||
// 힙 위치 보정
|
||||
panel.Add(BuildHipsSection(script, so));
|
||||
|
||||
// 무릎 위치 조정
|
||||
var kneeFoldout = new Foldout { text = "무릎 위치 조정", value = false };
|
||||
var kneeContainer = new VisualElement();
|
||||
var kneeFB = new Slider("무릎 앞/뒤 가중치", -1f, 1f) { showInputField = true };
|
||||
kneeFB.BindProperty(so.FindProperty("kneeFrontBackWeight"));
|
||||
kneeContainer.Add(kneeFB);
|
||||
var kneeIO = new Slider("무릎 안/밖 가중치", -1f, 1f) { showInputField = true };
|
||||
kneeIO.BindProperty(so.FindProperty("kneeInOutWeight"));
|
||||
kneeContainer.Add(kneeIO);
|
||||
kneeFoldout.Add(kneeContainer);
|
||||
kneeContainer.Bind(so);
|
||||
panel.Add(kneeFoldout);
|
||||
|
||||
// 발 IK 위치 조정
|
||||
var footFoldout = new Foldout { text = "발 IK 위치 조정", value = false };
|
||||
var footContainer = new VisualElement();
|
||||
@ -245,16 +232,6 @@ public class RetargetingControlWindow : EditorWindow
|
||||
if (headScaleProp != null)
|
||||
scaleContainer.Add(new PropertyField(headScaleProp, "머리 크기"));
|
||||
|
||||
// 아바타 크기 변경 시 다리 길이 자동 보정 (실시간)
|
||||
scaleContainer.TrackPropertyValue(so.FindProperty("avatarScale"), _ =>
|
||||
{
|
||||
if (!Application.isPlaying || script == null) return;
|
||||
var sox = new SerializedObject(script);
|
||||
sox.FindProperty("hipsOffsetY").floatValue = CalculateHipsOffsetFromLegDifference(script);
|
||||
sox.ApplyModifiedProperties();
|
||||
sox.Dispose();
|
||||
});
|
||||
|
||||
scaleContainer.Bind(so);
|
||||
scaleFoldout.Add(scaleContainer);
|
||||
panel.Add(scaleFoldout);
|
||||
@ -336,65 +313,18 @@ public class RetargetingControlWindow : EditorWindow
|
||||
}
|
||||
|
||||
// ========== Hips Settings ==========
|
||||
// 힙 상하 보정은 매 프레임 자동 (CustomRetargetingScript.ComputeAutoHipsOffsetY).
|
||||
// 의자 앉기 높이만 수동 조정.
|
||||
|
||||
private VisualElement BuildHipsSection(CustomRetargetingScript script, SerializedObject so)
|
||||
{
|
||||
var foldout = new Foldout { text = "힙 위치 보정 (로컬)", value = false };
|
||||
var foldout = new Foldout { text = "힙 위치 보정", value = false };
|
||||
var container = new VisualElement();
|
||||
|
||||
// 축 매핑 정보
|
||||
var axisLabel = new Label { style = { fontSize = 10, color = new Color(0.6f, 0.6f, 0.6f) } };
|
||||
container.Add(axisLabel);
|
||||
|
||||
container.schedule.Execute(() =>
|
||||
{
|
||||
try { if (so == null || so.targetObject == null) return; }
|
||||
catch (System.Exception) { return; }
|
||||
so.Update();
|
||||
var normProp = so.FindProperty("debugAxisNormalizer");
|
||||
if (normProp == null || !Application.isPlaying) { axisLabel.text = ""; return; }
|
||||
Vector3 m = normProp.vector3Value;
|
||||
if (m == Vector3.one) { axisLabel.text = ""; return; }
|
||||
string A(float v) => Mathf.RoundToInt(Mathf.Abs(v)) switch { 1 => (v > 0 ? "+X" : "-X"), 2 => (v > 0 ? "+Y" : "-Y"), 3 => (v > 0 ? "+Z" : "-Z"), _ => "?" };
|
||||
axisLabel.text = $"축 매핑: 좌우→{A(m.x)} 상하→{A(m.y)} 앞뒤→{A(m.z)}";
|
||||
}).Every(500);
|
||||
|
||||
var hx = new Slider("← 좌우 →", -1f, 1f) { showInputField = true };
|
||||
hx.BindProperty(so.FindProperty("hipsOffsetX"));
|
||||
container.Add(hx);
|
||||
|
||||
var hy = new Slider("↓ 상하 ↑", -1f, 1f) { showInputField = true };
|
||||
hy.BindProperty(so.FindProperty("hipsOffsetY"));
|
||||
container.Add(hy);
|
||||
|
||||
var hz = new Slider("← 앞뒤 →", -1f, 1f) { showInputField = true };
|
||||
hz.BindProperty(so.FindProperty("hipsOffsetZ"));
|
||||
container.Add(hz);
|
||||
|
||||
// 의자 앉기 높이
|
||||
var chairSlider = new Slider("의자 앉기 높이", -1f, 1f) { showInputField = true, tooltip = "의자에 앉을 때 엉덩이 높이 조정 (월드 Y 기준)" };
|
||||
chairSlider.BindProperty(so.FindProperty("limbWeight.chairSeatHeightOffset"));
|
||||
container.Add(chairSlider);
|
||||
|
||||
// 다리 길이 자동 보정 버튼
|
||||
var autoHipsBtn = new Button(() =>
|
||||
{
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
Debug.LogWarning("다리 길이 자동 보정은 플레이 모드에서만 사용 가능합니다.");
|
||||
return;
|
||||
}
|
||||
float offset = CalculateHipsOffsetFromLegDifference(script);
|
||||
var hipsProp = so.FindProperty("hipsOffsetY");
|
||||
hipsProp.floatValue = offset;
|
||||
so.ApplyModifiedProperties();
|
||||
script.SaveSettings();
|
||||
Debug.Log($"자동 보정 완료: hipsOffsetY = {offset:F4}");
|
||||
}) { text = "다리 길이 자동 보정", tooltip = "소스/타겟 다리 길이 차이로 힙 상하 오프셋을 자동 계산합니다. (플레이 모드 전용)" };
|
||||
autoHipsBtn.style.marginTop = 4;
|
||||
autoHipsBtn.style.height = 25;
|
||||
container.Add(autoHipsBtn);
|
||||
|
||||
container.Bind(so);
|
||||
foldout.Add(container);
|
||||
return foldout;
|
||||
@ -836,7 +766,8 @@ public class RetargetingControlWindow : EditorWindow
|
||||
// ========== Auto Full Calibration ==========
|
||||
|
||||
/// <summary>
|
||||
/// 소스/타겟 목 높이 비율로 avatarScale을 맞추고, 다리 길이 차이로 hipsOffsetY를 보정합니다.
|
||||
/// 소스/타겟 목 높이 비율로 avatarScale을 맞추고, 머리 정면을 캘리브레이션합니다.
|
||||
/// (힙 상하 보정은 매 프레임 자동 처리됨)
|
||||
/// </summary>
|
||||
private void AutoCalibrateAll(CustomRetargetingScript script, SerializedObject so)
|
||||
{
|
||||
@ -849,11 +780,10 @@ public class RetargetingControlWindow : EditorWindow
|
||||
return;
|
||||
}
|
||||
|
||||
// ── 프레임 1: 스케일 리셋 + 다리 보정 ──
|
||||
// ── 프레임 1: 스케일 리셋 ──
|
||||
script.ResetScale();
|
||||
var scaleProp = so.FindProperty("avatarScale");
|
||||
scaleProp.floatValue = 1f;
|
||||
so.FindProperty("hipsOffsetY").floatValue = CalculateHipsOffsetFromLegDifference(script);
|
||||
so.ApplyModifiedProperties();
|
||||
|
||||
// ── 프레임 2: 리타게팅 반영 후 목 높이 측정 → avatarScale 설정 ──
|
||||
@ -879,14 +809,12 @@ public class RetargetingControlWindow : EditorWindow
|
||||
|
||||
Debug.Log($"크기 보정: 소스 목 Y={sourceNeckY:F4}, 타겟 목 Y={targetNeckY:F4} → avatarScale = {scaleRatio:F3}");
|
||||
|
||||
// ── 프레임 3: 스케일 반영 후 다리 재보정 + 머리 정면 캘리 + 저장 ──
|
||||
// ── 프레임 3: 머리 정면 캘리 + 저장 ──
|
||||
EditorApplication.delayCall += () =>
|
||||
{
|
||||
if (script == null) return;
|
||||
|
||||
var so3 = new SerializedObject(script);
|
||||
float finalHipsOffset = CalculateHipsOffsetFromLegDifference(script);
|
||||
so3.FindProperty("hipsOffsetY").floatValue = finalHipsOffset;
|
||||
|
||||
var xProp = so3.FindProperty("headRotationOffsetX");
|
||||
var yProp = so3.FindProperty("headRotationOffsetY");
|
||||
@ -898,69 +826,11 @@ public class RetargetingControlWindow : EditorWindow
|
||||
so3.Dispose();
|
||||
|
||||
script.SaveSettings();
|
||||
Debug.Log($"전체 자동 보정 완료: avatarScale = {scaleRatio:F3}, hipsOffsetY = {finalHipsOffset:F4}m");
|
||||
Debug.Log($"전체 자동 보정 완료: avatarScale = {scaleRatio:F3}");
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
// ========== Auto Hips Offset ==========
|
||||
|
||||
/// <summary>
|
||||
/// 소스/타겟 다리 길이 차이로 힙 상하 오프셋을 계산합니다.
|
||||
/// 타겟 다리가 소스보다 짧으면 → 양수 (힙을 올려서 다리를 펴줌)
|
||||
/// 타겟 다리가 소스보다 길면 → 음수 (힙을 내려서 다리를 펴줌)
|
||||
/// </summary>
|
||||
private float CalculateHipsOffsetFromLegDifference(CustomRetargetingScript script)
|
||||
{
|
||||
var source = script.optitrackSource;
|
||||
Animator target = script.targetAnimator;
|
||||
|
||||
if (source == null || target == null || !target.isHuman)
|
||||
{
|
||||
Debug.LogWarning("소스 OptiTrack 또는 타겟 Animator가 설정되지 않았습니다.");
|
||||
return 0f;
|
||||
}
|
||||
|
||||
float sourceLeg = GetSourceLegLength(source);
|
||||
float targetLeg = GetLegLength(target);
|
||||
|
||||
if (sourceLeg < 0.01f || targetLeg < 0.01f)
|
||||
{
|
||||
Debug.LogWarning("다리 길이를 계산할 수 없습니다. 본이 올바르게 설정되어 있는지 확인해주세요.");
|
||||
return 0f;
|
||||
}
|
||||
|
||||
// 소스 다리가 더 길면 타겟이 뜨므로 힙을 내려야 함 (음수)
|
||||
// 소스 다리가 더 짧으면 타겟 다리가 구부러지므로 힙을 올려야 함 (양수)
|
||||
float diff = targetLeg - sourceLeg;
|
||||
Debug.Log($"소스 다리 길이: {sourceLeg:F4}, 타겟 다리 길이: {targetLeg:F4}, 힙 오프셋: {diff:F4}m");
|
||||
return diff;
|
||||
}
|
||||
|
||||
private float GetLegLength(Animator animator)
|
||||
{
|
||||
Transform upperLeg = animator.GetBoneTransform(HumanBodyBones.LeftUpperLeg);
|
||||
Transform lowerLeg = animator.GetBoneTransform(HumanBodyBones.LeftLowerLeg);
|
||||
Transform foot = animator.GetBoneTransform(HumanBodyBones.LeftFoot);
|
||||
|
||||
if (upperLeg == null || lowerLeg == null || foot == null) return 0f;
|
||||
|
||||
return Vector3.Distance(upperLeg.position, lowerLeg.position)
|
||||
+ Vector3.Distance(lowerLeg.position, foot.position);
|
||||
}
|
||||
|
||||
private float GetSourceLegLength(OptitrackSkeletonAnimator_Mingle source)
|
||||
{
|
||||
Transform upperLeg = source.GetBoneTransform(HumanBodyBones.LeftUpperLeg);
|
||||
Transform lowerLeg = source.GetBoneTransform(HumanBodyBones.LeftLowerLeg);
|
||||
Transform foot = source.GetBoneTransform(HumanBodyBones.LeftFoot);
|
||||
|
||||
if (upperLeg == null || lowerLeg == null || foot == null) return 0f;
|
||||
|
||||
return Vector3.Distance(upperLeg.position, lowerLeg.position)
|
||||
+ Vector3.Distance(lowerLeg.position, foot.position);
|
||||
}
|
||||
|
||||
private void CalibrateHeadToForward(SerializedObject so, SerializedProperty xProp, SerializedProperty yProp, SerializedProperty zProp)
|
||||
{
|
||||
CustomRetargetingScript script = so.targetObject as CustomRetargetingScript;
|
||||
|
||||
BIN
Assets/Scripts/KindRetargeting/README.md
(Stored with Git LFS)
BIN
Assets/Scripts/KindRetargeting/README.md
(Stored with Git LFS)
Binary file not shown.
@ -166,13 +166,6 @@ namespace KindRetargeting.Remote
|
||||
}
|
||||
break;
|
||||
|
||||
case "autoHipsOffset":
|
||||
{
|
||||
string charId = json["characterId"]?.ToString();
|
||||
AutoHipsOffset(charId);
|
||||
}
|
||||
break;
|
||||
|
||||
case "autoCalibrateAll":
|
||||
{
|
||||
string charId = json["characterId"]?.ToString();
|
||||
@ -233,15 +226,6 @@ namespace KindRetargeting.Remote
|
||||
|
||||
var data = new Dictionary<string, object>
|
||||
{
|
||||
// 힙 위치 보정 (로컬)
|
||||
{ "hipsVertical", GetPrivateField<float>(script, "hipsOffsetY") },
|
||||
{ "hipsForward", GetPrivateField<float>(script, "hipsOffsetZ") },
|
||||
{ "hipsHorizontal", GetPrivateField<float>(script, "hipsOffsetX") },
|
||||
|
||||
// 무릎 위치 조정
|
||||
{ "kneeFrontBackWeight", GetPrivateField<float>(script, "kneeFrontBackWeight") },
|
||||
{ "kneeInOutWeight", GetPrivateField<float>(script, "kneeInOutWeight") },
|
||||
|
||||
// 발 IK 위치 조정
|
||||
{ "feetForwardBackward", GetPrivateField<float>(script, "footFrontBackOffset") },
|
||||
{ "feetNarrow", GetPrivateField<float>(script, "footInOutOffset") },
|
||||
@ -316,25 +300,6 @@ namespace KindRetargeting.Remote
|
||||
|
||||
switch (property)
|
||||
{
|
||||
// 힙 위치 보정
|
||||
case "hipsVertical":
|
||||
SetPrivateField(script, "hipsOffsetY", value);
|
||||
break;
|
||||
case "hipsForward":
|
||||
SetPrivateField(script, "hipsOffsetZ", value);
|
||||
break;
|
||||
case "hipsHorizontal":
|
||||
SetPrivateField(script, "hipsOffsetX", value);
|
||||
break;
|
||||
|
||||
// 무릎 위치 조정
|
||||
case "kneeFrontBackWeight":
|
||||
SetPrivateField(script, "kneeFrontBackWeight", value);
|
||||
break;
|
||||
case "kneeInOutWeight":
|
||||
SetPrivateField(script, "kneeInOutWeight", value);
|
||||
break;
|
||||
|
||||
// 발 IK 위치 조정
|
||||
case "feetForwardBackward":
|
||||
SetPrivateField(script, "footFrontBackOffset", value);
|
||||
@ -570,19 +535,6 @@ namespace KindRetargeting.Remote
|
||||
SendStatus(true, "정면 캘리브레이션 완료");
|
||||
}
|
||||
|
||||
private void AutoHipsOffset(string characterId)
|
||||
{
|
||||
var script = FindCharacter(characterId);
|
||||
if (script == null) return;
|
||||
|
||||
float offset = CalculateHipsOffsetFromLegDifference(script);
|
||||
SetPrivateField(script, "hipsOffsetY", offset);
|
||||
script.SaveSettings();
|
||||
|
||||
SendCharacterData(characterId);
|
||||
SendStatus(true, $"다리 길이 자동 보정 완료: hipsOffsetY={offset:F4}");
|
||||
}
|
||||
|
||||
private void AutoCalibrateAll(string characterId)
|
||||
{
|
||||
var script = FindCharacter(characterId);
|
||||
@ -596,10 +548,9 @@ namespace KindRetargeting.Remote
|
||||
return;
|
||||
}
|
||||
|
||||
// Step 1: 크기 초기화 + 힙 오프셋 계산
|
||||
// Step 1: 크기 초기화
|
||||
script.ResetScale();
|
||||
SetPrivateField(script, "avatarScale", 1f);
|
||||
SetPrivateField(script, "hipsOffsetY", CalculateHipsOffsetFromLegDifference(script));
|
||||
|
||||
// Step 2: 1프레임 후 목 높이 비율로 크기 조정
|
||||
StartCoroutine(AutoCalibrateCoroutine(script, characterId));
|
||||
@ -626,8 +577,7 @@ namespace KindRetargeting.Remote
|
||||
|
||||
yield return null; // 1프레임 대기
|
||||
|
||||
// Step 3: 힙 오프셋 재계산 + 머리 정면 캘리브레이션
|
||||
SetPrivateField(script, "hipsOffsetY", CalculateHipsOffsetFromLegDifference(script));
|
||||
// Step 3: 머리 정면 캘리브레이션 + 저장 (힙 보정은 매 프레임 자동)
|
||||
script.CalibrateHeadToForward();
|
||||
script.SaveSettings();
|
||||
|
||||
@ -635,37 +585,6 @@ namespace KindRetargeting.Remote
|
||||
SendStatus(true, $"전체 자동 보정 완료: avatarScale={scaleRatio:F3}");
|
||||
}
|
||||
|
||||
private float CalculateHipsOffsetFromLegDifference(CustomRetargetingScript script)
|
||||
{
|
||||
var source = script.optitrackSource;
|
||||
Animator targetAnim = script.targetAnimator;
|
||||
if (source == null || targetAnim == null) return 0f;
|
||||
|
||||
float sourceLeg = GetSourceLegLength(source);
|
||||
float targetLeg = GetLegLength(targetAnim);
|
||||
if (sourceLeg < 0.01f || targetLeg < 0.01f) return 0f;
|
||||
|
||||
return targetLeg - sourceLeg;
|
||||
}
|
||||
|
||||
private float GetSourceLegLength(OptitrackSkeletonAnimator_Mingle source)
|
||||
{
|
||||
Transform upper = source.GetBoneTransform(HumanBodyBones.LeftUpperLeg);
|
||||
Transform lower = source.GetBoneTransform(HumanBodyBones.LeftLowerLeg);
|
||||
Transform foot = source.GetBoneTransform(HumanBodyBones.LeftFoot);
|
||||
if (upper == null || lower == null || foot == null) return 0f;
|
||||
return Vector3.Distance(upper.position, lower.position) + Vector3.Distance(lower.position, foot.position);
|
||||
}
|
||||
|
||||
private float GetLegLength(Animator animator)
|
||||
{
|
||||
Transform upper = animator.GetBoneTransform(HumanBodyBones.LeftUpperLeg);
|
||||
Transform lower = animator.GetBoneTransform(HumanBodyBones.LeftLowerLeg);
|
||||
Transform foot = animator.GetBoneTransform(HumanBodyBones.LeftFoot);
|
||||
if (upper == null || lower == null || foot == null) return 0f;
|
||||
return Vector3.Distance(upper.position, lower.position) + Vector3.Distance(lower.position, foot.position);
|
||||
}
|
||||
|
||||
private CustomRetargetingScript FindCharacter(string characterId)
|
||||
{
|
||||
foreach (var script in registeredCharacters)
|
||||
|
||||
@ -45,6 +45,8 @@ namespace KindRetargeting
|
||||
public LimbIK leftLeg = new LimbIK();
|
||||
public LimbIK rightLeg = new LimbIK();
|
||||
|
||||
[HideInInspector] public int fabrikIterations = 6;
|
||||
|
||||
private bool isInitialized;
|
||||
|
||||
public void Initialize(Animator targetAnimator)
|
||||
@ -149,54 +151,61 @@ namespace KindRetargeting
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 소스 무릎 위치를 hip→foot 직선 기준으로 분해(투영+수직)한 뒤
|
||||
/// 타겟 비율로 스케일하여 타겟 무릎 위치를 결정합니다.
|
||||
/// 소스가 역관절이면 수직 성분이 반대쪽 → 타겟도 자연스럽게 역관절.
|
||||
/// 소스 무릎 위치를 타겟 프레임으로 옮긴 raw 위치를 초기값으로 두고,
|
||||
/// FABRIK 2회 반복으로 hip↔knee=upperLen, knee↔target=lowerLen 을 모두 만족시킵니다.
|
||||
///
|
||||
/// 정규화(normalize)에 의한 부호 증폭이 없어 입력 노이즈가 출력에 그대로 비례 전달됩니다.
|
||||
/// → 다리 거의 펴진 상태에서 모캡 노이즈로 굽힘 부호가 흔들려도 점프 없이 연속적.
|
||||
/// → 의도된 역관절도 자연스럽게 따라감 (소스 무릎이 어느 쪽이든 raw로 받음).
|
||||
/// </summary>
|
||||
private Vector3 ComputeKneePosFromSource(LimbIK limb, float upperLen, float lowerLen, Vector3 targetPos)
|
||||
{
|
||||
// 소스 체인 길이
|
||||
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;
|
||||
float chainLength = upperLen + lowerLen;
|
||||
|
||||
if (sourceChain < 0.001f || targetChain < 0.001f)
|
||||
return limb.lower.position;
|
||||
|
||||
// 소스 hip → foot 방향
|
||||
// 소스 hip→foot 방향
|
||||
Vector3 sourceHipToFoot = limb.sourceEnd.position - limb.sourceUpper.position;
|
||||
float sourceHipToFootMag = sourceHipToFoot.magnitude;
|
||||
if (sourceHipToFootMag < 0.001f)
|
||||
return limb.lower.position;
|
||||
if (sourceHipToFootMag < 0.001f) return limb.lower.position;
|
||||
Vector3 sourceHipToFootDir = sourceHipToFoot / sourceHipToFootMag;
|
||||
|
||||
// 소스 hip → knee 벡터를 투영(직선 성분)과 수직(오프셋 성분)으로 분해
|
||||
// 소스 hip→knee (굽힘 정보 raw, 정규화 X → 노이즈 증폭 없음)
|
||||
Vector3 sourceHipToKnee = limb.sourceLower.position - limb.sourceUpper.position;
|
||||
float projection = Vector3.Dot(sourceHipToKnee, sourceHipToFootDir);
|
||||
Vector3 rejection = sourceHipToKnee - projection * sourceHipToFootDir;
|
||||
float sourceUpperLen = sourceHipToKnee.magnitude;
|
||||
float sourceLowerLen = Vector3.Distance(limb.sourceLower.position, limb.sourceEnd.position);
|
||||
float sourceChain = sourceUpperLen + sourceLowerLen;
|
||||
if (sourceChain < 0.001f) return limb.lower.position;
|
||||
|
||||
// 소스 체인 길이 대비 비율로 정규화 → 타겟 체인 길이로 스케일
|
||||
float scale = targetChain / sourceChain;
|
||||
float scaledProjection = projection * scale;
|
||||
Vector3 scaledRejection = rejection * scale;
|
||||
// 타겟 hip→target 방향 / 거리
|
||||
Vector3 toTarget = targetPos - limb.upper.position;
|
||||
float targetDist = toTarget.magnitude;
|
||||
if (targetDist < 0.001f) return limb.lower.position;
|
||||
Vector3 toTargetDir = toTarget / targetDist;
|
||||
|
||||
// 타겟 hip → foot 방향
|
||||
Vector3 targetHipToFoot = targetPos - limb.upper.position;
|
||||
float targetHipToFootMag = targetHipToFoot.magnitude;
|
||||
if (targetHipToFootMag < 0.001f)
|
||||
return limb.lower.position;
|
||||
Vector3 targetHipToFootDir = targetHipToFoot / targetHipToFootMag;
|
||||
// 도달 불가 거리는 클램프 (FABRIK 진동 방지)
|
||||
// |upper-lower| 보다 가깝거나 chainLength 보다 멀면 본 길이 제약을 만족하는 무릎 위치 없음
|
||||
float effectiveDist = Mathf.Clamp(targetDist, Mathf.Abs(upperLen - lowerLen) + 0.001f, chainLength - 0.001f);
|
||||
Vector3 effectiveTarget = limb.upper.position + toTargetDir * effectiveDist;
|
||||
|
||||
// 소스 프레임 → 타겟 프레임으로 수직 성분 회전
|
||||
// (소스와 타겟의 사지 방향이 다를 때 팔꿈치/무릎 오프셋 방향 보정)
|
||||
Quaternion frameRotation = Quaternion.FromToRotation(sourceHipToFootDir, targetHipToFootDir);
|
||||
Vector3 rotatedRejection = frameRotation * scaledRejection;
|
||||
// 소스 → 타겟 프레임 회전 + 다리 길이 비율 스케일 → 초기 무릎 위치
|
||||
Quaternion frameRotation = Quaternion.FromToRotation(sourceHipToFootDir, toTargetDir);
|
||||
float scale = (upperLen + lowerLen) / sourceChain;
|
||||
Vector3 kneePos = limb.upper.position + frameRotation * (sourceHipToKnee * scale);
|
||||
|
||||
// 타겟 관절 위치: 타겟 upper에서 투영 + 회전된 수직 성분 적용
|
||||
Vector3 kneePos = limb.upper.position
|
||||
+ targetHipToFootDir * scaledProjection
|
||||
+ rotatedRejection;
|
||||
// FABRIK: knee 를 lowerLen 구면(target 중심) ↔ upperLen 구면(hip 중심) 사이에서 번갈아 투영
|
||||
for (int i = 0; i < fabrikIterations; i++)
|
||||
{
|
||||
// 1) target 중심 lowerLen 구면에 투영 → knee↔target = lowerLen 보장
|
||||
Vector3 fromTarget = kneePos - effectiveTarget;
|
||||
float distFromTarget = fromTarget.magnitude;
|
||||
if (distFromTarget > 0.001f)
|
||||
kneePos = effectiveTarget + (fromTarget / distFromTarget) * lowerLen;
|
||||
|
||||
// 2) hip 중심 upperLen 구면에 투영 → hip↔knee = upperLen 보장
|
||||
Vector3 fromHip = kneePos - limb.upper.position;
|
||||
float distFromHip = fromHip.magnitude;
|
||||
if (distFromHip > 0.001f)
|
||||
kneePos = limb.upper.position + (fromHip / distFromHip) * upperLen;
|
||||
}
|
||||
|
||||
return kneePos;
|
||||
}
|
||||
@ -263,24 +272,5 @@ namespace KindRetargeting
|
||||
bendDir = Vector3.Cross(Vector3.up, toTargetDir);
|
||||
return bendDir.normalized;
|
||||
}
|
||||
|
||||
public float CalculateAutoFloorHeight(float comfortRatio = 0.98f)
|
||||
{
|
||||
if (animator == null || leftLeg.upper == null || leftLeg.lower == null || leftLeg.end == null) return 0f;
|
||||
|
||||
float upperLen = Vector3.Distance(leftLeg.upper.position, leftLeg.lower.position);
|
||||
float lowerLen = Vector3.Distance(leftLeg.lower.position, leftLeg.end.position);
|
||||
float totalLegLength = upperLen + lowerLen;
|
||||
float comfortHeight = totalLegLength * comfortRatio;
|
||||
|
||||
Transform hips = animator.GetBoneTransform(HumanBodyBones.Hips);
|
||||
Transform foot = leftLeg.end;
|
||||
if (hips == null || foot == null) return 0f;
|
||||
|
||||
float currentHipToFoot = hips.position.y - foot.position.y;
|
||||
float heightDiff = currentHipToFoot - comfortHeight;
|
||||
|
||||
return -heightDiff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user