Fix : 리타겟팅 시스템 추가 패치
This commit is contained in:
parent
f2a99cb426
commit
14874d5b6e
@ -1,353 +0,0 @@
|
||||
using UnityEngine;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace RootMotion.FinalIK
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// A full-body IK solver designed specifically for a VR HMD and hand controllers.
|
||||
/// </summary>
|
||||
//[HelpURL("http://www.root-motion.com/finalikdox/html/page16.html")]
|
||||
[DefaultExecutionOrder(5)]
|
||||
public class FullBodyInverseKinematics_RND : IK
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// VRIK-specific definition of a humanoid biped.
|
||||
/// </summary>
|
||||
[System.Serializable]
|
||||
public class References
|
||||
{
|
||||
public Transform root; // 0
|
||||
|
||||
[LargeHeader("Spine")]
|
||||
public Transform pelvis; // 1
|
||||
public Transform spine; // 2
|
||||
|
||||
[Tooltip("Optional")]
|
||||
public Transform chest; // 3 Optional
|
||||
|
||||
[Tooltip("Optional")]
|
||||
public Transform neck; // 4 Optional
|
||||
public Transform head; // 5
|
||||
|
||||
[LargeHeader("Left Arm")]
|
||||
[Tooltip("Optional")]
|
||||
public Transform leftShoulder; // 6 Optional
|
||||
[Tooltip("VRIK also supports armless characters.If you do not wish to use arms, leave all arm references empty.")]
|
||||
public Transform leftUpperArm; // 7
|
||||
[Tooltip("VRIK also supports armless characters.If you do not wish to use arms, leave all arm references empty.")]
|
||||
public Transform leftForearm; // 8
|
||||
[Tooltip("VRIK also supports armless characters.If you do not wish to use arms, leave all arm references empty.")]
|
||||
public Transform leftHand; // 9
|
||||
|
||||
[LargeHeader("Right Arm")]
|
||||
[Tooltip("Optional")]
|
||||
public Transform rightShoulder; // 10 Optional
|
||||
[Tooltip("VRIK also supports armless characters.If you do not wish to use arms, leave all arm references empty.")]
|
||||
public Transform rightUpperArm; // 11
|
||||
[Tooltip("VRIK also supports armless characters.If you do not wish to use arms, leave all arm references empty.")]
|
||||
public Transform rightForearm; // 12
|
||||
[Tooltip("VRIK also supports armless characters.If you do not wish to use arms, leave all arm references empty.")]
|
||||
public Transform rightHand; // 13
|
||||
|
||||
[LargeHeader("Left Leg")]
|
||||
[Tooltip("VRIK also supports legless characters.If you do not wish to use legs, leave all leg references empty.")]
|
||||
public Transform leftThigh; // 14 Optional
|
||||
|
||||
[Tooltip("VRIK also supports legless characters.If you do not wish to use legs, leave all leg references empty.")]
|
||||
public Transform leftCalf; // 15 Optional
|
||||
|
||||
[Tooltip("VRIK also supports legless characters.If you do not wish to use legs, leave all leg references empty.")]
|
||||
public Transform leftFoot; // 16 Optional
|
||||
|
||||
[Tooltip("Optional")]
|
||||
public Transform leftToes; // 17 Optional
|
||||
|
||||
[LargeHeader("Right Leg")]
|
||||
[Tooltip("VRIK also supports legless characters.If you do not wish to use legs, leave all leg references empty.")]
|
||||
public Transform rightThigh; // 18 Optional
|
||||
|
||||
[Tooltip("VRIK also supports legless characters.If you do not wish to use legs, leave all leg references empty.")]
|
||||
public Transform rightCalf; // 19 Optional
|
||||
|
||||
[Tooltip("VRIK also supports legless characters.If you do not wish to use legs, leave all leg references empty.")]
|
||||
public Transform rightFoot; // 20 Optional
|
||||
|
||||
[Tooltip("Optional")]
|
||||
public Transform rightToes; // 21 Optional
|
||||
|
||||
public References() { }
|
||||
|
||||
public References(BipedReferences b)
|
||||
{
|
||||
root = b.root;
|
||||
pelvis = b.pelvis;
|
||||
spine = b.spine[0];
|
||||
chest = b.spine.Length > 1 ? b.spine[1] : null;
|
||||
head = b.head;
|
||||
|
||||
leftShoulder = b.leftUpperArm.parent;
|
||||
leftUpperArm = b.leftUpperArm;
|
||||
leftForearm = b.leftForearm;
|
||||
leftHand = b.leftHand;
|
||||
|
||||
rightShoulder = b.rightUpperArm.parent;
|
||||
rightUpperArm = b.rightUpperArm;
|
||||
rightForearm = b.rightForearm;
|
||||
rightHand = b.rightHand;
|
||||
|
||||
leftThigh = b.leftThigh;
|
||||
leftCalf = b.leftCalf;
|
||||
leftFoot = b.leftFoot;
|
||||
leftToes = b.leftFoot.GetChild(0);
|
||||
|
||||
rightThigh = b.rightThigh;
|
||||
rightCalf = b.rightCalf;
|
||||
rightFoot = b.rightFoot;
|
||||
rightToes = b.rightFoot.GetChild(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an array of all the Transforms in the definition.
|
||||
/// </summary>
|
||||
public Transform[] GetTransforms()
|
||||
{
|
||||
return new Transform[22] {
|
||||
root, pelvis, spine, chest, neck, head, leftShoulder, leftUpperArm, leftForearm, leftHand, rightShoulder, rightUpperArm, rightForearm, rightHand, leftThigh, leftCalf, leftFoot, leftToes, rightThigh, rightCalf, rightFoot, rightToes
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if all required Transforms have been assigned (shoulder, toe and neck bones are optional).
|
||||
/// </summary>
|
||||
public bool isFilled
|
||||
{
|
||||
get
|
||||
{
|
||||
if (
|
||||
root == null ||
|
||||
pelvis == null ||
|
||||
spine == null ||
|
||||
head == null
|
||||
) return false;
|
||||
|
||||
bool noArmBones =
|
||||
leftUpperArm == null &&
|
||||
leftForearm == null &&
|
||||
leftHand == null &&
|
||||
rightUpperArm == null &&
|
||||
rightForearm == null &&
|
||||
rightHand == null;
|
||||
|
||||
bool atLeastOneArmBoneMissing =
|
||||
leftUpperArm == null ||
|
||||
leftForearm == null ||
|
||||
leftHand == null ||
|
||||
rightUpperArm == null ||
|
||||
rightForearm == null ||
|
||||
rightHand == null;
|
||||
|
||||
// If all leg bones are null, it is valid
|
||||
bool noLegBones =
|
||||
leftThigh == null &&
|
||||
leftCalf == null &&
|
||||
leftFoot == null &&
|
||||
rightThigh == null &&
|
||||
rightCalf == null &&
|
||||
rightFoot == null;
|
||||
|
||||
bool atLeastOneLegBoneMissing =
|
||||
leftThigh == null ||
|
||||
leftCalf == null ||
|
||||
leftFoot == null ||
|
||||
rightThigh == null ||
|
||||
rightCalf == null ||
|
||||
rightFoot == null;
|
||||
|
||||
if (atLeastOneLegBoneMissing && !noLegBones) return false;
|
||||
if (atLeastOneArmBoneMissing && !noArmBones) return false;
|
||||
|
||||
// Shoulder, toe and neck bones are optional
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if none of the Transforms have been assigned.
|
||||
/// </summary>
|
||||
public bool isEmpty
|
||||
{
|
||||
get
|
||||
{
|
||||
if (
|
||||
root != null ||
|
||||
pelvis != null ||
|
||||
spine != null ||
|
||||
chest != null ||
|
||||
neck != null ||
|
||||
head != null ||
|
||||
leftShoulder != null ||
|
||||
leftUpperArm != null ||
|
||||
leftForearm != null ||
|
||||
leftHand != null ||
|
||||
rightShoulder != null ||
|
||||
rightUpperArm != null ||
|
||||
rightForearm != null ||
|
||||
rightHand != null ||
|
||||
leftThigh != null ||
|
||||
leftCalf != null ||
|
||||
leftFoot != null ||
|
||||
leftToes != null ||
|
||||
rightThigh != null ||
|
||||
rightCalf != null ||
|
||||
rightFoot != null ||
|
||||
rightToes != null
|
||||
) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Auto-detects VRIK references. Works with a Humanoid Animator on the root gameobject only.
|
||||
/// </summary>
|
||||
public static bool AutoDetectReferences(Transform root, out References references)
|
||||
{
|
||||
references = new References();
|
||||
|
||||
var animator = root.GetComponentInChildren<Animator>();
|
||||
if (animator == null || !animator.isHuman)
|
||||
{
|
||||
Debug.LogWarning("VRIK needs a Humanoid Animator to auto-detect biped references. Please assign references manually.");
|
||||
return false;
|
||||
}
|
||||
|
||||
references.root = root;
|
||||
references.pelvis = animator.GetBoneTransform(HumanBodyBones.Hips);
|
||||
references.spine = animator.GetBoneTransform(HumanBodyBones.Spine);
|
||||
references.chest = animator.GetBoneTransform(HumanBodyBones.Chest);
|
||||
references.neck = animator.GetBoneTransform(HumanBodyBones.Neck);
|
||||
references.head = animator.GetBoneTransform(HumanBodyBones.Head);
|
||||
references.leftShoulder = animator.GetBoneTransform(HumanBodyBones.LeftShoulder);
|
||||
references.leftUpperArm = animator.GetBoneTransform(HumanBodyBones.LeftUpperArm);
|
||||
references.leftForearm = animator.GetBoneTransform(HumanBodyBones.LeftLowerArm);
|
||||
references.leftHand = animator.GetBoneTransform(HumanBodyBones.LeftHand);
|
||||
references.rightShoulder = animator.GetBoneTransform(HumanBodyBones.RightShoulder);
|
||||
references.rightUpperArm = animator.GetBoneTransform(HumanBodyBones.RightUpperArm);
|
||||
references.rightForearm = animator.GetBoneTransform(HumanBodyBones.RightLowerArm);
|
||||
references.rightHand = animator.GetBoneTransform(HumanBodyBones.RightHand);
|
||||
references.leftThigh = animator.GetBoneTransform(HumanBodyBones.LeftUpperLeg);
|
||||
references.leftCalf = animator.GetBoneTransform(HumanBodyBones.LeftLowerLeg);
|
||||
references.leftFoot = animator.GetBoneTransform(HumanBodyBones.LeftFoot);
|
||||
references.leftToes = animator.GetBoneTransform(HumanBodyBones.LeftToes);
|
||||
references.rightThigh = animator.GetBoneTransform(HumanBodyBones.RightUpperLeg);
|
||||
references.rightCalf = animator.GetBoneTransform(HumanBodyBones.RightLowerLeg);
|
||||
references.rightFoot = animator.GetBoneTransform(HumanBodyBones.RightFoot);
|
||||
references.rightToes = animator.GetBoneTransform(HumanBodyBones.RightToes);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Open the User Manual URL
|
||||
[ContextMenu("User Manual")]
|
||||
protected override void OpenUserManual()
|
||||
{
|
||||
Application.OpenURL("http://www.root-motion.com/finalikdox/html/page16.html");
|
||||
}
|
||||
|
||||
// Open the Script Reference URL
|
||||
[ContextMenu("Scrpt Reference")]
|
||||
protected override void OpenScriptReference()
|
||||
{
|
||||
Application.OpenURL("http://www.root-motion.com/finalikdox/html/class_root_motion_1_1_final_i_k_1_1_v_r_i_k.html");
|
||||
}
|
||||
|
||||
// Open a video tutorial about setting up the component
|
||||
[ContextMenu("TUTORIAL VIDEO (STEAMVR SETUP)")]
|
||||
void OpenSetupTutorial()
|
||||
{
|
||||
Application.OpenURL("https://www.youtube.com/watch?v=6Pfx7lYQiIA&feature=youtu.be");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bone mapping. Right-click on the component header and select 'Auto-detect References' of fill in manually if not a Humanoid character. Chest, neck, shoulder and toe bones are optional. VRIK also supports legless characters. If you do not wish to use legs, leave all leg references empty.
|
||||
/// </summary>
|
||||
[ContextMenuItem("Auto-detect References", "AutoDetectReferences")]
|
||||
[Tooltip("Bone mapping. Right-click on the component header and select 'Auto-detect References' of fill in manually if not a Humanoid character. Chest, neck, shoulder and toe bones are optional. VRIK also supports legless characters. If you do not wish to use legs, leave all leg references empty.")]
|
||||
public References references = new References();
|
||||
|
||||
/// <summary>
|
||||
/// The solver.
|
||||
/// </summary>
|
||||
[Tooltip("The VRIK solver.")]
|
||||
public IKSolverVR solver = new IKSolverVR();
|
||||
|
||||
public void Start()
|
||||
{
|
||||
AutoDetectReferences();
|
||||
|
||||
solver.scale = 0.01f;
|
||||
solver.LOD = 1;
|
||||
fixTransforms = false;
|
||||
|
||||
// 기존 설정들
|
||||
solver.leftLeg.bendGoalWeight = 1;
|
||||
solver.rightLeg.bendGoalWeight = 1;
|
||||
solver.leftLeg.bendToTargetWeight = 1;
|
||||
solver.rightLeg.bendToTargetWeight = 1;
|
||||
|
||||
solver.spine.positionWeight = 0;
|
||||
solver.spine.rotationWeight = 0;
|
||||
solver.locomotion.weight = 0;
|
||||
|
||||
//useInUpdate = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Auto-detects bone references for this VRIK. Works with a Humanoid Animator on the gameobject only.
|
||||
/// </summary>
|
||||
[ContextMenu("Auto-detect References")]
|
||||
public void AutoDetectReferences()
|
||||
{
|
||||
References.AutoDetectReferences(transform, out references);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fills in arm wristToPalmAxis and palmToThumbAxis.
|
||||
/// </summary>
|
||||
[ContextMenu("Guess Hand Orientations")]
|
||||
public void GuessHandOrientations()
|
||||
{
|
||||
solver.GuessHandOrientations_RND(references, false);
|
||||
}
|
||||
|
||||
public override IKSolver GetIKSolver()
|
||||
{
|
||||
return solver as IKSolver;
|
||||
}
|
||||
|
||||
protected override void InitiateSolver()
|
||||
{
|
||||
if (references.isEmpty) AutoDetectReferences();
|
||||
if (references.isFilled) solver.SetToReferences(references);
|
||||
|
||||
base.InitiateSolver();
|
||||
}
|
||||
|
||||
protected override void UpdateSolver()
|
||||
{
|
||||
if (references.root != null && references.root.localScale == Vector3.zero)
|
||||
{
|
||||
Debug.LogError("VRIK Root Transform's scale is zero, can not update VRIK. Make sure you have not calibrated the character to a zero scale.", transform);
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
base.UpdateSolver();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1eea6313d6040174a9a024a67b6bf9a1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -46,32 +46,6 @@ namespace RootMotion.FinalIK
|
||||
GuessHandOrientations(references, true);
|
||||
}
|
||||
|
||||
public void SetToReferences(FullBodyInverseKinematics_RND.References references)
|
||||
{
|
||||
if (!references.isFilled)
|
||||
{
|
||||
Debug.LogError("Invalid references, one or more Transforms are missing.");
|
||||
return;
|
||||
}
|
||||
|
||||
animator = references.root.GetComponent<Animator>();
|
||||
|
||||
solverTransforms = references.GetTransforms();
|
||||
|
||||
hasChest = solverTransforms[3] != null;
|
||||
hasNeck = solverTransforms[4] != null;
|
||||
hasShoulders = solverTransforms[6] != null && solverTransforms[10] != null;
|
||||
hasToes = solverTransforms[17] != null && solverTransforms[21] != null;
|
||||
hasLegs = solverTransforms[14] != null;
|
||||
hasArms = solverTransforms[7] != null;
|
||||
|
||||
readPositions = new Vector3[solverTransforms.Length];
|
||||
readRotations = new Quaternion[solverTransforms.Length];
|
||||
|
||||
DefaultAnimationCurves();
|
||||
GuessHandOrientations_RND(references, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Guesses the hand bones orientations ('Wrist To Palm Axis' and "Palm To Thumb Axis" of the arms) based on the provided references. if onlyIfZero is true, will only guess an orientation axis if it is Vector3.zero.
|
||||
/// </summary>
|
||||
@ -104,35 +78,6 @@ namespace RootMotion.FinalIK
|
||||
}
|
||||
}
|
||||
|
||||
public void GuessHandOrientations_RND(FullBodyInverseKinematics_RND.References references, bool onlyIfZero)
|
||||
{
|
||||
if (!references.isFilled)
|
||||
{
|
||||
Debug.LogError("VRIK References are not filled in, can not guess hand orientations. Right-click on VRIK header and slect 'Guess Hand Orientations' when you have filled in the References.", references.root);
|
||||
return;
|
||||
}
|
||||
|
||||
if (leftArm.wristToPalmAxis == Vector3.zero || !onlyIfZero)
|
||||
{
|
||||
leftArm.wristToPalmAxis = VRIKCalibrator.GuessWristToPalmAxis(references.leftHand, references.leftForearm);
|
||||
}
|
||||
|
||||
if (leftArm.palmToThumbAxis == Vector3.zero || !onlyIfZero)
|
||||
{
|
||||
leftArm.palmToThumbAxis = VRIKCalibrator.GuessPalmToThumbAxis(references.leftHand, references.leftForearm);
|
||||
}
|
||||
|
||||
if (rightArm.wristToPalmAxis == Vector3.zero || !onlyIfZero)
|
||||
{
|
||||
rightArm.wristToPalmAxis = VRIKCalibrator.GuessWristToPalmAxis(references.rightHand, references.rightForearm);
|
||||
}
|
||||
|
||||
if (rightArm.palmToThumbAxis == Vector3.zero || !onlyIfZero)
|
||||
{
|
||||
rightArm.palmToThumbAxis = VRIKCalibrator.GuessPalmToThumbAxis(references.rightHand, references.rightForearm);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set default values for the animation curves if they have no keys.
|
||||
/// </summary>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -18,12 +18,6 @@ namespace KindRetargeting
|
||||
private SerializedProperty hipsOffsetZProp;
|
||||
private SerializedProperty debugAxisNormalizerProp;
|
||||
private SerializedProperty fingerCopyModeProp;
|
||||
private SerializedProperty useMotionFilterProp;
|
||||
private SerializedProperty filterBufferSizeProp;
|
||||
private SerializedProperty useBodyRoughMotionProp;
|
||||
private SerializedProperty useFingerRoughMotionProp;
|
||||
private SerializedProperty bodyRoughnessProp;
|
||||
private SerializedProperty fingerRoughnessProp;
|
||||
private SerializedProperty kneeInOutWeightProp;
|
||||
private SerializedProperty kneeFrontBackWeightProp;
|
||||
private SerializedProperty footFrontBackOffsetProp;
|
||||
@ -32,7 +26,6 @@ namespace KindRetargeting
|
||||
private SerializedProperty avatarScaleProp;
|
||||
|
||||
// Dynamic UI
|
||||
private VisualElement calibrationContainer;
|
||||
private Label cacheStatusLabel;
|
||||
|
||||
protected override void OnEnable()
|
||||
@ -45,12 +38,6 @@ namespace KindRetargeting
|
||||
hipsOffsetZProp = serializedObject.FindProperty("hipsOffsetZ");
|
||||
debugAxisNormalizerProp = serializedObject.FindProperty("debugAxisNormalizer");
|
||||
fingerCopyModeProp = serializedObject.FindProperty("fingerCopyMode");
|
||||
useMotionFilterProp = serializedObject.FindProperty("useMotionFilter");
|
||||
filterBufferSizeProp = serializedObject.FindProperty("filterBufferSize");
|
||||
useBodyRoughMotionProp = serializedObject.FindProperty("useBodyRoughMotion");
|
||||
useFingerRoughMotionProp = serializedObject.FindProperty("useFingerRoughMotion");
|
||||
bodyRoughnessProp = serializedObject.FindProperty("bodyRoughness");
|
||||
fingerRoughnessProp = serializedObject.FindProperty("fingerRoughness");
|
||||
kneeInOutWeightProp = serializedObject.FindProperty("kneeInOutWeight");
|
||||
kneeFrontBackWeightProp = serializedObject.FindProperty("kneeFrontBackWeight");
|
||||
footFrontBackOffsetProp = serializedObject.FindProperty("footFrontBackOffset");
|
||||
@ -99,17 +86,15 @@ namespace KindRetargeting
|
||||
// 손가락 복제 설정
|
||||
root.Add(BuildFingerCopySection());
|
||||
|
||||
// 모션 필터링 설정
|
||||
root.Add(BuildMotionFilterSection());
|
||||
|
||||
// 러프 모션 설정
|
||||
root.Add(BuildRoughMotionSection());
|
||||
|
||||
// 바닥 높이 조정
|
||||
var floorFoldout = new Foldout { text = "바닥 높이 조정", value = false };
|
||||
floorFoldout.Add(new PropertyField(floorHeightProp, "바닥 높이 (-1 ~ 1)"));
|
||||
|
||||
root.Add(floorFoldout);
|
||||
|
||||
// 접지 설정 (FootGroundingController)
|
||||
root.Add(BuildGroundingSection());
|
||||
|
||||
// 캐시 상태 + 캘리브레이션 버튼
|
||||
root.Add(BuildCacheSection());
|
||||
|
||||
@ -165,6 +150,24 @@ namespace KindRetargeting
|
||||
foldout.Add(new PropertyField(hipsOffsetZProp, "앞뒤 오프셋 (←-/+→)") { tooltip = "캐릭터 기준 뒤(-) / 앞(+)" });
|
||||
foldout.Add(new HelpBox("로컬 좌표계 기반: 캐릭터의 회전 상태와 관계없이 항상 캐릭터 기준으로 이동합니다.", HelpBoxMessageType.Info));
|
||||
|
||||
// 다리 길이 자동 보정 버튼
|
||||
var autoHipsBtn = new Button(() =>
|
||||
{
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
Debug.LogWarning("다리 길이 자동 보정은 플레이 모드에서만 사용 가능합니다.");
|
||||
return;
|
||||
}
|
||||
var script = (CustomRetargetingScript)target;
|
||||
float offset = CalculateHipsOffsetFromLegDifference(script);
|
||||
hipsOffsetYProp.floatValue = offset;
|
||||
serializedObject.ApplyModifiedProperties();
|
||||
script.SaveSettings();
|
||||
Debug.Log($"자동 보정 완료: hipsOffsetY = {offset:F4}");
|
||||
}) { text = "다리 길이 자동 보정", tooltip = "소스/타겟 다리 길이 차이로 힙 상하 오프셋을 자동 계산합니다. (플레이 모드 전용)" };
|
||||
autoHipsBtn.style.marginTop = 4;
|
||||
foldout.Add(autoHipsBtn);
|
||||
|
||||
return foldout;
|
||||
}
|
||||
|
||||
@ -172,145 +175,54 @@ namespace KindRetargeting
|
||||
{
|
||||
var foldout = new Foldout { text = "손가락 복제 설정" };
|
||||
foldout.Add(new PropertyField(fingerCopyModeProp, "복제 방식") { tooltip = "손가락 포즈를 복제하는 방식을 선택합니다." });
|
||||
return foldout;
|
||||
}
|
||||
|
||||
// Mingle 모드 캘리브레이션 컨테이너
|
||||
calibrationContainer = new VisualElement();
|
||||
calibrationContainer.style.backgroundColor = new Color(0, 0, 0, 0.1f);
|
||||
calibrationContainer.style.borderTopLeftRadius = calibrationContainer.style.borderTopRightRadius =
|
||||
calibrationContainer.style.borderBottomLeftRadius = calibrationContainer.style.borderBottomRightRadius = 4;
|
||||
calibrationContainer.style.paddingTop = calibrationContainer.style.paddingBottom =
|
||||
calibrationContainer.style.paddingLeft = calibrationContainer.style.paddingRight = 6;
|
||||
calibrationContainer.style.marginTop = 4;
|
||||
private VisualElement BuildGroundingSection()
|
||||
{
|
||||
var foldout = new Foldout { text = "접지 설정 (FootGrounding)", value = false };
|
||||
|
||||
calibrationContainer.Add(new Label("Mingle 캘리브레이션") { style = { unityFontStyleAndWeight = FontStyle.Bold } });
|
||||
calibrationContainer.Add(new HelpBox(
|
||||
"Mingle 모드는 소스 아바타의 손가락 회전 범위를 캘리브레이션하여 타겟에 적용합니다.\n" +
|
||||
"1. 손가락을 완전히 펼친 상태에서 '펼침 기록' 클릭\n" +
|
||||
"2. 손가락을 완전히 모은(주먹) 상태에서 '모음 기록' 클릭",
|
||||
foldout.Add(new HelpBox(
|
||||
"HIK 스타일 2-Pass 접지 시스템:\n" +
|
||||
"• Pre-IK: IK 타겟을 조정하여 발이 바닥을 뚫지 않도록 보정\n" +
|
||||
"• Post-IK: Foot 회전으로 Toes 접지 잔차 미세 보정\n" +
|
||||
"• Toe Pivot: 발끝 고정 + 발목 회전 자동 감지",
|
||||
HelpBoxMessageType.Info));
|
||||
|
||||
// 수동 캘리브레이션 버튼
|
||||
var manualRow = new VisualElement { style = { flexDirection = FlexDirection.Row, marginTop = 4 } };
|
||||
var openBtn = new Button(() => ((CustomRetargetingScript)target).CalibrateMingleOpen()) { text = "펼침 기록 (Open)" };
|
||||
openBtn.style.flexGrow = 1; openBtn.style.marginRight = 2;
|
||||
manualRow.Add(openBtn);
|
||||
var closeBtn = new Button(() => ((CustomRetargetingScript)target).CalibrateMingleClose()) { text = "모음 기록 (Close)" };
|
||||
closeBtn.style.flexGrow = 1;
|
||||
manualRow.Add(closeBtn);
|
||||
calibrationContainer.Add(manualRow);
|
||||
|
||||
// 자동 캘리브레이션 버튼
|
||||
var autoBtn = new Button(() => ((CustomRetargetingScript)target).StartAutoCalibration())
|
||||
{ text = "자동 캘리브레이션 (3초 펼침 → 3초 모음)" };
|
||||
autoBtn.style.marginTop = 4;
|
||||
calibrationContainer.Add(autoBtn);
|
||||
|
||||
// 플레이 모드 경고
|
||||
var playWarning = new HelpBox("캘리브레이션은 플레이 모드에서만 가능합니다.", HelpBoxMessageType.Warning);
|
||||
calibrationContainer.Add(playWarning);
|
||||
|
||||
// 자동 캘리브레이션 진행 상태
|
||||
var autoCalibStatus = new VisualElement();
|
||||
autoCalibStatus.style.backgroundColor = new Color(0, 0, 0, 0.15f);
|
||||
autoCalibStatus.style.borderTopLeftRadius = autoCalibStatus.style.borderTopRightRadius =
|
||||
autoCalibStatus.style.borderBottomLeftRadius = autoCalibStatus.style.borderBottomRightRadius = 4;
|
||||
autoCalibStatus.style.paddingTop = autoCalibStatus.style.paddingBottom =
|
||||
autoCalibStatus.style.paddingLeft = autoCalibStatus.style.paddingRight = 4;
|
||||
autoCalibStatus.style.marginTop = 4;
|
||||
var statusLabel = new Label("자동 캘리브레이션 진행 중");
|
||||
statusLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||
autoCalibStatus.Add(statusLabel);
|
||||
var statusDetailLabel = new Label();
|
||||
autoCalibStatus.Add(statusDetailLabel);
|
||||
var timeLabel = new Label();
|
||||
autoCalibStatus.Add(timeLabel);
|
||||
var cancelBtn = new Button(() => ((CustomRetargetingScript)target).StopAutoCalibration()) { text = "취소" };
|
||||
autoCalibStatus.Add(cancelBtn);
|
||||
calibrationContainer.Add(autoCalibStatus);
|
||||
|
||||
foldout.Add(calibrationContainer);
|
||||
|
||||
// 주기적으로 Mingle 모드 표시/숨김 + 캘리브레이션 상태 갱신
|
||||
foldout.schedule.Execute(() =>
|
||||
{
|
||||
if (target == null) return;
|
||||
serializedObject.Update();
|
||||
bool isMingle = fingerCopyModeProp.enumValueIndex == (int)EnumsList.FingerCopyMode.Mingle;
|
||||
calibrationContainer.style.display = isMingle ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
|
||||
if (isMingle)
|
||||
{
|
||||
bool isPlaying = Application.isPlaying;
|
||||
// FootGroundingController의 SerializedObject를 직접 바인딩
|
||||
var script = (CustomRetargetingScript)target;
|
||||
bool isAutoCalib = isPlaying && script.IsAutoCalibrating;
|
||||
|
||||
openBtn.SetEnabled(isPlaying && !isAutoCalib);
|
||||
closeBtn.SetEnabled(isPlaying && !isAutoCalib);
|
||||
autoBtn.SetEnabled(isPlaying && !isAutoCalib);
|
||||
playWarning.style.display = isPlaying ? DisplayStyle.None : DisplayStyle.Flex;
|
||||
|
||||
manualRow.style.display = isAutoCalib ? DisplayStyle.None : DisplayStyle.Flex;
|
||||
autoBtn.style.display = isAutoCalib ? DisplayStyle.None : DisplayStyle.Flex;
|
||||
autoCalibStatus.style.display = isAutoCalib ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
|
||||
if (isAutoCalib)
|
||||
var grounding = script.GetComponent<FootGroundingController>();
|
||||
if (grounding != null)
|
||||
{
|
||||
statusDetailLabel.text = $"상태: {script.AutoCalibrationStatus}";
|
||||
timeLabel.text = $"남은 시간: {script.AutoCalibrationTimeRemaining:F1}초";
|
||||
var groundingSO = new SerializedObject(grounding);
|
||||
|
||||
var groundHeightField = new PropertyField(groundingSO.FindProperty("groundHeight"), "바닥 높이");
|
||||
foldout.Add(groundHeightField);
|
||||
|
||||
var weightSlider = new Slider("접지 강도", 0f, 1f) { showInputField = true };
|
||||
weightSlider.BindProperty(groundingSO.FindProperty("groundingWeight"));
|
||||
foldout.Add(weightSlider);
|
||||
|
||||
var activationField = new PropertyField(groundingSO.FindProperty("activationHeight"), "활성화 높이");
|
||||
activationField.tooltip = "발목이 이 높이 이상이면 접지 보정 비활성화 (점프 등)";
|
||||
foldout.Add(activationField);
|
||||
|
||||
var thresholdField = new PropertyField(groundingSO.FindProperty("plantThreshold"), "접지 판정 범위");
|
||||
thresholdField.tooltip = "Toes가 이 범위 안이면 접지 중으로 판정";
|
||||
foldout.Add(thresholdField);
|
||||
|
||||
var smoothField = new PropertyField(groundingSO.FindProperty("smoothSpeed"), "보정 스무딩 속도");
|
||||
smoothField.tooltip = "보정량 변화 속도 (높을수록 빠른 반응, 낮으면 부드러운 전환)";
|
||||
foldout.Add(smoothField);
|
||||
|
||||
foldout.Add(new HelpBox("힙 높이 보정은 '바닥 높이 조정' 섹션의 floorHeight로 제어합니다.", HelpBoxMessageType.Info));
|
||||
|
||||
foldout.TrackSerializedObjectValue(groundingSO, so => so.ApplyModifiedProperties());
|
||||
}
|
||||
else
|
||||
{
|
||||
foldout.Add(new HelpBox("FootGroundingController 컴포넌트를 찾을 수 없습니다.", HelpBoxMessageType.Warning));
|
||||
}
|
||||
}).Every(200);
|
||||
|
||||
// 초기 상태
|
||||
bool initMingle = fingerCopyModeProp.enumValueIndex == (int)EnumsList.FingerCopyMode.Mingle;
|
||||
calibrationContainer.style.display = initMingle ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
autoCalibStatus.style.display = DisplayStyle.None;
|
||||
|
||||
return foldout;
|
||||
}
|
||||
|
||||
private VisualElement BuildMotionFilterSection()
|
||||
{
|
||||
var foldout = new Foldout { text = "모션 필터링 설정" };
|
||||
foldout.Add(new PropertyField(useMotionFilterProp, "모션 필터 사용") { tooltip = "모션 필터링을 적용할지 여부를 설정합니다." });
|
||||
|
||||
var bufferField = new PropertyField(filterBufferSizeProp, "필터 버퍼 크기") { tooltip = "모션 필터링에 사용할 버퍼의 크기를 설정합니다. (2-10)" };
|
||||
foldout.Add(bufferField);
|
||||
|
||||
foldout.TrackPropertyValue(useMotionFilterProp, prop =>
|
||||
{
|
||||
bufferField.style.display = prop.boolValue ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
});
|
||||
bufferField.style.display = useMotionFilterProp.boolValue ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
|
||||
return foldout;
|
||||
}
|
||||
|
||||
private VisualElement BuildRoughMotionSection()
|
||||
{
|
||||
var foldout = new Foldout { text = "러프 모션 설정" };
|
||||
|
||||
// 몸
|
||||
foldout.Add(new PropertyField(useBodyRoughMotionProp, "몸 러프 모션 사용") { tooltip = "몸의 러프한 움직임을 적용할지 여부를 설정합니다." });
|
||||
var bodyRoughField = new PropertyField(bodyRoughnessProp, "몸 러프니스") { tooltip = "몸 전체의 러프한 정도 (0: 없음, 1: 최대)" };
|
||||
foldout.Add(bodyRoughField);
|
||||
|
||||
foldout.TrackPropertyValue(useBodyRoughMotionProp, prop =>
|
||||
{
|
||||
bodyRoughField.style.display = prop.boolValue ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
});
|
||||
bodyRoughField.style.display = useBodyRoughMotionProp.boolValue ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
|
||||
// 손가락
|
||||
foldout.Add(new PropertyField(useFingerRoughMotionProp, "손가락 러프 모션 사용") { tooltip = "손가락의 러프한 움직임을 적용할지 여부를 설정합니다." });
|
||||
var fingerRoughField = new PropertyField(fingerRoughnessProp, "손가락 러프니스") { tooltip = "손가락의 러프한 정도 (0: 없음, 1: 최대)" };
|
||||
foldout.Add(fingerRoughField);
|
||||
|
||||
foldout.TrackPropertyValue(useFingerRoughMotionProp, prop =>
|
||||
{
|
||||
fingerRoughField.style.display = prop.boolValue ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
});
|
||||
fingerRoughField.style.display = useFingerRoughMotionProp.boolValue ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
|
||||
return foldout;
|
||||
}
|
||||
@ -373,5 +285,50 @@ namespace KindRetargeting
|
||||
"캘리브레이션 데이터가 저장되어 있습니다." :
|
||||
"저장된 캘리브레이션 데이터가 없습니다.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 소스/타겟 다리 길이 차이로 힙 상하 오프셋을 계산합니다.
|
||||
/// 소스 다리가 더 길면 → 음수 (힙을 내려서 타겟이 뜨지 않게)
|
||||
/// 소스 다리가 더 짧으면 → 양수 (힙을 올려서 타겟 다리를 펴줌)
|
||||
/// </summary>
|
||||
private float CalculateHipsOffsetFromLegDifference(CustomRetargetingScript script)
|
||||
{
|
||||
Animator source = script.sourceAnimator;
|
||||
Animator target = script.targetAnimator;
|
||||
|
||||
if (source == null || target == null || !source.isHuman || !target.isHuman)
|
||||
{
|
||||
Debug.LogWarning("소스/타겟 Animator가 없거나 Humanoid가 아닙니다.");
|
||||
return 0f;
|
||||
}
|
||||
|
||||
float sourceLeg = GetLegLength(source);
|
||||
float targetLeg = GetLegLength(target);
|
||||
|
||||
if (sourceLeg < 0.01f || targetLeg < 0.01f)
|
||||
{
|
||||
Debug.LogWarning("다리 길이를 계산할 수 없습니다. 본이 올바르게 설정되어 있는지 확인해주세요.");
|
||||
return 0f;
|
||||
}
|
||||
|
||||
// 소스 다리가 더 길면 타겟이 뜨므로 힙을 내려야 함 (음수)
|
||||
// 소스 다리가 더 짧으면 타겟 다리가 구부러지므로 힙을 올려야 함 (양수)
|
||||
float diff = targetLeg - sourceLeg;
|
||||
Debug.Log($"소스 다리 길이: {sourceLeg:F4}, 타겟 다리 길이: {targetLeg:F4}, 힙 오프셋: {diff:F4}m");
|
||||
return diff;
|
||||
}
|
||||
|
||||
private float GetLegLength(Animator animator)
|
||||
{
|
||||
Transform upperLeg = animator.GetBoneTransform(HumanBodyBones.LeftUpperLeg);
|
||||
Transform lowerLeg = animator.GetBoneTransform(HumanBodyBones.LeftLowerLeg);
|
||||
Transform foot = animator.GetBoneTransform(HumanBodyBones.LeftFoot);
|
||||
|
||||
if (upperLeg == null || lowerLeg == null || foot == null) return 0f;
|
||||
|
||||
float upper = Vector3.Distance(upperLeg.position, lowerLeg.position);
|
||||
float lower = Vector3.Distance(lowerLeg.position, foot.position);
|
||||
return upper + lower;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,111 +0,0 @@
|
||||
using UnityEngine;
|
||||
using UnityEditor;
|
||||
using UnityEngine.UIElements;
|
||||
using UnityEditor.UIElements;
|
||||
using KindRetargeting;
|
||||
|
||||
[CustomEditor(typeof(FullBodyInverseKinematics))]
|
||||
public class FullBodyInverseKinematicsEditor : BaseRetargetingEditor
|
||||
{
|
||||
private const string CommonUssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/StreamingleCommon.uss";
|
||||
|
||||
private SerializedProperty iterationsProp;
|
||||
private SerializedProperty deltaThresholdProp;
|
||||
private SerializedProperty[] limbProps;
|
||||
private readonly string[] limbNames = { "왼팔", "오른팔", "왼다리", "오른다리" };
|
||||
private readonly string[] limbPropNames = { "leftArm", "rightArm", "leftLeg", "rightLeg" };
|
||||
|
||||
protected override void OnEnable()
|
||||
{
|
||||
base.OnEnable();
|
||||
iterationsProp = serializedObject.FindProperty("iterations");
|
||||
deltaThresholdProp = serializedObject.FindProperty("deltaThreshold");
|
||||
|
||||
limbProps = new SerializedProperty[4];
|
||||
for (int i = 0; i < 4; i++)
|
||||
limbProps[i] = serializedObject.FindProperty(limbPropNames[i]);
|
||||
}
|
||||
|
||||
public override VisualElement CreateInspectorGUI()
|
||||
{
|
||||
var root = new VisualElement();
|
||||
|
||||
var commonUss = AssetDatabase.LoadAssetAtPath<StyleSheet>(CommonUssPath);
|
||||
if (commonUss != null) root.styleSheets.Add(commonUss);
|
||||
|
||||
// IK 기본 설정
|
||||
var basicFoldout = new Foldout { text = "IK 기본 설정", value = true };
|
||||
basicFoldout.Add(new PropertyField(iterationsProp, "반복 횟수"));
|
||||
basicFoldout.Add(new PropertyField(deltaThresholdProp, "델타 임계값"));
|
||||
root.Add(basicFoldout);
|
||||
|
||||
// 사지 설정
|
||||
var limbFoldout = new Foldout { text = "사지 설정" };
|
||||
for (int i = 0; i < 4; i++)
|
||||
limbFoldout.Add(BuildLimbElement(i));
|
||||
root.Add(limbFoldout);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
private VisualElement BuildLimbElement(int index)
|
||||
{
|
||||
var container = new VisualElement();
|
||||
container.style.backgroundColor = new Color(0, 0, 0, 0.1f);
|
||||
container.style.borderTopLeftRadius = container.style.borderTopRightRadius =
|
||||
container.style.borderBottomLeftRadius = container.style.borderBottomRightRadius = 4;
|
||||
container.style.paddingTop = container.style.paddingBottom =
|
||||
container.style.paddingLeft = container.style.paddingRight = 4;
|
||||
container.style.marginBottom = 4;
|
||||
|
||||
if (index >= limbProps.Length || limbProps[index] == null)
|
||||
{
|
||||
container.Add(new HelpBox($"'{limbPropNames[index]}' 프로퍼티를 찾을 수 없습니다.", HelpBoxMessageType.Warning));
|
||||
return container;
|
||||
}
|
||||
|
||||
var foldout = new Foldout { text = limbNames[index] };
|
||||
|
||||
var enableIKProp = limbProps[index].FindPropertyRelative("enableIK");
|
||||
if (enableIKProp == null) { container.Add(foldout); return container; }
|
||||
foldout.Add(new PropertyField(enableIKProp, "IK 활성화"));
|
||||
|
||||
var ikSettings = new VisualElement { style = { marginTop = 4 } };
|
||||
|
||||
// 엔드 타겟
|
||||
var endTargetProp = limbProps[index].FindPropertyRelative("endTarget");
|
||||
if (endTargetProp != null)
|
||||
{
|
||||
ikSettings.Add(new PropertyField(endTargetProp.FindPropertyRelative("target"), "엔드 타겟"));
|
||||
ikSettings.Add(new PropertyField(endTargetProp.FindPropertyRelative("weight"), "엔드 가중치"));
|
||||
}
|
||||
|
||||
// 미들 타겟
|
||||
var middleTargetProp = limbProps[index].FindPropertyRelative("middleTarget");
|
||||
if (middleTargetProp != null)
|
||||
{
|
||||
ikSettings.Add(new PropertyField(middleTargetProp.FindPropertyRelative("target"), "미들 타겟"));
|
||||
ikSettings.Add(new PropertyField(middleTargetProp.FindPropertyRelative("weight"), "미들 가중치"));
|
||||
}
|
||||
|
||||
// 발목 타겟
|
||||
var ankleTargetProp = limbProps[index].FindPropertyRelative("ankleTarget");
|
||||
if (ankleTargetProp != null)
|
||||
{
|
||||
ikSettings.Add(new PropertyField(ankleTargetProp.FindPropertyRelative("target"), "발목 타겟"));
|
||||
ikSettings.Add(new PropertyField(ankleTargetProp.FindPropertyRelative("weight"), "발목 가중치"));
|
||||
}
|
||||
|
||||
foldout.Add(ikSettings);
|
||||
|
||||
// enableIK 토글에 따라 설정 표시/숨김
|
||||
foldout.TrackPropertyValue(enableIKProp, prop =>
|
||||
{
|
||||
ikSettings.style.display = prop.boolValue ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
});
|
||||
ikSettings.style.display = enableIKProp.boolValue ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
|
||||
container.Add(foldout);
|
||||
return container;
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cd91806c0fba3d34eae0081fd0a3de9b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -232,9 +232,6 @@ public class RetargetingControlWindow : EditorWindow
|
||||
// 손가락 복제 설정
|
||||
panel.Add(BuildFingerCopySection(script, so));
|
||||
|
||||
// 모션 설정
|
||||
panel.Add(BuildMotionSection(so));
|
||||
|
||||
// 바닥 높이 설정
|
||||
var floorFoldout = new Foldout { text = "바닥 높이 설정", value = false };
|
||||
var floorField = new PropertyField(so.FindProperty("floorHeight"), "바닥 높이 (-1 ~ 1)");
|
||||
@ -260,7 +257,7 @@ public class RetargetingControlWindow : EditorWindow
|
||||
panel.Add(BuildPropSection(script));
|
||||
|
||||
// 캘리브레이션
|
||||
panel.Add(BuildCalibrationSection(script));
|
||||
panel.Add(BuildCalibrationSection(script, so));
|
||||
|
||||
// 변경 감지
|
||||
panel.TrackSerializedObjectValue(so, _ =>
|
||||
@ -376,6 +373,25 @@ public class RetargetingControlWindow : EditorWindow
|
||||
chairSlider.Bind(limbSO);
|
||||
}
|
||||
|
||||
// 다리 길이 자동 보정 버튼
|
||||
var autoHipsBtn = new Button(() =>
|
||||
{
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
Debug.LogWarning("다리 길이 자동 보정은 플레이 모드에서만 사용 가능합니다.");
|
||||
return;
|
||||
}
|
||||
float offset = CalculateHipsOffsetFromLegDifference(script);
|
||||
var hipsProp = so.FindProperty("hipsOffsetY");
|
||||
hipsProp.floatValue = offset;
|
||||
so.ApplyModifiedProperties();
|
||||
script.SaveSettings();
|
||||
Debug.Log($"자동 보정 완료: hipsOffsetY = {offset:F4}");
|
||||
}) { text = "다리 길이 자동 보정", tooltip = "소스/타겟 다리 길이 차이로 힙 상하 오프셋을 자동 계산합니다. (플레이 모드 전용)" };
|
||||
autoHipsBtn.style.marginTop = 4;
|
||||
autoHipsBtn.style.height = 25;
|
||||
container.Add(autoHipsBtn);
|
||||
|
||||
container.Bind(so);
|
||||
foldout.Add(container);
|
||||
return foldout;
|
||||
@ -523,110 +539,6 @@ public class RetargetingControlWindow : EditorWindow
|
||||
var copyModeProp = so.FindProperty("fingerCopyMode");
|
||||
container.Add(new PropertyField(copyModeProp, "복제 방식"));
|
||||
|
||||
// Mingle 캘리브레이션 컨테이너
|
||||
var mingleBox = new VisualElement();
|
||||
mingleBox.style.backgroundColor = new Color(0, 0, 0, 0.1f);
|
||||
mingleBox.style.borderTopLeftRadius = mingleBox.style.borderTopRightRadius =
|
||||
mingleBox.style.borderBottomLeftRadius = mingleBox.style.borderBottomRightRadius = 4;
|
||||
mingleBox.style.paddingTop = mingleBox.style.paddingBottom =
|
||||
mingleBox.style.paddingLeft = mingleBox.style.paddingRight = 6;
|
||||
mingleBox.style.marginTop = 4;
|
||||
|
||||
mingleBox.Add(new Label("Mingle 캘리브레이션") { style = { unityFontStyleAndWeight = FontStyle.Bold } });
|
||||
mingleBox.Add(new HelpBox(
|
||||
"1. 손가락을 완전히 펼친 상태에서 '펼침 기록' 클릭\n2. 손가락을 완전히 모은(주먹) 상태에서 '모음 기록' 클릭",
|
||||
HelpBoxMessageType.Info));
|
||||
|
||||
var manualRow = new VisualElement { style = { flexDirection = FlexDirection.Row, marginTop = 4 } };
|
||||
var openBtn = new Button(() => script.CalibrateMingleOpen()) { text = "펼침 기록" };
|
||||
openBtn.style.flexGrow = 1; openBtn.style.marginRight = 2;
|
||||
manualRow.Add(openBtn);
|
||||
var closeBtn = new Button(() => script.CalibrateMingleClose()) { text = "모음 기록" };
|
||||
closeBtn.style.flexGrow = 1;
|
||||
manualRow.Add(closeBtn);
|
||||
mingleBox.Add(manualRow);
|
||||
|
||||
var autoBtn = new Button(() => script.StartAutoCalibration()) { text = "자동 캘리브레이션 (3초 펼침 → 3초 모음)" };
|
||||
autoBtn.style.marginTop = 4;
|
||||
mingleBox.Add(autoBtn);
|
||||
|
||||
var playWarning = new HelpBox("캘리브레이션은 플레이 모드에서만 가능합니다.", HelpBoxMessageType.Warning);
|
||||
mingleBox.Add(playWarning);
|
||||
|
||||
// 자동 캘리브레이션 상태
|
||||
var autoStatus = new VisualElement();
|
||||
autoStatus.style.display = DisplayStyle.None;
|
||||
var statusLabel = new Label();
|
||||
var timeLabel = new Label();
|
||||
var cancelBtn = new Button(() => script.StopAutoCalibration()) { text = "취소" };
|
||||
autoStatus.Add(statusLabel);
|
||||
autoStatus.Add(timeLabel);
|
||||
autoStatus.Add(cancelBtn);
|
||||
mingleBox.Add(autoStatus);
|
||||
|
||||
container.Add(mingleBox);
|
||||
|
||||
// 주기적 갱신
|
||||
container.schedule.Execute(() =>
|
||||
{
|
||||
if (so == null || so.targetObject == null || script == null) return;
|
||||
so.Update();
|
||||
bool isMingle = so.FindProperty("fingerCopyMode").enumValueIndex == (int)FingerCopyMode.Mingle;
|
||||
mingleBox.style.display = isMingle ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
if (!isMingle) return;
|
||||
|
||||
bool playing = Application.isPlaying;
|
||||
bool autoCalib = playing && script.IsAutoCalibrating;
|
||||
|
||||
openBtn.SetEnabled(playing && !autoCalib);
|
||||
closeBtn.SetEnabled(playing && !autoCalib);
|
||||
autoBtn.SetEnabled(playing && !autoCalib);
|
||||
playWarning.style.display = playing ? DisplayStyle.None : DisplayStyle.Flex;
|
||||
manualRow.style.display = autoCalib ? DisplayStyle.None : DisplayStyle.Flex;
|
||||
autoBtn.style.display = autoCalib ? DisplayStyle.None : DisplayStyle.Flex;
|
||||
autoStatus.style.display = autoCalib ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
|
||||
if (autoCalib)
|
||||
{
|
||||
statusLabel.text = $"상태: {script.AutoCalibrationStatus}";
|
||||
timeLabel.text = $"남은 시간: {script.AutoCalibrationTimeRemaining:F1}초";
|
||||
}
|
||||
}).Every(200);
|
||||
|
||||
container.Bind(so);
|
||||
foldout.Add(container);
|
||||
return foldout;
|
||||
}
|
||||
|
||||
// ========== Motion Settings ==========
|
||||
|
||||
private VisualElement BuildMotionSection(SerializedObject so)
|
||||
{
|
||||
var foldout = new Foldout { text = "모션 설정", value = false };
|
||||
var container = new VisualElement();
|
||||
|
||||
var useFilterProp = so.FindProperty("useMotionFilter");
|
||||
container.Add(new PropertyField(useFilterProp, "모션 필터 사용"));
|
||||
var bufferField = new PropertyField(so.FindProperty("filterBufferSize"), "필터 버퍼 크기");
|
||||
container.Add(bufferField);
|
||||
|
||||
var useBodyRoughProp = so.FindProperty("useBodyRoughMotion");
|
||||
container.Add(new PropertyField(useBodyRoughProp, "몸 러프 모션 사용"));
|
||||
var bodyRoughField = new PropertyField(so.FindProperty("bodyRoughness"), "몸 러프니스");
|
||||
container.Add(bodyRoughField);
|
||||
|
||||
// 조건부 표시
|
||||
container.schedule.Execute(() =>
|
||||
{
|
||||
if (so == null || so.targetObject == null) return;
|
||||
so.Update();
|
||||
bufferField.style.display = so.FindProperty("useMotionFilter").boolValue ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
bodyRoughField.style.display = so.FindProperty("useBodyRoughMotion").boolValue ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
}).Every(300);
|
||||
|
||||
bufferField.style.display = useFilterProp.boolValue ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
bodyRoughField.style.display = useBodyRoughProp.boolValue ? DisplayStyle.Flex : DisplayStyle.None;
|
||||
|
||||
container.Bind(so);
|
||||
foldout.Add(container);
|
||||
return foldout;
|
||||
@ -812,7 +724,7 @@ public class RetargetingControlWindow : EditorWindow
|
||||
|
||||
// ========== Calibration ==========
|
||||
|
||||
private VisualElement BuildCalibrationSection(CustomRetargetingScript script)
|
||||
private VisualElement BuildCalibrationSection(CustomRetargetingScript script, SerializedObject so)
|
||||
{
|
||||
var box = new VisualElement { style = { marginTop = 8 } };
|
||||
box.style.backgroundColor = new Color(0, 0, 0, 0.1f);
|
||||
@ -837,6 +749,20 @@ public class RetargetingControlWindow : EditorWindow
|
||||
btnRow.Add(resetBtn);
|
||||
box.Add(btnRow);
|
||||
|
||||
// 전체 자동 보정 버튼
|
||||
var autoBtn = new Button(() =>
|
||||
{
|
||||
if (!Application.isPlaying)
|
||||
{
|
||||
Debug.LogWarning("전체 자동 보정은 플레이 모드에서만 사용 가능합니다.");
|
||||
return;
|
||||
}
|
||||
AutoCalibrateAll(script, so);
|
||||
}) { text = "전체 자동 보정 (크기 + 힙 높이)", tooltip = "소스/타겟 목 높이 비율로 아바타 크기를 맞추고, 다리 길이 차이로 힙 높이를 자동 보정합니다. (플레이 모드 전용)" };
|
||||
autoBtn.style.marginTop = 4;
|
||||
autoBtn.style.height = 28;
|
||||
box.Add(autoBtn);
|
||||
|
||||
void UpdateCacheLabel()
|
||||
{
|
||||
if (script == null) return;
|
||||
@ -947,6 +873,123 @@ public class RetargetingControlWindow : EditorWindow
|
||||
|
||||
// ========== Head Calibration ==========
|
||||
|
||||
// ========== Auto Full Calibration ==========
|
||||
|
||||
/// <summary>
|
||||
/// 소스/타겟 목 높이 비율로 avatarScale을 맞추고, 다리 길이 차이로 hipsOffsetY를 보정합니다.
|
||||
/// </summary>
|
||||
private void AutoCalibrateAll(CustomRetargetingScript script, SerializedObject so)
|
||||
{
|
||||
Animator source = script.sourceAnimator;
|
||||
Animator target = script.targetAnimator;
|
||||
|
||||
if (source == null || target == null || !source.isHuman || !target.isHuman)
|
||||
{
|
||||
Debug.LogWarning("소스/타겟 Animator가 없거나 Humanoid가 아닙니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 아바타 크기를 1로 리셋 (즉시 적용)
|
||||
script.ResetScale();
|
||||
var scaleProp = so.FindProperty("avatarScale");
|
||||
scaleProp.floatValue = 1f;
|
||||
so.ApplyModifiedProperties();
|
||||
|
||||
// 2. 다리 길이 자동 보정 (스케일 1 상태)
|
||||
float hipsOffset0 = CalculateHipsOffsetFromLegDifference(script);
|
||||
so.FindProperty("hipsOffsetY").floatValue = hipsOffset0;
|
||||
so.ApplyModifiedProperties();
|
||||
|
||||
// 3. 목 높이 측정
|
||||
Transform sourceNeck = source.GetBoneTransform(HumanBodyBones.Neck);
|
||||
Transform targetNeck = target.GetBoneTransform(HumanBodyBones.Neck);
|
||||
|
||||
if (sourceNeck == null || targetNeck == null)
|
||||
{
|
||||
Debug.LogWarning("목 본을 찾을 수 없습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
float sourceNeckY = sourceNeck.position.y;
|
||||
float targetNeckY = targetNeck.position.y;
|
||||
|
||||
if (targetNeckY < 0.01f)
|
||||
{
|
||||
Debug.LogWarning("타겟 목 높이가 0에 가깝습니다.");
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 소스/타겟 비율 → avatarScale
|
||||
float scaleRatio = Mathf.Clamp(sourceNeckY / targetNeckY, 0.1f, 3f);
|
||||
script.SetAvatarScale(scaleRatio);
|
||||
scaleProp.floatValue = scaleRatio;
|
||||
so.ApplyModifiedProperties();
|
||||
|
||||
Debug.Log($"크기 보정: 소스 목 Y={sourceNeckY:F4}, 타겟 목 Y={targetNeckY:F4} → avatarScale = {scaleRatio:F3}");
|
||||
|
||||
// 5. 스케일 적용 후 다리 길이 자동 보정
|
||||
EditorApplication.delayCall += () =>
|
||||
{
|
||||
if (script == null) return;
|
||||
|
||||
// delayCall 시점에서 캡처된 so가 Dispose되었을 수 있으므로 새로 생성
|
||||
var freshSo = new SerializedObject(script);
|
||||
float hipsOffset = CalculateHipsOffsetFromLegDifference(script);
|
||||
freshSo.FindProperty("hipsOffsetY").floatValue = hipsOffset;
|
||||
freshSo.ApplyModifiedProperties();
|
||||
freshSo.Dispose();
|
||||
|
||||
script.SaveSettings();
|
||||
Debug.Log($"전체 자동 보정 완료: avatarScale = {scaleRatio:F3}, hipsOffsetY = {hipsOffset:F4}m");
|
||||
};
|
||||
}
|
||||
|
||||
// ========== Auto Hips Offset ==========
|
||||
|
||||
/// <summary>
|
||||
/// 소스/타겟 다리 길이 차이로 힙 상하 오프셋을 계산합니다.
|
||||
/// 타겟 다리가 소스보다 짧으면 → 양수 (힙을 올려서 다리를 펴줌)
|
||||
/// 타겟 다리가 소스보다 길면 → 음수 (힙을 내려서 다리를 펴줌)
|
||||
/// </summary>
|
||||
private float CalculateHipsOffsetFromLegDifference(CustomRetargetingScript script)
|
||||
{
|
||||
Animator source = script.sourceAnimator;
|
||||
Animator target = script.targetAnimator;
|
||||
|
||||
if (source == null || target == null || !source.isHuman || !target.isHuman)
|
||||
{
|
||||
Debug.LogWarning("소스/타겟 Animator가 없거나 Humanoid가 아닙니다.");
|
||||
return 0f;
|
||||
}
|
||||
|
||||
float sourceLeg = GetLegLength(source);
|
||||
float targetLeg = GetLegLength(target);
|
||||
|
||||
if (sourceLeg < 0.01f || targetLeg < 0.01f)
|
||||
{
|
||||
Debug.LogWarning("다리 길이를 계산할 수 없습니다. 본이 올바르게 설정되어 있는지 확인해주세요.");
|
||||
return 0f;
|
||||
}
|
||||
|
||||
// 소스 다리가 더 길면 타겟이 뜨므로 힙을 내려야 함 (음수)
|
||||
// 소스 다리가 더 짧으면 타겟 다리가 구부러지므로 힙을 올려야 함 (양수)
|
||||
float diff = targetLeg - sourceLeg;
|
||||
Debug.Log($"소스 다리 길이: {sourceLeg:F4}, 타겟 다리 길이: {targetLeg:F4}, 힙 오프셋: {diff:F4}m");
|
||||
return diff;
|
||||
}
|
||||
|
||||
private float GetLegLength(Animator animator)
|
||||
{
|
||||
Transform upperLeg = animator.GetBoneTransform(HumanBodyBones.LeftUpperLeg);
|
||||
Transform lowerLeg = animator.GetBoneTransform(HumanBodyBones.LeftLowerLeg);
|
||||
Transform foot = animator.GetBoneTransform(HumanBodyBones.LeftFoot);
|
||||
|
||||
if (upperLeg == null || lowerLeg == null || foot == null) return 0f;
|
||||
|
||||
return Vector3.Distance(upperLeg.position, lowerLeg.position)
|
||||
+ Vector3.Distance(lowerLeg.position, foot.position);
|
||||
}
|
||||
|
||||
private void CalibrateHeadToForward(SerializedObject so, SerializedProperty xProp, SerializedProperty yProp, SerializedProperty zProp)
|
||||
{
|
||||
CustomRetargetingScript script = so.targetObject as CustomRetargetingScript;
|
||||
|
||||
@ -15,39 +15,6 @@ namespace KindRetargeting.EnumsList
|
||||
|
||||
[Tooltip("Transform의 rotation 값을 직접 복제합니다")]
|
||||
Rotation, // 회전값 기반 복제
|
||||
|
||||
[Tooltip("소스 아바타의 손가락 회전 범위를 캘리브레이션하여 머슬에 매핑합니다")]
|
||||
Mingle, // 캘리브레이션 기반 머슬 매핑
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 모션 필터링 방식을 정의하는 열거형
|
||||
/// </summary>
|
||||
public enum MotionFilterMode
|
||||
{
|
||||
[Tooltip("필터링을 적용하지 않습니다")]
|
||||
None,
|
||||
|
||||
[Tooltip("평균 기반 필터링을 적용합니다")]
|
||||
Average,
|
||||
|
||||
[Tooltip("가중치 기반 필터링을 적용합니다")]
|
||||
Weighted
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 러프 모션 적용 방식을 정의하는 열거형
|
||||
/// </summary>
|
||||
public enum RoughMotionMode
|
||||
{
|
||||
[Tooltip("러프 모션을 적용하지 않습니다")]
|
||||
None,
|
||||
|
||||
[Tooltip("선형 보간을 사용한 러프 모션을 적용합니다")]
|
||||
Linear,
|
||||
|
||||
[Tooltip("스프링 시스템을 사용한 러프 모션을 적용합니다")]
|
||||
Spring
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
266
Assets/Scripts/KindRetargeting/FootGroundingController.cs
Normal file
266
Assets/Scripts/KindRetargeting/FootGroundingController.cs
Normal file
@ -0,0 +1,266 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace KindRetargeting
|
||||
{
|
||||
/// <summary>
|
||||
/// HIK 스타일 2-Pass 접지 시스템.
|
||||
///
|
||||
/// Pass 1 (Update, Order 5 → IK 전):
|
||||
/// IK 타겟 위치를 수정하여 Toes가 바닥을 뚫지 않도록 보정.
|
||||
/// Toe Pivot 감지: 발끝이 바닥에 있고 발목이 올라가면
|
||||
/// 발목 타겟을 역산하여 Toes가 groundHeight에 고정.
|
||||
///
|
||||
/// Pass 2 (LateUpdate → IK 후):
|
||||
/// IK 결과의 잔차를 Foot 회전으로 미세 보정.
|
||||
/// 위치 변경 없음 — 본 길이 보존.
|
||||
///
|
||||
/// 힙 높이 보정은 CRS의 floorHeight가 담당합니다 (이중 보정 방지).
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(5)]
|
||||
public class FootGroundingController : MonoBehaviour
|
||||
{
|
||||
[Header("접지 설정")]
|
||||
[Tooltip("바닥 Y 좌표 (월드 공간)")]
|
||||
public float groundHeight = 0f;
|
||||
|
||||
[Tooltip("접지 보정 강도")]
|
||||
[Range(0f, 1f)]
|
||||
public float groundingWeight = 1f;
|
||||
|
||||
[Tooltip("이 높이 이상이면 AIRBORNE (보정 안 함)")]
|
||||
public float activationHeight = 0.5f;
|
||||
|
||||
[Tooltip("Toes가 이 범위 안이면 접지 중으로 판정")]
|
||||
public float plantThreshold = 0.02f;
|
||||
|
||||
[Header("스무딩")]
|
||||
[Tooltip("보정량 변화 속도 (높을수록 빠른 반응)")]
|
||||
[Range(1f, 30f)]
|
||||
public float smoothSpeed = 10f;
|
||||
|
||||
private TwoBoneIKSolver ikSolver;
|
||||
private Animator animator;
|
||||
|
||||
// 타겟 아바타 캐싱
|
||||
private Transform leftFoot;
|
||||
private Transform rightFoot;
|
||||
private Transform leftToes;
|
||||
private Transform rightToes;
|
||||
|
||||
// Toes의 Foot 로컬 오프셋 (T-pose에서 캐싱)
|
||||
private Vector3 leftLocalToesOffset;
|
||||
private Vector3 rightLocalToesOffset;
|
||||
|
||||
// flat 상태에서 발목 최소 높이 (Foot.y - Toes.y)
|
||||
private float leftFootHeight;
|
||||
private float rightFootHeight;
|
||||
|
||||
// Toes 본 존재 여부
|
||||
private bool leftHasToes;
|
||||
private bool rightHasToes;
|
||||
|
||||
// 스무딩용: 이전 프레임 보정량
|
||||
private float leftPrevAdj;
|
||||
private float rightPrevAdj;
|
||||
|
||||
private bool isInitialized;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
ikSolver = GetComponent<TwoBoneIKSolver>();
|
||||
animator = GetComponent<Animator>();
|
||||
|
||||
if (animator == null || !animator.isHuman || ikSolver == null) return;
|
||||
if (leftFoot == null && rightFoot == null) return;
|
||||
|
||||
leftFoot = animator.GetBoneTransform(HumanBodyBones.LeftFoot);
|
||||
rightFoot = animator.GetBoneTransform(HumanBodyBones.RightFoot);
|
||||
leftToes = animator.GetBoneTransform(HumanBodyBones.LeftToes);
|
||||
rightToes = animator.GetBoneTransform(HumanBodyBones.RightToes);
|
||||
|
||||
if (leftFoot == null || rightFoot == null) return;
|
||||
|
||||
// Toes 존재 여부 + 캐싱
|
||||
leftHasToes = leftToes != null;
|
||||
rightHasToes = rightToes != null;
|
||||
|
||||
if (leftHasToes)
|
||||
{
|
||||
leftLocalToesOffset = leftFoot.InverseTransformPoint(leftToes.position);
|
||||
leftFootHeight = Mathf.Abs(leftFoot.position.y - leftToes.position.y);
|
||||
}
|
||||
else
|
||||
{
|
||||
leftFootHeight = 0.05f; // Toes 없을 때 기본 발목 높이
|
||||
}
|
||||
|
||||
if (rightHasToes)
|
||||
{
|
||||
rightLocalToesOffset = rightFoot.InverseTransformPoint(rightToes.position);
|
||||
rightFootHeight = Mathf.Abs(rightFoot.position.y - rightToes.position.y);
|
||||
}
|
||||
else
|
||||
{
|
||||
rightFootHeight = 0.05f;
|
||||
}
|
||||
|
||||
isInitialized = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pass 1: Pre-IK — IK 타겟 위치를 수정합니다.
|
||||
/// </summary>
|
||||
private void Update()
|
||||
{
|
||||
if (!isInitialized || groundingWeight < 0.001f) return;
|
||||
|
||||
float leftAdj = AdjustFootTarget(
|
||||
ikSolver.leftLeg, leftLocalToesOffset, leftFootHeight,
|
||||
leftHasToes, ikSolver.leftLeg.positionWeight);
|
||||
float rightAdj = AdjustFootTarget(
|
||||
ikSolver.rightLeg, rightLocalToesOffset, rightFootHeight,
|
||||
rightHasToes, ikSolver.rightLeg.positionWeight);
|
||||
|
||||
// 스무딩: 보정량 급변 방지
|
||||
float dt = Time.deltaTime * smoothSpeed;
|
||||
leftPrevAdj = Mathf.Lerp(leftPrevAdj, leftAdj, Mathf.Clamp01(dt));
|
||||
rightPrevAdj = Mathf.Lerp(rightPrevAdj, rightAdj, Mathf.Clamp01(dt));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 발 IK 타겟을 접지 모드에 따라 보정합니다.
|
||||
/// Toes가 없는 아바타는 발목 Y 클램프만 수행합니다.
|
||||
/// </summary>
|
||||
private float AdjustFootTarget(TwoBoneIKSolver.LimbIK limb, Vector3 localToesOffset,
|
||||
float footHeight, bool hasToes, float ikWeight)
|
||||
{
|
||||
if (limb.target == null || ikWeight < 0.01f) return 0f;
|
||||
|
||||
Transform ankleTarget = limb.target;
|
||||
Vector3 anklePos = ankleTarget.position;
|
||||
float ankleY = anklePos.y;
|
||||
|
||||
// AIRBORNE 체크
|
||||
if (ankleY - groundHeight > activationHeight) return 0f;
|
||||
|
||||
float weight = groundingWeight * ikWeight;
|
||||
|
||||
// === Toes 없는 아바타: 단순 Y 클램프 ===
|
||||
if (!hasToes)
|
||||
{
|
||||
float minAnkleY = groundHeight + footHeight;
|
||||
if (ankleY < minAnkleY)
|
||||
{
|
||||
float adj = (minAnkleY - ankleY) * weight;
|
||||
anklePos.y += adj;
|
||||
ankleTarget.position = anklePos;
|
||||
return adj;
|
||||
}
|
||||
return 0f;
|
||||
}
|
||||
|
||||
// === Toes 있는 아바타: 예측 기반 보정 ===
|
||||
Vector3 predictedToesWorld = anklePos + ankleTarget.rotation * localToesOffset;
|
||||
float predictedToesY = predictedToesWorld.y;
|
||||
|
||||
float adjustment = 0f;
|
||||
|
||||
if (predictedToesY < groundHeight + plantThreshold)
|
||||
{
|
||||
float toesError = groundHeight - predictedToesY;
|
||||
|
||||
if (ankleY < groundHeight + footHeight + plantThreshold)
|
||||
{
|
||||
// PLANTED: 발 전체가 바닥 근처
|
||||
float minAnkleY = groundHeight + footHeight;
|
||||
if (ankleY < minAnkleY)
|
||||
{
|
||||
adjustment = (minAnkleY - ankleY) * weight;
|
||||
anklePos.y += adjustment;
|
||||
}
|
||||
|
||||
if (toesError > 0f)
|
||||
{
|
||||
float extra = toesError * weight;
|
||||
anklePos.y += extra;
|
||||
adjustment += extra;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// TOE_PIVOT: 발끝 고정, 발목 올라감
|
||||
if (toesError > 0f)
|
||||
{
|
||||
adjustment = toesError * weight;
|
||||
anklePos.y += adjustment;
|
||||
}
|
||||
}
|
||||
|
||||
ankleTarget.position = anklePos;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Toes 충분히 위 → 발목만 바닥 아래 방지
|
||||
float minAnkleY = groundHeight + footHeight;
|
||||
if (ankleY < minAnkleY)
|
||||
{
|
||||
adjustment = (minAnkleY - ankleY) * weight;
|
||||
anklePos.y += adjustment;
|
||||
ankleTarget.position = anklePos;
|
||||
}
|
||||
}
|
||||
|
||||
return adjustment;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pass 2: Post-IK — 잔차를 Foot 회전으로 미세 보정합니다.
|
||||
/// </summary>
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (!isInitialized || groundingWeight < 0.001f) return;
|
||||
|
||||
if (leftHasToes)
|
||||
AlignFootToGround(leftFoot, leftToes, ikSolver.leftLeg.positionWeight);
|
||||
if (rightHasToes)
|
||||
AlignFootToGround(rightFoot, rightToes, ikSolver.rightLeg.positionWeight);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// IK 후 실제 Toes 위치를 확인하고, Foot 본을 pitch 회전하여 잔차 보정.
|
||||
/// 바닥 아래로 뚫린 경우만 보정합니다.
|
||||
/// </summary>
|
||||
private void AlignFootToGround(Transform foot, Transform toes, float ikWeight)
|
||||
{
|
||||
if (foot == null || toes == null) return;
|
||||
if (ikWeight < 0.01f) return;
|
||||
|
||||
if (foot.position.y - groundHeight > activationHeight) return;
|
||||
|
||||
float weight = groundingWeight * ikWeight;
|
||||
|
||||
float actualToesY = toes.position.y;
|
||||
float error = actualToesY - groundHeight;
|
||||
|
||||
if (Mathf.Abs(error) < 0.001f) return;
|
||||
|
||||
// 바닥 아래로 뚫린 경우만 보정
|
||||
if (error > plantThreshold) return;
|
||||
|
||||
Vector3 footToToes = toes.position - foot.position;
|
||||
float horizontalDist = new Vector2(footToToes.x, footToToes.z).magnitude;
|
||||
|
||||
if (horizontalDist < 0.001f) return;
|
||||
|
||||
float pitchAngle = Mathf.Atan2(error, horizontalDist) * Mathf.Rad2Deg;
|
||||
|
||||
Vector3 footForwardFlat = new Vector3(footToToes.x, 0f, footToToes.z).normalized;
|
||||
Vector3 pitchAxis = Vector3.Cross(Vector3.up, footForwardFlat);
|
||||
if (pitchAxis.sqrMagnitude < 0.001f) return;
|
||||
pitchAxis.Normalize();
|
||||
|
||||
Quaternion correction = Quaternion.AngleAxis(-pitchAngle, pitchAxis);
|
||||
foot.rotation = Quaternion.Slerp(foot.rotation, correction * foot.rotation, weight);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1752f1fe3cee0b24e95ae0d5a0468865
|
||||
@ -1,277 +0,0 @@
|
||||
using UnityEngine;
|
||||
using System.Linq;
|
||||
|
||||
namespace KindRetargeting
|
||||
{
|
||||
[DefaultExecutionOrder(3)]
|
||||
public class FullBodyInverseKinematics : MonoBehaviour
|
||||
{
|
||||
[System.Serializable]
|
||||
public class IKTarget
|
||||
{
|
||||
public Transform target;
|
||||
[Range(0, 1)] public float weight = 1f;
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class LimbIK
|
||||
{
|
||||
public bool enableIK = true; // IK 활성화 여부
|
||||
public IKTarget endTarget; // 손/발 타겟
|
||||
public IKTarget middleTarget; // 팔꿈치/무릎 타겟
|
||||
public IKTarget ankleTarget; // 발목 타겟 (다리용)
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class FootIK : LimbIK
|
||||
{
|
||||
[Range(0, 1)] public float footRotationWeight = 1f; // 발 회전 가중치만 남김
|
||||
}
|
||||
|
||||
[HideInInspector] public Animator animator;
|
||||
|
||||
[Header("팔 타겟")]
|
||||
public LimbIK leftArm = new LimbIK();
|
||||
public LimbIK rightArm = new LimbIK();
|
||||
|
||||
[Header("다리 타겟")]
|
||||
public LimbIK leftLeg = new LimbIK();
|
||||
public LimbIK rightLeg = new LimbIK();
|
||||
|
||||
[Header("IK 설정")]
|
||||
[Range(1, 20)] public int iterations = 10;
|
||||
public float deltaThreshold = 0.001f;
|
||||
|
||||
private Transform[][] limbs;
|
||||
private Vector3[][] positions;
|
||||
private float[][] boneLengths;
|
||||
private Vector3[][] initialDirections;
|
||||
private Quaternion[][] initialRotations;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (!ValidateAnimator()) return;
|
||||
|
||||
animator = GetComponent<Animator>();
|
||||
|
||||
// 팔과 다리의 본 체인 초기화
|
||||
limbs = new Transform[][]
|
||||
{
|
||||
GetBoneChain(HumanBodyBones.LeftUpperArm, HumanBodyBones.LeftLowerArm, HumanBodyBones.LeftHand),
|
||||
GetBoneChain(HumanBodyBones.RightUpperArm, HumanBodyBones.RightLowerArm, HumanBodyBones.RightHand),
|
||||
GetBoneChain(HumanBodyBones.LeftUpperLeg, HumanBodyBones.LeftLowerLeg, HumanBodyBones.LeftFoot, HumanBodyBones.LeftToes),
|
||||
GetBoneChain(HumanBodyBones.RightUpperLeg, HumanBodyBones.RightLowerLeg, HumanBodyBones.RightFoot, HumanBodyBones.RightToes)
|
||||
};
|
||||
|
||||
InitializeArrays();
|
||||
}
|
||||
|
||||
private bool ValidateAnimator()
|
||||
{
|
||||
if (animator == null || !animator.avatar || !animator.avatar.isHuman)
|
||||
{
|
||||
Debug.LogError("유효한 Humanoid Animator가 필요합니다.");
|
||||
enabled = false;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private Transform[] GetBoneChain(params HumanBodyBones[] bones)
|
||||
{
|
||||
return bones.Select(bone => animator.GetBoneTransform(bone)).ToArray();
|
||||
}
|
||||
|
||||
private void InitializeArrays()
|
||||
{
|
||||
positions = new Vector3[limbs.Length][];
|
||||
boneLengths = new float[limbs.Length][];
|
||||
initialDirections = new Vector3[limbs.Length][];
|
||||
initialRotations = new Quaternion[limbs.Length][];
|
||||
|
||||
for (int j = 0; j < limbs.Length; j++)
|
||||
{
|
||||
int chainLength = limbs[j].Length;
|
||||
positions[j] = new Vector3[chainLength];
|
||||
boneLengths[j] = new float[chainLength - 1];
|
||||
initialDirections[j] = new Vector3[chainLength];
|
||||
initialRotations[j] = new Quaternion[chainLength];
|
||||
|
||||
// 본 길이와 초기 방향 저장
|
||||
for (int i = 0; i < chainLength - 1; i++)
|
||||
{
|
||||
Vector3 boneVector = limbs[j][i + 1].position - limbs[j][i].position;
|
||||
Vector3 animatorScale = animator.transform.lossyScale;
|
||||
float averageScale = (animatorScale.x + animatorScale.y + animatorScale.z) / 3f;
|
||||
boneLengths[j][i] = boneVector.magnitude / averageScale;
|
||||
initialDirections[j][i] = limbs[j][i].InverseTransformDirection(
|
||||
(limbs[j][i + 1].position - limbs[j][i].position).normalized
|
||||
);
|
||||
initialRotations[j][i] = limbs[j][i].rotation;
|
||||
}
|
||||
initialRotations[j][chainLength - 1] = limbs[j][chainLength - 1].rotation;
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!ValidateAnimator()) return;
|
||||
|
||||
// IK 계산 및 적용
|
||||
SolveIK();
|
||||
}
|
||||
|
||||
private void SolveIK()
|
||||
{
|
||||
if (animator == null) return;
|
||||
|
||||
LimbIK[] limbTargets = { leftArm, rightArm, leftLeg, rightLeg };
|
||||
|
||||
for (int j = 0; j < limbs.Length; j++)
|
||||
{
|
||||
// IK가 비활성화되어 있으면 해당 팔다리는 처리하지 않고 스킵
|
||||
if (!limbTargets[j].enableIK) continue;
|
||||
|
||||
// 타겟이 없으면 스킵
|
||||
if (limbTargets[j].endTarget.target == null) continue;
|
||||
|
||||
// 현재 위치 저장
|
||||
for (int i = 0; i < limbs[j].Length; i++)
|
||||
positions[j][i] = limbs[j][i].position;
|
||||
|
||||
bool isArm = j < 2;
|
||||
SolveIK(j, limbTargets[j], isArm);
|
||||
ApplyRotations(j, limbTargets[j]);
|
||||
}
|
||||
}
|
||||
|
||||
private void SolveIK(int chainIndex, LimbIK limbIK, bool isArm)
|
||||
{
|
||||
// 현재 애니메이터 스케일 계산
|
||||
Vector3 animatorScale = animator.transform.lossyScale;
|
||||
float averageScale = (animatorScale.x + animatorScale.y + animatorScale.z) / 3f;
|
||||
|
||||
// 중간 관절 처리
|
||||
if (limbIK.middleTarget.target != null && limbIK.middleTarget.weight > 0)
|
||||
{
|
||||
if (isArm)
|
||||
ProcessArmElbow(chainIndex, limbIK.middleTarget);
|
||||
else
|
||||
ProcessLegKnee(chainIndex, limbIK.middleTarget);
|
||||
}
|
||||
|
||||
// 발목 타겟 처리 (다리인 경우에만)
|
||||
if (!isArm && limbIK is FootIK footIK && footIK.ankleTarget.target != null && footIK.ankleTarget.weight > 0)
|
||||
{
|
||||
ProcessAnkle(chainIndex, footIK.ankleTarget);
|
||||
}
|
||||
|
||||
// FABRIK 반복
|
||||
for (int iteration = 0; iteration < iterations; iteration++)
|
||||
{
|
||||
// Forward
|
||||
positions[chainIndex][positions[chainIndex].Length - 1] = Vector3.Lerp(
|
||||
positions[chainIndex][positions[chainIndex].Length - 1],
|
||||
limbIK.endTarget.target.position,
|
||||
limbIK.endTarget.weight
|
||||
);
|
||||
|
||||
for (int i = positions[chainIndex].Length - 2; i >= 0; i--)
|
||||
{
|
||||
Vector3 direction = (positions[chainIndex][i] - positions[chainIndex][i + 1]).normalized;
|
||||
positions[chainIndex][i] = positions[chainIndex][i + 1] + direction * (boneLengths[chainIndex][i] * averageScale);
|
||||
}
|
||||
|
||||
// Backward
|
||||
positions[chainIndex][0] = limbs[chainIndex][0].position;
|
||||
for (int i = 1; i < positions[chainIndex].Length; i++)
|
||||
{
|
||||
Vector3 direction = (positions[chainIndex][i] - positions[chainIndex][i - 1]).normalized;
|
||||
positions[chainIndex][i] = positions[chainIndex][i - 1] + direction * (boneLengths[chainIndex][i - 1] * averageScale);
|
||||
}
|
||||
|
||||
if ((positions[chainIndex][positions[chainIndex].Length - 1] - limbIK.endTarget.target.position).sqrMagnitude
|
||||
< deltaThreshold * deltaThreshold)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessArmElbow(int chainIndex, IKTarget middleTarget)
|
||||
{
|
||||
if (middleTarget?.target == null || middleTarget.weight <= 0) return;
|
||||
|
||||
// 팔 상완(Upper Arm)을 기준으로 계산
|
||||
Vector3 upperArmPos = positions[chainIndex][0]; // 상완 시작점 (이전의 1에서 0으로 변경)
|
||||
Vector3 elbowPos = positions[chainIndex][1]; // 팔꿈치 위치 (이전의 2에서 1로 변경)
|
||||
Vector3 handPos = positions[chainIndex][2]; // 손 위치 (이전의 3에서 2로 변경)
|
||||
|
||||
// 팔꿈치 타겟 방향 계산
|
||||
Vector3 currentElbowDir = (elbowPos - upperArmPos).normalized;
|
||||
Vector3 targetElbowDir = (middleTarget.target.position - upperArmPos).normalized;
|
||||
|
||||
// 팔꿈치 회전 계산
|
||||
Quaternion elbowRotation = Quaternion.FromToRotation(currentElbowDir, targetElbowDir);
|
||||
float elbowLength = Vector3.Distance(upperArmPos, elbowPos);
|
||||
|
||||
// 새로운 팔꿈치 위치 계산
|
||||
Vector3 newElbowPos = upperArmPos + (elbowRotation * currentElbowDir) * elbowLength;
|
||||
|
||||
// 가중치를 적용하여 팔꿈치 위치 보간
|
||||
positions[chainIndex][1] = Vector3.Lerp(elbowPos, newElbowPos, middleTarget.weight);
|
||||
}
|
||||
|
||||
private void ProcessLegKnee(int chainIndex, IKTarget middleTarget)
|
||||
{
|
||||
Vector3 rootPos = positions[chainIndex][0];
|
||||
Vector3 middlePos = positions[chainIndex][1];
|
||||
|
||||
Vector3 currentDirection = (middlePos - rootPos).normalized;
|
||||
Vector3 targetDirection = (middleTarget.target.position - rootPos).normalized;
|
||||
|
||||
Quaternion middleRotation = Quaternion.FromToRotation(currentDirection, targetDirection);
|
||||
positions[chainIndex][1] = rootPos +
|
||||
Vector3.Lerp(currentDirection, middleRotation * currentDirection, middleTarget.weight) *
|
||||
Vector3.Distance(rootPos, middlePos);
|
||||
}
|
||||
|
||||
private void ProcessAnkle(int chainIndex, IKTarget ankleTarget)
|
||||
{
|
||||
if (ankleTarget?.target == null || ankleTarget.weight <= 0) return;
|
||||
|
||||
Vector3 anklePos = positions[chainIndex][2]; // 발목 위치
|
||||
Vector3 newAnklePos = Vector3.Lerp(anklePos, ankleTarget.target.position, ankleTarget.weight);
|
||||
positions[chainIndex][2] = newAnklePos;
|
||||
}
|
||||
|
||||
private void ApplyRotations(int chainIndex, LimbIK limbIK)
|
||||
{
|
||||
// 기본 회전 로직만 남김
|
||||
for (int i = 0; i < limbs[chainIndex].Length - 1; i++)
|
||||
{
|
||||
ApplyBasicRotation(chainIndex, i);
|
||||
}
|
||||
|
||||
// 마지막 본(손/발)의 회전
|
||||
var lastBone = limbs[chainIndex].Length - 1;
|
||||
limbs[chainIndex][lastBone].position = positions[chainIndex][lastBone];
|
||||
limbs[chainIndex][lastBone].rotation = Quaternion.Lerp(
|
||||
limbs[chainIndex][lastBone].rotation,
|
||||
limbIK.endTarget.target.rotation,
|
||||
limbIK.endTarget.weight
|
||||
);
|
||||
}
|
||||
|
||||
private void ApplyBasicRotation(int chainIndex, int boneIndex)
|
||||
{
|
||||
limbs[chainIndex][boneIndex].position = positions[chainIndex][boneIndex];
|
||||
|
||||
Vector3 toNext = (positions[chainIndex][boneIndex + 1] - positions[chainIndex][boneIndex]).normalized;
|
||||
Quaternion fromToRotation = Quaternion.FromToRotation(
|
||||
limbs[chainIndex][boneIndex].TransformDirection(initialDirections[chainIndex][boneIndex]),
|
||||
toNext
|
||||
);
|
||||
|
||||
limbs[chainIndex][boneIndex].rotation = fromToRotation * limbs[chainIndex][boneIndex].rotation;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 321bd2b7036a9ee4ca0edf6bb929cc00
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@ -1,8 +1,6 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
using RootMotion.FinalIK;
|
||||
|
||||
namespace KindRetargeting
|
||||
{
|
||||
@ -37,7 +35,7 @@ namespace KindRetargeting
|
||||
[SerializeField, Range(-1f, 1f)]
|
||||
public float chairSeatHeightOffset = 0.05f;
|
||||
|
||||
private FullBodyInverseKinematics_RND fbik;
|
||||
private TwoBoneIKSolver ikSolver;
|
||||
|
||||
private CustomRetargetingScript crs;
|
||||
private Dictionary<string, Dictionary<int, float>> weightLayers = new Dictionary<string, Dictionary<int, float>>();
|
||||
@ -98,7 +96,7 @@ namespace KindRetargeting
|
||||
|
||||
void Start()
|
||||
{
|
||||
fbik = GetComponent<FullBodyInverseKinematics_RND>();
|
||||
ikSolver = GetComponent<TwoBoneIKSolver>();
|
||||
|
||||
crs = GetComponent<CustomRetargetingScript>();
|
||||
|
||||
@ -178,12 +176,12 @@ namespace KindRetargeting
|
||||
|
||||
private void InitWeightLayers()
|
||||
{
|
||||
fbik.solver.leftArm.positionWeight = 0f;
|
||||
fbik.solver.leftArm.rotationWeight = 0f;
|
||||
fbik.solver.leftArm.bendGoalWeight = 0f;
|
||||
fbik.solver.rightArm.positionWeight = 0f;
|
||||
fbik.solver.rightArm.rotationWeight = 0f;
|
||||
fbik.solver.rightArm.bendGoalWeight = 0f;
|
||||
ikSolver.leftArm.positionWeight = 0f;
|
||||
ikSolver.leftArm.rotationWeight = 0f;
|
||||
ikSolver.leftArm.bendGoalWeight = 0f;
|
||||
ikSolver.rightArm.positionWeight = 0f;
|
||||
ikSolver.rightArm.rotationWeight = 0f;
|
||||
ikSolver.rightArm.bendGoalWeight = 0f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -191,10 +189,10 @@ namespace KindRetargeting
|
||||
/// </summary>
|
||||
private void HandDistances()
|
||||
{
|
||||
if (fbik == null || crs == null) return;
|
||||
if (ikSolver == null || crs == null) return;
|
||||
|
||||
// 왼쪽 팔 가중치 업데이트
|
||||
if (fbik.solver.leftArm.target != null && fbik.solver.rightArm.target != null)
|
||||
if (ikSolver.leftArm.target != null && ikSolver.rightArm.target != null)
|
||||
{
|
||||
Transform leftHandTransform = crs?.sourceAnimator?.GetBoneTransform(HumanBodyBones.LeftHand);
|
||||
Transform rightHandTransform = crs?.sourceAnimator?.GetBoneTransform(HumanBodyBones.RightHand);
|
||||
@ -217,7 +215,7 @@ namespace KindRetargeting
|
||||
|
||||
void SitLegDistances()
|
||||
{
|
||||
if (fbik == null || crs == null || characterRoot == null) return;
|
||||
if (ikSolver == null || crs == null || characterRoot == null) return;
|
||||
|
||||
Transform hips = crs?.sourceAnimator?.GetBoneTransform(HumanBodyBones.Hips);
|
||||
if (hips == null) return;
|
||||
@ -234,10 +232,10 @@ namespace KindRetargeting
|
||||
if (hipHeight <= groundHipsMaxHeight)
|
||||
{
|
||||
// 왼쪽 다리 처리
|
||||
ProcessSitLegWeight(hips, fbik.solver.leftLeg.target, leftLegEndWeights, 0);
|
||||
ProcessSitLegWeight(hips, ikSolver.leftLeg.target, leftLegEndWeights, 0);
|
||||
|
||||
// 오른쪽 다리 처리
|
||||
ProcessSitLegWeight(hips, fbik.solver.rightLeg.target, rightLegEndWeights, 0);
|
||||
ProcessSitLegWeight(hips, ikSolver.rightLeg.target, rightLegEndWeights, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@ -265,7 +263,7 @@ namespace KindRetargeting
|
||||
|
||||
void PropDistances()
|
||||
{
|
||||
if (fbik == null || crs == null) return;
|
||||
if (ikSolver == null || crs == null) return;
|
||||
|
||||
// 프랍과의 거리에 따른 웨이트 업데이트
|
||||
Transform leftHandTransform = crs?.sourceAnimator?.GetBoneTransform(HumanBodyBones.LeftHand);
|
||||
@ -441,65 +439,33 @@ namespace KindRetargeting
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 마스터 가중치 값을 FBIK에 적용합니다.
|
||||
/// 마스터 가중치 값을 IK 솔버에 적용합니다.
|
||||
/// </summary>
|
||||
private void ApplyWeightsToFBIK()
|
||||
{
|
||||
if (fbik == null) return;
|
||||
if (ikSolver == null) return;
|
||||
|
||||
// 왼쪽 팔 가중치 적용
|
||||
if (fbik.solver.leftArm != null)
|
||||
{
|
||||
if (fbik.solver.leftArm.target != null)
|
||||
{
|
||||
float finalLeftArmWeight = enableLeftArmIK ? MasterleftArmEndWeights : 0f;
|
||||
fbik.solver.leftArm.positionWeight = finalLeftArmWeight;
|
||||
fbik.solver.leftArm.rotationWeight = finalLeftArmWeight;
|
||||
}
|
||||
|
||||
if (fbik.solver.leftArm.bendGoal != null)
|
||||
fbik.solver.leftArm.bendGoalWeight = enableLeftArmIK ? MasterleftArmEndWeights : 0f;
|
||||
}
|
||||
ikSolver.leftArm.positionWeight = finalLeftArmWeight;
|
||||
ikSolver.leftArm.rotationWeight = finalLeftArmWeight;
|
||||
ikSolver.leftArm.bendGoalWeight = finalLeftArmWeight;
|
||||
|
||||
// 오른쪽 팔 가중치 적용
|
||||
if (fbik.solver.rightArm != null)
|
||||
{
|
||||
if (fbik.solver.rightArm.target != null)
|
||||
{
|
||||
float finalRightArmWeight = enableRightArmIK ? MasterrightArmEndWeights : 0f;
|
||||
fbik.solver.rightArm.positionWeight = finalRightArmWeight;
|
||||
fbik.solver.rightArm.rotationWeight = finalRightArmWeight;
|
||||
}
|
||||
|
||||
if (fbik.solver.rightArm.bendGoal != null)
|
||||
fbik.solver.rightArm.bendGoalWeight = enableRightArmIK ? MasterrightArmEndWeights : 0f;
|
||||
}
|
||||
ikSolver.rightArm.positionWeight = finalRightArmWeight;
|
||||
ikSolver.rightArm.rotationWeight = finalRightArmWeight;
|
||||
ikSolver.rightArm.bendGoalWeight = finalRightArmWeight;
|
||||
|
||||
// 왼쪽 다리 가중치 적용
|
||||
if (fbik.solver.leftLeg != null)
|
||||
{
|
||||
if (fbik.solver.leftLeg.target != null)
|
||||
{
|
||||
fbik.solver.leftLeg.positionWeight = MasterleftLegEndWeights;
|
||||
fbik.solver.leftLeg.rotationWeight = MasterleftLegEndWeights;
|
||||
}
|
||||
|
||||
if (fbik.solver.leftLeg.bendGoal != null)
|
||||
fbik.solver.leftLeg.bendGoalWeight = MasterleftLegBendWeights; // 골 가중치 적용
|
||||
}
|
||||
ikSolver.leftLeg.positionWeight = MasterleftLegEndWeights;
|
||||
ikSolver.leftLeg.rotationWeight = MasterleftLegEndWeights;
|
||||
ikSolver.leftLeg.bendGoalWeight = MasterleftLegBendWeights;
|
||||
|
||||
// 오른쪽 다리 가중치 적용
|
||||
if (fbik.solver.rightLeg != null)
|
||||
{
|
||||
if (fbik.solver.rightLeg.target != null)
|
||||
{
|
||||
fbik.solver.rightLeg.positionWeight = MasterrightLegEndWeights;
|
||||
fbik.solver.rightLeg.rotationWeight = MasterrightLegEndWeights;
|
||||
}
|
||||
|
||||
if (fbik.solver.rightLeg.bendGoal != null)
|
||||
fbik.solver.rightLeg.bendGoalWeight = MasterrightLegBendWeights; // 골 가중치 적용
|
||||
}
|
||||
ikSolver.rightLeg.positionWeight = MasterrightLegEndWeights;
|
||||
ikSolver.rightLeg.rotationWeight = MasterrightLegEndWeights;
|
||||
ikSolver.rightLeg.bendGoalWeight = MasterrightLegBendWeights;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -507,7 +473,7 @@ namespace KindRetargeting
|
||||
/// </summary>
|
||||
private void UpdateGroundBasedHipsWeight()
|
||||
{
|
||||
if (crs == null || fbik == null || fbik == null) return;
|
||||
if (crs == null || ikSolver == null) return;
|
||||
|
||||
Transform hipsTransform = crs.sourceAnimator.GetBoneTransform(HumanBodyBones.Hips);
|
||||
if (hipsTransform != null)
|
||||
@ -525,20 +491,20 @@ namespace KindRetargeting
|
||||
/// </summary>
|
||||
private void UpdateFootHeightBasedWeight()
|
||||
{
|
||||
if (fbik == null || crs == null || characterRoot == null) return;
|
||||
if (ikSolver == null || crs == null || characterRoot == null) return;
|
||||
|
||||
// 왼발 처리
|
||||
ProcessFootHeightWeight(
|
||||
fbik.solver.leftLeg.target,
|
||||
ikSolver.leftLeg.target,
|
||||
leftLegEndWeights,
|
||||
1 // 새로 추가된 레이어의 인덱스
|
||||
1
|
||||
);
|
||||
|
||||
// 오른발 처리
|
||||
ProcessFootHeightWeight(
|
||||
fbik.solver.rightLeg.target,
|
||||
ikSolver.rightLeg.target,
|
||||
rightLegEndWeights,
|
||||
1 // 새로 추가된 레이어의 인덱스
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -157,20 +157,6 @@ namespace KindRetargeting.Remote
|
||||
}
|
||||
break;
|
||||
|
||||
case "calibrateMingleOpen":
|
||||
{
|
||||
string charId = json["characterId"]?.ToString();
|
||||
CalibrateMingleOpen(charId);
|
||||
}
|
||||
break;
|
||||
|
||||
case "calibrateMingleClose":
|
||||
{
|
||||
string charId = json["characterId"]?.ToString();
|
||||
CalibrateMingleClose(charId);
|
||||
}
|
||||
break;
|
||||
|
||||
case "calibrateHeadForward":
|
||||
{
|
||||
string charId = json["characterId"]?.ToString();
|
||||
@ -264,12 +250,6 @@ namespace KindRetargeting.Remote
|
||||
// 손가락 복제 모드
|
||||
{ "fingerCopyMode", (int)GetPrivateField<EnumsList.FingerCopyMode>(script, "fingerCopyMode") },
|
||||
|
||||
// 모션 설정
|
||||
{ "useMotionFilter", GetPrivateField<bool>(script, "useMotionFilter") },
|
||||
{ "filterBufferSize", GetPrivateField<int>(script, "filterBufferSize") },
|
||||
{ "useBodyRoughMotion", GetPrivateField<bool>(script, "useBodyRoughMotion") },
|
||||
{ "bodyRoughness", GetPrivateField<float>(script, "bodyRoughness") },
|
||||
|
||||
// 캘리브레이션 상태
|
||||
{ "hasCalibrationData", script.HasCachedSettings() }
|
||||
};
|
||||
@ -381,20 +361,6 @@ namespace KindRetargeting.Remote
|
||||
SetPrivateField(script, "fingerCopyMode", (EnumsList.FingerCopyMode)(int)value);
|
||||
break;
|
||||
|
||||
// 모션 설정
|
||||
case "useMotionFilter":
|
||||
SetPrivateField(script, "useMotionFilter", value > 0.5f);
|
||||
break;
|
||||
case "filterBufferSize":
|
||||
SetPrivateField(script, "filterBufferSize", (int)value);
|
||||
break;
|
||||
case "useBodyRoughMotion":
|
||||
SetPrivateField(script, "useBodyRoughMotion", value > 0.5f);
|
||||
break;
|
||||
case "bodyRoughness":
|
||||
SetPrivateField(script, "bodyRoughness", value);
|
||||
break;
|
||||
|
||||
// LimbWeightController 속성
|
||||
case "limbMinDistance":
|
||||
if (limbWeight != null) limbWeight.minDistance = value;
|
||||
@ -542,30 +508,6 @@ namespace KindRetargeting.Remote
|
||||
SendStatus(true, "캘리브레이션 초기화됨");
|
||||
}
|
||||
|
||||
private void CalibrateMingleOpen(string characterId)
|
||||
{
|
||||
var script = FindCharacter(characterId);
|
||||
if (script == null) return;
|
||||
|
||||
script.CalibrateMingleOpen();
|
||||
script.SaveSettings();
|
||||
|
||||
SendCharacterData(characterId);
|
||||
SendStatus(true, "Mingle 펼침 캘리브레이션 완료");
|
||||
}
|
||||
|
||||
private void CalibrateMingleClose(string characterId)
|
||||
{
|
||||
var script = FindCharacter(characterId);
|
||||
if (script == null) return;
|
||||
|
||||
script.CalibrateMingleClose();
|
||||
script.SaveSettings();
|
||||
|
||||
SendCharacterData(characterId);
|
||||
SendStatus(true, "Mingle 모음 캘리브레이션 완료");
|
||||
}
|
||||
|
||||
private void CalibrateHeadForward(string characterId)
|
||||
{
|
||||
var script = FindCharacter(characterId);
|
||||
|
||||
@ -1,97 +0,0 @@
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace KindRetargeting
|
||||
{
|
||||
/// <summary>
|
||||
/// 모션 데이터 필터링을 위한 클래스
|
||||
/// </summary>
|
||||
public class MotionFilter
|
||||
{
|
||||
private Vector3[] positionBuffer;
|
||||
private Quaternion[] rotationBuffer;
|
||||
private int bufferSize;
|
||||
private int currentIndex;
|
||||
|
||||
public MotionFilter(int bufferSize = 5)
|
||||
{
|
||||
this.bufferSize = bufferSize;
|
||||
positionBuffer = new Vector3[bufferSize];
|
||||
rotationBuffer = new Quaternion[bufferSize];
|
||||
currentIndex = 0;
|
||||
}
|
||||
|
||||
public Vector3 FilterPosition(Vector3 newPosition)
|
||||
{
|
||||
positionBuffer[currentIndex] = newPosition;
|
||||
Vector3 smoothedPosition = Vector3.zero;
|
||||
|
||||
for (int i = 0; i < bufferSize; i++)
|
||||
{
|
||||
smoothedPosition += positionBuffer[i];
|
||||
}
|
||||
|
||||
currentIndex = (currentIndex + 1) % bufferSize;
|
||||
return smoothedPosition / bufferSize;
|
||||
}
|
||||
|
||||
public Quaternion FilterRotation(Quaternion newRotation)
|
||||
{
|
||||
rotationBuffer[currentIndex] = newRotation;
|
||||
Vector4 average = Vector4.zero;
|
||||
|
||||
for (int i = 0; i < bufferSize; i++)
|
||||
{
|
||||
if (Quaternion.Dot(rotationBuffer[0], rotationBuffer[i]) < 0)
|
||||
{
|
||||
rotationBuffer[i] = new Quaternion(-rotationBuffer[i].x, -rotationBuffer[i].y,
|
||||
-rotationBuffer[i].z, -rotationBuffer[i].w);
|
||||
}
|
||||
average.x += rotationBuffer[i].x;
|
||||
average.y += rotationBuffer[i].y;
|
||||
average.z += rotationBuffer[i].z;
|
||||
average.w += rotationBuffer[i].w;
|
||||
}
|
||||
|
||||
average /= bufferSize;
|
||||
currentIndex = (currentIndex + 1) % bufferSize;
|
||||
return new Quaternion(average.x, average.y, average.z, average.w).normalized;
|
||||
}
|
||||
|
||||
public void UpdateSettings(bool useFilter, bool useRoughMotion)
|
||||
{
|
||||
// 필요한 경우 여기에 설정 업데이트 로직 추가
|
||||
}
|
||||
}
|
||||
|
||||
public class RoughMotion
|
||||
{
|
||||
private Vector3 lastPosition;
|
||||
private Quaternion lastRotation;
|
||||
private float smoothSpeed;
|
||||
|
||||
public RoughMotion(float smoothSpeed = 5f)
|
||||
{
|
||||
this.smoothSpeed = smoothSpeed;
|
||||
lastPosition = Vector3.zero;
|
||||
lastRotation = Quaternion.identity;
|
||||
}
|
||||
|
||||
public Vector3 ProcessPosition(Vector3 newPosition)
|
||||
{
|
||||
lastPosition = Vector3.Lerp(lastPosition, newPosition, smoothSpeed * Time.deltaTime);
|
||||
return lastPosition;
|
||||
}
|
||||
|
||||
public Quaternion ProcessRotation(Quaternion newRotation)
|
||||
{
|
||||
lastRotation = Quaternion.Lerp(lastRotation, newRotation, smoothSpeed * Time.deltaTime);
|
||||
return lastRotation;
|
||||
}
|
||||
|
||||
public void SetSmoothSpeed(float speed)
|
||||
{
|
||||
smoothSpeed = speed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e877d21674d38014f855d5fc4a09ef26
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
177
Assets/Scripts/KindRetargeting/TwoBoneIKSolver.cs
Normal file
177
Assets/Scripts/KindRetargeting/TwoBoneIKSolver.cs
Normal file
@ -0,0 +1,177 @@
|
||||
using UnityEngine;
|
||||
using RootMotion.FinalIK;
|
||||
|
||||
namespace KindRetargeting
|
||||
{
|
||||
/// <summary>
|
||||
/// FinalIK IKSolverTrigonometric.Solve()를 사용하는 IK 래퍼.
|
||||
/// 4개 사지(양팔, 양다리)에 대해 FinalIK의 검증된 코사인 법칙 솔버를 호출합니다.
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(6)]
|
||||
public class TwoBoneIKSolver : MonoBehaviour
|
||||
{
|
||||
[System.Serializable]
|
||||
public class LimbIK
|
||||
{
|
||||
public bool enabled = true;
|
||||
public Transform target;
|
||||
public Transform bendGoal;
|
||||
|
||||
[Range(0f, 1f)] public float positionWeight = 0f;
|
||||
[Range(0f, 1f)] public float rotationWeight = 0f;
|
||||
[Range(0f, 1f)] public float bendGoalWeight = 0f;
|
||||
|
||||
[HideInInspector] public Transform upper;
|
||||
[HideInInspector] public Transform lower;
|
||||
[HideInInspector] public Transform end;
|
||||
[HideInInspector] public float upperLength;
|
||||
[HideInInspector] public float lowerLength;
|
||||
|
||||
// 초기 벤드 법선 (upper 본 로컬 공간 — FinalIK 방식)
|
||||
[HideInInspector] public Vector3 localBendNormal;
|
||||
}
|
||||
|
||||
[HideInInspector] public Animator animator;
|
||||
|
||||
[Header("팔")]
|
||||
public LimbIK leftArm = new LimbIK();
|
||||
public LimbIK rightArm = new LimbIK();
|
||||
|
||||
[Header("다리")]
|
||||
public LimbIK leftLeg = new LimbIK();
|
||||
public LimbIK rightLeg = new LimbIK();
|
||||
|
||||
private bool isInitialized;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
Initialize();
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
if (animator == null)
|
||||
animator = GetComponent<Animator>();
|
||||
if (animator == null || !animator.isHuman) return;
|
||||
|
||||
CacheLimb(leftArm, HumanBodyBones.LeftUpperArm, HumanBodyBones.LeftLowerArm, HumanBodyBones.LeftHand);
|
||||
CacheLimb(rightArm, HumanBodyBones.RightUpperArm, HumanBodyBones.RightLowerArm, HumanBodyBones.RightHand);
|
||||
CacheLimb(leftLeg, HumanBodyBones.LeftUpperLeg, HumanBodyBones.LeftLowerLeg, HumanBodyBones.LeftFoot);
|
||||
CacheLimb(rightLeg, HumanBodyBones.RightUpperLeg, HumanBodyBones.RightLowerLeg, HumanBodyBones.RightFoot);
|
||||
|
||||
isInitialized = true;
|
||||
}
|
||||
|
||||
private void CacheLimb(LimbIK limb, HumanBodyBones upperBone, HumanBodyBones lowerBone, HumanBodyBones endBone)
|
||||
{
|
||||
limb.upper = animator.GetBoneTransform(upperBone);
|
||||
limb.lower = animator.GetBoneTransform(lowerBone);
|
||||
limb.end = animator.GetBoneTransform(endBone);
|
||||
|
||||
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);
|
||||
|
||||
// 초기 벤드 법선을 upper 본의 로컬 공간에 캐싱 (FinalIK TrigonometricBone 방식)
|
||||
// 런타임에 upper.rotation * localBendNormal로 안정적인 월드 법선 획득
|
||||
Vector3 ab = limb.lower.position - limb.upper.position;
|
||||
Vector3 bc = limb.end.position - limb.lower.position;
|
||||
Vector3 bendNormal = Vector3.Cross(ab, bc);
|
||||
|
||||
if (bendNormal.sqrMagnitude < 0.0001f)
|
||||
{
|
||||
bendNormal = Vector3.Cross(ab.normalized, Vector3.up);
|
||||
if (bendNormal.sqrMagnitude < 0.0001f)
|
||||
bendNormal = Vector3.Cross(ab.normalized, Vector3.forward);
|
||||
}
|
||||
|
||||
bendNormal.Normalize();
|
||||
limb.localBendNormal = Quaternion.Inverse(limb.upper.rotation) * bendNormal;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!isInitialized) return;
|
||||
|
||||
SolveLimb(leftArm);
|
||||
SolveLimb(rightArm);
|
||||
SolveLimb(leftLeg);
|
||||
SolveLimb(rightLeg);
|
||||
}
|
||||
|
||||
private void SolveLimb(LimbIK limb)
|
||||
{
|
||||
if (!limb.enabled || limb.target == null || limb.positionWeight < 0.001f) return;
|
||||
if (limb.upper == null || limb.lower == null || limb.end == null) return;
|
||||
|
||||
// 벤드 법선 계산
|
||||
Vector3 bendNormal = GetBendNormal(limb);
|
||||
|
||||
// bendGoal 적용
|
||||
if (limb.bendGoal != null && limb.bendGoalWeight > 0.001f)
|
||||
{
|
||||
Vector3 goalNormal = Vector3.Cross(
|
||||
limb.bendGoal.position - limb.upper.position,
|
||||
limb.target.position - limb.upper.position
|
||||
);
|
||||
if (goalNormal.sqrMagnitude > 0.0001f)
|
||||
{
|
||||
bendNormal = Vector3.Lerp(bendNormal, goalNormal.normalized, limb.bendGoalWeight);
|
||||
}
|
||||
}
|
||||
|
||||
// FinalIK 정적 솔버 호출
|
||||
IKSolverTrigonometric.Solve(
|
||||
limb.upper,
|
||||
limb.lower,
|
||||
limb.end,
|
||||
limb.target.position,
|
||||
bendNormal,
|
||||
limb.positionWeight
|
||||
);
|
||||
|
||||
// 끝단 회전
|
||||
if (limb.rotationWeight > 0.001f)
|
||||
{
|
||||
limb.end.rotation = Quaternion.Slerp(
|
||||
limb.end.rotation,
|
||||
limb.target.rotation,
|
||||
limb.rotationWeight
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 벤드 법선을 upper 본의 회전에서 유도합니다 (FinalIK 방식).
|
||||
/// 위치 기반 Cross(ab, bc)는 직선 근처에서 불안정하지만,
|
||||
/// 회전 기반은 본의 회전을 그대로 따르므로 안정적입니다.
|
||||
/// </summary>
|
||||
private Vector3 GetBendNormal(LimbIK limb)
|
||||
{
|
||||
return limb.upper.rotation * limb.localBendNormal;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 히프 높이 자동 보정값을 계산합니다.
|
||||
/// </summary>
|
||||
public float CalculateAutoFloorHeight(float comfortRatio = 0.98f)
|
||||
{
|
||||
if (animator == null || leftLeg.upper == null || leftLeg.lower == null || leftLeg.end == null) return 0f;
|
||||
|
||||
float upperLen = Vector3.Distance(leftLeg.upper.position, leftLeg.lower.position);
|
||||
float lowerLen = Vector3.Distance(leftLeg.lower.position, leftLeg.end.position);
|
||||
float totalLegLength = upperLen + lowerLen;
|
||||
float comfortHeight = totalLegLength * comfortRatio;
|
||||
|
||||
Transform hips = animator.GetBoneTransform(HumanBodyBones.Hips);
|
||||
Transform foot = leftLeg.end;
|
||||
if (hips == null || foot == null) return 0f;
|
||||
|
||||
float currentHipToFoot = hips.position.y - foot.position.y;
|
||||
float heightDiff = currentHipToFoot - comfortHeight;
|
||||
|
||||
return -heightDiff;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/KindRetargeting/TwoBoneIKSolver.cs.meta
Normal file
2
Assets/Scripts/KindRetargeting/TwoBoneIKSolver.cs.meta
Normal file
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6e10af2f66483d94cbd426200d301d66
|
||||
Loading…
x
Reference in New Issue
Block a user