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;