Refactor : KindRetargeting dead code 제거 + IK 안정성/성능 개선

- TwoBoneIK: cosine law → FABRIK 6회 반복 (역관절 안정 + 본 길이 제약)
- 자동 힙 상하 보정 매 프레임 적용, 수동 hipsOffsetX/Y/Z 제거
- kneeFrontBackWeight/InOutWeight, GetAvatarScale 등 dead code 정리
- FingerShapedController GC 제거 (HumanPose/Transform 캐싱)
- IK 본 길이 / FieldInfo / Chair prop / 가중치 배열(List→float[]) 캐싱
- localAxisForWorldRight/Forward, IKJoints 다리 필드 등 미사용 정리
- 매직 넘버 55 → BoneCount 상수

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
user 2026-05-04 00:07:54 +09:00
parent db9e968499
commit b9361a9d17
6 changed files with 166 additions and 139 deletions

View File

@ -732,14 +732,10 @@ namespace KindRetargeting
} }
/// <summary> /// <summary>
/// 머리 본에 회전 오프셋을 적용합니다. /// 머리 본에 회전 오프셋을 적용합니다. headBone 멤버 캐시 재사용 (ApplyHeadScale과 일관).
/// </summary> /// </summary>
private void ApplyHeadRotationOffset() private void ApplyHeadRotationOffset()
{ {
if (targetAnimator == null) return;
// 머리 본 가져오기
Transform headBone = targetAnimator.GetBoneTransform(HumanBodyBones.Head);
if (headBone == null) return; if (headBone == null) return;
// 오프셋이 모두 0이면 스킵 // 오프셋이 모두 0이면 스킵
@ -913,16 +909,30 @@ namespace KindRetargeting
SyncBoneRotations(skipBone: HumanBodyBones.Hips); SyncBoneRotations(skipBone: HumanBodyBones.Hips);
} }
// 자동 힙 보정 캐시 (다리 본 길이는 스케일 변경 시에만 갱신됨)
private float cachedAutoHipsOffsetY = 0f;
private bool autoHipsOffsetCacheValid = false;
/// <summary> /// <summary>
/// 매 프레임 자동으로 적용되는 힙 상하 보정값. /// 매 프레임 호출되는 자동 힙 보정값. 본 길이는 변하지 않으므로 캐시된 값을 반환.
/// = (타겟 다리길이 - 소스 다리길이) + (타겟 Hips↔UpperLeg 갭) × avatarScale /// avatarScale 변경 시 ApplyScale → RefreshAutoHipsOffsetCache 로 자동 갱신.
///
/// 첫 항: 다리 길이 차이로 발이 뜨거나 묻히는 현상 보정.
/// 둘째 항: Hips 본이 UpperLeg보다 위에 있는 아바타 추가 보정.
/// </summary> /// </summary>
private float ComputeAutoHipsOffsetY() private float ComputeAutoHipsOffsetY()
{ {
if (optitrackSource == null || targetAnimator == null) return 0f; if (!autoHipsOffsetCacheValid) RefreshAutoHipsOffsetCache();
return cachedAutoHipsOffsetY;
}
/// <summary>
/// 자동 힙 보정 캐시를 재계산합니다 (Initialize / avatarScale 변경 시 호출).
/// = (타겟 다리길이 - 소스 다리길이) + (타겟 Hips↔UpperLeg 갭) × avatarScale
/// </summary>
private void RefreshAutoHipsOffsetCache()
{
cachedAutoHipsOffsetY = 0f;
autoHipsOffsetCacheValid = true;
if (optitrackSource == null || targetAnimator == null) return;
Transform sUp = optitrackSource.GetBoneTransform(HumanBodyBones.LeftUpperLeg); Transform sUp = optitrackSource.GetBoneTransform(HumanBodyBones.LeftUpperLeg);
Transform sLo = optitrackSource.GetBoneTransform(HumanBodyBones.LeftLowerLeg); Transform sLo = optitrackSource.GetBoneTransform(HumanBodyBones.LeftLowerLeg);
@ -934,14 +944,14 @@ namespace KindRetargeting
if (sUp == null || sLo == null || sFt == null if (sUp == null || sLo == null || sFt == null
|| tUp == null || tLo == null || tFt == null || tHi == null) || tUp == null || tLo == null || tFt == null || tHi == null)
return 0f; return;
float sourceLeg = Vector3.Distance(sUp.position, sLo.position) + Vector3.Distance(sLo.position, sFt.position); 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); float targetLeg = Vector3.Distance(tUp.position, tLo.position) + Vector3.Distance(tLo.position, tFt.position);
if (sourceLeg < 0.01f || targetLeg < 0.01f) return 0f; if (sourceLeg < 0.01f || targetLeg < 0.01f) return;
float hipsToLegGap = tHi.position.y - tUp.position.y; float hipsToLegGap = tHi.position.y - tUp.position.y;
return (targetLeg - sourceLeg) + hipsToLegGap * avatarScale; cachedAutoHipsOffsetY = (targetLeg - sourceLeg) + hipsToLegGap * avatarScale;
} }
/// <summary> /// <summary>
@ -1465,6 +1475,10 @@ namespace KindRetargeting
} }
} }
} }
// 스케일 변경으로 본 길이가 바뀌었으니 IK 캐시 + 자동 힙 보정 캐시 갱신
ikSolver?.RefreshLimbLengths();
RefreshAutoHipsOffsetCache();
} }
// 리셋 기능 추가 // 리셋 기능 추가

View File

@ -1,5 +1,4 @@
using UnityEngine; using UnityEngine;
using System.Collections.Generic;
namespace KindRetargeting namespace KindRetargeting
{ {
@ -9,8 +8,10 @@ namespace KindRetargeting
private Animator animator; private Animator animator;
private HumanPoseHandler humanPoseHandler; private HumanPoseHandler humanPoseHandler;
// 손가락을 제외한 모든 본의 로컬 회전 저장용 (SetHumanPose 호출 시 몸 복원용) // 매 프레임 재사용되는 캐시 (GC 압박 제거)
private Dictionary<HumanBodyBones, Quaternion> savedBoneLocalRotations = new Dictionary<HumanBodyBones, Quaternion>(); private Transform[] cachedNonFingerBones;
private Quaternion[] savedBoneRotations;
private HumanPose cachedHumanPose;
// 손가락을 제외한 모든 휴먼본 목록 // 손가락을 제외한 모든 휴먼본 목록
private static readonly HumanBodyBones[] nonFingerBones = new HumanBodyBones[] private static readonly HumanBodyBones[] nonFingerBones = new HumanBodyBones[]
@ -71,6 +72,18 @@ namespace KindRetargeting
if (animator == null || !animator.isHuman) return; if (animator == null || !animator.isHuman) return;
humanPoseHandler = new HumanPoseHandler(animator.avatar, animator.transform); humanPoseHandler = new HumanPoseHandler(animator.avatar, animator.transform);
// Transform 배열 + 회전 배열 사전 캐싱 (매 프레임 GetBoneTransform 50회 → 0회)
cachedNonFingerBones = new Transform[nonFingerBones.Length];
savedBoneRotations = new Quaternion[nonFingerBones.Length];
for (int i = 0; i < nonFingerBones.Length; i++)
{
cachedNonFingerBones[i] = animator.GetBoneTransform(nonFingerBones[i]);
}
// HumanPose 사전 할당 (muscles 배열 95개 float, GC 회피)
humanPoseHandler.GetHumanPose(ref cachedHumanPose);
isInitialized = true; isInitialized = true;
} }
@ -92,40 +105,29 @@ namespace KindRetargeting
private void UpdateMuscleValues() private void UpdateMuscleValues()
{ {
// 1. 손가락을 제외한 모든 본의 로컬 회전 저장 (SetHumanPose 호출 전) // 1. 비손가락 본의 로컬 회전을 배열에 저장 (캐시된 Transform 재사용)
savedBoneLocalRotations.Clear(); for (int i = 0; i < cachedNonFingerBones.Length; i++)
for (int i = 0; i < nonFingerBones.Length; i++)
{ {
Transform bone = animator.GetBoneTransform(nonFingerBones[i]); Transform bone = cachedNonFingerBones[i];
if (bone != null) if (bone != null) savedBoneRotations[i] = bone.localRotation;
{
savedBoneLocalRotations[nonFingerBones[i]] = bone.localRotation;
}
} }
// 2. HumanPose 가져오기 및 손가락 머슬 설정 // 2. HumanPose 가져오기 (재사용 필드, GC 없음) + 손가락 머슬 설정
HumanPose humanPose = new HumanPose(); humanPoseHandler.GetHumanPose(ref cachedHumanPose);
humanPoseHandler.GetHumanPose(ref humanPose);
// 왼손 제어
SetHandMuscles(true, leftThumbCurl, leftIndexCurl, leftMiddleCurl, leftRingCurl, SetHandMuscles(true, leftThumbCurl, leftIndexCurl, leftMiddleCurl, leftRingCurl,
leftPinkyCurl, leftSpreadFingers, ref humanPose); leftPinkyCurl, leftSpreadFingers, ref cachedHumanPose);
// 오른손 제어
SetHandMuscles(false, rightThumbCurl, rightIndexCurl, rightMiddleCurl, rightRingCurl, SetHandMuscles(false, rightThumbCurl, rightIndexCurl, rightMiddleCurl, rightRingCurl,
rightPinkyCurl, rightSpreadFingers, ref humanPose); rightPinkyCurl, rightSpreadFingers, ref cachedHumanPose);
// 3. 머슬 포즈 적용 (손가락 포함 전체 본에 영향) // 3. 머슬 포즈 적용 (손가락 포함 전체 본에 영향)
humanPoseHandler.SetHumanPose(ref humanPose); humanPoseHandler.SetHumanPose(ref cachedHumanPose);
// 4. 손가락을 제외한 모든 로컬 회전 복원 (본 길이 변형 방지) // 4. 손가락 본 로컬 회전 복원 (본 길이 변형 방지)
foreach (var kvp in savedBoneLocalRotations) for (int i = 0; i < cachedNonFingerBones.Length; i++)
{ {
Transform bone = animator.GetBoneTransform(kvp.Key); Transform bone = cachedNonFingerBones[i];
if (bone != null) if (bone != null) bone.localRotation = savedBoneRotations[i];
{
bone.localRotation = kvp.Value;
}
} }
} }

View File

@ -39,13 +39,14 @@ namespace KindRetargeting
private CustomRetargetingScript crs; private CustomRetargetingScript crs;
private Transform characterRoot; private Transform characterRoot;
List<float> leftArmEndWeights = new List<float>(); // 가중치 배열: 인덱스 의미는 Initialize 주석 참조 (크기 고정 → float[] 사용으로 GC/인덱싱 비용 절감)
List<float> rightArmEndWeights = new List<float>(); readonly float[] leftArmEndWeights = new float[2]; // [0] 양손거리 [1] 프랍거리
List<float> leftLegEndWeights = new List<float>(); readonly float[] rightArmEndWeights = new float[2];
List<float> rightLegEndWeights = new List<float>(); readonly float[] leftLegEndWeights = new float[2]; // [0] 앉기 수평거리 [1] 발 높이
readonly float[] rightLegEndWeights = new float[2];
List<float> leftLegBendWeights = new List<float>(); readonly float[] leftLegBendWeights = new float[1] { 1f };
List<float> rightLegBendWeights = new List<float>(); readonly float[] rightLegBendWeights = new float[1] { 1f };
private float MasterleftArmEndWeights = 0f; private float MasterleftArmEndWeights = 0f;
private float MasterrightArmEndWeights = 0f; private float MasterrightArmEndWeights = 0f;
@ -55,9 +56,11 @@ namespace KindRetargeting
private float MasterrightLegBendWeights = 0f; private float MasterrightLegBendWeights = 0f;
public List<Transform> props = new List<Transform>(); public List<Transform> props = new List<Transform>();
// SitChairDistances 매 프레임 GetComponent 비용 회피용 (Initialize 시점 한 번 캐싱)
private List<Transform> chairProps = new List<Transform>();
// 힙스 가중치 리스트 추가 // 힙스 가중치: [0] 의자 거리 [1] 지면 높이
List<float> hipsWeights = new List<float>(); readonly float[] hipsWeights = new float[2] { 1f, 1f };
private float MasterHipsWeight = 1f; private float MasterHipsWeight = 1f;
// 의자 좌석 높이 오프셋 (월드 Y 기준) // 의자 좌석 높이 오프셋 (월드 Y 기준)
@ -76,44 +79,26 @@ namespace KindRetargeting
InitWeightLayers(); InitWeightLayers();
//프랍 오브젝트 찾기 //프랍 오브젝트 찾기 + 의자 타입 별도 캐싱
props = Object.FindObjectsByType<PropTypeController>(FindObjectsSortMode.None).Select(controller => controller.transform).ToList(); var allPropControllers = Object.FindObjectsByType<PropTypeController>(FindObjectsSortMode.None);
props = allPropControllers.Select(c => c.transform).ToList();
chairProps = allPropControllers
.Where(c => c.propType == EnumsList.PropType.Chair)
.Select(c => c.transform)
.ToList();
// 다른 캐릭터의 손을 props에 추가 // 다른 캐릭터의 손을 props에 추가
GetHand(); GetHand();
//HandDistances()에서 사용을 위한 리스트 추가 // 가중치 배열은 필드 선언 시 고정 크기로 초기화됨 (다리 EndWeight[1]만 1f 기본값)
//손 거리에 따른 웨이트 업데이트 인덱스 0번 leftLegEndWeights[1] = 1f;
leftArmEndWeights.Add(0); rightLegEndWeights[1] = 1f;
rightArmEndWeights.Add(0);
// 프랍과의 거리에 따른 웨이트 업데이트 인덱스 1번
leftArmEndWeights.Add(0);
rightArmEndWeights.Add(0);
// 앉아있을 때 다리와의 거리에 따른 가중치 적용 인덱스 0번
leftLegEndWeights.Add(0);
rightLegEndWeights.Add(0);
// 다리 골 가중치 초기화
leftLegBendWeights.Add(1f); // 기본 가중치
rightLegBendWeights.Add(1f); // 기본 가중치
if (this.characterRoot == null) if (this.characterRoot == null)
{ {
this.characterRoot = crs.transform; this.characterRoot = crs.transform;
} }
// 힙스 가중치 초기화 인덱스 0번
hipsWeights.Add(1f); // 의자 거리 기반 가중치
// 지면 높이 기반 가중치 초기화 인덱스 1번
hipsWeights.Add(1f); // 지면 높이 기반 가중치
// 발 높이 기반 가중치 초기화 인덱스 1번
leftLegEndWeights.Add(1f);
rightLegEndWeights.Add(1f);
isInitialized = true; isInitialized = true;
} }
@ -237,7 +222,7 @@ namespace KindRetargeting
} }
} }
private void ProcessSitLegWeight(Transform hips, Transform footTarget, List<float> weightList, int weightIndex) private void ProcessSitLegWeight(Transform hips, Transform footTarget, float[] weightArr, int weightIndex)
{ {
if (footTarget == null) return; if (footTarget == null) return;
@ -256,7 +241,7 @@ namespace KindRetargeting
float weight = 1f - Mathf.Clamp01((horizontalDistance - MIN_LEG_DISTANCE_RATIO) / float weight = 1f - Mathf.Clamp01((horizontalDistance - MIN_LEG_DISTANCE_RATIO) /
(MAX_LEG_DISTANCE_RATIO - MIN_LEG_DISTANCE_RATIO)); (MAX_LEG_DISTANCE_RATIO - MIN_LEG_DISTANCE_RATIO));
weightList[weightIndex] = weight; weightArr[weightIndex] = weight;
} }
void PropDistances() void PropDistances()
@ -298,40 +283,29 @@ namespace KindRetargeting
{ {
if (crs == null) return; if (crs == null) return;
Transform hipsTransform = crs.optitrackSource.GetBoneTransform(HumanBodyBones.Hips); Transform hipsTransform = crs?.optitrackSource?.GetBoneTransform(HumanBodyBones.Hips);
if (hipsTransform != null && props != null) if (hipsTransform == null || chairProps.Count == 0)
{ {
float minDistance = float.MaxValue; hipsWeights[0] = 1f;
bool foundChair = false; targetChairSeatOffset = 0f;
return;
foreach (Transform prop in props)
{
PropTypeController ptc = prop.GetComponent<PropTypeController>();
if (ptc != null && ptc.propType == EnumsList.PropType.Chair)
{
float distance = Vector3.Distance(hipsTransform.position, prop.childCount > 0 ? prop.GetChild(0).position : prop.position);
if (distance < minDistance)
{
minDistance = distance;
foundChair = true;
}
}
}
float t = Mathf.Clamp01((minDistance - hipsMinDistance) / (hipsMaxDistance - hipsMinDistance));
hipsWeights[0] = t; // 직접 HipsWeightOffset 수정 대신 배열에 저장
// 의자 좌석 높이 오프셋 계산 (가까울수록 더 적용) - 캐릭터별 설정 사용
if (foundChair)
{
// t가 0에 가까울수록 의자에 가까움 → 좌석 오프셋 더 적용
targetChairSeatOffset = chairSeatHeightOffset * (1f - t);
}
else
{
targetChairSeatOffset = 0f;
}
} }
// chairProps는 Initialize에서 미리 필터링됨 (매 프레임 GetComponent 호출 회피)
float minDistance = float.MaxValue;
for (int i = 0; i < chairProps.Count; i++)
{
Transform chair = chairProps[i];
if (chair == null) continue;
Vector3 chairPos = chair.childCount > 0 ? chair.GetChild(0).position : chair.position;
float distance = Vector3.Distance(hipsTransform.position, chairPos);
if (distance < minDistance) minDistance = distance;
}
float t = Mathf.Clamp01((minDistance - hipsMinDistance) / (hipsMaxDistance - hipsMinDistance));
hipsWeights[0] = t;
// t가 0에 가까울수록 의자에 가까움 → 좌석 오프셋 더 적용
targetChairSeatOffset = chairSeatHeightOffset * (1f - t);
} }
/// <summary> /// <summary>
@ -400,18 +374,18 @@ namespace KindRetargeting
} }
/// <summary> /// <summary>
/// 리스트에서 최대값을 찾습니다. /// 배열에서 최대값을 찾습니다.
/// </summary> /// </summary>
private float GetMaxValue(List<float> list) private float GetMaxValue(float[] arr)
{ {
if (list.Count == 0) return 0f; if (arr.Length == 0) return 0f;
float max = list[0]; float max = arr[0];
for (int i = 1; i < list.Count; i++) for (int i = 1; i < arr.Length; i++)
{ {
if (list[i] > max) if (arr[i] > max)
{ {
max = list[i]; max = arr[i];
} }
} }
return max; return max;
@ -419,18 +393,18 @@ namespace KindRetargeting
/// <summary> /// <summary>
/// 리스트에서 최소값을 찾습니다. /// 배열에서 최소값을 찾습니다.
/// </summary> /// </summary>
private float GetMinValue(List<float> list) private float GetMinValue(float[] arr)
{ {
if (list.Count == 0) return 0f; if (arr.Length == 0) return 0f;
float min = list[0]; float min = arr[0];
for (int i = 1; i < list.Count; i++) for (int i = 1; i < arr.Length; i++)
{ {
if (list[i] < min) if (arr[i] < min)
{ {
min = list[i]; min = arr[i];
} }
} }
return min; return min;
@ -473,7 +447,7 @@ namespace KindRetargeting
{ {
if (crs == null || ikSolver == null) return; if (crs == null || ikSolver == null) return;
Transform hipsTransform = crs.optitrackSource.GetBoneTransform(HumanBodyBones.Hips); Transform hipsTransform = crs?.optitrackSource?.GetBoneTransform(HumanBodyBones.Hips);
if (hipsTransform != null) if (hipsTransform != null)
{ {
float groundHeight = characterRoot.position.y; float groundHeight = characterRoot.position.y;
@ -506,7 +480,7 @@ namespace KindRetargeting
); );
} }
private void ProcessFootHeightWeight(Transform footTarget, List<float> weightList, int weightIndex) private void ProcessFootHeightWeight(Transform footTarget, float[] weightArr, int weightIndex)
{ {
if (footTarget == null) return; if (footTarget == null) return;
@ -518,7 +492,7 @@ namespace KindRetargeting
(footHeightMaxThreshold - footHeightMinThreshold)); (footHeightMaxThreshold - footHeightMinThreshold));
// 계산된 가중치 설정 // 계산된 가중치 설정
weightList[weightIndex] = weight; weightArr[weightIndex] = weight;
} }
} }
} }

View File

@ -624,11 +624,25 @@ namespace KindRetargeting.Remote
#region Reflection Helpers #region Reflection Helpers
// 슬라이더 드래그(60fps) 시 매번 GetField 호출되는 것을 방지.
// (Type, fieldName) → FieldInfo 1회 lookup 후 캐싱.
private static readonly Dictionary<(System.Type, string), FieldInfo> _fieldCache = new Dictionary<(System.Type, string), FieldInfo>();
private const BindingFlags FIELD_FLAGS = BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public;
private static FieldInfo ResolveField(System.Type type, string fieldName)
{
var key = (type, fieldName);
if (_fieldCache.TryGetValue(key, out FieldInfo cached)) return cached;
FieldInfo field = type.GetField(fieldName, FIELD_FLAGS);
_fieldCache[key] = field;
return field;
}
private T GetPrivateField<T>(object obj, string fieldName) private T GetPrivateField<T>(object obj, string fieldName)
{ {
try try
{ {
var field = obj.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public); var field = ResolveField(obj.GetType(), fieldName);
if (field != null) if (field != null)
return (T)field.GetValue(obj); return (T)field.GetValue(obj);
@ -646,7 +660,7 @@ namespace KindRetargeting.Remote
{ {
try try
{ {
var field = obj.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public); var field = ResolveField(obj.GetType(), fieldName);
if (field != null) if (field != null)
{ {
field.SetValue(obj, value); field.SetValue(obj, value);

View File

@ -5,6 +5,9 @@ using KindRetargeting;
[DefaultExecutionOrder(16001)] [DefaultExecutionOrder(16001)]
public class SimplePoseTransfer : MonoBehaviour public class SimplePoseTransfer : MonoBehaviour
{ {
// 0~54: 휴머노이드 본 (몸체 + 손가락). HumanBodyBones.LastBone 직전까지.
private const int BoneCount = 55;
[System.Serializable] [System.Serializable]
public class TargetEntry public class TargetEntry
{ {
@ -121,7 +124,7 @@ public class SimplePoseTransfer : MonoBehaviour
private void InitializeTargetBones() private void InitializeTargetBones()
{ {
boneRotationDifferences = new Quaternion[targets.Count, 55]; boneRotationDifferences = new Quaternion[targets.Count, BoneCount];
for (int i = 0; i < targets.Count; i++) for (int i = 0; i < targets.Count; i++)
{ {
@ -133,7 +136,7 @@ public class SimplePoseTransfer : MonoBehaviour
} }
// 55개의 휴머노이드 본에 대해 회전 차이 계산 // 55개의 휴머노이드 본에 대해 회전 차이 계산
for (int j = 0; j < 55; j++) for (int j = 0; j < BoneCount; j++)
{ {
Transform sourceBoneTransform = sourceBone.GetBoneTransform((HumanBodyBones)j); Transform sourceBoneTransform = sourceBone.GetBoneTransform((HumanBodyBones)j);
Transform targetBoneTransform = animator.GetBoneTransform((HumanBodyBones)j); Transform targetBoneTransform = animator.GetBoneTransform((HumanBodyBones)j);
@ -149,19 +152,19 @@ public class SimplePoseTransfer : MonoBehaviour
private void CacheAllBoneTransforms() private void CacheAllBoneTransforms()
{ {
// 소스 본 캐싱 // 소스 본 캐싱
cachedSourceBones = new Transform[55]; cachedSourceBones = new Transform[BoneCount];
for (int i = 0; i < 55; i++) for (int i = 0; i < BoneCount; i++)
{ {
cachedSourceBones[i] = sourceBone.GetBoneTransform((HumanBodyBones)i); cachedSourceBones[i] = sourceBone.GetBoneTransform((HumanBodyBones)i);
} }
// 타겟 본 캐싱 // 타겟 본 캐싱
cachedTargetBones = new Transform[targets.Count, 55]; cachedTargetBones = new Transform[targets.Count, BoneCount];
for (int t = 0; t < targets.Count; t++) for (int t = 0; t < targets.Count; t++)
{ {
Animator animator = targets[t].animator; Animator animator = targets[t].animator;
if (animator == null) continue; if (animator == null) continue;
for (int i = 0; i < 55; i++) for (int i = 0; i < BoneCount; i++)
{ {
cachedTargetBones[t, i] = animator.GetBoneTransform((HumanBodyBones)i); cachedTargetBones[t, i] = animator.GetBoneTransform((HumanBodyBones)i);
} }
@ -205,7 +208,7 @@ public class SimplePoseTransfer : MonoBehaviour
} }
// 모든 본에 대해 포즈 전송 // 모든 본에 대해 포즈 전송
for (int i = 0; i < 55; i++) for (int i = 0; i < BoneCount; i++)
{ {
Transform targetBoneTransform = cachedTargetBones[targetIndex, i]; Transform targetBoneTransform = cachedTargetBones[targetIndex, i];
Transform sourceBoneTransform = cachedSourceBones[i]; Transform sourceBoneTransform = cachedSourceBones[i];

View File

@ -88,6 +88,25 @@ namespace KindRetargeting
limb.localBendNormal = Quaternion.Inverse(limb.upper.rotation) * bendNormal; limb.localBendNormal = Quaternion.Inverse(limb.upper.rotation) * bendNormal;
} }
/// <summary>
/// avatarScale 등으로 아바타 크기가 바뀌었을 때 호출하여 본 길이 캐시를 재계산.
/// </summary>
public void RefreshLimbLengths()
{
if (!isInitialized) return;
RecacheLength(leftArm);
RecacheLength(rightArm);
RecacheLength(leftLeg);
RecacheLength(rightLeg);
}
private void RecacheLength(LimbIK limb)
{
if (limb.upper == null || limb.lower == null || limb.end == null) return;
limb.upperLength = Vector3.Distance(limb.upper.position, limb.lower.position);
limb.lowerLength = Vector3.Distance(limb.lower.position, limb.end.position);
}
public void OnUpdate() public void OnUpdate()
{ {
if (!isInitialized) return; if (!isInitialized) return;
@ -111,8 +130,9 @@ namespace KindRetargeting
Quaternion fkLowerRot = limb.lower.rotation; Quaternion fkLowerRot = limb.lower.rotation;
Quaternion fkEndRot = limb.end.rotation; Quaternion fkEndRot = limb.end.rotation;
float upperLen = Vector3.Distance(limb.upper.position, limb.lower.position); // 본 길이는 Initialize/RefreshLimbLengths에서 캐싱됨 (avatarScale 변경 시 갱신)
float lowerLen = Vector3.Distance(limb.lower.position, limb.end.position); float upperLen = limb.upperLength;
float lowerLen = limb.lowerLength;
Vector3 targetPos = limb.target.position; Vector3 targetPos = limb.target.position;