diff --git a/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs b/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs
index e927e1d7c..03cf7b812 100644
--- a/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs
+++ b/Assets/Scripts/KindRetargeting/CustomRetargetingScript.cs
@@ -732,14 +732,10 @@ namespace KindRetargeting
}
///
- /// 머리 본에 회전 오프셋을 적용합니다.
+ /// 머리 본에 회전 오프셋을 적용합니다. headBone 멤버 캐시 재사용 (ApplyHeadScale과 일관).
///
private void ApplyHeadRotationOffset()
{
- if (targetAnimator == null) return;
-
- // 머리 본 가져오기
- Transform headBone = targetAnimator.GetBoneTransform(HumanBodyBones.Head);
if (headBone == null) return;
// 오프셋이 모두 0이면 스킵
@@ -913,16 +909,30 @@ namespace KindRetargeting
SyncBoneRotations(skipBone: HumanBodyBones.Hips);
}
+ // 자동 힙 보정 캐시 (다리 본 길이는 스케일 변경 시에만 갱신됨)
+ private float cachedAutoHipsOffsetY = 0f;
+ private bool autoHipsOffsetCacheValid = false;
+
///
- /// 매 프레임 자동으로 적용되는 힙 상하 보정값.
- /// = (타겟 다리길이 - 소스 다리길이) + (타겟 Hips↔UpperLeg 갭) × avatarScale
- ///
- /// 첫 항: 다리 길이 차이로 발이 뜨거나 묻히는 현상 보정.
- /// 둘째 항: Hips 본이 UpperLeg보다 위에 있는 아바타 추가 보정.
+ /// 매 프레임 호출되는 자동 힙 보정값. 본 길이는 변하지 않으므로 캐시된 값을 반환.
+ /// avatarScale 변경 시 ApplyScale → RefreshAutoHipsOffsetCache 로 자동 갱신.
///
private float ComputeAutoHipsOffsetY()
{
- if (optitrackSource == null || targetAnimator == null) return 0f;
+ if (!autoHipsOffsetCacheValid) RefreshAutoHipsOffsetCache();
+ return cachedAutoHipsOffsetY;
+ }
+
+ ///
+ /// 자동 힙 보정 캐시를 재계산합니다 (Initialize / avatarScale 변경 시 호출).
+ /// = (타겟 다리길이 - 소스 다리길이) + (타겟 Hips↔UpperLeg 갭) × avatarScale
+ ///
+ private void RefreshAutoHipsOffsetCache()
+ {
+ cachedAutoHipsOffsetY = 0f;
+ autoHipsOffsetCacheValid = true;
+
+ if (optitrackSource == null || targetAnimator == null) return;
Transform sUp = optitrackSource.GetBoneTransform(HumanBodyBones.LeftUpperLeg);
Transform sLo = optitrackSource.GetBoneTransform(HumanBodyBones.LeftLowerLeg);
@@ -934,14 +944,14 @@ namespace KindRetargeting
if (sUp == null || sLo == null || sFt == 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 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;
- return (targetLeg - sourceLeg) + hipsToLegGap * avatarScale;
+ cachedAutoHipsOffsetY = (targetLeg - sourceLeg) + hipsToLegGap * avatarScale;
}
///
@@ -1465,6 +1475,10 @@ namespace KindRetargeting
}
}
}
+
+ // 스케일 변경으로 본 길이가 바뀌었으니 IK 캐시 + 자동 힙 보정 캐시 갱신
+ ikSolver?.RefreshLimbLengths();
+ RefreshAutoHipsOffsetCache();
}
// 리셋 기능 추가
diff --git a/Assets/Scripts/KindRetargeting/FingerShapedController.cs b/Assets/Scripts/KindRetargeting/FingerShapedController.cs
index 533caaa49..335c84c54 100644
--- a/Assets/Scripts/KindRetargeting/FingerShapedController.cs
+++ b/Assets/Scripts/KindRetargeting/FingerShapedController.cs
@@ -1,5 +1,4 @@
using UnityEngine;
-using System.Collections.Generic;
namespace KindRetargeting
{
@@ -9,8 +8,10 @@ namespace KindRetargeting
private Animator animator;
private HumanPoseHandler humanPoseHandler;
- // 손가락을 제외한 모든 본의 로컬 회전 저장용 (SetHumanPose 호출 시 몸 복원용)
- private Dictionary savedBoneLocalRotations = new Dictionary();
+ // 매 프레임 재사용되는 캐시 (GC 압박 제거)
+ private Transform[] cachedNonFingerBones;
+ private Quaternion[] savedBoneRotations;
+ private HumanPose cachedHumanPose;
// 손가락을 제외한 모든 휴먼본 목록
private static readonly HumanBodyBones[] nonFingerBones = new HumanBodyBones[]
@@ -71,6 +72,18 @@ namespace KindRetargeting
if (animator == null || !animator.isHuman) return;
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;
}
@@ -92,40 +105,29 @@ namespace KindRetargeting
private void UpdateMuscleValues()
{
- // 1. 손가락을 제외한 모든 본의 로컬 회전 저장 (SetHumanPose 호출 전)
- savedBoneLocalRotations.Clear();
- for (int i = 0; i < nonFingerBones.Length; i++)
+ // 1. 비손가락 본의 로컬 회전을 배열에 저장 (캐시된 Transform 재사용)
+ for (int i = 0; i < cachedNonFingerBones.Length; i++)
{
- Transform bone = animator.GetBoneTransform(nonFingerBones[i]);
- if (bone != null)
- {
- savedBoneLocalRotations[nonFingerBones[i]] = bone.localRotation;
- }
+ Transform bone = cachedNonFingerBones[i];
+ if (bone != null) savedBoneRotations[i] = bone.localRotation;
}
- // 2. HumanPose 가져오기 및 손가락 머슬 설정
- HumanPose humanPose = new HumanPose();
- humanPoseHandler.GetHumanPose(ref humanPose);
+ // 2. HumanPose 가져오기 (재사용 필드, GC 없음) + 손가락 머슬 설정
+ humanPoseHandler.GetHumanPose(ref cachedHumanPose);
- // 왼손 제어
SetHandMuscles(true, leftThumbCurl, leftIndexCurl, leftMiddleCurl, leftRingCurl,
- leftPinkyCurl, leftSpreadFingers, ref humanPose);
-
- // 오른손 제어
+ leftPinkyCurl, leftSpreadFingers, ref cachedHumanPose);
SetHandMuscles(false, rightThumbCurl, rightIndexCurl, rightMiddleCurl, rightRingCurl,
- rightPinkyCurl, rightSpreadFingers, ref humanPose);
+ rightPinkyCurl, rightSpreadFingers, ref cachedHumanPose);
// 3. 머슬 포즈 적용 (손가락 포함 전체 본에 영향)
- humanPoseHandler.SetHumanPose(ref humanPose);
+ humanPoseHandler.SetHumanPose(ref cachedHumanPose);
- // 4. 손가락을 제외한 모든 본의 로컬 회전 복원 (본 길이 변형 방지)
- foreach (var kvp in savedBoneLocalRotations)
+ // 4. 비손가락 본 로컬 회전 복원 (본 길이 변형 방지)
+ for (int i = 0; i < cachedNonFingerBones.Length; i++)
{
- Transform bone = animator.GetBoneTransform(kvp.Key);
- if (bone != null)
- {
- bone.localRotation = kvp.Value;
- }
+ Transform bone = cachedNonFingerBones[i];
+ if (bone != null) bone.localRotation = savedBoneRotations[i];
}
}
diff --git a/Assets/Scripts/KindRetargeting/LimbWeightController.cs b/Assets/Scripts/KindRetargeting/LimbWeightController.cs
index 6d434913f..e721c3ce2 100644
--- a/Assets/Scripts/KindRetargeting/LimbWeightController.cs
+++ b/Assets/Scripts/KindRetargeting/LimbWeightController.cs
@@ -39,13 +39,14 @@ namespace KindRetargeting
private CustomRetargetingScript crs;
private Transform characterRoot;
- List leftArmEndWeights = new List();
- List rightArmEndWeights = new List();
- List leftLegEndWeights = new List();
- List rightLegEndWeights = new List();
+ // 가중치 배열: 인덱스 의미는 Initialize 주석 참조 (크기 고정 → float[] 사용으로 GC/인덱싱 비용 절감)
+ readonly float[] leftArmEndWeights = new float[2]; // [0] 양손거리 [1] 프랍거리
+ readonly float[] rightArmEndWeights = new float[2];
+ readonly float[] leftLegEndWeights = new float[2]; // [0] 앉기 수평거리 [1] 발 높이
+ readonly float[] rightLegEndWeights = new float[2];
- List leftLegBendWeights = new List();
- List rightLegBendWeights = new List();
+ readonly float[] leftLegBendWeights = new float[1] { 1f };
+ readonly float[] rightLegBendWeights = new float[1] { 1f };
private float MasterleftArmEndWeights = 0f;
private float MasterrightArmEndWeights = 0f;
@@ -55,9 +56,11 @@ namespace KindRetargeting
private float MasterrightLegBendWeights = 0f;
public List props = new List();
+ // SitChairDistances 매 프레임 GetComponent 비용 회피용 (Initialize 시점 한 번 캐싱)
+ private List chairProps = new List();
- // 힙스 가중치 리스트 추가
- List hipsWeights = new List();
+ // 힙스 가중치: [0] 의자 거리 [1] 지면 높이
+ readonly float[] hipsWeights = new float[2] { 1f, 1f };
private float MasterHipsWeight = 1f;
// 의자 좌석 높이 오프셋 (월드 Y 기준)
@@ -76,44 +79,26 @@ namespace KindRetargeting
InitWeightLayers();
- //프랍 오브젝트 찾기
- props = Object.FindObjectsByType(FindObjectsSortMode.None).Select(controller => controller.transform).ToList();
+ //프랍 오브젝트 찾기 + 의자 타입 별도 캐싱
+ var allPropControllers = Object.FindObjectsByType(FindObjectsSortMode.None);
+ props = allPropControllers.Select(c => c.transform).ToList();
+ chairProps = allPropControllers
+ .Where(c => c.propType == EnumsList.PropType.Chair)
+ .Select(c => c.transform)
+ .ToList();
// 다른 캐릭터의 손을 props에 추가
GetHand();
- //HandDistances()에서 사용을 위한 리스트 추가
- //손 거리에 따른 웨이트 업데이트 인덱스 0번
- leftArmEndWeights.Add(0);
- rightArmEndWeights.Add(0);
-
- // 프랍과의 거리에 따른 웨이트 업데이트 인덱스 1번
- leftArmEndWeights.Add(0);
- rightArmEndWeights.Add(0);
-
- // 앉아있을 때 다리와의 거리에 따른 가중치 적용 인덱스 0번
- leftLegEndWeights.Add(0);
- rightLegEndWeights.Add(0);
-
- // 다리 골 가중치 초기화
- leftLegBendWeights.Add(1f); // 기본 가중치
- rightLegBendWeights.Add(1f); // 기본 가중치
+ // 가중치 배열은 필드 선언 시 고정 크기로 초기화됨 (다리 EndWeight[1]만 1f 기본값)
+ leftLegEndWeights[1] = 1f;
+ rightLegEndWeights[1] = 1f;
if (this.characterRoot == null)
{
this.characterRoot = crs.transform;
}
- // 힙스 가중치 초기화 인덱스 0번
- hipsWeights.Add(1f); // 의자 거리 기반 가중치
-
- // 지면 높이 기반 가중치 초기화 인덱스 1번
- hipsWeights.Add(1f); // 지면 높이 기반 가중치
-
- // 발 높이 기반 가중치 초기화 인덱스 1번
- leftLegEndWeights.Add(1f);
- rightLegEndWeights.Add(1f);
-
isInitialized = true;
}
@@ -237,7 +222,7 @@ namespace KindRetargeting
}
}
- private void ProcessSitLegWeight(Transform hips, Transform footTarget, List weightList, int weightIndex)
+ private void ProcessSitLegWeight(Transform hips, Transform footTarget, float[] weightArr, int weightIndex)
{
if (footTarget == null) return;
@@ -256,7 +241,7 @@ namespace KindRetargeting
float weight = 1f - Mathf.Clamp01((horizontalDistance - MIN_LEG_DISTANCE_RATIO) /
(MAX_LEG_DISTANCE_RATIO - MIN_LEG_DISTANCE_RATIO));
- weightList[weightIndex] = weight;
+ weightArr[weightIndex] = weight;
}
void PropDistances()
@@ -298,40 +283,29 @@ namespace KindRetargeting
{
if (crs == null) return;
- Transform hipsTransform = crs.optitrackSource.GetBoneTransform(HumanBodyBones.Hips);
- if (hipsTransform != null && props != null)
+ Transform hipsTransform = crs?.optitrackSource?.GetBoneTransform(HumanBodyBones.Hips);
+ if (hipsTransform == null || chairProps.Count == 0)
{
- float minDistance = float.MaxValue;
- bool foundChair = false;
-
- foreach (Transform prop in props)
- {
- PropTypeController ptc = prop.GetComponent();
- 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;
- }
+ hipsWeights[0] = 1f;
+ targetChairSeatOffset = 0f;
+ return;
}
+
+ // 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);
}
///
@@ -400,18 +374,18 @@ namespace KindRetargeting
}
///
- /// 리스트에서 최대값을 찾습니다.
+ /// 배열에서 최대값을 찾습니다.
///
- private float GetMaxValue(List list)
+ private float GetMaxValue(float[] arr)
{
- if (list.Count == 0) return 0f;
+ if (arr.Length == 0) return 0f;
- float max = list[0];
- for (int i = 1; i < list.Count; i++)
+ float max = arr[0];
+ for (int i = 1; i < arr.Length; i++)
{
- if (list[i] > max)
+ if (arr[i] > max)
{
- max = list[i];
+ max = arr[i];
}
}
return max;
@@ -419,18 +393,18 @@ namespace KindRetargeting
///
- /// 리스트에서 최소값을 찾습니다.
+ /// 배열에서 최소값을 찾습니다.
///
- private float GetMinValue(List list)
+ private float GetMinValue(float[] arr)
{
- if (list.Count == 0) return 0f;
+ if (arr.Length == 0) return 0f;
- float min = list[0];
- for (int i = 1; i < list.Count; i++)
+ float min = arr[0];
+ for (int i = 1; i < arr.Length; i++)
{
- if (list[i] < min)
+ if (arr[i] < min)
{
- min = list[i];
+ min = arr[i];
}
}
return min;
@@ -473,7 +447,7 @@ namespace KindRetargeting
{
if (crs == null || ikSolver == null) return;
- Transform hipsTransform = crs.optitrackSource.GetBoneTransform(HumanBodyBones.Hips);
+ Transform hipsTransform = crs?.optitrackSource?.GetBoneTransform(HumanBodyBones.Hips);
if (hipsTransform != null)
{
float groundHeight = characterRoot.position.y;
@@ -506,7 +480,7 @@ namespace KindRetargeting
);
}
- private void ProcessFootHeightWeight(Transform footTarget, List weightList, int weightIndex)
+ private void ProcessFootHeightWeight(Transform footTarget, float[] weightArr, int weightIndex)
{
if (footTarget == null) return;
@@ -518,7 +492,7 @@ namespace KindRetargeting
(footHeightMaxThreshold - footHeightMinThreshold));
// 계산된 가중치 설정
- weightList[weightIndex] = weight;
+ weightArr[weightIndex] = weight;
}
}
}
diff --git a/Assets/Scripts/KindRetargeting/Remote/RetargetingRemoteController.cs b/Assets/Scripts/KindRetargeting/Remote/RetargetingRemoteController.cs
index 252be0e71..bf4fa3e69 100644
--- a/Assets/Scripts/KindRetargeting/Remote/RetargetingRemoteController.cs
+++ b/Assets/Scripts/KindRetargeting/Remote/RetargetingRemoteController.cs
@@ -624,11 +624,25 @@ namespace KindRetargeting.Remote
#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(object obj, string fieldName)
{
try
{
- var field = obj.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
+ var field = ResolveField(obj.GetType(), fieldName);
if (field != null)
return (T)field.GetValue(obj);
@@ -646,7 +660,7 @@ namespace KindRetargeting.Remote
{
try
{
- var field = obj.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
+ var field = ResolveField(obj.GetType(), fieldName);
if (field != null)
{
field.SetValue(obj, value);
diff --git a/Assets/Scripts/KindRetargeting/SimplePoseTransfer.cs b/Assets/Scripts/KindRetargeting/SimplePoseTransfer.cs
index 7bed289b4..3a8e62fe9 100644
--- a/Assets/Scripts/KindRetargeting/SimplePoseTransfer.cs
+++ b/Assets/Scripts/KindRetargeting/SimplePoseTransfer.cs
@@ -5,6 +5,9 @@ using KindRetargeting;
[DefaultExecutionOrder(16001)]
public class SimplePoseTransfer : MonoBehaviour
{
+ // 0~54: 휴머노이드 본 (몸체 + 손가락). HumanBodyBones.LastBone 직전까지.
+ private const int BoneCount = 55;
+
[System.Serializable]
public class TargetEntry
{
@@ -121,7 +124,7 @@ public class SimplePoseTransfer : MonoBehaviour
private void InitializeTargetBones()
{
- boneRotationDifferences = new Quaternion[targets.Count, 55];
+ boneRotationDifferences = new Quaternion[targets.Count, BoneCount];
for (int i = 0; i < targets.Count; i++)
{
@@ -133,7 +136,7 @@ public class SimplePoseTransfer : MonoBehaviour
}
// 55개의 휴머노이드 본에 대해 회전 차이 계산
- for (int j = 0; j < 55; j++)
+ for (int j = 0; j < BoneCount; j++)
{
Transform sourceBoneTransform = sourceBone.GetBoneTransform((HumanBodyBones)j);
Transform targetBoneTransform = animator.GetBoneTransform((HumanBodyBones)j);
@@ -149,19 +152,19 @@ public class SimplePoseTransfer : MonoBehaviour
private void CacheAllBoneTransforms()
{
// 소스 본 캐싱
- cachedSourceBones = new Transform[55];
- for (int i = 0; i < 55; i++)
+ cachedSourceBones = new Transform[BoneCount];
+ for (int i = 0; i < BoneCount; 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++)
{
Animator animator = targets[t].animator;
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);
}
@@ -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 sourceBoneTransform = cachedSourceBones[i];
diff --git a/Assets/Scripts/KindRetargeting/TwoBoneIKSolver.cs b/Assets/Scripts/KindRetargeting/TwoBoneIKSolver.cs
index f726c1800..8322a8b4d 100644
--- a/Assets/Scripts/KindRetargeting/TwoBoneIKSolver.cs
+++ b/Assets/Scripts/KindRetargeting/TwoBoneIKSolver.cs
@@ -88,6 +88,25 @@ namespace KindRetargeting
limb.localBendNormal = Quaternion.Inverse(limb.upper.rotation) * bendNormal;
}
+ ///
+ /// avatarScale 등으로 아바타 크기가 바뀌었을 때 호출하여 본 길이 캐시를 재계산.
+ ///
+ 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()
{
if (!isInitialized) return;
@@ -111,8 +130,9 @@ namespace KindRetargeting
Quaternion fkLowerRot = limb.lower.rotation;
Quaternion fkEndRot = limb.end.rotation;
- float upperLen = Vector3.Distance(limb.upper.position, limb.lower.position);
- float lowerLen = Vector3.Distance(limb.lower.position, limb.end.position);
+ // 본 길이는 Initialize/RefreshLimbLengths에서 캐싱됨 (avatarScale 변경 시 갱신)
+ float upperLen = limb.upperLength;
+ float lowerLen = limb.lowerLength;
Vector3 targetPos = limb.target.position;