Fix : 리타겟팅 웨이브 특성 강화

This commit is contained in:
DESKTOP-S4BOTN2\user 2026-03-21 03:02:09 +09:00
parent 3f3741719c
commit f00567b48e
12 changed files with 1451 additions and 440 deletions

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 11bcd309fb41db841bca0a02919c3778
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@ -0,0 +1,525 @@
fileFormatVersion: 2
guid: 08341f64ea9feaf448b13cb539f2c4c8
ModelImporter:
serializedVersion: 24200
internalIDToNameTable: []
externalObjects: {}
materials:
materialImportMode: 2
materialName: 0
materialSearch: 1
materialLocation: 1
animations:
legacyGenerateAnimations: 4
bakeSimulation: 0
resampleCurves: 1
optimizeGameObjects: 0
removeConstantScaleCurves: 0
motionNodeName:
animationImportErrors:
animationImportWarnings:
animationRetargetingWarnings:
animationDoRetargetingWarnings: 0
importAnimatedCustomProperties: 0
importConstraints: 0
animationCompression: 3
animationRotationError: 0.5
animationPositionError: 0.5
animationScaleError: 0.5
animationWrapMode: 0
extraExposedTransformPaths: []
extraUserProperties: []
clipAnimations: []
isReadable: 0
meshes:
lODScreenPercentages: []
globalScale: 1
meshCompression: 0
addColliders: 0
useSRGBMaterialColor: 1
sortHierarchyByName: 1
importPhysicalCameras: 1
importVisibility: 1
importBlendShapes: 1
importCameras: 1
importLights: 1
nodeNameCollisionStrategy: 1
fileIdsGeneration: 2
swapUVChannels: 0
generateSecondaryUV: 0
useFileUnits: 1
keepQuads: 0
weldVertices: 1
bakeAxisConversion: 0
preserveHierarchy: 0
skinWeightsMode: 0
maxBonesPerVertex: 4
minBoneWeight: 0.001
optimizeBones: 1
generateMeshLods: 0
meshLodGenerationFlags: 0
maximumMeshLod: -1
meshOptimizationFlags: -1
indexFormat: 0
secondaryUVAngleDistortion: 8
secondaryUVAreaDistortion: 15.000001
secondaryUVHardAngle: 88
secondaryUVMarginMethod: 1
secondaryUVMinLightmapResolution: 40
secondaryUVMinObjectScale: 1
secondaryUVPackMargin: 4
useFileScale: 1
strictVertexDataChecks: 0
tangentSpace:
normalSmoothAngle: 60
normalImportMode: 0
tangentImportMode: 3
normalCalculationMode: 4
legacyComputeAllNormalsFromSmoothingGroupsWhenMeshHasBlendShapes: 0
blendShapeNormalImportMode: 1
normalSmoothingSource: 0
referencedClips: []
importAnimation: 0
humanDescription:
serializedVersion: 3
human: []
skeleton:
- name: BaseAvatar - OptiTrack 5 bone(Clone)
parentName:
position: {x: 0, y: 0, z: 0}
rotation: {x: 0, y: 0, z: 0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_Root
parentName: BaseAvatar - OptiTrack 5 bone(Clone)
position: {x: -0, y: 0, z: 0}
rotation: {x: -0.7071068, y: 0, z: -0, w: 0.7071067}
scale: {x: 1, y: 1, z: 1}
- name: 001_Hips
parentName: 001_Root
position: {x: -0, y: -0.00000014102463, z: 0.865603}
rotation: {x: 0.7071068, y: 0, z: -0, w: 0.7071067}
scale: {x: 1, y: 1, z: 1}
- name: 001_LeftUpLeg
parentName: 001_Hips
position: {x: -0.0897235, y: 3.3639758e-14, z: 0}
rotation: {x: -7.105428e-14, y: 0, z: -0, w: 1}
scale: {x: 0.99999994, y: 0.99999994, z: 0.99999994}
- name: 001_LeftLeg
parentName: 001_LeftUpLeg
position: {x: -0.000000007450581, y: -0.365626, z: 2.8421713e-14}
rotation: {x: -2.1316282e-14, y: 0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_LeftFoot
parentName: 001_LeftLeg
position: {x: -0.000000007450581, y: -0.42819893, z: -9.7699635e-15}
rotation: {x: 2.842171e-14, y: 0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_LeftToeBase
parentName: 001_LeftFoot
position: {x: -0, y: -0.058320336, z: 0.134585}
rotation: {x: 0.00000016933137, y: 0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_LToeEnd
parentName: 001_LeftToeBase
position: {x: 0.000000007450581, y: 0.000000020170326, z: 0.043999992}
rotation: {x: -0.0000000846657, y: 0, z: -0, w: 1}
scale: {x: 1, y: 0.99999994, z: 1}
- name: 001_LToeEnd_end
parentName: 001_LToeEnd
position: {x: -0, y: 0.043999996, z: 0}
rotation: {x: 0, y: -0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_RightUpLeg
parentName: 001_Hips
position: {x: 0.0897235, y: 3.3639758e-14, z: 0}
rotation: {x: -7.1054274e-14, y: 0, z: -0, w: 1}
scale: {x: 0.99999994, y: 1, z: 0.99999994}
- name: 001_RightLeg
parentName: 001_RightUpLeg
position: {x: 0.000000007450581, y: -0.36562604, z: 2.1316285e-14}
rotation: {x: -2.4868997e-14, y: 0, z: -0, w: 1}
scale: {x: 1, y: 0.99999994, z: 1}
- name: 001_RightFoot
parentName: 001_RightLeg
position: {x: -0, y: -0.42819893, z: -1.332268e-14}
rotation: {x: 2.4868996e-14, y: 0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_RightToeBase
parentName: 001_RightFoot
position: {x: -0, y: -0.058320336, z: 0.134585}
rotation: {x: 0.00000016933137, y: 0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_RToeEnd
parentName: 001_RightToeBase
position: {x: -0.000000007450581, y: 0.000000020170326, z: 0.043999992}
rotation: {x: -0.0000000846657, y: 0, z: -0, w: 1}
scale: {x: 1, y: 0.99999994, z: 1}
- name: 001_RToeEnd_end
parentName: 001_RToeEnd
position: {x: -0, y: 0.043999996, z: 0}
rotation: {x: 0, y: -0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_Spine
parentName: 001_Hips
position: {x: -0, y: 0.03325814, z: 0}
rotation: {x: 0.0888559, y: 0, z: -0, w: 0.9960445}
scale: {x: 1, y: 0.99999994, z: 0.99999994}
- name: 001_Spine1
parentName: 001_Spine
position: {x: -0, y: 0.059864596, z: 0}
rotation: {x: 0.0035778934, y: 0, z: -0, w: 0.9999936}
scale: {x: 1, y: 0.99999994, z: 1}
- name: 001_Spine2
parentName: 001_Spine1
position: {x: -0, y: 0.066516146, z: 0.000000011175871}
rotation: {x: -0.09237056, y: 0, z: -0, w: 0.9957247}
scale: {x: 1, y: 1, z: 1}
- name: 001_Spine3
parentName: 001_Spine2
position: {x: -0, y: 0.07815656, z: 7.712515e-10}
rotation: {x: -0.049721405, y: 0, z: -0, w: 0.99876314}
scale: {x: 0.99999994, y: 0.99999994, z: 1}
- name: 001_Spine4
parentName: 001_Spine3
position: {x: -0, y: 0.17460515, z: -0.0000000069849193}
rotation: {x: -0.024432134, y: 0, z: -0, w: 0.9997015}
scale: {x: 1, y: 1, z: 0.9999999}
- name: 001_LeftShoulder
parentName: 001_Spine4
position: {x: -0.0358696, y: 0.045248758, z: -0.0049734665}
rotation: {x: -0.00000016391277, y: 0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_LeftArm
parentName: 001_LeftShoulder
position: {x: -0.134977, y: 0.000000013154931, z: 0.000000019557774}
rotation: {x: 0.000000059604645, y: 0, z: -0, w: 1}
scale: {x: 0.99999994, y: 1, z: 1}
- name: 001_LeftForeArm
parentName: 001_LeftArm
position: {x: -0.273732, y: 0.000000011816155, z: -0.000000006519258}
rotation: {x: 0.0000000037252903, y: 0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_LeftHand
parentName: 001_LeftForeArm
position: {x: -0.19546905, y: 0.000000011990778, z: -0.000000008381903}
rotation: {x: -0.00000008195639, y: 0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_LeftHandIndex1
parentName: 001_LeftHand
position: {x: -0.07671362, y: 0.000000024912879, z: 0.029833097}
rotation: {x: 0.087155744, y: 0, z: -0, w: 0.9961947}
scale: {x: 1, y: 1, z: 1}
- name: 001_LeftHandIndex2
parentName: 001_LeftHandIndex1
position: {x: -0.042618692, y: -0.0000000088475645, z: -0.0000000018626451}
rotation: {x: 0.000000058207664, y: 0, z: -0, w: 1}
scale: {x: 1, y: 0.99999994, z: 1}
- name: 001_LeftHandIndex3
parentName: 001_LeftHandIndex2
position: {x: -0.021309257, y: -0.0000000060535967, z: -9.313226e-10}
rotation: {x: -0.000000041443855, y: 0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_LIndex3End
parentName: 001_LeftHandIndex3
position: {x: -0.023440301, y: -0.000000007916242, z: -9.313226e-10}
rotation: {x: -4.656613e-10, y: 0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_LIndex3End_end
parentName: 001_LIndex3End
position: {x: -0, y: 0.023440227, z: 0}
rotation: {x: 0, y: -0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_LeftHandMiddle1
parentName: 001_LeftHand
position: {x: -0.07671362, y: 0.00000009732321, z: 0.009887521}
rotation: {x: 0.000000067055225, y: 0, z: -0, w: 1}
scale: {x: 1, y: 0.99999994, z: 1}
- name: 001_LeftHandMiddle2
parentName: 001_LeftHandMiddle1
position: {x: -0.046880484, y: -0.000000022235326, z: 0.000000010244548}
rotation: {x: -0.000000040978193, y: 0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_LeftHandMiddle3
parentName: 001_LeftHandMiddle2
position: {x: -0.025571287, y: 0.00000009633368, z: 0.0000000121071935}
rotation: {x: -0.000000048428774, y: 0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_LMiddle3End
parentName: 001_LeftHandMiddle3
position: {x: -0.028128326, y: 0.00000009551877, z: 0.000000014901161}
rotation: {x: 0.0000000037252903, y: 0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_LMiddle3End_end
parentName: 001_LMiddle3End
position: {x: -0, y: 0.028128315, z: 0}
rotation: {x: 0, y: -0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_LeftHandPinky1
parentName: 001_LeftHand
position: {x: -0.06818998, y: 0.00000012246892, z: -0.0298331}
rotation: {x: -0.08715575, y: 0, z: -0, w: 0.9961947}
scale: {x: 1, y: 1.0000001, z: 0.99999994}
- name: 001_LeftHandPinky2
parentName: 001_LeftHandPinky1
position: {x: -0.03409493, y: 0.00000008754432, z: -0.000000044703484}
rotation: {x: 0.0000005662441, y: 0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_LeftHandPinky3
parentName: 001_LeftHandPinky2
position: {x: -0.017047465, y: 0.00000005122274, z: -0.0000000037252903}
rotation: {x: -0.000000692904, y: 0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_LPinky3End
parentName: 001_LeftHandPinky3
position: {x: -0.018752158, y: 0.00000009872019, z: -0.000000048428774}
rotation: {x: 0.000000014901161, y: 0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_LPinky3End_end
parentName: 001_LPinky3End
position: {x: -0, y: 0.018752106, z: 0}
rotation: {x: 0, y: -0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_LeftHandRing1
parentName: 001_LeftHand
position: {x: -0.07245177, y: -0.00000006821938, z: -0.009887559}
rotation: {x: 0.000000115484, y: 0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_LeftHandRing2
parentName: 001_LeftHandRing1
position: {x: -0.04261875, y: 0.000000047963113, z: 0.000000013038516}
rotation: {x: -0.0000000037252903, y: 0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_LeftHandRing3
parentName: 001_LeftHandRing2
position: {x: -0.021309197, y: 0.000000047730282, z: 0.000000031664968}
rotation: {x: 0.00000033527613, y: 0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_LRing3End
parentName: 001_LeftHandRing3
position: {x: -0.023440242, y: 0.00000015622936, z: 0.000000018626451}
rotation: {x: -0.000000007450581, y: 0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_LRing3End_end
parentName: 001_LRing3End
position: {x: -0, y: 0.02344036, z: 0}
rotation: {x: 0, y: -0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_LeftHandThumb1
parentName: 001_LeftHand
position: {x: -0.018752277, y: -0.021309385, z: 0.029833077}
rotation: {x: 0.4072523, y: 0.34298575, z: 0.23624216, w: 0.81283206}
scale: {x: 1, y: 1, z: 1}
- name: 001_LeftHandThumb2
parentName: 001_LeftHandThumb1
position: {x: -0.02832238, y: 0.000000027939677, z: -0.000000055909595}
rotation: {x: 0, y: 0, z: 0, w: 1}
scale: {x: 1, y: 0.99999994, z: 1}
- name: 001_LeftHandThumb3
parentName: 001_LeftHandThumb2
position: {x: -0.024207503, y: -0.000000015943257, z: -0.0000000066128223}
rotation: {x: 0.0000014528632, y: 0.00000007765939, z: 9.01501e-14, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_LThumb3End
parentName: 001_LeftHandThumb3
position: {x: -0.026628096, y: -0.0000000408308, z: -0.000000017800573}
rotation: {x: 0.000000014901161, y: 1.4210855e-13, z: -1.4654944e-14, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_LThumb3End_end
parentName: 001_LThumb3End
position: {x: -0, y: 0.026628118, z: 0}
rotation: {x: 0, y: -0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_Neck
parentName: 001_Spine4
position: {x: -0, y: 0.10975203, z: 0.010808894}
rotation: {x: 0.13785088, y: 0, z: -0, w: 0.990453}
scale: {x: 1, y: 0.9999999, z: 0.9999999}
- name: 001_Neck1
parentName: 001_Neck
position: {x: -0, y: 0.0740802, z: -9.313226e-10}
rotation: {x: 0.000000052154068, y: 0, z: -0, w: 1}
scale: {x: 1, y: 0.99999994, z: 1}
- name: 001_Head
parentName: 001_Neck1
position: {x: -0, y: 0.07597939, z: -0.000000016763806}
rotation: {x: -0.10018798, y: 0, z: -0, w: 0.99496853}
scale: {x: 1, y: 1, z: 0.99999994}
- name: 001_HeadEnd
parentName: 001_Head
position: {x: -0, y: 0.22000016, z: -0.000000004656613}
rotation: {x: -0.000000018626451, y: 0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_HeadEnd_end
parentName: 001_HeadEnd
position: {x: -0, y: 0.22000012, z: 0}
rotation: {x: 0, y: -0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_RightShoulder
parentName: 001_Spine4
position: {x: 0.0358696, y: 0.045248758, z: -0.0049734665}
rotation: {x: -0.00000016391277, y: 0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_RightArm
parentName: 001_RightShoulder
position: {x: 0.134977, y: 0.000000013154931, z: 0.000000019557774}
rotation: {x: 0.000000059604645, y: 0, z: -0, w: 1}
scale: {x: 0.99999994, y: 1, z: 1}
- name: 001_RightForeArm
parentName: 001_RightArm
position: {x: 0.273732, y: 0.000000011816155, z: -0.000000006519258}
rotation: {x: 0.000000007450581, y: 0, z: -0, w: 1}
scale: {x: 1, y: 0.99999994, z: 1}
- name: 001_RightHand
parentName: 001_RightForeArm
position: {x: 0.19546905, y: 0.00000013131648, z: -0.000000008381903}
rotation: {x: -0.000000078231096, y: 0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_RightHandIndex1
parentName: 001_RightHand
position: {x: 0.07671362, y: -0.00000009406358, z: 0.029833067}
rotation: {x: 0.08715574, y: 0, z: -0, w: 0.9961947}
scale: {x: 1, y: 1, z: 1}
- name: 001_RightHandIndex2
parentName: 001_RightHandIndex1
position: {x: 0.04261881, y: -0.0000000088475645, z: 0.0000000027939677}
rotation: {x: 0.000000058673326, y: 0, z: -0, w: 1}
scale: {x: 1, y: 0.99999994, z: 1}
- name: 001_RightHandIndex3
parentName: 001_RightHandIndex2
position: {x: 0.021309316, y: -0.00000012526289, z: 0.000000004656613}
rotation: {x: -0.000000040512532, y: 0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_RIndex3End
parentName: 001_RightHandIndex3
position: {x: 0.023440242, y: -0.00000012712553, z: 0.000000004656613}
rotation: {x: 4.656613e-10, y: 0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_RIndex3End_end
parentName: 001_RIndex3End
position: {x: -0, y: 0.023440227, z: 0}
rotation: {x: 0, y: -0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_RightHandMiddle1
parentName: 001_RightHand
position: {x: 0.07671362, y: -0.00000002188608, z: 0.009887507}
rotation: {x: 0.000000059604645, y: 0, z: -0, w: 1}
scale: {x: 1, y: 0.99999994, z: 1}
- name: 001_RightHandMiddle2
parentName: 001_RightHandMiddle1
position: {x: 0.046880543, y: 0.00000009697396, z: -0.0000000044237822}
rotation: {x: -0.000000052154064, y: 0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_RightHandMiddle3
parentName: 001_RightHandMiddle2
position: {x: 0.025571287, y: -0.00000002287561, z: -0.000000002561137}
rotation: {x: -0.000000052154064, y: 0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_RMiddle3End
parentName: 001_RightHandMiddle3
position: {x: 0.028128386, y: -0.000000023690518, z: -0.0000000146683306}
rotation: {x: 0, y: -0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_RMiddle3End_end
parentName: 001_RMiddle3End
position: {x: -0, y: 0.028128315, z: 0}
rotation: {x: 0, y: -0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_RightHandPinky1
parentName: 001_RightHand
position: {x: 0.06818998, y: 0.0000000027939677, z: -0.029833127}
rotation: {x: -0.08715576, y: 0, z: -0, w: 0.9961947}
scale: {x: 1, y: 1.0000001, z: 0.99999994}
- name: 001_RightHandPinky2
parentName: 001_RightHandPinky1
position: {x: 0.03409493, y: -0.000000029802322, z: -0.000000011175871}
rotation: {x: 0.0000005736947, y: 0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_RightHandPinky3
parentName: 001_RightHandPinky2
position: {x: 0.017047405, y: 0.000000053085387, z: -0.000000033527613}
rotation: {x: -0.00000068545336, y: 0, z: -0, w: 1}
scale: {x: 1, y: 1.0000001, z: 1.0000001}
- name: 001_RPinky3End
parentName: 001_RightHandPinky3
position: {x: 0.018752217, y: -0.000000018626451, z: -0.000000022351742}
rotation: {x: 0.000000014901161, y: 0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_RPinky3End_end
parentName: 001_RPinky3End
position: {x: -0, y: 0.018752106, z: 0}
rotation: {x: 0, y: -0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_RightHandRing1
parentName: 001_RightHand
position: {x: 0.07245177, y: -0.00000018742867, z: -0.009887574}
rotation: {x: 0.00000010803342, y: 0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_RightHandRing2
parentName: 001_RightHandRing1
position: {x: 0.042618692, y: 0.000000047963113, z: -0.0000000018626451}
rotation: {x: -0.000000007450581, y: 0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_RightHandRing3
parentName: 001_RightHandRing2
position: {x: 0.021309316, y: 0.000000047730282, z: 0.0000000018626451}
rotation: {x: 0.00000032782555, y: 0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_RRing3End
parentName: 001_RightHandRing3
position: {x: 0.023440242, y: 0.000000037020072, z: -0.000000013038516}
rotation: {x: -0.000000011175871, y: 0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_RRing3End_end
parentName: 001_RRing3End
position: {x: -0, y: 0.02344036, z: 0}
rotation: {x: 0, y: -0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_RightHandThumb1
parentName: 001_RightHand
position: {x: 0.018752277, y: -0.021309385, z: 0.029833062}
rotation: {x: 0.4072523, y: -0.34298575, z: -0.23624216, w: 0.81283206}
scale: {x: 1, y: 1, z: 1}
- name: 001_RightHandThumb2
parentName: 001_RightHandThumb1
position: {x: 0.028322378, y: -0.000000031664968, z: 0.000000053137}
rotation: {x: 0, y: 0, z: 0, w: 1}
scale: {x: 1.0000001, y: 1, z: 1}
- name: 001_RightHandThumb3
parentName: 001_RightHandThumb2
position: {x: 0.024207383, y: -0.000000059786174, z: -0.000000082027555}
rotation: {x: 0.0000014454124, y: -0.00000007765938, z: -9.8754345e-14, w: 1}
scale: {x: 1, y: 1.0000001, z: 1.0000001}
- name: 001_RThumb3End
parentName: 001_RightHandThumb3
position: {x: 0.026628036, y: -0.000000084079474, z: -0.00000003786181}
rotation: {x: -8.285039e-15, y: -1.2789769e-13, z: 5.162537e-15, w: 1}
scale: {x: 1, y: 1, z: 1}
- name: 001_RThumb3End_end
parentName: 001_RThumb3End
position: {x: -0, y: 0.026628118, z: 0}
rotation: {x: 0, y: -0, z: -0, w: 1}
scale: {x: 1, y: 1, z: 1}
armTwist: 0.5
foreArmTwist: 0.5
upperLegTwist: 0.5
legTwist: 0.5
armStretch: 0.05
legStretch: 0.05
feetSpacing: 0
globalScale: 1
rootMotionBoneName:
hasTranslationDoF: 0
hasExtraRoot: 1
skeletonHasParents: 1
lastHumanDescriptionAvatarSource: {instanceID: 0}
autoGenerateAvatarMappingIfUnspecified: 1
animationType: 2
humanoidOversampling: 1
avatarSetup: 0
addHumanoidExtraRootOnlyWhenUsingAvatar: 1
importBlendShapeDeformPercent: 1
remapMaterialsIfMaterialImportModeIsNone: 0
additionalBone: 0
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: b1469350b1e555d45a488d3dd26f3f0c
PrefabImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,9 +1,13 @@
using UnityEditor; using UnityEditor;
using UnityEngine; using UnityEngine;
using System.Collections.Generic;
[CustomEditor(typeof(OptitrackSkeletonAnimator_Mingle))] [CustomEditor(typeof(OptitrackSkeletonAnimator_Mingle))]
public class OptitrackSkeletonAnimatorEditor : Editor public class OptitrackSkeletonAnimatorEditor : Editor
{ {
private bool showMappingFoldout = true;
private bool showDebugBonesFoldout = false;
public override void OnInspectorGUI() public override void OnInspectorGUI()
{ {
DrawDefaultInspector(); DrawDefaultInspector();
@ -29,6 +33,232 @@ public class OptitrackSkeletonAnimatorEditor : Editor
EditorGUILayout.HelpBox("Motive에서 스켈레톤이 활성화되어 있는지 확인해주세요.", MessageType.Warning); EditorGUILayout.HelpBox("Motive에서 스켈레톤이 활성화되어 있는지 확인해주세요.", MessageType.Warning);
} }
// ── 본 매핑 도구 ──
GUILayout.Space(10);
EditorGUILayout.LabelField("본 매핑 도구", EditorStyles.boldLabel);
GUI.backgroundColor = new Color(0.4f, 0.8f, 0.4f);
if (GUILayout.Button("FBX 분석 → 자동 매핑 생성", GUILayout.Height(35)))
{
Undo.RecordObject(skeletonAnimator, "Auto Generate Bone Mappings");
AutoGenerateMappingsEditor(skeletonAnimator);
EditorUtility.SetDirty(skeletonAnimator);
}
GUI.backgroundColor = Color.white;
// 매핑 상태 요약
if (skeletonAnimator.boneMappings != null && skeletonAnimator.boneMappings.Count > 0)
{
int mapped = 0;
int unmapped = 0;
foreach (var m in skeletonAnimator.boneMappings)
{
if (m.isMapped) mapped++;
else unmapped++;
}
EditorGUILayout.Space(5);
EditorGUILayout.HelpBox(
$"매핑 상태: {mapped}개 성공 / {unmapped}개 실패 (총 {skeletonAnimator.boneMappings.Count}개)",
unmapped > 0 ? MessageType.Warning : MessageType.Info);
// 매핑 리스트
EditorGUILayout.Space(5);
showMappingFoldout = EditorGUILayout.Foldout(showMappingFoldout, $"본 매핑 목록 ({skeletonAnimator.boneMappings.Count})", true);
if (showMappingFoldout)
{
EditorGUI.indentLevel++;
for (int i = 0; i < skeletonAnimator.boneMappings.Count; i++)
{
var mapping = skeletonAnimator.boneMappings[i];
Color labelColor = mapping.isMapped ? Color.green : Color.red;
EditorGUILayout.BeginHorizontal();
var prevColor = GUI.contentColor;
GUI.contentColor = labelColor;
EditorGUILayout.LabelField(mapping.isMapped ? "●" : "○", GUILayout.Width(15));
GUI.contentColor = prevColor;
EditorGUILayout.LabelField(mapping.optiTrackBoneName, GUILayout.Width(100));
EditorGUILayout.LabelField("→", GUILayout.Width(20));
// FBX 노드 이름 — 수동 편집 가능
EditorGUI.BeginChangeCheck();
string newName = EditorGUILayout.TextField(mapping.fbxNodeName);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(skeletonAnimator, "Edit Bone Mapping");
mapping.fbxNodeName = newName;
var allTransforms = skeletonAnimator.GetComponentsInChildren<Transform>(true);
mapping.cachedTransform = null;
mapping.isMapped = false;
foreach (var t in allTransforms)
{
if (t.name == newName)
{
mapping.cachedTransform = t;
mapping.isMapped = true;
break;
}
}
EditorUtility.SetDirty(skeletonAnimator);
}
// Position / Rotation 토글
EditorGUI.BeginChangeCheck();
mapping.applyPosition = EditorGUILayout.ToggleLeft("P", mapping.applyPosition, GUILayout.Width(30));
mapping.applyRotation = EditorGUILayout.ToggleLeft("R", mapping.applyRotation, GUILayout.Width(30));
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(skeletonAnimator, "Toggle Bone Apply");
EditorUtility.SetDirty(skeletonAnimator);
}
EditorGUILayout.EndHorizontal();
}
EditorGUI.indentLevel--;
}
}
EditorGUILayout.Space(5);
GUI.backgroundColor = new Color(0.9f, 0.4f, 0.4f);
if (GUILayout.Button("매핑 초기화"))
{
Undo.RecordObject(skeletonAnimator, "Clear Bone Mappings");
skeletonAnimator.boneMappings.Clear();
EditorUtility.SetDirty(skeletonAnimator);
}
GUI.backgroundColor = Color.white;
// ── 런타임 수신 본 디버그 ──
GUILayout.Space(10);
EditorGUILayout.LabelField("Motive 수신 본 디버그 (런타임)", EditorStyles.boldLabel);
if (!Application.isPlaying)
{
EditorGUILayout.HelpBox("플레이 모드에서 Motive 연결 후 확인할 수 있습니다.", MessageType.Info);
}
else if (skeletonAnimator.debugReceivedBoneNames.Count == 0)
{
EditorGUILayout.HelpBox("아직 본 데이터를 수신하지 못했습니다. Motive 스켈레톤이 활성화되어 있는지 확인해주세요.", MessageType.Warning);
}
else
{
EditorGUILayout.LabelField($"수신된 본 수: {skeletonAnimator.debugReceivedBoneCount}");
showDebugBonesFoldout = EditorGUILayout.Foldout(showDebugBonesFoldout, $"Motive 본 목록 ({skeletonAnimator.debugReceivedBoneNames.Count})", true);
if (showDebugBonesFoldout)
{
EditorGUI.indentLevel++;
foreach (var boneName in skeletonAnimator.debugReceivedBoneNames)
{
EditorGUILayout.LabelField(boneName);
}
EditorGUI.indentLevel--;
}
EditorGUILayout.Space(3);
if (GUILayout.Button("Console에 전체 본 목록 출력"))
{
var sb = new System.Text.StringBuilder();
sb.AppendLine($"=== Motive 수신 본 목록 ({skeletonAnimator.SkeletonAssetName}) ===");
sb.AppendLine($"총 {skeletonAnimator.debugReceivedBoneCount}개");
sb.AppendLine();
foreach (var bn in skeletonAnimator.debugReceivedBoneNames)
{
sb.AppendLine(bn);
}
Debug.Log(sb.ToString());
}
}
// 플레이 모드일 때 자동 갱신
if (Application.isPlaying)
Repaint();
GUILayout.Space(10); GUILayout.Space(10);
} }
private void AutoGenerateMappingsEditor(OptitrackSkeletonAnimator_Mingle script)
{
script.boneMappings.Clear();
var allTransforms = script.GetComponentsInChildren<Transform>(true);
var transformMap = new Dictionary<string, Transform>();
foreach (var t in allTransforms)
{
if (!transformMap.ContainsKey(t.name))
transformMap[t.name] = t;
}
// 접두사 자동 감지 (예: "001_Hips" → "001_")
string prefix = "";
foreach (var kvp in transformMap)
{
if (kvp.Key.EndsWith("Hips"))
{
int idx = kvp.Key.LastIndexOf("Hips");
if (idx > 0)
{
prefix = kvp.Key.Substring(0, idx);
break;
}
}
}
if (!string.IsNullOrEmpty(prefix))
Debug.Log($"[OptiTrack 매핑] 감지된 접두사: \"{prefix}\"");
int mapped = 0;
foreach (var kvp in OptitrackSkeletonAnimator_Mingle.DefaultOptiToFbxSuffix)
{
var mapping = new OptiTrackBoneMapping
{
optiTrackBoneName = kvp.Key,
applyPosition = true,
applyRotation = true,
isMapped = false
};
// 1순위: 접두사 + FBX 접미사
string fullName = prefix + kvp.Value;
if (transformMap.TryGetValue(fullName, out Transform found))
{
mapping.fbxNodeName = fullName;
mapping.cachedTransform = found;
mapping.isMapped = true;
}
// 2순위: 접미사만
else if (transformMap.TryGetValue(kvp.Value, out Transform found2))
{
mapping.fbxNodeName = kvp.Value;
mapping.cachedTransform = found2;
mapping.isMapped = true;
}
// 3순위: 끝나는 이름으로 부분 검색
else
{
foreach (var t in transformMap)
{
if (t.Key.EndsWith(kvp.Value) || t.Key.EndsWith("_" + kvp.Value))
{
mapping.fbxNodeName = t.Key;
mapping.cachedTransform = t.Value;
mapping.isMapped = true;
break;
}
}
}
if (!mapping.isMapped)
mapping.fbxNodeName = prefix + kvp.Value + " (미발견)";
else
mapped++;
script.boneMappings.Add(mapping);
}
Debug.Log($"[OptiTrack 매핑] 완료: {mapped}/{script.boneMappings.Count} 본 매핑 성공");
}
} }

View File

@ -1,17 +1,21 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Unity.Mathematics;
using UnityEngine; using UnityEngine;
using System.Collections; using System.Collections;
[System.Serializable] [System.Serializable]
public enum MotionApplicationScope public class OptiTrackBoneMapping
{ {
All, // 전신 적용 public string optiTrackBoneName; // OptiTrack 본 이름 (예: "Hip", "Ab")
ExcludeFingersOnly, // 손가락만 제외 public string fbxNodeName; // FBX 노드 이름 (예: "001_Hips", "001_Spine")
ExcludeHandsAndFingers // 손목 + 손가락 제외 [HideInInspector]
public Transform cachedTransform; // 런타임 캐시
public bool applyPosition = true;
public bool applyRotation = true;
public bool isMapped = false; // 매핑 성공 여부
} }
[DefaultExecutionOrder(-100)]
public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
{ {
[Header("OptiTrack 설정")] [Header("OptiTrack 설정")]
@ -20,232 +24,285 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
[Tooltip("Motive의 스켈레톤 에셋 이름")] [Tooltip("Motive의 스켈레톤 에셋 이름")]
public string SkeletonAssetName = "Skeleton1"; public string SkeletonAssetName = "Skeleton1";
private Animator TargetAnimator;
[Header("모션 적용 범위")] [Header("본 매핑 (OptiTrack → FBX 노드)")]
[Tooltip("모션 캡처 데이터를 적용할 범위 선택")] [Tooltip("자동 매핑 후 수동 조정 가능")]
public MotionApplicationScope motionScope = MotionApplicationScope.All; public List<OptiTrackBoneMapping> boneMappings = new List<OptiTrackBoneMapping>();
[Header("디버그")]
public bool logUnmappedBones = false;
private OptitrackSkeletonDefinition m_skeletonDef; private OptitrackSkeletonDefinition m_skeletonDef;
private Dictionary<string, HumanBodyBones> m_optitrackToHumanBoneMap;
private string previousSkeletonName; private string previousSkeletonName;
[HideInInspector] [HideInInspector]
public bool isSkeletonFound = false; public bool isSkeletonFound = false;
// 에디터 디버그용 — 런타임에 Motive에서 실제로 수신된 본 목록
[HideInInspector]
public List<string> debugReceivedBoneNames = new List<string>();
[HideInInspector]
public int debugReceivedBoneCount = 0;
private float updateInterval = 0.1f; private float updateInterval = 0.1f;
// 본 ID → 매핑 인덱스 (빠른 룩업)
private Dictionary<Int32, int> m_boneIdToMappingIndex = new Dictionary<Int32, int>();
// 본 이름 → Transform 캐시 (전체 하이어라키)
private Dictionary<string, Transform> m_allTransforms = new Dictionary<string, Transform>();
// torn read 방지용 스냅샷 버퍼
private Dictionary<Int32, Vector3> m_snapshotPositions = new Dictionary<Int32, Vector3>();
private Dictionary<Int32, Quaternion> m_snapshotOrientations = new Dictionary<Int32, Quaternion>();
// OptiTrack 본 이름 → FBX 노드 접미사 기본 매핑
public static readonly Dictionary<string, string> DefaultOptiToFbxSuffix = new Dictionary<string, string>
{
// 몸통 (Motive 5본 스파인 체인)
// Motive: Hip → Ab → Spine2 → Spine3 → Spine4 → Chest → Neck → Neck2 → Head
// FBX: Hips → Spine → Spine1 → Spine2 → Spine3 → Spine4 → Neck → Neck1 → Head
{"Hip", "Hips"},
{"Ab", "Spine"},
{"Spine2", "Spine1"},
{"Spine3", "Spine2"},
{"Spine4", "Spine3"},
{"Chest", "Spine4"},
{"Neck", "Neck"},
{"Neck2", "Neck1"},
{"Head", "Head"},
// 왼쪽 팔
{"LShoulder", "LeftShoulder"},
{"LUArm", "LeftArm"},
{"LFArm", "LeftForeArm"},
{"LHand", "LeftHand"},
// 오른쪽 팔
{"RShoulder", "RightShoulder"},
{"RUArm", "RightArm"},
{"RFArm", "RightForeArm"},
{"RHand", "RightHand"},
// 왼쪽 다리
{"LThigh", "LeftUpLeg"},
{"LShin", "LeftLeg"},
{"LFoot", "LeftFoot"},
{"LToe", "LeftToeBase"},
// 오른쪽 다리
{"RThigh", "RightUpLeg"},
{"RShin", "RightLeg"},
{"RFoot", "RightFoot"},
{"RToe", "RightToeBase"},
// 왼쪽 손가락
{"LThumb1", "LeftHandThumb1"},
{"LThumb2", "LeftHandThumb2"},
{"LThumb3", "LeftHandThumb3"},
{"LIndex1", "LeftHandIndex1"},
{"LIndex2", "LeftHandIndex2"},
{"LIndex3", "LeftHandIndex3"},
{"LMiddle1", "LeftHandMiddle1"},
{"LMiddle2", "LeftHandMiddle2"},
{"LMiddle3", "LeftHandMiddle3"},
{"LRing1", "LeftHandRing1"},
{"LRing2", "LeftHandRing2"},
{"LRing3", "LeftHandRing3"},
{"LPinky1", "LeftHandPinky1"},
{"LPinky2", "LeftHandPinky2"},
{"LPinky3", "LeftHandPinky3"},
// 오른쪽 손가락
{"RThumb1", "RightHandThumb1"},
{"RThumb2", "RightHandThumb2"},
{"RThumb3", "RightHandThumb3"},
{"RIndex1", "RightHandIndex1"},
{"RIndex2", "RightHandIndex2"},
{"RIndex3", "RightHandIndex3"},
{"RMiddle1", "RightHandMiddle1"},
{"RMiddle2", "RightHandMiddle2"},
{"RMiddle3", "RightHandMiddle3"},
{"RRing1", "RightHandRing1"},
{"RRing2", "RightHandRing2"},
{"RRing3", "RightHandRing3"},
{"RPinky1", "RightHandPinky1"},
{"RPinky2", "RightHandPinky2"},
{"RPinky3", "RightHandPinky3"},
};
void Start() void Start()
{ {
BuildTransformCache();
TargetAnimator = GetComponent<Animator>();
InitializeStreamingClient(); InitializeStreamingClient();
// StreamingClient 등록 추가
if (StreamingClient != null) if (StreamingClient != null)
{ {
StreamingClient.RegisterSkeleton(this, this.SkeletonAssetName); StreamingClient.RegisterSkeleton(this, this.SkeletonAssetName);
//Debug.Log($"[OptiTrack] 스켈레톤 '{SkeletonAssetName}'이(가) 등록되었습니다.");
} }
InitializeBoneMapping(); // 에디터에서 세팅한 매핑의 Transform 캐시 갱신
if (boneMappings.Count > 0)
{
RefreshTransformCache();
}
else
{
Debug.LogWarning("[OptiTrack] 본 매핑이 비어있습니다. Inspector에서 'FBX 분석 → 자동 매핑 생성' 버튼을 눌러주세요.", this);
}
// 주기적으로 스켈레톤 연결 상태를 확인하는 코루틴 시작
StartCoroutine(CheckSkeletonConnectionPeriodically()); StartCoroutine(CheckSkeletonConnectionPeriodically());
} }
void Update() void Update()
{ {
if (TargetAnimator == null)
return;
// StreamingClient 체크
if (StreamingClient == null) if (StreamingClient == null)
{ {
InitializeStreamingClient(); InitializeStreamingClient();
return; return;
} }
// 스켈레톤 이름이 변경되었을 때 // 스켈레톤 이름 변경 감지
if (previousSkeletonName != SkeletonAssetName) if (previousSkeletonName != SkeletonAssetName)
{ {
// 새 스켈레톤 등록
StreamingClient.RegisterSkeleton(this, SkeletonAssetName); StreamingClient.RegisterSkeleton(this, SkeletonAssetName);
//Debug.Log($"[OptiTrack] 새 스켈레톤 '{SkeletonAssetName}' 등록");
// 스켈레톤 정의 새로 가져오기
m_skeletonDef = StreamingClient.GetSkeletonDefinitionByName(SkeletonAssetName); m_skeletonDef = StreamingClient.GetSkeletonDefinitionByName(SkeletonAssetName);
previousSkeletonName = SkeletonAssetName;
if (m_skeletonDef == null) if (m_skeletonDef != null)
{ RebuildBoneIdMapping();
//Debug.LogWarning($"[OptiTrack] 스켈레톤 '{SkeletonAssetName}'을(를) 찾을 수 없습니다. Motive에서 올바른 스켈레톤 이름을 확인해주세요.", this); return;
previousSkeletonName = SkeletonAssetName; // 이름 업데이트
return;
}
//Debug.Log($"[OptiTrack] 스켈레톤 '{SkeletonAssetName}'을(를) 성공적으로 찾았습니다.", this);
previousSkeletonName = SkeletonAssetName; // 이름 업데이트
} }
// 스켈레톤 정의가 없는 경우 체크
if (m_skeletonDef == null) if (m_skeletonDef == null)
{ {
m_skeletonDef = StreamingClient.GetSkeletonDefinitionByName(SkeletonAssetName); m_skeletonDef = StreamingClient.GetSkeletonDefinitionByName(SkeletonAssetName);
if (m_skeletonDef == null) if (m_skeletonDef == null)
{
return; return;
} RebuildBoneIdMapping();
} }
// 최신 스켈레톤 상태 가져오기 // 최신 스켈레톤 상태
OptitrackSkeletonState skelState = StreamingClient.GetLatestSkeletonState(m_skeletonDef.Id); OptitrackSkeletonState skelState = StreamingClient.GetLatestSkeletonState(m_skeletonDef.Id);
if (skelState == null) if (skelState == null)
{
//Debug.LogWarning($"[OptiTrack] 스켈레톤 '{SkeletonAssetName}'의 상태가 null입니다. Motive에 마커가 제대로 트래킹되고 있는지 확인해주세요.", this);
return; return;
// torn read 방지 — 스냅샷 복사
m_snapshotPositions.Clear();
m_snapshotOrientations.Clear();
foreach (var kvp in skelState.BonePoses)
{
m_snapshotPositions[kvp.Key] = kvp.Value.Position;
m_snapshotOrientations[kvp.Key] = kvp.Value.Orientation;
} }
// 각 본 업데이트
// 각 본 업데이트 — Transform 직접 적용
foreach (var bone in m_skeletonDef.Bones) foreach (var bone in m_skeletonDef.Bones)
{ {
string boneName = bone.Name; if (!m_boneIdToMappingIndex.TryGetValue(bone.Id, out int mappingIdx))
string optitrackBoneName = boneName.Contains("_") ? boneName.Substring(boneName.IndexOf('_') + 1) : boneName; continue;
if (m_optitrackToHumanBoneMap.TryGetValue(optitrackBoneName, out HumanBodyBones humanBone)) var mapping = boneMappings[mappingIdx];
if (!mapping.isMapped || mapping.cachedTransform == null)
continue;
if (m_snapshotOrientations.TryGetValue(bone.Id, out Quaternion ori))
{ {
// 모션 스코프에 따른 본 필터링 if (mapping.applyPosition && m_snapshotPositions.TryGetValue(bone.Id, out Vector3 pos))
if (!ShouldApplyMotionToBone(optitrackBoneName))
continue;
Transform boneTransform = TargetAnimator.GetBoneTransform(humanBone);
if (boneTransform != null)
{ {
if (skelState.BonePoses.TryGetValue(bone.Id, out OptitrackPose bonePose)) mapping.cachedTransform.localPosition = SnapPosition(pos);
{ }
// 손가락의 경우 로컬 포지션 데이터를 받지 않음
if (!IsFingerBone(optitrackBoneName))
{
// 위치는 항상 업데이트 (Hip 등 루트 본의 경우)
boneTransform.localPosition = bonePose.Position;
}
// 회전 업데이트 if (mapping.applyRotation)
boneTransform.localRotation = bonePose.Orientation; {
} mapping.cachedTransform.localRotation = SnapQuaternion(ori);
} }
} }
} }
} }
/// <summary>
/// 하이어라키의 모든 Transform을 이름으로 캐싱
/// </summary>
private void BuildTransformCache()
{
m_allTransforms.Clear();
var allChildren = GetComponentsInChildren<Transform>(true);
foreach (var t in allChildren)
{
if (!m_allTransforms.ContainsKey(t.name))
{
m_allTransforms[t.name] = t;
}
}
}
/// <summary>
/// 기존 매핑의 Transform 캐시만 갱신
/// </summary>
private void RefreshTransformCache()
{
foreach (var mapping in boneMappings)
{
if (!string.IsNullOrEmpty(mapping.fbxNodeName) && m_allTransforms.TryGetValue(mapping.fbxNodeName, out Transform t))
{
mapping.cachedTransform = t;
mapping.isMapped = true;
}
}
}
/// <summary>
/// OptiTrack 본 ID → boneMappings 인덱스 매핑 구축
/// </summary>
private void RebuildBoneIdMapping()
{
m_boneIdToMappingIndex.Clear();
debugReceivedBoneNames.Clear();
if (m_skeletonDef == null) return;
debugReceivedBoneCount = m_skeletonDef.Bones.Count;
var nameToIdx = new Dictionary<string, int>();
for (int i = 0; i < boneMappings.Count; i++)
{
if (!nameToIdx.ContainsKey(boneMappings[i].optiTrackBoneName))
nameToIdx[boneMappings[i].optiTrackBoneName] = i;
}
int matchCount = 0;
foreach (var bone in m_skeletonDef.Bones)
{
string boneName = bone.Name;
string optiName = boneName.Contains("_") ? boneName.Substring(boneName.IndexOf('_') + 1) : boneName;
// 디버그: Motive에서 수신된 모든 본 기록
string parentInfo = m_skeletonDef.BoneIdToParentIdMap.TryGetValue(bone.Id, out Int32 parentId)
? $"parent={parentId}" : "root";
debugReceivedBoneNames.Add($"[ID:{bone.Id}] {boneName} (suffix: {optiName}, {parentInfo})");
if (nameToIdx.TryGetValue(optiName, out int idx))
{
m_boneIdToMappingIndex[bone.Id] = idx;
matchCount++;
}
else if (logUnmappedBones)
{
Debug.LogWarning($"[OptiTrack] 매핑되지 않은 OptiTrack 본: {boneName} (suffix: {optiName})");
}
}
Debug.Log($"[OptiTrack] 본 ID 매핑 완료: {matchCount}/{m_skeletonDef.Bones.Count} 매칭");
}
private void InitializeStreamingClient() private void InitializeStreamingClient()
{ {
if (StreamingClient == null) if (StreamingClient == null)
{ {
// 씬에서 OptitrackStreamingClient 찾기 StreamingClient = FindAnyObjectByType<OptitrackStreamingClient>();
StreamingClient = FindObjectOfType<OptitrackStreamingClient>(); if (StreamingClient != null)
if (StreamingClient == null)
{
//Debug.LogWarning("씬에서 OptiTrack Streaming Client를 찾을 수 없습니다. 다음 프레임에서 다시 시도합니다.", this);
}
else
{
Debug.Log("OptiTrack Streaming Client를 찾았습니다.", this); Debug.Log("OptiTrack Streaming Client를 찾았습니다.", this);
}
} }
} }
private void InitializeBoneMapping()
{
m_optitrackToHumanBoneMap = new Dictionary<string, HumanBodyBones>();
if (TargetAnimator == null || !TargetAnimator.isHuman)
{
Debug.LogError("휴머노이드 아바타가 설정되지 않았습니다.", this);
return;
}
// OptiTrack 본 이름을 HumanBodyBones enum과 매핑
// 스켈레톤 에셋 이름을 제외한 기본 매핑 설정
SetupBoneNameMapping();
}
private void SetupBoneNameMapping()
{
// 기본 본 매핑 (스켈레톤 에셋 이름 없이)
m_optitrackToHumanBoneMap.Add("Hip", HumanBodyBones.Hips);
m_optitrackToHumanBoneMap.Add("Ab", HumanBodyBones.Spine);
m_optitrackToHumanBoneMap.Add("Chest", HumanBodyBones.Chest);
m_optitrackToHumanBoneMap.Add("Neck", HumanBodyBones.Neck);
m_optitrackToHumanBoneMap.Add("Head", HumanBodyBones.Head);
// 왼쪽 팔
m_optitrackToHumanBoneMap.Add("LShoulder", HumanBodyBones.LeftShoulder);
m_optitrackToHumanBoneMap.Add("LUArm", HumanBodyBones.LeftUpperArm);
m_optitrackToHumanBoneMap.Add("LFArm", HumanBodyBones.LeftLowerArm);
m_optitrackToHumanBoneMap.Add("LHand", HumanBodyBones.LeftHand);
// 오른쪽 팔
m_optitrackToHumanBoneMap.Add("RShoulder", HumanBodyBones.RightShoulder);
m_optitrackToHumanBoneMap.Add("RUArm", HumanBodyBones.RightUpperArm);
m_optitrackToHumanBoneMap.Add("RFArm", HumanBodyBones.RightLowerArm);
m_optitrackToHumanBoneMap.Add("RHand", HumanBodyBones.RightHand);
// 왼쪽 다리
m_optitrackToHumanBoneMap.Add("LThigh", HumanBodyBones.LeftUpperLeg);
m_optitrackToHumanBoneMap.Add("LShin", HumanBodyBones.LeftLowerLeg);
m_optitrackToHumanBoneMap.Add("LFoot", HumanBodyBones.LeftFoot);
m_optitrackToHumanBoneMap.Add("LToe", HumanBodyBones.LeftToes);
// 오른쪽 다리
m_optitrackToHumanBoneMap.Add("RThigh", HumanBodyBones.RightUpperLeg);
m_optitrackToHumanBoneMap.Add("RShin", HumanBodyBones.RightLowerLeg);
m_optitrackToHumanBoneMap.Add("RFoot", HumanBodyBones.RightFoot);
m_optitrackToHumanBoneMap.Add("RToe", HumanBodyBones.RightToes);
// 왼쪽 손가락들
m_optitrackToHumanBoneMap.Add("LThumb1", HumanBodyBones.LeftThumbProximal);
m_optitrackToHumanBoneMap.Add("LThumb2", HumanBodyBones.LeftThumbIntermediate);
m_optitrackToHumanBoneMap.Add("LThumb3", HumanBodyBones.LeftThumbDistal);
m_optitrackToHumanBoneMap.Add("LIndex1", HumanBodyBones.LeftIndexProximal);
m_optitrackToHumanBoneMap.Add("LIndex2", HumanBodyBones.LeftIndexIntermediate);
m_optitrackToHumanBoneMap.Add("LIndex3", HumanBodyBones.LeftIndexDistal);
m_optitrackToHumanBoneMap.Add("LMiddle1", HumanBodyBones.LeftMiddleProximal);
m_optitrackToHumanBoneMap.Add("LMiddle2", HumanBodyBones.LeftMiddleIntermediate);
m_optitrackToHumanBoneMap.Add("LMiddle3", HumanBodyBones.LeftMiddleDistal);
m_optitrackToHumanBoneMap.Add("LRing1", HumanBodyBones.LeftRingProximal);
m_optitrackToHumanBoneMap.Add("LRing2", HumanBodyBones.LeftRingIntermediate);
m_optitrackToHumanBoneMap.Add("LRing3", HumanBodyBones.LeftRingDistal);
m_optitrackToHumanBoneMap.Add("LPinky1", HumanBodyBones.LeftLittleProximal);
m_optitrackToHumanBoneMap.Add("LPinky2", HumanBodyBones.LeftLittleIntermediate);
m_optitrackToHumanBoneMap.Add("LPinky3", HumanBodyBones.LeftLittleDistal);
// 오른쪽 손가락들
m_optitrackToHumanBoneMap.Add("RThumb1", HumanBodyBones.RightThumbProximal);
m_optitrackToHumanBoneMap.Add("RThumb2", HumanBodyBones.RightThumbIntermediate);
m_optitrackToHumanBoneMap.Add("RThumb3", HumanBodyBones.RightThumbDistal);
m_optitrackToHumanBoneMap.Add("RIndex1", HumanBodyBones.RightIndexProximal);
m_optitrackToHumanBoneMap.Add("RIndex2", HumanBodyBones.RightIndexIntermediate);
m_optitrackToHumanBoneMap.Add("RIndex3", HumanBodyBones.RightIndexDistal);
m_optitrackToHumanBoneMap.Add("RMiddle1", HumanBodyBones.RightMiddleProximal);
m_optitrackToHumanBoneMap.Add("RMiddle2", HumanBodyBones.RightMiddleIntermediate);
m_optitrackToHumanBoneMap.Add("RMiddle3", HumanBodyBones.RightMiddleDistal);
m_optitrackToHumanBoneMap.Add("RRing1", HumanBodyBones.RightRingProximal);
m_optitrackToHumanBoneMap.Add("RRing2", HumanBodyBones.RightRingIntermediate);
m_optitrackToHumanBoneMap.Add("RRing3", HumanBodyBones.RightRingDistal);
m_optitrackToHumanBoneMap.Add("RPinky1", HumanBodyBones.RightLittleProximal);
m_optitrackToHumanBoneMap.Add("RPinky2", HumanBodyBones.RightLittleIntermediate);
m_optitrackToHumanBoneMap.Add("RPinky3", HumanBodyBones.RightLittleDistal);
}
private IEnumerator CheckSkeletonConnectionPeriodically() private IEnumerator CheckSkeletonConnectionPeriodically()
{ {
while (true) while (true)
@ -257,12 +314,14 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
if (m_skeletonDef != null) if (m_skeletonDef != null)
{ {
OptitrackSkeletonState skelState = StreamingClient.GetLatestSkeletonState(m_skeletonDef.Id); OptitrackSkeletonState skelState = StreamingClient.GetLatestSkeletonState(m_skeletonDef.Id);
bool wasFound = isSkeletonFound;
isSkeletonFound = (skelState != null); isSkeletonFound = (skelState != null);
if (isSkeletonFound && previousSkeletonName != SkeletonAssetName) if (isSkeletonFound && !wasFound)
{ {
StreamingClient.RegisterSkeleton(this, SkeletonAssetName); StreamingClient.RegisterSkeleton(this, SkeletonAssetName);
previousSkeletonName = SkeletonAssetName; previousSkeletonName = SkeletonAssetName;
RebuildBoneIdMapping();
Debug.Log($"[OptiTrack] 스켈레톤 '{SkeletonAssetName}' 연결 성공"); Debug.Log($"[OptiTrack] 스켈레톤 '{SkeletonAssetName}' 연결 성공");
} }
} }
@ -276,38 +335,221 @@ public class OptitrackSkeletonAnimator_Mingle : MonoBehaviour
} }
} }
private bool IsFingerBone(string boneName) // 소수점 4자리 이하 제거 (0.0001m = 0.1mm 미만 노이즈 제거)
private const float POS_PRECISION = 10000f;
private static Vector3 SnapPosition(Vector3 v)
{ {
// 손가락 관련 본들 확인 (양손 모두) return new Vector3(
return boneName.Contains("Thumb") || Mathf.Round(v.x * POS_PRECISION) / POS_PRECISION,
boneName.Contains("Index") || Mathf.Round(v.y * POS_PRECISION) / POS_PRECISION,
boneName.Contains("Middle") || Mathf.Round(v.z * POS_PRECISION) / POS_PRECISION
boneName.Contains("Ring") || );
boneName.Contains("Pinky");
} }
private bool IsHandBone(string boneName) // 쿼터니언 소수점 5자리 이하 제거 + 정규화
private const float ROT_PRECISION = 100000f;
private static Quaternion SnapQuaternion(Quaternion q)
{ {
// 손 관련 본들 확인 (양손 모두) return new Quaternion(
return boneName.Contains("LHand") || Mathf.Round(q.x * ROT_PRECISION) / ROT_PRECISION,
boneName.Contains("RHand"); Mathf.Round(q.y * ROT_PRECISION) / ROT_PRECISION,
Mathf.Round(q.z * ROT_PRECISION) / ROT_PRECISION,
Mathf.Round(q.w * ROT_PRECISION) / ROT_PRECISION
).normalized;
} }
private bool ShouldApplyMotionToBone(string boneName) #region
// HumanBodyBones → OptiTrack 본 이름 매핑 (Humanoid 호환 레이어)
public static readonly Dictionary<HumanBodyBones, string> HumanBoneToOptiName = new Dictionary<HumanBodyBones, string>
{ {
switch (motionScope) { HumanBodyBones.Hips, "Hip" },
{ HumanBodyBones.Head, "Head" },
// 스파인/넥은 분배 처리되므로 1:1 매핑은 참고용
{ HumanBodyBones.Spine, "Ab" },
{ HumanBodyBones.Chest, "Chest" },
{ HumanBodyBones.Neck, "Neck" },
// 왼쪽 팔
{ HumanBodyBones.LeftShoulder, "LShoulder" },
{ HumanBodyBones.LeftUpperArm, "LUArm" },
{ HumanBodyBones.LeftLowerArm, "LFArm" },
{ HumanBodyBones.LeftHand, "LHand" },
// 오른쪽 팔
{ HumanBodyBones.RightShoulder, "RShoulder" },
{ HumanBodyBones.RightUpperArm, "RUArm" },
{ HumanBodyBones.RightLowerArm, "RFArm" },
{ HumanBodyBones.RightHand, "RHand" },
// 왼쪽 다리
{ HumanBodyBones.LeftUpperLeg, "LThigh" },
{ HumanBodyBones.LeftLowerLeg, "LShin" },
{ HumanBodyBones.LeftFoot, "LFoot" },
{ HumanBodyBones.LeftToes, "LToe" },
// 오른쪽 다리
{ HumanBodyBones.RightUpperLeg, "RThigh" },
{ HumanBodyBones.RightLowerLeg, "RShin" },
{ HumanBodyBones.RightFoot, "RFoot" },
{ HumanBodyBones.RightToes, "RToe" },
// 왼쪽 손가락
{ HumanBodyBones.LeftThumbProximal, "LThumb1" },
{ HumanBodyBones.LeftThumbIntermediate, "LThumb2" },
{ HumanBodyBones.LeftThumbDistal, "LThumb3" },
{ HumanBodyBones.LeftIndexProximal, "LIndex1" },
{ HumanBodyBones.LeftIndexIntermediate, "LIndex2" },
{ HumanBodyBones.LeftIndexDistal, "LIndex3" },
{ HumanBodyBones.LeftMiddleProximal, "LMiddle1" },
{ HumanBodyBones.LeftMiddleIntermediate, "LMiddle2" },
{ HumanBodyBones.LeftMiddleDistal, "LMiddle3" },
{ HumanBodyBones.LeftRingProximal, "LRing1" },
{ HumanBodyBones.LeftRingIntermediate, "LRing2" },
{ HumanBodyBones.LeftRingDistal, "LRing3" },
{ HumanBodyBones.LeftLittleProximal, "LPinky1" },
{ HumanBodyBones.LeftLittleIntermediate, "LPinky2" },
{ HumanBodyBones.LeftLittleDistal, "LPinky3" },
// 오른쪽 손가락
{ HumanBodyBones.RightThumbProximal, "RThumb1" },
{ HumanBodyBones.RightThumbIntermediate, "RThumb2" },
{ HumanBodyBones.RightThumbDistal, "RThumb3" },
{ HumanBodyBones.RightIndexProximal, "RIndex1" },
{ HumanBodyBones.RightIndexIntermediate, "RIndex2" },
{ HumanBodyBones.RightIndexDistal, "RIndex3" },
{ HumanBodyBones.RightMiddleProximal, "RMiddle1" },
{ HumanBodyBones.RightMiddleIntermediate, "RMiddle2" },
{ HumanBodyBones.RightMiddleDistal, "RMiddle3" },
{ HumanBodyBones.RightRingProximal, "RRing1" },
{ HumanBodyBones.RightRingIntermediate, "RRing2" },
{ HumanBodyBones.RightRingDistal, "RRing3" },
{ HumanBodyBones.RightLittleProximal, "RPinky1" },
{ HumanBodyBones.RightLittleIntermediate, "RPinky2" },
{ HumanBodyBones.RightLittleDistal, "RPinky3" },
};
/// <summary>
/// HumanBodyBones enum으로 OptiTrack 매핑된 Transform 반환
/// (Humanoid 호환 레이어 — sourceAnimator.GetBoneTransform() 대체)
/// </summary>
public Transform GetBoneTransform(HumanBodyBones bone)
{
if (HumanBoneToOptiName.TryGetValue(bone, out string optiName))
return GetMappedTransform(optiName);
return null;
}
// 스파인 체인 OptiTrack 이름 (Hips 제외, 순서대로)
public static readonly string[] SpineChainOptiNames = { "Ab", "Spine2", "Spine3", "Spine4", "Chest" };
// 넥 체인
public static readonly string[] NeckChainOptiNames = { "Neck", "Neck2" };
/// <summary>
/// OptiTrack 본 이름으로 매핑된 Transform 반환
/// </summary>
public Transform GetMappedTransform(string optiTrackBoneName)
{
foreach (var mapping in boneMappings)
{ {
case MotionApplicationScope.All: if (mapping.optiTrackBoneName == optiTrackBoneName && mapping.isMapped)
return true; return mapping.cachedTransform;
}
return null;
}
case MotionApplicationScope.ExcludeFingersOnly: /// <summary>
return !IsFingerBone(boneName); /// 스파인 체인의 모든 Transform을 순서대로 반환 (Ab → Spine2 → Spine3 → Spine4 → Chest)
/// </summary>
public List<Transform> GetSpineChainTransforms()
{
var chain = new List<Transform>();
foreach (var name in SpineChainOptiNames)
{
Transform t = GetMappedTransform(name);
if (t != null)
chain.Add(t);
}
return chain;
}
case MotionApplicationScope.ExcludeHandsAndFingers: /// <summary>
return !IsFingerBone(boneName) && !IsHandBone(boneName); /// 넥 체인의 모든 Transform을 순서대로 반환
/// </summary>
public List<Transform> GetNeckChainTransforms()
{
var chain = new List<Transform>();
foreach (var name in NeckChainOptiNames)
{
Transform t = GetMappedTransform(name);
if (t != null)
chain.Add(t);
}
return chain;
}
default: /// <summary>
return true; /// 체인의 누적 로컬 회전을 계산 (각 본의 localRotation을 순서대로 곱함)
/// </summary>
public static Quaternion ComputeChainRotation(List<Transform> chain)
{
Quaternion total = Quaternion.identity;
foreach (var t in chain)
{
total *= t.localRotation;
}
return total;
}
/// <summary>
/// 초기 포즈(T-포즈) 캐싱 — Humanoid가 아니므로 직접 캐싱
/// </summary>
[HideInInspector] public Dictionary<string, Quaternion> restLocalRotations = new Dictionary<string, Quaternion>();
[HideInInspector] public Dictionary<string, Vector3> restLocalPositions = new Dictionary<string, Vector3>();
[HideInInspector] public bool isRestPoseCached = false;
/// <summary>
/// 현재 포즈를 기준 포즈(T-포즈)로 캐싱
/// </summary>
public void CacheRestPose()
{
restLocalRotations.Clear();
restLocalPositions.Clear();
foreach (var mapping in boneMappings)
{
if (mapping.isMapped && mapping.cachedTransform != null)
{
restLocalRotations[mapping.optiTrackBoneName] = mapping.cachedTransform.localRotation;
restLocalPositions[mapping.optiTrackBoneName] = mapping.cachedTransform.localPosition;
}
}
isRestPoseCached = true;
Debug.Log($"[OptiTrack] 기준 포즈 캐싱 완료: {restLocalRotations.Count}개 본");
}
/// <summary>
/// 캐싱된 기준 포즈로 복원 (T-포즈 복원)
/// </summary>
public void RestoreRestPose()
{
if (!isRestPoseCached) return;
foreach (var mapping in boneMappings)
{
if (mapping.isMapped && mapping.cachedTransform != null)
{
if (restLocalRotations.TryGetValue(mapping.optiTrackBoneName, out Quaternion rot))
mapping.cachedTransform.localRotation = rot;
if (restLocalPositions.TryGetValue(mapping.optiTrackBoneName, out Vector3 pos))
mapping.cachedTransform.localPosition = pos;
}
} }
} }
/// <summary>
/// 특정 본의 기준 포즈 로컬 회전 반환
/// </summary>
public Quaternion GetRestLocalRotation(string optiTrackBoneName)
{
if (restLocalRotations.TryGetValue(optiTrackBoneName, out Quaternion rot))
return rot;
return Quaternion.identity;
}
#endregion
} }

View File

@ -9,13 +9,14 @@ namespace KindRetargeting
/// 이 스크립트는 원본 아바타(Source)의 포즈 손가락 움직임을 대상 아바타(Target)에 리타게팅(Retargeting)합니다. /// 이 스크립트는 원본 아바타(Source)의 포즈 손가락 움직임을 대상 아바타(Target)에 리타게팅(Retargeting)합니다.
/// 또한 IK 타겟을 생성하여 대상 아바타의 관절 움직임을 자연스럽게 조정합니다. /// 또한 IK 타겟을 생성하여 대상 아바타의 관절 움직임을 자연스럽게 조정합니다.
/// </summary> /// </summary>
[DefaultExecutionOrder(1)] //[DefaultExecutionOrder(-200)]
public class CustomRetargetingScript : MonoBehaviour public class CustomRetargetingScript : MonoBehaviour
{ {
#region #region
[Header("원본 및 대상 아바타 Animator")] [Header("원본 아바타 (OptiTrack)")]
[SerializeField] public Animator sourceAnimator; // 원본 아바타의 Animator [SerializeField] public OptitrackSkeletonAnimator_Mingle optitrackSource;
[HideInInspector] public Animator sourceAnimator; // 하위 호환용 (외부 스크립트 참조)
[HideInInspector] public Animator targetAnimator; // 대상 아바타의 Animator [HideInInspector] public Animator targetAnimator; // 대상 아바타의 Animator
// IK 컴포넌트 참조 // IK 컴포넌트 참조
@ -43,24 +44,37 @@ namespace KindRetargeting
[SerializeField] [SerializeField]
public Vector3 debugAxisNormalizer = Vector3.one; public Vector3 debugAxisNormalizer = Vector3.one;
// HumanPoseHandler를 이용하여 원본 및 대상 아바타의 포즈를 관리 /// <summary>
private HumanPoseHandler sourcePoseHandler; /// 소스 본 Transform 접근 래퍼 (OptiTrack 매핑 사용)
/// </summary>
private Transform GetSourceBoneTransform(HumanBodyBones bone)
{
if (optitrackSource != null)
return optitrackSource.GetBoneTransform(bone);
return null;
}
// OptiTrack 스파인/넥 분배용 캐시
private List<Transform> sourceSpineChain; // 소스 스파인 체인
private List<Transform> sourceNeckChain; // 소스 넥 체인
private List<Transform> targetSpineBones; // 타겟 스파인 본들
private List<Transform> targetNeckBones; // 타겟 넥 본들
private List<Quaternion> spineOffsets; // T-포즈 기준: Inv(가상본 월드회전) * 타겟본 월드회전
private List<Quaternion> neckOffsets; // 넥용 오프셋
private bool useOptiTrackSpineDistribution = false;
// HumanPoseHandler를 이용하여 대상 아바타의 포즈를 관리
private HumanPoseHandler targetPoseHandler; private HumanPoseHandler targetPoseHandler;
private HumanPose sourcePose;
private HumanPose targetPose; private HumanPose targetPose;
// 최적화: 프레임당 한 번만 GetHumanPose 호출하기 위한 플래그 // 최적화: 프레임당 한 번만 GetHumanPose 호출하기 위한 플래그
private bool isSourcePoseCachedThisFrame = false;
private bool isTargetPoseCachedThisFrame = false; private bool isTargetPoseCachedThisFrame = false;
// 본별 회전 오프셋을 저장하는 딕셔너리 // 본별 회전 오프셋을 저장하는 딕셔너리
private Dictionary<HumanBodyBones, Quaternion> rotationOffsets = new Dictionary<HumanBodyBones, Quaternion>(); private Dictionary<HumanBodyBones, Quaternion> rotationOffsets = new Dictionary<HumanBodyBones, Quaternion>();
// HumanBodyBones.LastBone을 이용한 본 순회 범위 // HumanBodyBones.LastBone을 이용한 본 순회 범위
private int lastBoneIndex = 23; private int lastBoneIndex = 55; // 0~54: 몸체 + 손가락 전부
[Header("손가락 복제 설정")]
[SerializeField] private EnumsList.FingerCopyMode fingerCopyMode = EnumsList.FingerCopyMode.Rotation;
[Header("무릎 안/밖 조정")] [Header("무릎 안/밖 조정")]
[SerializeField, Range(-1f, 1f)] [SerializeField, Range(-1f, 1f)]
@ -156,7 +170,6 @@ namespace KindRetargeting
public float footFrontBackOffset; // 발 앞뒤 오프셋 public float footFrontBackOffset; // 발 앞뒤 오프셋
public float footInOutOffset; // 발 안쪽/바깥쪽 오프셋 public float footInOutOffset; // 발 안쪽/바깥쪽 오프셋
public float floorHeight; public float floorHeight;
public EnumsList.FingerCopyMode fingerCopyMode;
public List<RotationOffsetData> rotationOffsetCache; public List<RotationOffsetData> rotationOffsetCache;
public float initialHipsHeight; public float initialHipsHeight;
public float avatarScale; public float avatarScale;
@ -170,40 +183,6 @@ namespace KindRetargeting
public float headScale; public float headScale;
} }
// CopyFingerPoseByMuscle에서 사용할 본 로컬 회전 저장용 (메모리 재사용)
// 위치는 SetHumanPose가 처리하므로 회전만 저장
private Dictionary<HumanBodyBones, Quaternion> savedBoneLocalRotations =
new Dictionary<HumanBodyBones, Quaternion>();
// 손가락을 제외한 모든 휴먼본 목록 (캐싱)
private static readonly HumanBodyBones[] nonFingerBones = new HumanBodyBones[]
{
HumanBodyBones.Hips,
HumanBodyBones.Spine,
HumanBodyBones.Chest,
HumanBodyBones.UpperChest,
HumanBodyBones.Neck,
HumanBodyBones.Head,
HumanBodyBones.LeftShoulder,
HumanBodyBones.LeftUpperArm,
HumanBodyBones.LeftLowerArm,
HumanBodyBones.LeftHand,
HumanBodyBones.RightShoulder,
HumanBodyBones.RightUpperArm,
HumanBodyBones.RightLowerArm,
HumanBodyBones.RightHand,
HumanBodyBones.LeftUpperLeg,
HumanBodyBones.LeftLowerLeg,
HumanBodyBones.LeftFoot,
HumanBodyBones.LeftToes,
HumanBodyBones.RightUpperLeg,
HumanBodyBones.RightLowerLeg,
HumanBodyBones.RightFoot,
HumanBodyBones.RightToes,
HumanBodyBones.LeftEye,
HumanBodyBones.RightEye,
HumanBodyBones.Jaw
};
// IK 조인트 싱을 위한 구조체 // IK 조인트 싱을 위한 구조체
private struct IKJoints private struct IKJoints
@ -215,56 +194,6 @@ namespace KindRetargeting
} }
private IKJoints sourceIKJoints; private IKJoints sourceIKJoints;
// 손가락 본 인덱스 → 머슬 인덱스 매핑
// 머슬 순서: 1 Stretched, Spread, 2 Stretched, 3 Stretched (4개씩)
// 소스 아바타 로컬 회전: 엄지는 Y = Spread, 나머지는 X = Spread, 굽힘은 모두 Z축
private static readonly Dictionary<int, (int stretchedMuscle, int spreadMuscle)> fingerBoneToMuscleIndex = new Dictionary<int, (int, int)>
{
// 왼손 엄지 (24-26) → 머슬 55-58 (55: Stretched1, 56: Spread, 57: Stretched2, 58: Stretched3)
{ 24, (55, 56) }, // LeftThumbProximal → Left Thumb 1 Stretched, Spread
{ 25, (57, -1) }, // LeftThumbIntermediate → Left Thumb 2 Stretched
{ 26, (58, -1) }, // LeftThumbDistal → Left Thumb 3 Stretched
// 왼손 검지 (27-29) → 머슬 59-62 (59: Stretched1, 60: Spread, 61: Stretched2, 62: Stretched3)
{ 27, (59, 60) }, // LeftIndexProximal → Left Index 1 Stretched, Spread
{ 28, (61, -1) }, // LeftIndexIntermediate → Left Index 2 Stretched (Spread 없음)
{ 29, (62, -1) }, // LeftIndexDistal → Left Index 3 Stretched (Spread 없음)
// 왼손 중지 (30-32) → 머슬 63-66
{ 30, (63, 64) }, // LeftMiddleProximal → Left Middle 1 Stretched, Spread
{ 31, (65, -1) }, // LeftMiddleIntermediate → Left Middle 2 Stretched
{ 32, (66, -1) }, // LeftMiddleDistal → Left Middle 3 Stretched
// 왼손 약지 (33-35) → 머슬 67-70
{ 33, (67, 68) }, // LeftRingProximal → Left Ring 1 Stretched, Spread
{ 34, (69, -1) }, // LeftRingIntermediate → Left Ring 2 Stretched
{ 35, (70, -1) }, // LeftRingDistal → Left Ring 3 Stretched
// 왼손 소지 (36-38) → 머슬 71-74
{ 36, (71, 72) }, // LeftLittleProximal → Left Little 1 Stretched, Spread
{ 37, (73, -1) }, // LeftLittleIntermediate → Left Little 2 Stretched
{ 38, (74, -1) }, // LeftLittleDistal → Left Little 3 Stretched
// 오른손 엄지 (39-41) → 머슬 75-78
{ 39, (75, 76) }, // RightThumbProximal → Right Thumb 1 Stretched, Spread
{ 40, (77, -1) }, // RightThumbIntermediate → Right Thumb 2 Stretched
{ 41, (78, -1) }, // RightThumbDistal → Right Thumb 3 Stretched
// 오른손 검지 (42-44) → 머슬 79-82
{ 42, (79, 80) }, // RightIndexProximal → Right Index 1 Stretched, Spread
{ 43, (81, -1) }, // RightIndexIntermediate → Right Index 2 Stretched
{ 44, (82, -1) }, // RightIndexDistal → Right Index 3 Stretched
// 오른손 중지 (45-47) → 머슬 83-86
{ 45, (83, 84) }, // RightMiddleProximal → Right Middle 1 Stretched, Spread
{ 46, (85, -1) }, // RightMiddleIntermediate → Right Middle 2 Stretched
{ 47, (86, -1) }, // RightMiddleDistal → Right Middle 3 Stretched
// 오른손 약지 (48-50) → 머슬 87-90
{ 48, (87, 88) }, // RightRingProximal → Right Ring 1 Stretched, Spread
{ 49, (89, -1) }, // RightRingIntermediate → Right Ring 2 Stretched
{ 50, (90, -1) }, // RightRingDistal → Right Ring 3 Stretched
// 오른손 소지 (51-53) → 머슬 91-94
{ 51, (91, 92) }, // RightLittleProximal → Right Little 1 Stretched, Spread
{ 52, (93, -1) }, // RightLittleIntermediate → Right Little 2 Stretched
{ 53, (94, -1) }, // RightLittleDistal → Right Little 3 Stretched
};
// 엄지 본 인덱스 (24-26: 왼손, 39-41: 오른손) - Spread 축이 Y임
private static readonly HashSet<int> thumbBoneIndices = new HashSet<int> { 24, 25, 26, 39, 40, 41 };
#endregion #endregion
#region #region
@ -287,7 +216,11 @@ namespace KindRetargeting
CalculateAxisNormalizer(); CalculateAxisNormalizer();
// 원본 및 대상 아바타를 T-포즈로 복원 // 원본 및 대상 아바타를 T-포즈로 복원
SetTPose(sourceAnimator); // OptiTrack 소스는 Humanoid가 아니므로 현재 포즈를 기준으로 캐싱
if (optitrackSource != null)
{
optitrackSource.CacheRestPose(); // 현재 포즈(T-포즈)를 기준으로 캐싱
}
SetTPose(targetAnimator); SetTPose(targetAnimator);
// T-포즈에서의 머리 정면 방향 캐싱 (캘리브레이션용) // T-포즈에서의 머리 정면 방향 캐싱 (캘리브레이션용)
@ -333,6 +266,9 @@ namespace KindRetargeting
} }
} }
// OptiTrack 스파인 분배 초기화
InitializeOptiTrackSpineDistribution();
// 어깨 보정 모듈 초기화 // 어깨 보정 모듈 초기화
if (targetAnimator != null) if (targetAnimator != null)
shoulderCorrection.Initialize(targetAnimator); shoulderCorrection.Initialize(targetAnimator);
@ -456,29 +392,12 @@ namespace KindRetargeting
/// </summary> /// </summary>
private void InitializeHumanPoseHandlers() private void InitializeHumanPoseHandlers()
{ {
if (sourceAnimator != null && sourceAnimator.avatar != null)
{
sourcePoseHandler = new HumanPoseHandler(sourceAnimator.avatar, sourceAnimator.transform);
}
if (targetAnimator != null && targetAnimator.avatar != null) if (targetAnimator != null && targetAnimator.avatar != null)
{ {
targetPoseHandler = new HumanPoseHandler(targetAnimator.avatar, targetAnimator.transform); targetPoseHandler = new HumanPoseHandler(targetAnimator.avatar, targetAnimator.transform);
} }
} }
/// <summary>
/// 소스 포즈를 캐싱하여 반환합니다. 프레임당 한 번만 GetHumanPose 호출.
/// </summary>
private void EnsureSourcePoseCached()
{
if (!isSourcePoseCachedThisFrame && sourcePoseHandler != null)
{
sourcePoseHandler.GetHumanPose(ref sourcePose);
isSourcePoseCachedThisFrame = true;
}
}
/// <summary> /// <summary>
/// 타겟 포즈를 캐싱하여 반환합니다. 프레임당 한 번만 GetHumanPose 호출. /// 타겟 포즈를 캐싱하여 반환합니다. 프레임당 한 번만 GetHumanPose 호출.
/// </summary> /// </summary>
@ -496,9 +415,9 @@ namespace KindRetargeting
/// </summary> /// </summary>
private void CalculateRotationOffsets(bool isIPose = false) private void CalculateRotationOffsets(bool isIPose = false)
{ {
if (sourceAnimator == null || targetAnimator == null) if (optitrackSource == null || targetAnimator == null)
{ {
Debug.LogError("소스 또는 타겟 Animator가 설정되지 않았습니다."); Debug.LogError("소스 OptiTrack 또는 타겟 Animator가 설정되지 않았습니다.");
return; return;
} }
@ -512,7 +431,7 @@ namespace KindRetargeting
for (int i = 0; i <= (isIPose ? 23 : 54); i++) for (int i = 0; i <= (isIPose ? 23 : 54); i++)
{ {
HumanBodyBones bone = (HumanBodyBones)i; HumanBodyBones bone = (HumanBodyBones)i;
Transform sourceBone = sourceAnimator.GetBoneTransform(bone); Transform sourceBone = GetSourceBoneTransform(bone);
Transform targetBone = targetAnimator.GetBoneTransform(bone); Transform targetBone = targetAnimator.GetBoneTransform(bone);
if (sourceBone != null && targetBone != null) if (sourceBone != null && targetBone != null)
@ -559,7 +478,6 @@ namespace KindRetargeting
footFrontBackOffset = footFrontBackOffset, footFrontBackOffset = footFrontBackOffset,
footInOutOffset = footInOutOffset, footInOutOffset = footInOutOffset,
floorHeight = floorHeight, floorHeight = floorHeight,
fingerCopyMode = fingerCopyMode,
rotationOffsetCache = offsetCache, rotationOffsetCache = offsetCache,
initialHipsHeight = initialHipsHeight, initialHipsHeight = initialHipsHeight,
avatarScale = avatarScale, avatarScale = avatarScale,
@ -618,7 +536,6 @@ namespace KindRetargeting
footFrontBackOffset = settings.footFrontBackOffset; footFrontBackOffset = settings.footFrontBackOffset;
footInOutOffset = settings.footInOutOffset; footInOutOffset = settings.footInOutOffset;
floorHeight = settings.floorHeight; floorHeight = settings.floorHeight;
fingerCopyMode = settings.fingerCopyMode;
initialHipsHeight = settings.initialHipsHeight; initialHipsHeight = settings.initialHipsHeight;
avatarScale = settings.avatarScale; avatarScale = settings.avatarScale;
previousScale = avatarScale; previousScale = avatarScale;
@ -765,14 +682,14 @@ namespace KindRetargeting
/// </summary> /// </summary>
private void CalculateFingerRotationOffsets() private void CalculateFingerRotationOffsets()
{ {
if (sourceAnimator == null || targetAnimator == null) if (optitrackSource == null || targetAnimator == null)
return; return;
// 손가락 본들 (24~54)의 오프셋 계산 // 손가락 본들 (24~54)의 오프셋 계산
for (int i = 24; i <= 54; i++) for (int i = 24; i <= 54; i++)
{ {
HumanBodyBones bone = (HumanBodyBones)i; HumanBodyBones bone = (HumanBodyBones)i;
Transform sourceBone = sourceAnimator.GetBoneTransform(bone); Transform sourceBone = GetSourceBoneTransform(bone);
Transform targetBone = targetAnimator.GetBoneTransform(bone); Transform targetBone = targetAnimator.GetBoneTransform(bone);
if (sourceBone != null && targetBone != null) if (sourceBone != null && targetBone != null)
@ -794,7 +711,7 @@ namespace KindRetargeting
private void InitializeIKJoints() private void InitializeIKJoints()
{ {
// IK 루트 찾기 // IK 루트 찾기
Transform sourceIKRoot = sourceAnimator.transform.Find("IK"); Transform sourceIKRoot = optitrackSource.transform.Find("IK");
if (sourceIKRoot == null) if (sourceIKRoot == null)
{ {
Debug.LogError("소스 아바타에서 IK 루트를 찾을 수 없습니다."); Debug.LogError("소스 아바타에서 IK 루트를 찾을 수 없습니다.");
@ -821,43 +738,20 @@ namespace KindRetargeting
/// </summary> /// </summary>
void Update() void Update()
{ {
// 최적화: 프레임 시작 시 포즈 캐시 플래그 리셋
isSourcePoseCachedThisFrame = false;
isTargetPoseCachedThisFrame = false; isTargetPoseCachedThisFrame = false;
// 포즈 복사 및 동기화
CopyPoseToTarget(); CopyPoseToTarget();
UpdateIKTargets(); UpdateIKTargets();
// IK 중간 타겟 업데이트
// 손가락 포즈 동기화 // 손가락은 SyncBoneRotations에서 함께 처리됨 (lastBoneIndex=55)
switch (fingerCopyMode)
{
case EnumsList.FingerCopyMode.MuscleData:
CopyFingerPoseByMuscle();
break;
case EnumsList.FingerCopyMode.Rotation:
CopyFingerPoseByRotation();
break;
}
// 손가락 셰이핑 (기존 ExecutionOrder 2)
fingerShaped.OnUpdate(); fingerShaped.OnUpdate();
// 어깨 보정 (기존 ExecutionOrder 3)
shoulderCorrection.OnUpdate(); shoulderCorrection.OnUpdate();
// 사지 가중치 (기존 ExecutionOrder 4)
limbWeight.OnUpdate(); limbWeight.OnUpdate();
// 발 접지 Pre-IK (기존 ExecutionOrder 5)
footGrounding.OnUpdate(); footGrounding.OnUpdate();
// IK 솔버 (기존 ExecutionOrder 6)
ikSolver.OnUpdate(); ikSolver.OnUpdate();
// 스케일 변경 확인 및 적용
if (!Mathf.Approximately(previousScale, avatarScale)) if (!Mathf.Approximately(previousScale, avatarScale))
{ {
ApplyScale(); ApplyScale();
@ -870,9 +764,7 @@ namespace KindRetargeting
/// </summary> /// </summary>
void LateUpdate() void LateUpdate()
{ {
// 발 접지 Post-IK (기존 FootGroundingController LateUpdate)
footGrounding.OnLateUpdate(); footGrounding.OnLateUpdate();
ApplyHeadRotationOffset(); ApplyHeadRotationOffset();
ApplyHeadScale(); ApplyHeadScale();
} }
@ -1008,50 +900,6 @@ namespace KindRetargeting
Debug.Log($"[{gameObject.name}] 정면 캘리브레이션 완료 - Offset X: {euler.x:F1}°, Y: {euler.y:F1}°, Z: {euler.z:F1}°"); Debug.Log($"[{gameObject.name}] 정면 캘리브레이션 완료 - Offset X: {euler.x:F1}°, Y: {euler.y:F1}°, Z: {euler.z:F1}°");
} }
/// <summary>
/// 머슬 데이터를 사용하여 손가락 포즈를 복제합니다.
/// SetHumanPose가 모든 본에 영향을 미치므로, 손가락을 제외한 모든 본의 Transform을 저장하고 복원합니다.
/// </summary>
private void CopyFingerPoseByMuscle()
{
if (sourcePoseHandler == null || targetPoseHandler == null)
return;
// 1. 손가락을 제외한 모든 본의 로컬 회전 저장 (위치는 복원 불필요)
savedBoneLocalRotations.Clear();
for (int i = 0; i < nonFingerBones.Length; i++)
{
Transform bone = targetAnimator.GetBoneTransform(nonFingerBones[i]);
if (bone != null)
{
savedBoneLocalRotations[nonFingerBones[i]] = bone.localRotation;
}
}
// 2. 머슬 데이터 업데이트 (최적화: 캐싱된 포즈 사용)
EnsureSourcePoseCached();
EnsureTargetPoseCached();
for (int i = 0; i < 40; i++)
{
int muscleIndex = 55 + i;
targetPose.muscles[muscleIndex] = sourcePose.muscles[muscleIndex];
}
// 3. 머슬 포즈 적용 (손가락 포함 전체 본에 영향)
targetPoseHandler.SetHumanPose(ref targetPose);
// 4. 손가락을 제외한 모든 본의 로컬 회전 복원 (위치는 복원하지 않음 - 본 길이 변형 방지)
foreach (var kvp in savedBoneLocalRotations)
{
Transform bone = targetAnimator.GetBoneTransform(kvp.Key);
if (bone != null)
{
bone.localRotation = kvp.Value;
}
}
}
/// <summary> /// <summary>
/// 회전값을 사용하여 손가락 포즈를 복제합니다. /// 회전값을 사용하여 손가락 포즈를 복제합니다.
/// </summary> /// </summary>
@ -1060,7 +908,7 @@ namespace KindRetargeting
for (int i = 24; i <= 53; i++) for (int i = 24; i <= 53; i++)
{ {
HumanBodyBones bone = (HumanBodyBones)i; HumanBodyBones bone = (HumanBodyBones)i;
Transform sourceBone = sourceAnimator.GetBoneTransform(bone); Transform sourceBone = GetSourceBoneTransform(bone);
Transform targetBone = targetAnimator.GetBoneTransform(bone); Transform targetBone = targetAnimator.GetBoneTransform(bone);
if (sourceBone != null && targetBone != null) if (sourceBone != null && targetBone != null)
@ -1091,7 +939,7 @@ namespace KindRetargeting
private void CopyPoseToTarget() private void CopyPoseToTarget()
{ {
// 힙(루트 본) 동기화 // 힙(루트 본) 동기화
Transform sourceHips = sourceAnimator.GetBoneTransform(HumanBodyBones.Hips); Transform sourceHips = GetSourceBoneTransform(HumanBodyBones.Hips);
Transform targetHips = targetAnimator.GetBoneTransform(HumanBodyBones.Hips); Transform targetHips = targetAnimator.GetBoneTransform(HumanBodyBones.Hips);
if (sourceHips != null && targetHips != null) if (sourceHips != null && targetHips != null)
@ -1141,6 +989,8 @@ namespace KindRetargeting
// 힙을 제외한 본들의 회전 동기화 // 힙을 제외한 본들의 회전 동기화
SyncBoneRotations(skipBone: HumanBodyBones.Hips); SyncBoneRotations(skipBone: HumanBodyBones.Hips);
ApplyOptiTrackSpineNeckDistribution();
} }
/// <summary> /// <summary>
@ -1149,7 +999,6 @@ namespace KindRetargeting
/// <param name="skipBone">동기화에서 제외할 본</param> /// <param name="skipBone">동기화에서 제외할 본</param>
private void SyncBoneRotations(HumanBodyBones skipBone) private void SyncBoneRotations(HumanBodyBones skipBone)
{ {
// 기본 몸체 본만 동기화 (손가락 제외)
for (int i = 0; i < lastBoneIndex; i++) for (int i = 0; i < lastBoneIndex; i++)
{ {
HumanBodyBones bone = (HumanBodyBones)i; HumanBodyBones bone = (HumanBodyBones)i;
@ -1157,7 +1006,10 @@ namespace KindRetargeting
if (bone == skipBone) if (bone == skipBone)
continue; continue;
Transform sourceBone = sourceAnimator.GetBoneTransform(bone); if (useOptiTrackSpineDistribution && IsOptiTrackDistributedBone(bone))
continue;
Transform sourceBone = GetSourceBoneTransform(bone);
Transform targetBone = targetAnimator.GetBoneTransform(bone); Transform targetBone = targetAnimator.GetBoneTransform(bone);
if (sourceBone != null && targetBone != null) if (sourceBone != null && targetBone != null)
@ -1179,6 +1031,134 @@ namespace KindRetargeting
#endregion #endregion
#region OptiTrack /
/// <summary>
/// OptiTrack 소스 감지 및 스파인/넥 분배 초기화
/// </summary>
private void InitializeOptiTrackSpineDistribution()
{
if (optitrackSource == null || targetAnimator == null)
{
useOptiTrackSpineDistribution = false;
return;
}
// 소스 스파인/넥 체인 가져오기
sourceSpineChain = optitrackSource.GetSpineChainTransforms();
sourceNeckChain = optitrackSource.GetNeckChainTransforms();
// 타겟 Humanoid 스파인 본들 수집 (존재하는 본만)
targetSpineBones = new List<Transform>();
HumanBodyBones[] spineEnums = { HumanBodyBones.Spine, HumanBodyBones.Chest, HumanBodyBones.UpperChest };
foreach (var bone in spineEnums)
{
Transform t = targetAnimator.GetBoneTransform(bone);
if (t != null)
targetSpineBones.Add(t);
}
// 타겟 Humanoid 넥 본 수집
targetNeckBones = new List<Transform>();
HumanBodyBones[] neckEnums = { HumanBodyBones.Neck };
foreach (var bone in neckEnums)
{
Transform t = targetAnimator.GetBoneTransform(bone);
if (t != null)
targetNeckBones.Add(t);
}
useOptiTrackSpineDistribution = sourceSpineChain.Count > 0 && targetSpineBones.Count > 0;
if (!useOptiTrackSpineDistribution) return;
// ── T-포즈 기준으로 가상 본 오프셋 계산 (OffsetTransfer 패턴) ──
// 소스와 타겟 모두 T-포즈 상태에서 호출되어야 함
spineOffsets = CalculateVirtualBoneOffsets(sourceSpineChain, targetSpineBones);
neckOffsets = CalculateVirtualBoneOffsets(sourceNeckChain, targetNeckBones);
Debug.Log($"[Retargeting] OptiTrack 스파인 분배 활성화: " +
$"소스 스파인 {sourceSpineChain.Count}본 → 타겟 {targetSpineBones.Count}본, " +
$"소스 넥 {sourceNeckChain.Count}본 → 타겟 {targetNeckBones.Count}본");
}
/// <summary>
/// T-포즈 기준으로 가상 본의 월드 회전과 타겟 본의 월드 회전 사이 오프셋을 계산.
/// offset = Inv(가상본 월드회전) * 타겟본 월드회전
/// </summary>
private List<Quaternion> CalculateVirtualBoneOffsets(List<Transform> sourceChain, List<Transform> targetBones)
{
var offsets = new List<Quaternion>();
int srcCount = sourceChain.Count;
int tgtCount = targetBones.Count;
for (int t = 0; t < tgtCount; t++)
{
// 이 타겟 본이 담당할 소스 범위의 마지막 본의 월드 회전을 가상 본 회전으로 사용
// 예) 5본→2본: 가상본0 = sourceChain[2], 가상본1 = sourceChain[4]
int lastSrcIdx = Mathf.Min(Mathf.RoundToInt((float)(t + 1) / tgtCount * srcCount) - 1, srcCount - 1);
Quaternion virtualBoneWorldRot = sourceChain[lastSrcIdx].rotation;
Quaternion targetWorldRot = targetBones[t].rotation;
// OffsetTransfer 패턴: offset = Inv(source) * target
offsets.Add(Quaternion.Inverse(virtualBoneWorldRot) * targetWorldRot);
}
return offsets;
}
/// <summary>
/// OptiTrack 분배 대상 본인지 확인 (스파인 + 넥)
/// </summary>
private bool IsOptiTrackDistributedBone(HumanBodyBones bone)
{
return bone == HumanBodyBones.Spine ||
bone == HumanBodyBones.Chest ||
bone == HumanBodyBones.UpperChest ||
bone == HumanBodyBones.Neck;
}
/// <summary>
/// 소스 스파인/넥 체인을 가상 본으로 그룹핑하여 월드 회전 + 오프셋으로 타겟에 적용
/// </summary>
private void ApplyOptiTrackSpineNeckDistribution()
{
if (!useOptiTrackSpineDistribution) return;
// 스파인 분배
if (sourceSpineChain.Count > 0 && targetSpineBones.Count > 0 && spineOffsets != null)
{
ApplyVirtualBoneRotations(sourceSpineChain, targetSpineBones, spineOffsets);
}
// 넥 분배
if (sourceNeckChain.Count > 0 && targetNeckBones.Count > 0 && neckOffsets != null)
{
ApplyVirtualBoneRotations(sourceNeckChain, targetNeckBones, neckOffsets);
}
}
/// <summary>
/// 소스 체인을 가상 본으로 그룹핑 → 가상 본 월드 회전 계산 → 오프셋 적용하여 타겟에 세팅
/// </summary>
private void ApplyVirtualBoneRotations(List<Transform> sourceChain, List<Transform> targetBones, List<Quaternion> offsets)
{
int srcCount = sourceChain.Count;
int tgtCount = targetBones.Count;
for (int t = 0; t < tgtCount; t++)
{
// 가상 본 = 이 타겟이 담당하는 소스 그룹의 마지막 본 월드 회전
int lastSrcIdx = Mathf.Min(Mathf.RoundToInt((float)(t + 1) / tgtCount * srcCount) - 1, srcCount - 1);
Quaternion virtualBoneWorldRot = sourceChain[lastSrcIdx].rotation;
// OffsetTransfer 패턴: 타겟.rotation = 가상본월드회전 * offset
targetBones[t].rotation = virtualBoneWorldRot * offsets[t];
}
}
#endregion
#region IK #region IK
/// <summary> /// <summary>
@ -1272,7 +1252,7 @@ namespace KindRetargeting
{ {
if (endTarget == null) return; if (endTarget == null) return;
Transform sourceBone = sourceAnimator.GetBoneTransform(endBone); Transform sourceBone = GetSourceBoneTransform(endBone);
Transform targetBone = targetAnimator.GetBoneTransform(endBone); Transform targetBone = targetAnimator.GetBoneTransform(endBone);
if (sourceBone != null && targetBone != null) if (sourceBone != null && targetBone != null)
@ -1369,16 +1349,6 @@ namespace KindRetargeting
{ {
var pose = humanPoseClip.GetPose(); var pose = humanPoseClip.GetPose();
HumanPoseTransfer.SetPose(avatar, transform, pose); HumanPoseTransfer.SetPose(avatar, transform, pose);
// 소스 아바타의 UpperChest 본 로컬 포지션 초기화
if (animator == sourceAnimator)
{
Transform upperChest = animator.GetBoneTransform(HumanBodyBones.UpperChest);
if (upperChest != null)
{
upperChest.localPosition = Vector3.zero;
}
}
} }
else else
{ {
@ -1416,7 +1386,6 @@ namespace KindRetargeting
/// </summary> /// </summary>
void OnDestroy() void OnDestroy()
{ {
sourcePoseHandler?.Dispose();
targetPoseHandler?.Dispose(); targetPoseHandler?.Dispose();
fingerShaped.Cleanup(); fingerShaped.Cleanup();
} }
@ -1477,18 +1446,12 @@ namespace KindRetargeting
return; return;
} }
// T-포즈로 복원 // 포즈 복원
SetTPose(sourceAnimator); if (optitrackSource != null)
optitrackSource.RestoreRestPose();
SetTPose(targetAnimator); SetTPose(targetAnimator);
// 소스 아바타의 UpperChest 본 로컬 포지션 초기화
Transform upperChest = sourceAnimator.GetBoneTransform(HumanBodyBones.UpperChest);
if (upperChest != null)
{
upperChest.localPosition = Vector3.zero;
}
// HumanPoseHandler 초기화 // HumanPoseHandler 초기화
InitializeHumanPoseHandlers(); InitializeHumanPoseHandlers();
@ -1505,15 +1468,15 @@ namespace KindRetargeting
originalScales.Clear(); originalScales.Clear();
originalPositions.Clear(); originalPositions.Clear();
Transform parentTransform = sourceAnimator.transform.parent; Transform parentTransform = optitrackSource.transform.parent;
if (parentTransform != null) if (parentTransform != null)
{ {
for (int i = 0; i < parentTransform.childCount; i++) for (int i = 0; i < parentTransform.childCount; i++)
{ {
Transform child = parentTransform.GetChild(i); Transform child = parentTransform.GetChild(i);
// sourceAnimator를 제외한 모든 자식 오브젝트 추가 // optitrackSource를 제외한 모든 자식 오브젝트 추가
if (child != sourceAnimator.transform) if (child != optitrackSource.transform)
{ {
scalableObjects.Add(child); scalableObjects.Add(child);
// 초기 스케일과 위치 저장 // 초기 스케일과 위치 저장

View File

@ -23,7 +23,7 @@ namespace KindRetargeting
if (commonUss != null) root.styleSheets.Add(commonUss); if (commonUss != null) root.styleSheets.Add(commonUss);
// ── 기본 설정 ── // ── 기본 설정 ──
root.Add(new PropertyField(serializedObject.FindProperty("sourceAnimator"), "원본 Animator")); root.Add(new PropertyField(serializedObject.FindProperty("optitrackSource"), "원본 OptiTrack"));
// ── 아바타 크기 ── // ── 아바타 크기 ──
var scaleFoldout = new Foldout { text = "아바타 크기 설정", value = true }; var scaleFoldout = new Foldout { text = "아바타 크기 설정", value = true };
@ -476,11 +476,11 @@ namespace KindRetargeting
private void AutoCalibrateAll(CustomRetargetingScript script, SerializedObject so) private void AutoCalibrateAll(CustomRetargetingScript script, SerializedObject so)
{ {
Animator source = script.sourceAnimator; var source = script.optitrackSource;
Animator targetAnim = script.targetAnimator; Animator targetAnim = script.targetAnimator;
if (source == null || targetAnim == null || !source.isHuman || !targetAnim.isHuman) if (source == null || targetAnim == null || !targetAnim.isHuman)
{ {
Debug.LogWarning("소스/타겟 Animator가 없거나 Humanoid가 아닙니다."); Debug.LogWarning("소스 OptiTrack 또는 타겟 Animator가 설정되지 않았습니다.");
return; return;
} }
@ -526,11 +526,11 @@ namespace KindRetargeting
private float CalculateHipsOffsetFromLegDifference(CustomRetargetingScript script) private float CalculateHipsOffsetFromLegDifference(CustomRetargetingScript script)
{ {
Animator source = script.sourceAnimator; var source = script.optitrackSource;
Animator targetAnim = script.targetAnimator; Animator targetAnim = script.targetAnimator;
if (source == null || targetAnim == null) return 0f; if (source == null || targetAnim == null) return 0f;
float sourceLeg = GetLegLength(source); float sourceLeg = GetSourceLegLength(source);
float targetLeg = GetLegLength(targetAnim); float targetLeg = GetLegLength(targetAnim);
if (sourceLeg < 0.01f || targetLeg < 0.01f) return 0f; if (sourceLeg < 0.01f || targetLeg < 0.01f) return 0f;
@ -546,6 +546,15 @@ namespace KindRetargeting
return Vector3.Distance(upper.position, lower.position) + Vector3.Distance(lower.position, foot.position); 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) private void CalibrateHeadToForward(SerializedObject so, SerializedProperty xProp, SerializedProperty yProp, SerializedProperty zProp)
{ {
CustomRetargetingScript script = so.targetObject as CustomRetargetingScript; CustomRetargetingScript script = so.targetObject as CustomRetargetingScript;

View File

@ -906,12 +906,12 @@ public class RetargetingControlWindow : EditorWindow
/// </summary> /// </summary>
private void AutoCalibrateAll(CustomRetargetingScript script, SerializedObject so) private void AutoCalibrateAll(CustomRetargetingScript script, SerializedObject so)
{ {
Animator source = script.sourceAnimator; var source = script.optitrackSource;
Animator target = script.targetAnimator; Animator target = script.targetAnimator;
if (source == null || target == null || !source.isHuman || !target.isHuman) if (source == null || target == null || !target.isHuman)
{ {
Debug.LogWarning("소스/타겟 Animator가 없거나 Humanoid가 아닙니다."); Debug.LogWarning("소스 OptiTrack 또는 타겟 Animator가 설정되지 않았습니다.");
return; return;
} }
@ -978,16 +978,16 @@ public class RetargetingControlWindow : EditorWindow
/// </summary> /// </summary>
private float CalculateHipsOffsetFromLegDifference(CustomRetargetingScript script) private float CalculateHipsOffsetFromLegDifference(CustomRetargetingScript script)
{ {
Animator source = script.sourceAnimator; var source = script.optitrackSource;
Animator target = script.targetAnimator; Animator target = script.targetAnimator;
if (source == null || target == null || !source.isHuman || !target.isHuman) if (source == null || target == null || !target.isHuman)
{ {
Debug.LogWarning("소스/타겟 Animator가 없거나 Humanoid가 아닙니다."); Debug.LogWarning("소스 OptiTrack 또는 타겟 Animator가 설정되지 않았습니다.");
return 0f; return 0f;
} }
float sourceLeg = GetLegLength(source); float sourceLeg = GetSourceLegLength(source);
float targetLeg = GetLegLength(target); float targetLeg = GetLegLength(target);
if (sourceLeg < 0.01f || targetLeg < 0.01f) if (sourceLeg < 0.01f || targetLeg < 0.01f)
@ -1015,6 +1015,18 @@ public class RetargetingControlWindow : EditorWindow
+ Vector3.Distance(lowerLeg.position, foot.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) private void CalibrateHeadToForward(SerializedObject so, SerializedProperty xProp, SerializedProperty yProp, SerializedProperty zProp)
{ {
CustomRetargetingScript script = so.targetObject as CustomRetargetingScript; CustomRetargetingScript script = so.targetObject as CustomRetargetingScript;

View File

@ -157,8 +157,8 @@ namespace KindRetargeting
if (otherCrs == crs) continue; if (otherCrs == crs) continue;
// 왼손과 오른손 Transform 가져오기 // 왼손과 오른손 Transform 가져오기
Transform leftHand = otherCrs?.sourceAnimator?.GetBoneTransform(HumanBodyBones.LeftHand); Transform leftHand = otherCrs?.optitrackSource?.GetBoneTransform(HumanBodyBones.LeftHand);
Transform rightHand = otherCrs?.sourceAnimator?.GetBoneTransform(HumanBodyBones.RightHand); Transform rightHand = otherCrs?.optitrackSource?.GetBoneTransform(HumanBodyBones.RightHand);
// 손이 존재하면 props 리스트에 추가 // 손이 존재하면 props 리스트에 추가
if (leftHand != null) if (leftHand != null)
@ -192,8 +192,8 @@ namespace KindRetargeting
// 왼쪽 팔 가중치 업데이트 // 왼쪽 팔 가중치 업데이트
if (ikSolver.leftArm.target != null && ikSolver.rightArm.target != null) if (ikSolver.leftArm.target != null && ikSolver.rightArm.target != null)
{ {
Transform leftHandTransform = crs?.sourceAnimator?.GetBoneTransform(HumanBodyBones.LeftHand); Transform leftHandTransform = crs?.optitrackSource?.GetBoneTransform(HumanBodyBones.LeftHand);
Transform rightHandTransform = crs?.sourceAnimator?.GetBoneTransform(HumanBodyBones.RightHand); Transform rightHandTransform = crs?.optitrackSource?.GetBoneTransform(HumanBodyBones.RightHand);
if (leftHandTransform != null && rightHandTransform != null && if (leftHandTransform != null && rightHandTransform != null &&
props != null && props.Count > 0) props != null && props.Count > 0)
{ {
@ -215,7 +215,7 @@ namespace KindRetargeting
{ {
if (ikSolver == null || crs == null || characterRoot == null) return; if (ikSolver == null || crs == null || characterRoot == null) return;
Transform hips = crs?.sourceAnimator?.GetBoneTransform(HumanBodyBones.Hips); Transform hips = crs?.optitrackSource?.GetBoneTransform(HumanBodyBones.Hips);
if (hips == null) return; if (hips == null) return;
// 캐릭터 루트 기준으로 히프의 로컬 높이 계산 // 캐릭터 루트 기준으로 히프의 로컬 높이 계산
@ -264,8 +264,8 @@ namespace KindRetargeting
if (ikSolver == null || crs == null) return; if (ikSolver == null || crs == null) return;
// 프랍과의 거리에 따른 웨이트 업데이트 // 프랍과의 거리에 따른 웨이트 업데이트
Transform leftHandTransform = crs?.sourceAnimator?.GetBoneTransform(HumanBodyBones.LeftHand); Transform leftHandTransform = crs?.optitrackSource?.GetBoneTransform(HumanBodyBones.LeftHand);
Transform rightHandTransform = crs?.sourceAnimator?.GetBoneTransform(HumanBodyBones.RightHand); Transform rightHandTransform = crs?.optitrackSource?.GetBoneTransform(HumanBodyBones.RightHand);
if (leftHandTransform != null && rightHandTransform != null && props != null) if (leftHandTransform != null && rightHandTransform != null && props != null)
{ {
@ -298,7 +298,7 @@ namespace KindRetargeting
{ {
if (crs == null) return; if (crs == null) return;
Transform hipsTransform = crs.sourceAnimator.GetBoneTransform(HumanBodyBones.Hips); Transform hipsTransform = crs.optitrackSource.GetBoneTransform(HumanBodyBones.Hips);
if (hipsTransform != null && props != null) if (hipsTransform != null && props != null)
{ {
float minDistance = float.MaxValue; float minDistance = float.MaxValue;
@ -473,7 +473,7 @@ namespace KindRetargeting
{ {
if (crs == null || ikSolver == null) return; if (crs == null || ikSolver == null) return;
Transform hipsTransform = crs.sourceAnimator.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;

View File

@ -647,11 +647,11 @@ namespace KindRetargeting.Remote
var script = FindCharacter(characterId); var script = FindCharacter(characterId);
if (script == null) return; if (script == null) return;
Animator source = script.sourceAnimator; var source = script.optitrackSource;
Animator targetAnim = script.targetAnimator; Animator targetAnim = script.targetAnimator;
if (source == null || targetAnim == null || !source.isHuman || !targetAnim.isHuman) if (source == null || targetAnim == null || !targetAnim.isHuman)
{ {
SendStatus(false, "소스/타겟 Animator가 없거나 Humanoid가 아닙니다."); SendStatus(false, "소스 OptiTrack 또는 타겟 Animator가 설정되지 않았습니다.");
return; return;
} }
@ -668,7 +668,7 @@ namespace KindRetargeting.Remote
{ {
yield return null; // 1프레임 대기 yield return null; // 1프레임 대기
Animator source = script.sourceAnimator; var source = script.optitrackSource;
Animator targetAnim = script.targetAnimator; Animator targetAnim = script.targetAnimator;
Transform sourceNeck = source.GetBoneTransform(HumanBodyBones.Neck); Transform sourceNeck = source.GetBoneTransform(HumanBodyBones.Neck);
@ -696,17 +696,26 @@ namespace KindRetargeting.Remote
private float CalculateHipsOffsetFromLegDifference(CustomRetargetingScript script) private float CalculateHipsOffsetFromLegDifference(CustomRetargetingScript script)
{ {
Animator source = script.sourceAnimator; var source = script.optitrackSource;
Animator targetAnim = script.targetAnimator; Animator targetAnim = script.targetAnimator;
if (source == null || targetAnim == null) return 0f; if (source == null || targetAnim == null) return 0f;
float sourceLeg = GetLegLength(source); float sourceLeg = GetSourceLegLength(source);
float targetLeg = GetLegLength(targetAnim); float targetLeg = GetLegLength(targetAnim);
if (sourceLeg < 0.01f || targetLeg < 0.01f) return 0f; if (sourceLeg < 0.01f || targetLeg < 0.01f) return 0f;
return targetLeg - sourceLeg; 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) private float GetLegLength(Animator animator)
{ {
Transform upper = animator.GetBoneTransform(HumanBodyBones.LeftUpperLeg); Transform upper = animator.GetBoneTransform(HumanBodyBones.LeftUpperLeg);