Fix : 리타겟팅 시스템 업데이트 대부분의 아바타 업데이트

This commit is contained in:
user 2026-05-03 23:44:04 +09:00
parent 8954204bb2
commit db9e968499
13 changed files with 5387 additions and 1074 deletions

Binary file not shown.

View File

@ -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

View File

@ -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)
{

View File

@ -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;

View File

@ -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)

Binary file not shown.

View File

@ -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)

View File

@ -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;
if (sourceChain < 0.001f || targetChain < 0.001f)
return limb.lower.position;
float chainLength = upperLen + lowerLen;
// 소스 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;
}
}
}