Compare commits

...

5 Commits

Author SHA1 Message Date
e4ca30b98a Refactor : LimbWeightController, FingerShapedController를 Serializable 모듈로 전환
- LimbWeightController: MonoBehaviour → [Serializable] 모듈, CRS.limbWeight로 통합
- FingerShapedController: MonoBehaviour → [Serializable] 모듈, CRS.fingerShaped로 통합
- GetHand()를 FindObjectsOfType<LimbWeightController> → FindObjectsByType<CustomRetargetingScript>로 리팩토링
- HumanPoseHandler 라이프사이클을 Initialize/Cleanup 패턴으로 전환
- RetargetingControlWindow: 모든 GetComponent 호출을 CRS SO의 중첩 프로퍼티 경로로 변경
- RetargetingRemoteController: 직접 script.limbWeight/fingerShaped 접근으로 변경
- LimbWeightControllerEditor, FingerShapedControllerEditor 삭제

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 23:21:12 +09:00
62a5a9bbb5 Refactor : FootGroundingController를 Serializable 모듈로 전환
- FootGroundingController: MonoBehaviour → [Serializable] 클래스
- Start() → Initialize(TwoBoneIKSolver, Animator)
- Update()/LateUpdate() → OnUpdate()/OnLateUpdate()
- CRS에서 footGrounding 필드로 소유, Update/LateUpdate에서 호출
- CustomRetargetingScriptEditor: groundingSO 제거, serializedObject 경로로 접근

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 23:06:57 +09:00
64a2069b69 Refactor : TwoBoneIKSolver를 Serializable 모듈로 전환
- TwoBoneIKSolver: MonoBehaviour → [Serializable] 클래스
- Start()/Update() → Initialize(Animator)/OnUpdate()
- CRS에서 ikSolver 필드로 소유 및 호출
- FootGroundingController/LimbWeightController: GetComponent<TwoBoneIKSolver> → crs.ikSolver로 변경

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 23:04:55 +09:00
5c65185a61 Refactor : PropLocationController를 Serializable 모듈로 전환
- PropLocationController: MonoBehaviour → [Serializable] 클래스
- Start() → Initialize(Animator), GetComponent 제거
- CRS에서 propLocation 필드로 소유 및 초기화
- RetargetingControlWindow: GetComponent → script.propLocation 직접 접근
- PropLocationControllerEditor 삭제 (MonoBehaviour 아니므로 불필요)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 23:03:06 +09:00
52d6960710 Refactor : ShoulderCorrectionFunction을 Serializable 모듈로 전환
- RequireComponent 6개 제거 (모듈화 준비)
- ShoulderCorrectionFunction: MonoBehaviour → [Serializable] 클래스
- Start() → Initialize(Animator), Update() → OnUpdate()
- CustomRetargetingScript에서 shoulderCorrection 필드로 소유 및 호출

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 23:00:35 +09:00
16 changed files with 413 additions and 1094 deletions

View File

@ -9,12 +9,6 @@ namespace KindRetargeting
/// 이 스크립트는 원본 아바타(Source)의 포즈 손가락 움직임을 대상 아바타(Target)에 리타게팅(Retargeting)합니다. /// 이 스크립트는 원본 아바타(Source)의 포즈 손가락 움직임을 대상 아바타(Target)에 리타게팅(Retargeting)합니다.
/// 또한 IK 타겟을 생성하여 대상 아바타의 관절 움직임을 자연스럽게 조정합니다. /// 또한 IK 타겟을 생성하여 대상 아바타의 관절 움직임을 자연스럽게 조정합니다.
/// </summary> /// </summary>
[RequireComponent(typeof(LimbWeightController))]
[RequireComponent(typeof(ShoulderCorrectionFunction))]
[RequireComponent(typeof(TwoBoneIKSolver))]
[RequireComponent(typeof(FootGroundingController))]
[RequireComponent(typeof(PropLocationController))]
[RequireComponent(typeof(FingerShapedController))]
[DefaultExecutionOrder(1)] [DefaultExecutionOrder(1)]
public class CustomRetargetingScript : MonoBehaviour public class CustomRetargetingScript : MonoBehaviour
{ {
@ -25,7 +19,7 @@ namespace KindRetargeting
[HideInInspector] public Animator targetAnimator; // 대상 아바타의 Animator [HideInInspector] public Animator targetAnimator; // 대상 아바타의 Animator
// IK 컴포넌트 참조 // IK 컴포넌트 참조
private TwoBoneIKSolver ikSolver; [SerializeField] public TwoBoneIKSolver ikSolver = new TwoBoneIKSolver();
[Header("힙 위치 보정 (로컬 좌표계 기반)")] [Header("힙 위치 보정 (로컬 좌표계 기반)")]
[SerializeField, Range(-1, 1)] [SerializeField, Range(-1, 1)]
@ -101,6 +95,21 @@ namespace KindRetargeting
[HideInInspector] public Vector3 tPoseHeadForward = Vector3.forward; [HideInInspector] public Vector3 tPoseHeadForward = Vector3.forward;
[HideInInspector] public Vector3 tPoseHeadUp = Vector3.up; [HideInInspector] public Vector3 tPoseHeadUp = Vector3.up;
[Header("어깨 보정")]
[SerializeField] public ShoulderCorrectionFunction shoulderCorrection = new ShoulderCorrectionFunction();
[Header("발 접지")]
[SerializeField] public FootGroundingController footGrounding = new FootGroundingController();
[Header("프랍 부착")]
[SerializeField] public PropLocationController propLocation = new PropLocationController();
[Header("사지 가중치")]
[SerializeField] public LimbWeightController limbWeight = new LimbWeightController();
[Header("손가락 셰이핑")]
[SerializeField] public FingerShapedController fingerShaped = new FingerShapedController();
[Header("아바타 크기 조정")] [Header("아바타 크기 조정")]
[SerializeField, Range(0.1f, 3f)] private float avatarScale = 1f; [SerializeField, Range(0.1f, 3f)] private float avatarScale = 1f;
private float previousScale = 1f; private float previousScale = 1f;
@ -269,8 +278,7 @@ namespace KindRetargeting
// 설정 로드 // 설정 로드
LoadSettings(); LoadSettings();
// IK 컴포넌트 참조 가져오기 // IK 모듈은 InitializeIKJoints에서 초기화
ikSolver = GetComponent<TwoBoneIKSolver>();
// IK 타겟 생성 (무릎 시각화 오브젝트 포함) // IK 타겟 생성 (무릎 시각화 오브젝트 포함)
CreateIKTargets(); CreateIKTargets();
@ -324,6 +332,24 @@ namespace KindRetargeting
Debug.LogWarning("[CustomRetargetingScript] 머리 본을 찾을 수 없습니다!"); Debug.LogWarning("[CustomRetargetingScript] 머리 본을 찾을 수 없습니다!");
} }
} }
// 어깨 보정 모듈 초기화
if (targetAnimator != null)
shoulderCorrection.Initialize(targetAnimator);
// 발 접지 모듈 초기화
footGrounding.Initialize(ikSolver, targetAnimator);
// 프랍 부착 모듈 초기화
if (targetAnimator != null)
propLocation.Initialize(targetAnimator);
// 사지 가중치 모듈 초기화
limbWeight.Initialize(ikSolver, this, transform);
// 손가락 셰이핑 모듈 초기화
if (targetAnimator != null)
fingerShaped.Initialize(targetAnimator);
} }
/// <summary> /// <summary>
@ -521,8 +547,7 @@ namespace KindRetargeting
} }
// LimbWeightController에서 의자 높이 오프셋 가져오기 // LimbWeightController에서 의자 높이 오프셋 가져오기
var limbController = GetComponent<LimbWeightController>(); float chairOffset = limbWeight.chairSeatHeightOffset;
float chairOffset = limbController != null ? limbController.chairSeatHeightOffset : 0.05f;
var settings = new RetargetingSettings var settings = new RetargetingSettings
{ {
@ -600,11 +625,7 @@ namespace KindRetargeting
headScale = settings.headScale; headScale = settings.headScale;
// LimbWeightController에 의자 높이 오프셋 적용 // LimbWeightController에 의자 높이 오프셋 적용
var limbController = GetComponent<LimbWeightController>(); limbWeight.chairSeatHeightOffset = settings.chairSeatHeightOffset;
if (limbController != null)
{
limbController.chairSeatHeightOffset = settings.chairSeatHeightOffset;
}
// 머리 회전 오프셋 로드 // 머리 회전 오프셋 로드
headRotationOffsetX = settings.headRotationOffsetX; headRotationOffsetX = settings.headRotationOffsetX;
@ -821,6 +842,21 @@ namespace KindRetargeting
break; break;
} }
// 손가락 셰이핑 (기존 ExecutionOrder 2)
fingerShaped.OnUpdate();
// 어깨 보정 (기존 ExecutionOrder 3)
shoulderCorrection.OnUpdate();
// 사지 가중치 (기존 ExecutionOrder 4)
limbWeight.OnUpdate();
// 발 접지 Pre-IK (기존 ExecutionOrder 5)
footGrounding.OnUpdate();
// IK 솔버 (기존 ExecutionOrder 6)
ikSolver.OnUpdate();
// 스케일 변경 확인 및 적용 // 스케일 변경 확인 및 적용
if (!Mathf.Approximately(previousScale, avatarScale)) if (!Mathf.Approximately(previousScale, avatarScale))
{ {
@ -834,6 +870,9 @@ namespace KindRetargeting
/// </summary> /// </summary>
void LateUpdate() void LateUpdate()
{ {
// 발 접지 Post-IK (기존 FootGroundingController LateUpdate)
footGrounding.OnLateUpdate();
ApplyHeadRotationOffset(); ApplyHeadRotationOffset();
ApplyHeadScale(); ApplyHeadScale();
} }
@ -1147,11 +1186,6 @@ namespace KindRetargeting
/// </summary> /// </summary>
private void CreateIKTargets() private void CreateIKTargets()
{ {
// IK 컴포넌트 가져오기 또는 새로 추가
ikSolver = GetComponent<TwoBoneIKSolver>();
if (ikSolver == null)
ikSolver = gameObject.AddComponent<TwoBoneIKSolver>();
ikSolver.animator = targetAnimator; ikSolver.animator = targetAnimator;
// IK 타겟들을 담을 부모 오브젝트 생성 // IK 타겟들을 담을 부모 오브젝트 생성
@ -1183,7 +1217,7 @@ namespace KindRetargeting
ikSolver.rightLeg.bendGoal = rightLegGoal.transform; ikSolver.rightLeg.bendGoal = rightLegGoal.transform;
// TwoBoneIKSolver 본 캐싱 초기화 // TwoBoneIKSolver 본 캐싱 초기화
ikSolver.Initialize(); ikSolver.Initialize(targetAnimator);
} }
/// <summary> /// <summary>
@ -1384,6 +1418,7 @@ namespace KindRetargeting
{ {
sourcePoseHandler?.Dispose(); sourcePoseHandler?.Dispose();
targetPoseHandler?.Dispose(); targetPoseHandler?.Dispose();
fingerShaped.Cleanup();
} }
/// <summary> /// <summary>

View File

@ -10,8 +10,6 @@ namespace KindRetargeting
{ {
private const string CommonUssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/StreamingleCommon.uss"; private const string CommonUssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/StreamingleCommon.uss";
private SerializedObject groundingSO;
// SerializedProperty // SerializedProperty
private SerializedProperty sourceAnimatorProp; private SerializedProperty sourceAnimatorProp;
private SerializedProperty targetAnimatorProp; private SerializedProperty targetAnimatorProp;
@ -33,7 +31,7 @@ namespace KindRetargeting
protected override void OnDisable() protected override void OnDisable()
{ {
base.OnDisable(); base.OnDisable();
groundingSO = null; // groundingSO 삭제됨 — footGrounding은 CRS 내부 모듈
} }
protected override void OnEnable() protected override void OnEnable()
@ -197,36 +195,27 @@ namespace KindRetargeting
"• Toe Pivot: 발끝 고정 + 발목 회전 자동 감지", "• Toe Pivot: 발끝 고정 + 발목 회전 자동 감지",
HelpBoxMessageType.Info)); HelpBoxMessageType.Info));
// FootGroundingController의 SerializedObject를 직접 바인딩 // FootGroundingController는 CRS 내부 모듈 — serializedObject의 프로퍼티 경로로 접근
var script = (CustomRetargetingScript)target; var groundHeightField = new PropertyField(serializedObject.FindProperty("footGrounding.groundHeight"), "바닥 높이");
var grounding = script.GetComponent<FootGroundingController>(); foldout.Add(groundHeightField);
if (grounding != null)
{
groundingSO = new SerializedObject(grounding);
var groundHeightField = new PropertyField(groundingSO.FindProperty("groundHeight"), "바닥 높이"); var weightSlider = new Slider("접지 강도", 0f, 1f) { showInputField = true };
foldout.Add(groundHeightField); weightSlider.BindProperty(serializedObject.FindProperty("footGrounding.groundingWeight"));
foldout.Add(weightSlider);
var weightSlider = new Slider("접지 강도", 0f, 1f) { showInputField = true }; var activationField = new PropertyField(serializedObject.FindProperty("footGrounding.activationHeight"), "활성화 높이");
weightSlider.BindProperty(groundingSO.FindProperty("groundingWeight")); activationField.tooltip = "발목이 이 높이 이상이면 접지 보정 비활성화 (점프 등)";
foldout.Add(weightSlider); foldout.Add(activationField);
var activationField = new PropertyField(groundingSO.FindProperty("activationHeight"), "활성화 높이"); var thresholdField = new PropertyField(serializedObject.FindProperty("footGrounding.plantThreshold"), "접지 판정 범위");
activationField.tooltip = "발목이 이 높이 이상이면 접지 보정 비활성화 (점프 등)"; thresholdField.tooltip = "Toes가 이 범위 안이면 접지 중으로 판정";
foldout.Add(activationField); foldout.Add(thresholdField);
var thresholdField = new PropertyField(groundingSO.FindProperty("plantThreshold"), "접지 판정 범위"); var smoothField = new PropertyField(serializedObject.FindProperty("footGrounding.smoothSpeed"), "보정 스무딩 속도");
thresholdField.tooltip = "Toes가 이 범위 안이면 접지 중으로 판정"; smoothField.tooltip = "보정량 변화 속도 (높을수록 빠른 반응, 낮으면 부드러운 전환)";
foldout.Add(thresholdField); foldout.Add(smoothField);
var smoothField = new PropertyField(groundingSO.FindProperty("smoothSpeed"), "보정 스무딩 속도"); foldout.Add(new HelpBox("힙 높이 보정은 '바닥 높이 조정' 섹션의 floorHeight로 제어합니다.", HelpBoxMessageType.Info));
smoothField.tooltip = "보정량 변화 속도 (높을수록 빠른 반응, 낮으면 부드러운 전환)";
foldout.Add(smoothField);
foldout.Add(new HelpBox("힙 높이 보정은 '바닥 높이 조정' 섹션의 floorHeight로 제어합니다.", HelpBoxMessageType.Info));
foldout.TrackSerializedObjectValue(groundingSO, so => so.ApplyModifiedProperties());
}
else else
{ {
foldout.Add(new HelpBox("FootGroundingController 컴포넌트를 찾을 수 없습니다.", HelpBoxMessageType.Warning)); foldout.Add(new HelpBox("FootGroundingController 컴포넌트를 찾을 수 없습니다.", HelpBoxMessageType.Warning));

View File

@ -1,193 +0,0 @@
using UnityEngine;
using UnityEditor;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
[CustomEditor(typeof(FingerShapedController))]
public class FingerShapedControllerEditor : Editor
{
private const string CommonUssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/StreamingleCommon.uss";
private static readonly string[] fingerPropNames = { "ThumbCurl", "IndexCurl", "MiddleCurl", "RingCurl", "PinkyCurl" };
private static readonly string[] fingerKoreanNames = { "엄지", "검지", "중지", "약지", "새끼" };
private FingerShapedController controller;
public override VisualElement CreateInspectorGUI()
{
controller = target as FingerShapedController;
if (controller == null) return new VisualElement();
var root = new VisualElement();
var commonUss = AssetDatabase.LoadAssetAtPath<StyleSheet>(CommonUssPath);
if (commonUss != null) root.styleSheets.Add(commonUss);
// 활성화 토글
var enabledProp = serializedObject.FindProperty("m_Enabled");
var enableToggle = new PropertyField(enabledProp, "손가락 제어 활성화");
root.Add(enableToggle);
// 왼손
var leftFoldout = new Foldout { text = "왼손", value = true };
leftFoldout.Add(BuildHandControls("left"));
root.Add(leftFoldout);
// 오른손
var rightFoldout = new Foldout { text = "오른손", value = true };
rightFoldout.Add(BuildHandControls("right"));
root.Add(rightFoldout);
// 프리셋 버튼
root.Add(BuildPresetButtons());
return root;
}
private VisualElement BuildHandControls(string prefix)
{
var container = new VisualElement();
container.style.backgroundColor = new Color(0, 0, 0, 0.08f);
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 = 6;
// 헤더: 활성화 토글 + 초기화 버튼
var header = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center, marginBottom = 4 } };
var handEnabledProp = serializedObject.FindProperty($"{prefix}HandEnabled");
header.Add(new PropertyField(handEnabledProp, "제어 활성화") { style = { flexGrow = 1 } });
var resetBtn = new Button(() => ResetHandValues(prefix)) { text = "초기화" };
resetBtn.style.width = 60;
header.Add(resetBtn);
container.Add(header);
// 손가락 슬라이더 (가로 배치)
var slidersRow = new VisualElement { style = { flexDirection = FlexDirection.Row, justifyContent = Justify.Center, marginTop = 4 } };
for (int i = 0; i < fingerPropNames.Length; i++)
{
var fingerProp = serializedObject.FindProperty($"{prefix}{fingerPropNames[i]}");
slidersRow.Add(BuildVerticalSlider(fingerKoreanNames[i], fingerProp));
}
container.Add(slidersRow);
// 벌리기 슬라이더
var spreadProp = serializedObject.FindProperty($"{prefix}SpreadFingers");
var spreadSlider = new Slider("벌리기", -1f, 1f) { showInputField = true };
spreadSlider.BindProperty(spreadProp);
spreadSlider.style.marginTop = 8;
container.Add(spreadSlider);
// 비활성 시 숨기기
container.TrackPropertyValue(handEnabledProp, prop =>
{
slidersRow.style.display = prop.boolValue ? DisplayStyle.Flex : DisplayStyle.None;
spreadSlider.style.display = prop.boolValue ? DisplayStyle.Flex : DisplayStyle.None;
});
slidersRow.style.display = handEnabledProp.boolValue ? DisplayStyle.Flex : DisplayStyle.None;
spreadSlider.style.display = handEnabledProp.boolValue ? DisplayStyle.Flex : DisplayStyle.None;
return container;
}
private VisualElement BuildVerticalSlider(string label, SerializedProperty prop)
{
var column = new VisualElement { style = { alignItems = Align.Center, width = 45, marginLeft = 2, marginRight = 2 } };
column.Add(new Label(label) { style = { fontSize = 10, color = new Color(0.6f, 0.6f, 0.6f) } });
var valueLabel = new Label(prop.floatValue.ToString("F1")) { style = { fontSize = 10, color = new Color(0.6f, 0.6f, 0.6f) } };
column.Add(valueLabel);
// UI Toolkit Slider in vertical mode (direction: Vertical 대신 세로용 Slider 사용)
// SliderDirection.Vertical 사용
var slider = new Slider(-1f, 1f) { direction = SliderDirection.Vertical };
slider.style.height = 100;
slider.style.width = 25;
slider.BindProperty(prop);
slider.RegisterValueChangedCallback(evt => valueLabel.text = evt.newValue.ToString("F1"));
// 초기값 동기화
column.TrackPropertyValue(prop, p => valueLabel.text = p.floatValue.ToString("F1"));
column.Add(slider);
return column;
}
private VisualElement BuildPresetButtons()
{
var container = new VisualElement { style = { marginTop = 10 } };
container.Add(new Label("손 모양 프리셋") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginBottom = 4 } });
string[,] presets = {
{ "가위", "바위", "보" },
{ "브이", "검지", "초기화" }
};
for (int row = 0; row < 2; row++)
{
var btnRow = new VisualElement { style = { flexDirection = FlexDirection.Row, justifyContent = Justify.Center, marginBottom = 4 } };
for (int col = 0; col < 3; col++)
{
string presetName = presets[row, col];
var btn = new Button(() => ApplyPreset(presetName)) { text = presetName };
btn.style.height = 30;
btn.style.width = 100;
btn.style.marginLeft = btn.style.marginRight = 4;
btnRow.Add(btn);
}
container.Add(btnRow);
}
return container;
}
private void ResetHandValues(string prefix)
{
serializedObject.Update();
string[] props = { "ThumbCurl", "IndexCurl", "MiddleCurl", "RingCurl", "PinkyCurl", "SpreadFingers" };
foreach (var prop in props)
serializedObject.FindProperty($"{prefix}{prop}").floatValue = 0f;
serializedObject.ApplyModifiedProperties();
}
private void ApplyPreset(string presetName)
{
if (!controller.enabled)
controller.enabled = true;
switch (presetName)
{
case "가위": SetPreset(1f, 1f, -1f, -1f, -1f, 0.3f); break;
case "바위": SetPreset(-1f, -1f, -1f, -1f, -1f, 0f); break;
case "보": SetPreset(1f, 1f, 1f, 1f, 1f, 1f); break;
case "브이": SetPreset(-1f, 1f, 1f, -1f, -1f, 1f); break;
case "검지": SetPreset(-1f, 1f, -1f, -1f, -1f, 0f); break;
case "초기화": SetPreset(0.8f, 0.8f, 0.8f, 0.8f, 0.8f, 0.8f); break;
}
}
private void SetPreset(float thumb, float index, float middle, float ring, float pinky, float spread)
{
if (controller.leftHandEnabled)
{
controller.leftThumbCurl = thumb;
controller.leftIndexCurl = index;
controller.leftMiddleCurl = middle;
controller.leftRingCurl = ring;
controller.leftPinkyCurl = pinky;
controller.leftSpreadFingers = spread;
}
if (controller.rightHandEnabled)
{
controller.rightThumbCurl = thumb;
controller.rightIndexCurl = index;
controller.rightMiddleCurl = middle;
controller.rightRingCurl = ring;
controller.rightPinkyCurl = pinky;
controller.rightSpreadFingers = spread;
}
EditorUtility.SetDirty(controller);
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 291a583b9a953e041a119ba6c332d187
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,185 +0,0 @@
using UnityEngine;
using UnityEditor;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
namespace KindRetargeting
{
[CustomEditor(typeof(LimbWeightController))]
public class LimbWeightControllerEditor : BaseRetargetingEditor
{
private const string CommonUssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/StreamingleCommon.uss";
SerializedProperty maxDistance;
SerializedProperty minDistance;
SerializedProperty weightSmoothSpeed;
SerializedProperty middleWeightMultiplier;
SerializedProperty hipsMinDistance;
SerializedProperty hipsMaxDistance;
SerializedProperty props;
SerializedProperty characterRoot;
SerializedProperty groundHipsMinHeight;
SerializedProperty groundHipsMaxHeight;
SerializedProperty footHeightMinThreshold;
SerializedProperty footHeightMaxThreshold;
SerializedProperty enableLeftArmIK;
SerializedProperty enableRightArmIK;
SerializedProperty chairSeatHeightOffset;
protected override void OnEnable()
{
base.OnEnable();
if (serializedObject == null) return;
maxDistance = serializedObject.FindProperty("maxDistance");
minDistance = serializedObject.FindProperty("minDistance");
weightSmoothSpeed = serializedObject.FindProperty("weightSmoothSpeed");
middleWeightMultiplier = serializedObject.FindProperty("middleWeightMultiplier");
hipsMinDistance = serializedObject.FindProperty("hipsMinDistance");
hipsMaxDistance = serializedObject.FindProperty("hipsMaxDistance");
props = serializedObject.FindProperty("props");
characterRoot = serializedObject.FindProperty("characterRoot");
groundHipsMinHeight = serializedObject.FindProperty("groundHipsMinHeight");
groundHipsMaxHeight = serializedObject.FindProperty("groundHipsMaxHeight");
footHeightMinThreshold = serializedObject.FindProperty("footHeightMinThreshold");
footHeightMaxThreshold = serializedObject.FindProperty("footHeightMaxThreshold");
enableLeftArmIK = serializedObject.FindProperty("enableLeftArmIK");
enableRightArmIK = serializedObject.FindProperty("enableRightArmIK");
chairSeatHeightOffset = serializedObject.FindProperty("chairSeatHeightOffset");
}
public override VisualElement CreateInspectorGUI()
{
if (serializedObject == null || target == null)
return new VisualElement();
var root = new VisualElement();
var commonUss = AssetDatabase.LoadAssetAtPath<StyleSheet>(CommonUssPath);
if (commonUss != null) root.styleSheets.Add(commonUss);
// IK 활성화 설정
var ikFoldout = new Foldout { text = "IK 활성화 설정", value = true };
ikFoldout.Add(new PropertyField(enableLeftArmIK, "왼팔 IK 활성화"));
ikFoldout.Add(new PropertyField(enableRightArmIK, "오른팔 IK 활성화"));
root.Add(ikFoldout);
// 거리 기반 가중치 설정
var distFoldout = new Foldout { text = "거리 기반 가중치 설정", value = true };
distFoldout.Add(BuildMinMaxRange("거리 범위 (가중치 1 → 0)", minDistance, maxDistance, 0f, 1f));
root.Add(distFoldout);
// 가중치 변화 설정
var weightFoldout = new Foldout { text = "가중치 변화 설정", value = true };
weightFoldout.Add(new PropertyField(weightSmoothSpeed, "가중치 변화 속도"));
root.Add(weightFoldout);
// 허리 가중치 설정
var hipsFoldout = new Foldout { text = "허리 가중치 설정", value = true };
hipsFoldout.Add(BuildMinMaxRange("허리 거리 범위 (가중치 1 → 0)", hipsMinDistance, hipsMaxDistance, 0f, 1f));
root.Add(hipsFoldout);
// 바닥 기준 히프 보정 설정
var groundFoldout = new Foldout { text = "바닥 기준 히프 보정 설정", value = true };
groundFoldout.Add(BuildMinMaxRange("바닥 기준 히프 높이 범위 (가중치 0 → 1)", groundHipsMinHeight, groundHipsMaxHeight, 0f, 2f));
root.Add(groundFoldout);
// 발 높이 기반 가중치 설정
var footFoldout = new Foldout { text = "발 높이 기반 가중치 설정", value = true };
footFoldout.Add(BuildMinMaxRange("발 높이 범위 (가중치 1 → 0)", footHeightMinThreshold, footHeightMaxThreshold, 0.1f, 1f));
root.Add(footFoldout);
// 의자 앉기 높이 설정
var chairFoldout = new Foldout { text = "의자 앉기 높이 설정", value = true };
chairFoldout.Add(new PropertyField(chairSeatHeightOffset, "좌석 높이 오프셋"));
root.Add(chairFoldout);
// 참조 설정
var refFoldout = new Foldout { text = "참조 설정", value = true };
refFoldout.Add(new PropertyField(props, "프랍 오브젝트"));
refFoldout.Add(new PropertyField(characterRoot, "캐릭터 루트"));
root.Add(refFoldout);
// 변경 감지
root.TrackSerializedObjectValue(serializedObject, so =>
{
if (target == null) return;
MarkDirty();
var script = (LimbWeightController)target;
var retargeting = script.GetComponent<CustomRetargetingScript>();
if (retargeting != null)
EditorUtility.SetDirty(retargeting);
});
return root;
}
private VisualElement BuildMinMaxRange(string label, SerializedProperty minProp, SerializedProperty maxProp, float limitMin, float limitMax)
{
var container = new VisualElement { style = { marginBottom = 4 } };
if (minProp == null || maxProp == null)
{
container.Add(new HelpBox($"'{label}' 프로퍼티를 찾을 수 없습니다.", HelpBoxMessageType.Warning));
return container;
}
container.Add(new Label(label) { style = { marginBottom = 2 } });
var row = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center } };
var minField = new FloatField { value = minProp.floatValue, style = { width = 50 } };
var slider = new MinMaxSlider(minProp.floatValue, maxProp.floatValue, limitMin, limitMax);
slider.style.flexGrow = 1;
slider.style.marginLeft = slider.style.marginRight = 4;
var maxField = new FloatField { value = maxProp.floatValue, style = { width = 50 } };
// MinMaxSlider → sync fields + properties
slider.RegisterValueChangedCallback(evt =>
{
minProp.floatValue = evt.newValue.x;
maxProp.floatValue = evt.newValue.y;
serializedObject.ApplyModifiedProperties();
minField.SetValueWithoutNotify(evt.newValue.x);
maxField.SetValueWithoutNotify(evt.newValue.y);
});
// FloatField min → sync slider + property
minField.RegisterValueChangedCallback(evt =>
{
float clamped = Mathf.Clamp(evt.newValue, limitMin, maxProp.floatValue);
minProp.floatValue = clamped;
serializedObject.ApplyModifiedProperties();
slider.SetValueWithoutNotify(new Vector2(clamped, maxProp.floatValue));
minField.SetValueWithoutNotify(clamped);
});
// FloatField max → sync slider + property
maxField.RegisterValueChangedCallback(evt =>
{
float clamped = Mathf.Clamp(evt.newValue, minProp.floatValue, limitMax);
maxProp.floatValue = clamped;
serializedObject.ApplyModifiedProperties();
slider.SetValueWithoutNotify(new Vector2(minProp.floatValue, clamped));
maxField.SetValueWithoutNotify(clamped);
});
// Track external changes (undo, etc.)
container.TrackPropertyValue(minProp, p =>
{
minField.SetValueWithoutNotify(p.floatValue);
slider.SetValueWithoutNotify(new Vector2(p.floatValue, maxProp.floatValue));
});
container.TrackPropertyValue(maxProp, p =>
{
maxField.SetValueWithoutNotify(p.floatValue);
slider.SetValueWithoutNotify(new Vector2(minProp.floatValue, p.floatValue));
});
row.Add(minField);
row.Add(slider);
row.Add(maxField);
container.Add(row);
return container;
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: 199539b34f08aac41a86f4767bc49def
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,167 +0,0 @@
using UnityEngine;
using UnityEditor;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
namespace KindRetargeting
{
[CustomEditor(typeof(PropLocationController))]
public class PropLocationControllerEditor : BaseRetargetingEditor
{
private const string CommonUssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/StreamingleCommon.uss";
private VisualElement offsetContainer;
private VisualElement propListContainer;
private PropLocationController controller;
public override VisualElement CreateInspectorGUI()
{
controller = target as PropLocationController;
if (controller == null) return new VisualElement();
var root = new VisualElement();
var commonUss = AssetDatabase.LoadAssetAtPath<StyleSheet>(CommonUssPath);
if (commonUss != null) root.styleSheets.Add(commonUss);
// 기본 프로퍼티
var iterator = serializedObject.GetIterator();
iterator.NextVisible(true); // skip m_Script
while (iterator.NextVisible(false))
{
var field = new PropertyField(iterator.Copy());
root.Add(field);
}
// 오프셋 조정 섹션
root.Add(new Label("오프셋 조정") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginTop = 10 } });
offsetContainer = new VisualElement();
root.Add(offsetContainer);
// 부착된 프랍 목록 섹션
root.Add(new Label("부착된 프랍 목록") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginTop = 10 } });
propListContainer = new VisualElement();
root.Add(propListContainer);
// 프랍 위치 이동 버튼
root.Add(new Label("프랍 위치 이동") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginTop = 10 } });
var btnRow = new VisualElement { style = { flexDirection = FlexDirection.Row, marginTop = 4 } };
var headBtn = new Button(() => controller.MoveToHead()) { text = "머리로 이동" };
headBtn.style.flexGrow = 1; headBtn.style.height = 30; headBtn.style.marginRight = 2;
btnRow.Add(headBtn);
var leftBtn = new Button(() => controller.MoveToLeftHand()) { text = "왼손으로 이동" };
leftBtn.style.flexGrow = 1; leftBtn.style.height = 30; leftBtn.style.marginRight = 2;
btnRow.Add(leftBtn);
var rightBtn = new Button(() => controller.MoveToRightHand()) { text = "오른손으로 이동" };
rightBtn.style.flexGrow = 1; rightBtn.style.height = 30;
btnRow.Add(rightBtn);
root.Add(btnRow);
var detachBtn = new Button(() =>
{
if (Selection.activeGameObject != null)
Undo.RecordObject(Selection.activeGameObject.transform, "Detach Prop");
controller.DetachProp();
}) { text = "프랍 해제" };
detachBtn.style.height = 30;
detachBtn.style.marginTop = 4;
root.Add(detachBtn);
// 주기적으로 동적 UI 갱신
root.schedule.Execute(() => RebuildDynamicUI()).Every(500);
RebuildDynamicUI();
return root;
}
private void RebuildDynamicUI()
{
if (controller == null) return;
RebuildOffsets();
RebuildPropLists();
}
private void RebuildOffsets()
{
if (offsetContainer == null) return;
offsetContainer.Clear();
BuildOffsetSection(offsetContainer, "왼손 오프셋", controller.GetLeftHandOffset());
BuildOffsetSection(offsetContainer, "오른손 오프셋", controller.GetRightHandOffset());
BuildOffsetSection(offsetContainer, "머리 오프셋", controller.GetHeadOffset());
}
private void BuildOffsetSection(VisualElement parent, string label, Transform offset)
{
if (offset == null) return;
var foldout = new Foldout { text = label, value = true };
var posField = new Vector3Field("위치") { value = offset.localPosition };
posField.RegisterValueChangedCallback(evt =>
{
Undo.RecordObject(offset, $"Update {label}");
offset.localPosition = evt.newValue;
EditorUtility.SetDirty(offset);
});
foldout.Add(posField);
var rotField = new Vector3Field("회전") { value = offset.localRotation.eulerAngles };
rotField.RegisterValueChangedCallback(evt =>
{
Undo.RecordObject(offset, $"Update {label}");
offset.localRotation = Quaternion.Euler(evt.newValue);
EditorUtility.SetDirty(offset);
});
foldout.Add(rotField);
parent.Add(foldout);
}
private void RebuildPropLists()
{
if (propListContainer == null) return;
propListContainer.Clear();
BuildPropListSection(propListContainer, "머리 프랍", controller.GetHeadProps());
BuildPropListSection(propListContainer, "왼손 프랍", controller.GetLeftHandProps());
BuildPropListSection(propListContainer, "오른손 프랍", controller.GetRightHandProps());
}
private void BuildPropListSection(VisualElement parent, string title, GameObject[] propList)
{
var box = new VisualElement();
box.style.backgroundColor = new Color(0, 0, 0, 0.1f);
box.style.borderTopLeftRadius = box.style.borderTopRightRadius =
box.style.borderBottomLeftRadius = box.style.borderBottomRightRadius = 4;
box.style.paddingTop = box.style.paddingBottom =
box.style.paddingLeft = box.style.paddingRight = 4;
box.style.marginBottom = 4;
box.Add(new Label(title) { style = { unityFontStyleAndWeight = FontStyle.Bold } });
if (propList.Length > 0)
{
foreach (var prop in propList)
{
if (prop == null) continue;
var row = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center, marginTop = 2 } };
row.Add(new Label(prop.name) { style = { flexGrow = 1 } });
var selectBtn = new Button(() => Selection.activeGameObject = prop) { text = "선택" };
selectBtn.style.width = 60;
row.Add(selectBtn);
box.Add(row);
}
}
else
{
box.Add(new Label("부착된 프랍 없음") { style = { color = new Color(0.6f, 0.6f, 0.6f) } });
}
parent.Add(box);
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: a49ee1ae55b970e4c8ca00ccae5d6f97
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -144,12 +144,7 @@ public class RetargetingControlWindow : EditorWindow
foreach (var s in retargetingScripts) foreach (var s in retargetingScripts)
{ {
if (s == null) continue; if (s == null) continue;
if (s.GetComponent<LimbWeightController>() == null) // 모든 컴포넌트는 CRS 내부 모듈로 이동됨
s.gameObject.AddComponent<LimbWeightController>();
if (s.GetComponent<FingerShapedController>() == null)
s.gameObject.AddComponent<FingerShapedController>();
if (s.GetComponent<PropLocationController>() == null)
s.gameObject.AddComponent<PropLocationController>();
EditorUtility.SetDirty(s.gameObject); EditorUtility.SetDirty(s.gameObject);
} }
@ -198,7 +193,7 @@ public class RetargetingControlWindow : EditorWindow
panel.Add(BuildHeader(script)); panel.Add(BuildHeader(script));
// 가중치 설정 // 가중치 설정
panel.Add(BuildWeightSection(script)); panel.Add(BuildWeightSection(script, so));
// 힙 위치 보정 // 힙 위치 보정
panel.Add(BuildHipsSection(script, so)); panel.Add(BuildHipsSection(script, so));
@ -230,7 +225,7 @@ public class RetargetingControlWindow : EditorWindow
panel.Add(footFoldout); panel.Add(footFoldout);
// 손가락 제어 설정 // 손가락 제어 설정
panel.Add(BuildFingerControlSection(script)); panel.Add(BuildFingerControlSection(script, so));
// 손가락 복제 설정 // 손가락 복제 설정
panel.Add(BuildFingerCopySection(script, so)); panel.Add(BuildFingerCopySection(script, so));
@ -304,27 +299,22 @@ public class RetargetingControlWindow : EditorWindow
// ========== Weight Settings ========== // ========== Weight Settings ==========
private VisualElement BuildWeightSection(CustomRetargetingScript script) private VisualElement BuildWeightSection(CustomRetargetingScript script, SerializedObject so)
{ {
var foldout = new Foldout { text = "가중치 설정", value = false }; var foldout = new Foldout { text = "가중치 설정", value = false };
var limb = script.GetComponent<LimbWeightController>();
if (limb == null) { foldout.Add(new HelpBox("LimbWeightController가 없습니다.", HelpBoxMessageType.Warning)); return foldout; }
var limbSO = CreateTrackedSO(limb);
var container = new VisualElement(); var container = new VisualElement();
container.Add(BuildMinMaxRange("손과 프랍과의 범위 (가중치 1 → 0)", container.Add(BuildMinMaxRange("손과 프랍과의 범위 (가중치 1 → 0)",
limbSO.FindProperty("minDistance"), limbSO.FindProperty("maxDistance"), 0f, 1f, limbSO)); so.FindProperty("limbWeight.minDistance"), so.FindProperty("limbWeight.maxDistance"), 0f, 1f, so));
container.Add(BuildMinMaxRange("의자와 허리 거리 범위 (가중치 1 → 0)", container.Add(BuildMinMaxRange("의자와 허리 거리 범위 (가중치 1 → 0)",
limbSO.FindProperty("hipsMinDistance"), limbSO.FindProperty("hipsMaxDistance"), 0f, 1f, limbSO)); so.FindProperty("limbWeight.hipsMinDistance"), so.FindProperty("limbWeight.hipsMaxDistance"), 0f, 1f, so));
container.Add(BuildMinMaxRange("바닥과 허리 높이에 의한 블렌딩 (가중치 0 → 1)", container.Add(BuildMinMaxRange("바닥과 허리 높이에 의한 블렌딩 (가중치 0 → 1)",
limbSO.FindProperty("groundHipsMinHeight"), limbSO.FindProperty("groundHipsMaxHeight"), 0f, 2f, limbSO)); so.FindProperty("limbWeight.groundHipsMinHeight"), so.FindProperty("limbWeight.groundHipsMaxHeight"), 0f, 2f, so));
container.Add(BuildMinMaxRange("지면으로부터 발의 범위에 의한 IK 블렌딩 (가중치 1 → 0)", container.Add(BuildMinMaxRange("지면으로부터 발의 범위에 의한 IK 블렌딩 (가중치 1 → 0)",
limbSO.FindProperty("footHeightMinThreshold"), limbSO.FindProperty("footHeightMaxThreshold"), 0.1f, 1f, limbSO)); so.FindProperty("limbWeight.footHeightMinThreshold"), so.FindProperty("limbWeight.footHeightMaxThreshold"), 0.1f, 1f, so));
var smoothField = new PropertyField(limbSO.FindProperty("weightSmoothSpeed"), "가중치 변화 속도"); var smoothField = new PropertyField(so.FindProperty("limbWeight.weightSmoothSpeed"), "가중치 변화 속도");
container.Add(smoothField); container.Add(smoothField);
container.Bind(limbSO);
foldout.Add(container); foldout.Add(container);
return foldout; return foldout;
@ -367,15 +357,9 @@ public class RetargetingControlWindow : EditorWindow
container.Add(hz); container.Add(hz);
// 의자 앉기 높이 // 의자 앉기 높이
var limb = script.GetComponent<LimbWeightController>(); var chairSlider = new Slider("의자 앉기 높이", -1f, 1f) { showInputField = true, tooltip = "의자에 앉을 때 엉덩이 높이 조정 (월드 Y 기준)" };
if (limb != null) chairSlider.BindProperty(so.FindProperty("limbWeight.chairSeatHeightOffset"));
{ container.Add(chairSlider);
var limbSO = CreateTrackedSO(limb);
var chairSlider = new Slider("의자 앉기 높이", -1f, 1f) { showInputField = true, tooltip = "의자에 앉을 때 엉덩이 높이 조정 (월드 Y 기준)" };
chairSlider.BindProperty(limbSO.FindProperty("chairSeatHeightOffset"));
container.Add(chairSlider);
chairSlider.Bind(limbSO);
}
// 다리 길이 자동 보정 버튼 // 다리 길이 자동 보정 버튼
var autoHipsBtn = new Button(() => var autoHipsBtn = new Button(() =>
@ -403,48 +387,28 @@ public class RetargetingControlWindow : EditorWindow
// ========== Finger Control ========== // ========== Finger Control ==========
private VisualElement BuildFingerControlSection(CustomRetargetingScript script) private VisualElement BuildFingerControlSection(CustomRetargetingScript script, SerializedObject so)
{ {
var foldout = new Foldout { text = "손가락 제어 설정", value = false }; var foldout = new Foldout { text = "손가락 제어 설정", value = false };
var fingerController = script.GetComponent<FingerShapedController>();
if (fingerController == null)
{
foldout.Add(new HelpBox("FingerShapedController가 없습니다.", HelpBoxMessageType.Warning));
var addBtn = new Button(() =>
{
script.gameObject.AddComponent<FingerShapedController>();
EditorUtility.SetDirty(script.gameObject);
RebuildCharacterPanels();
}) { text = "FingerShapedController 추가" };
foldout.Add(addBtn);
return foldout;
}
var fso = CreateTrackedSO(fingerController);
var container = new VisualElement(); var container = new VisualElement();
// 활성화 토글 // 활성화 토글
var enableToggle = new Toggle("손가락 제어 활성화") { value = fingerController.enabled }; var enabledProp = so.FindProperty("fingerShaped.enabled");
enableToggle.RegisterValueChangedCallback(evt => var enableToggle = new PropertyField(enabledProp, "손가락 제어 활성화");
{
fingerController.enabled = evt.newValue;
EditorUtility.SetDirty(fingerController);
});
container.Add(enableToggle); container.Add(enableToggle);
// 왼손 // 왼손
container.Add(BuildHandSection("왼손", "left", fso, fingerController)); container.Add(BuildHandSection("왼손", "left", so, script.fingerShaped));
// 오른손 // 오른손
container.Add(BuildHandSection("오른손", "right", fso, fingerController)); container.Add(BuildHandSection("오른손", "right", so, script.fingerShaped));
// 프리셋 // 프리셋
container.Add(BuildFingerPresets(fingerController)); container.Add(BuildFingerPresets(script, script.fingerShaped));
container.Bind(fso);
foldout.Add(container); foldout.Add(container);
return foldout; return foldout;
} }
private VisualElement BuildHandSection(string label, string prefix, SerializedObject fso, FingerShapedController fc) private VisualElement BuildHandSection(string label, string prefix, SerializedObject so, FingerShapedController fc)
{ {
var box = new VisualElement(); var box = new VisualElement();
box.style.backgroundColor = new Color(0, 0, 0, 0.08f); box.style.backgroundColor = new Color(0, 0, 0, 0.08f);
@ -456,13 +420,13 @@ public class RetargetingControlWindow : EditorWindow
var header = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center } }; var header = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center } };
var handFoldout = new Foldout { text = label, value = false }; var handFoldout = new Foldout { text = label, value = false };
var handEnabledProp = fso.FindProperty($"{prefix}HandEnabled"); var handEnabledProp = so.FindProperty($"fingerShaped.{prefix}HandEnabled");
header.Add(new PropertyField(handEnabledProp, "활성화") { style = { flexGrow = 1 } }); header.Add(new PropertyField(handEnabledProp, "활성화") { style = { flexGrow = 1 } });
var resetBtn = new Button(() => var resetBtn = new Button(() =>
{ {
string[] props = { "ThumbCurl", "IndexCurl", "MiddleCurl", "RingCurl", "PinkyCurl", "SpreadFingers" }; string[] props = { "ThumbCurl", "IndexCurl", "MiddleCurl", "RingCurl", "PinkyCurl", "SpreadFingers" };
foreach (var p in props) fso.FindProperty($"{prefix}{p}").floatValue = 0f; foreach (var p in props) so.FindProperty($"fingerShaped.{prefix}{p}").floatValue = 0f;
fso.ApplyModifiedProperties(); so.ApplyModifiedProperties();
}) { text = "초기화" }; }) { text = "초기화" };
resetBtn.style.width = 60; resetBtn.style.width = 60;
header.Add(resetBtn); header.Add(resetBtn);
@ -478,7 +442,7 @@ public class RetargetingControlWindow : EditorWindow
var col = new VisualElement { style = { alignItems = Align.Center, width = 45, marginLeft = 2, marginRight = 2 } }; var col = new VisualElement { style = { alignItems = Align.Center, width = 45, marginLeft = 2, marginRight = 2 } };
col.Add(new Label(korNames[i]) { style = { fontSize = 10, color = new Color(0.6f, 0.6f, 0.6f) } }); col.Add(new Label(korNames[i]) { style = { fontSize = 10, color = new Color(0.6f, 0.6f, 0.6f) } });
var prop = fso.FindProperty($"{prefix}{fingerNames[i]}Curl"); var prop = so.FindProperty($"fingerShaped.{prefix}{fingerNames[i]}Curl");
var valLabel = new Label(prop.floatValue.ToString("F1")) { style = { fontSize = 10, color = new Color(0.6f, 0.6f, 0.6f) } }; var valLabel = new Label(prop.floatValue.ToString("F1")) { style = { fontSize = 10, color = new Color(0.6f, 0.6f, 0.6f) } };
col.Add(valLabel); col.Add(valLabel);
@ -495,16 +459,18 @@ public class RetargetingControlWindow : EditorWindow
// 벌리기 // 벌리기
var spreadSlider = new Slider("벌리기", -1f, 1f) { showInputField = true }; var spreadSlider = new Slider("벌리기", -1f, 1f) { showInputField = true };
spreadSlider.BindProperty(fso.FindProperty($"{prefix}SpreadFingers")); spreadSlider.BindProperty(so.FindProperty($"fingerShaped.{prefix}SpreadFingers"));
handFoldout.Add(spreadSlider); handFoldout.Add(spreadSlider);
// 비활성 시 숨김 // 비활성 시 숨김
handFoldout.schedule.Execute(() => handFoldout.schedule.Execute(() =>
{ {
try { if (fso == null || fso.targetObject == null) return; } try { if (so == null || so.targetObject == null) return; }
catch (System.Exception) { return; } catch (System.Exception) { return; }
fso.Update(); so.Update();
bool enabled = fso.FindProperty($"{prefix}HandEnabled").boolValue; var enabledProp = so.FindProperty($"fingerShaped.{prefix}HandEnabled");
if (enabledProp == null) return;
bool enabled = enabledProp.boolValue;
slidersRow.style.display = enabled ? DisplayStyle.Flex : DisplayStyle.None; slidersRow.style.display = enabled ? DisplayStyle.Flex : DisplayStyle.None;
spreadSlider.style.display = enabled ? DisplayStyle.Flex : DisplayStyle.None; spreadSlider.style.display = enabled ? DisplayStyle.Flex : DisplayStyle.None;
}).Every(300); }).Every(300);
@ -513,7 +479,7 @@ public class RetargetingControlWindow : EditorWindow
return box; return box;
} }
private VisualElement BuildFingerPresets(FingerShapedController controller) private VisualElement BuildFingerPresets(CustomRetargetingScript script, FingerShapedController controller)
{ {
var container = new VisualElement { style = { marginTop = 6 } }; var container = new VisualElement { style = { marginTop = 6 } };
container.Add(new Label("손 모양 프리셋") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginBottom = 4 } }); container.Add(new Label("손 모양 프리셋") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginBottom = 4 } });
@ -525,7 +491,7 @@ public class RetargetingControlWindow : EditorWindow
for (int col = 0; col < 3; col++) for (int col = 0; col < 3; col++)
{ {
string name = presets[row, col]; string name = presets[row, col];
var btn = new Button(() => ApplyFingerPreset(controller, name)) { text = name }; var btn = new Button(() => ApplyFingerPreset(script, controller, name)) { text = name };
btn.style.height = 30; btn.style.width = 100; btn.style.marginLeft = btn.style.marginRight = 4; btn.style.height = 30; btn.style.width = 100; btn.style.marginLeft = btn.style.marginRight = 4;
btnRow.Add(btn); btnRow.Add(btn);
} }
@ -606,19 +572,7 @@ public class RetargetingControlWindow : EditorWindow
private VisualElement BuildPropSection(CustomRetargetingScript script) private VisualElement BuildPropSection(CustomRetargetingScript script)
{ {
var foldout = new Foldout { text = "프랍 설정", value = false }; var foldout = new Foldout { text = "프랍 설정", value = false };
var propController = script.GetComponent<PropLocationController>(); var propController = script.propLocation;
if (propController == null)
{
foldout.Add(new HelpBox("PropLocationController가 없습니다.", HelpBoxMessageType.Warning));
var addBtn = new Button(() =>
{
script.gameObject.AddComponent<PropLocationController>();
EditorUtility.SetDirty(script.gameObject);
RebuildCharacterPanels();
}) { text = "PropLocationController 추가" };
foldout.Add(addBtn);
return foldout;
}
var dynamicContainer = new VisualElement(); var dynamicContainer = new VisualElement();
foldout.Add(dynamicContainer); foldout.Add(dynamicContainer);
@ -848,7 +802,7 @@ public class RetargetingControlWindow : EditorWindow
return container; return container;
} }
private void ApplyFingerPreset(FingerShapedController controller, string presetName) private void ApplyFingerPreset(CustomRetargetingScript script, FingerShapedController controller, string presetName)
{ {
if (!controller.enabled) controller.enabled = true; if (!controller.enabled) controller.enabled = true;
@ -873,7 +827,7 @@ public class RetargetingControlWindow : EditorWindow
controller.rightThumbCurl = t; controller.rightIndexCurl = i; controller.rightMiddleCurl = m; controller.rightThumbCurl = t; controller.rightIndexCurl = i; controller.rightMiddleCurl = m;
controller.rightRingCurl = r; controller.rightPinkyCurl = p; controller.rightSpreadFingers = s; controller.rightRingCurl = r; controller.rightPinkyCurl = p; controller.rightSpreadFingers = s;
} }
EditorUtility.SetDirty(controller); EditorUtility.SetDirty(script);
} }
// ========== Head Calibration ========== // ========== Head Calibration ==========

View File

@ -1,168 +1,173 @@
using UnityEngine; using UnityEngine;
using System.Collections.Generic; using System.Collections.Generic;
[DefaultExecutionOrder(2)] namespace KindRetargeting
public class FingerShapedController : MonoBehaviour
{ {
private Animator animator; [System.Serializable]
private HumanPoseHandler humanPoseHandler; public class FingerShapedController
// 손가락을 제외한 모든 본의 로컬 회전 저장용 (SetHumanPose 호출 시 몸 복원용)
private Dictionary<HumanBodyBones, Quaternion> savedBoneLocalRotations = new Dictionary<HumanBodyBones, Quaternion>();
// 손가락을 제외한 모든 휴먼본 목록
private static readonly HumanBodyBones[] nonFingerBones = new HumanBodyBones[]
{ {
HumanBodyBones.Hips, private Animator animator;
HumanBodyBones.Spine, private HumanPoseHandler humanPoseHandler;
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
};
[Header("왼손 제어 값")] // 손가락을 제외한 모든 본의 로컬 회전 저장용 (SetHumanPose 호출 시 몸 복원용)
[Range(-1, 1)] public float leftPinkyCurl; // 새끼손가락 구부리기 private Dictionary<HumanBodyBones, Quaternion> savedBoneLocalRotations = new Dictionary<HumanBodyBones, Quaternion>();
[Range(-1, 1)] public float leftRingCurl; // 약지 구부리기
[Range(-1, 1)] public float leftMiddleCurl; // 중지 구부리기
[Range(-1, 1)] public float leftIndexCurl; // 검지 구부리기
[Range(-1, 1)] public float leftThumbCurl; // 엄지 구부리기
[Range(-1, 1)] public float leftSpreadFingers; // 손가락 벌리기
[Header("오른손 제어 값")] // 손가락을 제외한 모든 휴먼본 목록
[Range(-1, 1)] public float rightPinkyCurl; // 새끼손가락 구부리기 private static readonly HumanBodyBones[] nonFingerBones = new HumanBodyBones[]
[Range(-1, 1)] public float rightRingCurl; // 약지 구부리기
[Range(-1, 1)] public float rightMiddleCurl; // 중지 구부리기
[Range(-1, 1)] public float rightIndexCurl; // 검지 구부리기
[Range(-1, 1)] public float rightThumbCurl; // 엄지 구부리기
[Range(-1, 1)] public float rightSpreadFingers; // 손가락 벌리기
public bool leftHandEnabled = false; // 왼손 제어 활성화 상태
public bool rightHandEnabled = false; // 오른손 제어 활성화 상태
private void Reset()
{
// 컴포넌트가 처음 추가될 때 자동으로 비활성화
enabled = false;
leftHandEnabled = false;
rightHandEnabled = false;
}
private void Awake()
{
animator = GetComponent<Animator>();
humanPoseHandler = new HumanPoseHandler(animator.avatar, animator.transform);
}
private void OnDestroy()
{
if (humanPoseHandler != null)
{ {
humanPoseHandler.Dispose(); 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
};
public bool enabled = false;
[Header("왼손 제어 값")]
[Range(-1, 1)] public float leftPinkyCurl; // 새끼손가락 구부리기
[Range(-1, 1)] public float leftRingCurl; // 약지 구부리기
[Range(-1, 1)] public float leftMiddleCurl; // 중지 구부리기
[Range(-1, 1)] public float leftIndexCurl; // 검지 구부리기
[Range(-1, 1)] public float leftThumbCurl; // 엄지 구부리기
[Range(-1, 1)] public float leftSpreadFingers; // 손가락 벌리기
[Header("오른손 제어 값")]
[Range(-1, 1)] public float rightPinkyCurl; // 새끼손가락 구부리기
[Range(-1, 1)] public float rightRingCurl; // 약지 구부리기
[Range(-1, 1)] public float rightMiddleCurl; // 중지 구부리기
[Range(-1, 1)] public float rightIndexCurl; // 검지 구부리기
[Range(-1, 1)] public float rightThumbCurl; // 엄지 구부리기
[Range(-1, 1)] public float rightSpreadFingers; // 손가락 벌리기
public bool leftHandEnabled = false; // 왼손 제어 활성화 상태
public bool rightHandEnabled = false; // 오른손 제어 활성화 상태
private bool isInitialized;
public void Initialize(Animator targetAnimator)
{
animator = targetAnimator;
if (animator == null || !animator.isHuman) return;
humanPoseHandler = new HumanPoseHandler(animator.avatar, animator.transform);
isInitialized = true;
} }
}
private void Update() public void Cleanup()
{
UpdateMuscleValues();
}
private void UpdateMuscleValues()
{
// 1. 손가락을 제외한 모든 본의 로컬 회전 저장 (SetHumanPose 호출 전)
savedBoneLocalRotations.Clear();
for (int i = 0; i < nonFingerBones.Length; i++)
{ {
Transform bone = animator.GetBoneTransform(nonFingerBones[i]); if (humanPoseHandler != null)
if (bone != null)
{ {
savedBoneLocalRotations[nonFingerBones[i]] = bone.localRotation; humanPoseHandler.Dispose();
humanPoseHandler = null;
}
isInitialized = false;
}
public void OnUpdate()
{
if (!isInitialized || !enabled) return;
UpdateMuscleValues();
}
private void UpdateMuscleValues()
{
// 1. 손가락을 제외한 모든 본의 로컬 회전 저장 (SetHumanPose 호출 전)
savedBoneLocalRotations.Clear();
for (int i = 0; i < nonFingerBones.Length; i++)
{
Transform bone = animator.GetBoneTransform(nonFingerBones[i]);
if (bone != null)
{
savedBoneLocalRotations[nonFingerBones[i]] = bone.localRotation;
}
}
// 2. HumanPose 가져오기 및 손가락 머슬 설정
HumanPose humanPose = new HumanPose();
humanPoseHandler.GetHumanPose(ref humanPose);
// 왼손 제어
SetHandMuscles(true, leftThumbCurl, leftIndexCurl, leftMiddleCurl, leftRingCurl,
leftPinkyCurl, leftSpreadFingers, ref humanPose);
// 오른손 제어
SetHandMuscles(false, rightThumbCurl, rightIndexCurl, rightMiddleCurl, rightRingCurl,
rightPinkyCurl, rightSpreadFingers, ref humanPose);
// 3. 머슬 포즈 적용 (손가락 포함 전체 본에 영향)
humanPoseHandler.SetHumanPose(ref humanPose);
// 4. 손가락을 제외한 모든 본의 로컬 회전 복원 (본 길이 변형 방지)
foreach (var kvp in savedBoneLocalRotations)
{
Transform bone = animator.GetBoneTransform(kvp.Key);
if (bone != null)
{
bone.localRotation = kvp.Value;
}
} }
} }
// 2. HumanPose 가져오기 및 손가락 머슬 설정 private void SetHandMuscles(bool isLeft, float thumb, float index, float middle, float ring,
HumanPose humanPose = new HumanPose(); float pinky, float spread, ref HumanPose humanPose)
humanPoseHandler.GetHumanPose(ref humanPose);
// 왼손 제어
SetHandMuscles(true, leftThumbCurl, leftIndexCurl, leftMiddleCurl, leftRingCurl,
leftPinkyCurl, leftSpreadFingers, ref humanPose);
// 오른손 제어
SetHandMuscles(false, rightThumbCurl, rightIndexCurl, rightMiddleCurl, rightRingCurl,
rightPinkyCurl, rightSpreadFingers, ref humanPose);
// 3. 머슬 포즈 적용 (손가락 포함 전체 본에 영향)
humanPoseHandler.SetHumanPose(ref humanPose);
// 4. 손가락을 제외한 모든 본의 로컬 회전 복원 (본 길이 변형 방지)
foreach (var kvp in savedBoneLocalRotations)
{ {
Transform bone = animator.GetBoneTransform(kvp.Key); // 해당 손이 비활성화 상태면 건너뛰기
if (bone != null) if (isLeft && !leftHandEnabled) return;
{ if (!isLeft && !rightHandEnabled) return;
bone.localRotation = kvp.Value;
} int baseOffset = isLeft ? 55 : 75; // 왼손은 55부터, 오른손은 75부터 시작
int muscleCount = humanPose.muscles.Length;
// 엄지손가락
if (baseOffset < muscleCount) humanPose.muscles[baseOffset] = thumb; // Thumb 1
if (baseOffset + 1 < muscleCount) humanPose.muscles[baseOffset + 1] = thumb; // Thumb Spread
if (baseOffset + 2 < muscleCount) humanPose.muscles[baseOffset + 2] = thumb; // Thumb 2
if (baseOffset + 3 < muscleCount) humanPose.muscles[baseOffset + 3] = thumb; // Thumb 3
// 검지
if (baseOffset + 4 < muscleCount) humanPose.muscles[baseOffset + 4] = index; // Index 1
if (baseOffset + 5 < muscleCount) humanPose.muscles[baseOffset + 5] = spread; // Index Spread
if (baseOffset + 6 < muscleCount) humanPose.muscles[baseOffset + 6] = index; // Index 2
if (baseOffset + 7 < muscleCount) humanPose.muscles[baseOffset + 7] = index; // Index 3
// 중지
if (baseOffset + 8 < muscleCount) humanPose.muscles[baseOffset + 8] = middle; // Middle 1
if (baseOffset + 9 < muscleCount) humanPose.muscles[baseOffset + 9] = spread; // Middle Spread
if (baseOffset + 10 < muscleCount) humanPose.muscles[baseOffset + 10] = middle; // Middle 2
if (baseOffset + 11 < muscleCount) humanPose.muscles[baseOffset + 11] = middle; // Middle 3
// 약지
if (baseOffset + 12 < muscleCount) humanPose.muscles[baseOffset + 12] = ring; // Ring 1
if (baseOffset + 13 < muscleCount) humanPose.muscles[baseOffset + 13] = spread; // Ring Spread
if (baseOffset + 14 < muscleCount) humanPose.muscles[baseOffset + 14] = ring; // Ring 2
if (baseOffset + 15 < muscleCount) humanPose.muscles[baseOffset + 15] = ring; // Ring 3
// 새끼손가락
if (baseOffset + 16 < muscleCount) humanPose.muscles[baseOffset + 16] = pinky; // Little 1
if (baseOffset + 17 < muscleCount) humanPose.muscles[baseOffset + 17] = spread; // Little Spread
if (baseOffset + 18 < muscleCount) humanPose.muscles[baseOffset + 18] = pinky; // Little 2
if (baseOffset + 19 < muscleCount) humanPose.muscles[baseOffset + 19] = pinky; // Little 3
} }
} }
private void SetHandMuscles(bool isLeft, float thumb, float index, float middle, float ring,
float pinky, float spread, ref HumanPose humanPose)
{
// 해당 손이 비활성화 상태면 건너뛰기
if (isLeft && !leftHandEnabled) return;
if (!isLeft && !rightHandEnabled) return;
int baseOffset = isLeft ? 55 : 75; // 왼손은 55부터, 오른손은 75부터 시작
int muscleCount = humanPose.muscles.Length;
// 엄지손가락
if (baseOffset < muscleCount) humanPose.muscles[baseOffset] = thumb; // Thumb 1
if (baseOffset + 1 < muscleCount) humanPose.muscles[baseOffset + 1] = thumb; // Thumb Spread
if (baseOffset + 2 < muscleCount) humanPose.muscles[baseOffset + 2] = thumb; // Thumb 2
if (baseOffset + 3 < muscleCount) humanPose.muscles[baseOffset + 3] = thumb; // Thumb 3
// 검지
if (baseOffset + 4 < muscleCount) humanPose.muscles[baseOffset + 4] = index; // Index 1
if (baseOffset + 5 < muscleCount) humanPose.muscles[baseOffset + 5] = spread; // Index Spread
if (baseOffset + 6 < muscleCount) humanPose.muscles[baseOffset + 6] = index; // Index 2
if (baseOffset + 7 < muscleCount) humanPose.muscles[baseOffset + 7] = index; // Index 3
// 중지
if (baseOffset + 8 < muscleCount) humanPose.muscles[baseOffset + 8] = middle; // Middle 1
if (baseOffset + 9 < muscleCount) humanPose.muscles[baseOffset + 9] = spread; // Middle Spread
if (baseOffset + 10 < muscleCount) humanPose.muscles[baseOffset + 10] = middle; // Middle 2
if (baseOffset + 11 < muscleCount) humanPose.muscles[baseOffset + 11] = middle; // Middle 3
// 약지
if (baseOffset + 12 < muscleCount) humanPose.muscles[baseOffset + 12] = ring; // Ring 1
if (baseOffset + 13 < muscleCount) humanPose.muscles[baseOffset + 13] = spread; // Ring Spread
if (baseOffset + 14 < muscleCount) humanPose.muscles[baseOffset + 14] = ring; // Ring 2
if (baseOffset + 15 < muscleCount) humanPose.muscles[baseOffset + 15] = ring; // Ring 3
// 새끼손가락
if (baseOffset + 16 < muscleCount) humanPose.muscles[baseOffset + 16] = pinky; // Little 1
if (baseOffset + 17 < muscleCount) humanPose.muscles[baseOffset + 17] = spread; // Little Spread
if (baseOffset + 18 < muscleCount) humanPose.muscles[baseOffset + 18] = pinky; // Little 2
if (baseOffset + 19 < muscleCount) humanPose.muscles[baseOffset + 19] = pinky; // Little 3
}
} }

View File

@ -5,19 +5,14 @@ namespace KindRetargeting
/// <summary> /// <summary>
/// HIK 스타일 2-Pass 접지 시스템. /// HIK 스타일 2-Pass 접지 시스템.
/// ///
/// Pass 1 (Update, Order 5 → IK 전): /// Pass 1 (OnUpdate, Order 5 → IK 전):
/// IK 타겟 위치를 수정하여 Toes가 바닥을 뚫지 않도록 보정. /// IK 타겟 위치를 수정하여 Toes가 바닥을 뚫지 않도록 보정.
/// Toe Pivot 감지: 발끝이 바닥에 있고 발목이 올라가면
/// 발목 타겟을 역산하여 Toes가 groundHeight에 고정.
/// ///
/// Pass 2 (LateUpdate → IK 후): /// Pass 2 (OnLateUpdate → IK 후):
/// IK 결과의 잔차를 Foot 회전으로 미세 보정. /// IK 결과의 잔차를 Foot 회전으로 미세 보정.
/// 위치 변경 없음 — 본 길이 보존.
///
/// 힙 높이 보정은 CRS의 floorHeight가 담당합니다 (이중 보정 방지).
/// </summary> /// </summary>
[DefaultExecutionOrder(5)] [System.Serializable]
public class FootGroundingController : MonoBehaviour public class FootGroundingController
{ {
[Header("접지 설정")] [Header("접지 설정")]
[Tooltip("바닥 Y 좌표 (월드 공간)")] [Tooltip("바닥 Y 좌표 (월드 공간)")]
@ -41,37 +36,31 @@ namespace KindRetargeting
private TwoBoneIKSolver ikSolver; private TwoBoneIKSolver ikSolver;
private Animator animator; private Animator animator;
// 타겟 아바타 캐싱
private Transform leftFoot; private Transform leftFoot;
private Transform rightFoot; private Transform rightFoot;
private Transform leftToes; private Transform leftToes;
private Transform rightToes; private Transform rightToes;
// Toes의 Foot 로컬 오프셋 (T-pose에서 캐싱)
private Vector3 leftLocalToesOffset; private Vector3 leftLocalToesOffset;
private Vector3 rightLocalToesOffset; private Vector3 rightLocalToesOffset;
// flat 상태에서 발목 최소 높이 (Foot.y - Toes.y)
private float leftFootHeight; private float leftFootHeight;
private float rightFootHeight; private float rightFootHeight;
// Toes 본 존재 여부
private bool leftHasToes; private bool leftHasToes;
private bool rightHasToes; private bool rightHasToes;
// 스무딩용: 이전 프레임 보정량
private float leftPrevAdj; private float leftPrevAdj;
private float rightPrevAdj; private float rightPrevAdj;
private bool isInitialized; private bool isInitialized;
private void Start() public void Initialize(TwoBoneIKSolver ikSolver, Animator animator)
{ {
ikSolver = GetComponent<TwoBoneIKSolver>(); this.ikSolver = ikSolver;
animator = GetComponent<Animator>(); this.animator = animator;
if (animator == null || !animator.isHuman || ikSolver == null) return; if (animator == null || !animator.isHuman || ikSolver == null) return;
if (leftFoot == null && rightFoot == null) return;
leftFoot = animator.GetBoneTransform(HumanBodyBones.LeftFoot); leftFoot = animator.GetBoneTransform(HumanBodyBones.LeftFoot);
rightFoot = animator.GetBoneTransform(HumanBodyBones.RightFoot); rightFoot = animator.GetBoneTransform(HumanBodyBones.RightFoot);
@ -80,7 +69,6 @@ namespace KindRetargeting
if (leftFoot == null || rightFoot == null) return; if (leftFoot == null || rightFoot == null) return;
// Toes 존재 여부 + 캐싱
leftHasToes = leftToes != null; leftHasToes = leftToes != null;
rightHasToes = rightToes != null; rightHasToes = rightToes != null;
@ -91,7 +79,7 @@ namespace KindRetargeting
} }
else else
{ {
leftFootHeight = 0.05f; // Toes 없을 때 기본 발목 높이 leftFootHeight = 0.05f;
} }
if (rightHasToes) if (rightHasToes)
@ -110,7 +98,7 @@ namespace KindRetargeting
/// <summary> /// <summary>
/// Pass 1: Pre-IK — IK 타겟 위치를 수정합니다. /// Pass 1: Pre-IK — IK 타겟 위치를 수정합니다.
/// </summary> /// </summary>
private void Update() public void OnUpdate()
{ {
if (!isInitialized || groundingWeight < 0.001f) return; if (!isInitialized || groundingWeight < 0.001f) return;
@ -121,16 +109,11 @@ namespace KindRetargeting
ikSolver.rightLeg, rightLocalToesOffset, rightFootHeight, ikSolver.rightLeg, rightLocalToesOffset, rightFootHeight,
rightHasToes, ikSolver.rightLeg.positionWeight); rightHasToes, ikSolver.rightLeg.positionWeight);
// 스무딩: 보정량 급변 방지
float dt = Time.deltaTime * smoothSpeed; float dt = Time.deltaTime * smoothSpeed;
leftPrevAdj = Mathf.Lerp(leftPrevAdj, leftAdj, Mathf.Clamp01(dt)); leftPrevAdj = Mathf.Lerp(leftPrevAdj, leftAdj, Mathf.Clamp01(dt));
rightPrevAdj = Mathf.Lerp(rightPrevAdj, rightAdj, Mathf.Clamp01(dt)); rightPrevAdj = Mathf.Lerp(rightPrevAdj, rightAdj, Mathf.Clamp01(dt));
} }
/// <summary>
/// 발 IK 타겟을 접지 모드에 따라 보정합니다.
/// Toes가 없는 아바타는 발목 Y 클램프만 수행합니다.
/// </summary>
private float AdjustFootTarget(TwoBoneIKSolver.LimbIK limb, Vector3 localToesOffset, private float AdjustFootTarget(TwoBoneIKSolver.LimbIK limb, Vector3 localToesOffset,
float footHeight, bool hasToes, float ikWeight) float footHeight, bool hasToes, float ikWeight)
{ {
@ -140,12 +123,10 @@ namespace KindRetargeting
Vector3 anklePos = ankleTarget.position; Vector3 anklePos = ankleTarget.position;
float ankleY = anklePos.y; float ankleY = anklePos.y;
// AIRBORNE 체크
if (ankleY - groundHeight > activationHeight) return 0f; if (ankleY - groundHeight > activationHeight) return 0f;
float weight = groundingWeight * ikWeight; float weight = groundingWeight * ikWeight;
// === Toes 없는 아바타: 단순 Y 클램프 ===
if (!hasToes) if (!hasToes)
{ {
float minAnkleY = groundHeight + footHeight; float minAnkleY = groundHeight + footHeight;
@ -159,7 +140,6 @@ namespace KindRetargeting
return 0f; return 0f;
} }
// === Toes 있는 아바타: 예측 기반 보정 ===
Vector3 predictedToesWorld = anklePos + ankleTarget.rotation * localToesOffset; Vector3 predictedToesWorld = anklePos + ankleTarget.rotation * localToesOffset;
float predictedToesY = predictedToesWorld.y; float predictedToesY = predictedToesWorld.y;
@ -171,7 +151,6 @@ namespace KindRetargeting
if (ankleY < groundHeight + footHeight + plantThreshold) if (ankleY < groundHeight + footHeight + plantThreshold)
{ {
// PLANTED: 발 전체가 바닥 근처
float minAnkleY = groundHeight + footHeight; float minAnkleY = groundHeight + footHeight;
if (ankleY < minAnkleY) if (ankleY < minAnkleY)
{ {
@ -188,7 +167,6 @@ namespace KindRetargeting
} }
else else
{ {
// TOE_PIVOT: 발끝 고정, 발목 올라감
if (toesError > 0f) if (toesError > 0f)
{ {
adjustment = toesError * weight; adjustment = toesError * weight;
@ -200,7 +178,6 @@ namespace KindRetargeting
} }
else else
{ {
// Toes 충분히 위 → 발목만 바닥 아래 방지
float minAnkleY = groundHeight + footHeight; float minAnkleY = groundHeight + footHeight;
if (ankleY < minAnkleY) if (ankleY < minAnkleY)
{ {
@ -216,7 +193,7 @@ namespace KindRetargeting
/// <summary> /// <summary>
/// Pass 2: Post-IK — 잔차를 Foot 회전으로 미세 보정합니다. /// Pass 2: Post-IK — 잔차를 Foot 회전으로 미세 보정합니다.
/// </summary> /// </summary>
private void LateUpdate() public void OnLateUpdate()
{ {
if (!isInitialized || groundingWeight < 0.001f) return; if (!isInitialized || groundingWeight < 0.001f) return;
@ -226,10 +203,6 @@ namespace KindRetargeting
AlignFootToGround(rightFoot, rightToes, ikSolver.rightLeg.positionWeight); AlignFootToGround(rightFoot, rightToes, ikSolver.rightLeg.positionWeight);
} }
/// <summary>
/// IK 후 실제 Toes 위치를 확인하고, Foot 본을 pitch 회전하여 잔차 보정.
/// 바닥 아래로 뚫린 경우만 보정합니다.
/// </summary>
private void AlignFootToGround(Transform foot, Transform toes, float ikWeight) private void AlignFootToGround(Transform foot, Transform toes, float ikWeight)
{ {
if (foot == null || toes == null) return; if (foot == null || toes == null) return;
@ -244,7 +217,6 @@ namespace KindRetargeting
if (Mathf.Abs(error) < 0.001f) return; if (Mathf.Abs(error) < 0.001f) return;
// 바닥 아래로 뚫린 경우만 보정
if (error > plantThreshold) return; if (error > plantThreshold) return;
Vector3 footToToes = toes.position - foot.position; Vector3 footToToes = toes.position - foot.position;

View File

@ -4,8 +4,8 @@ using UnityEngine;
namespace KindRetargeting namespace KindRetargeting
{ {
[DefaultExecutionOrder(4)] [System.Serializable]
public class LimbWeightController : MonoBehaviour public class LimbWeightController
{ {
[Header("거리 기반 가중치 설정")] [Header("거리 기반 가중치 설정")]
[SerializeField, Range(0.3f, 1f)] public float maxDistance = 0.5f; // 가중치가 0이 되는 최대 거리 [SerializeField, Range(0.3f, 1f)] public float maxDistance = 0.5f; // 가중치가 0이 되는 최대 거리
@ -36,9 +36,8 @@ namespace KindRetargeting
public float chairSeatHeightOffset = 0.05f; public float chairSeatHeightOffset = 0.05f;
private TwoBoneIKSolver ikSolver; private TwoBoneIKSolver ikSolver;
private CustomRetargetingScript crs; private CustomRetargetingScript crs;
private Dictionary<string, Dictionary<int, float>> weightLayers = new Dictionary<string, Dictionary<int, float>>(); private Transform characterRoot;
List<float> leftArmEndWeights = new List<float>(); List<float> leftArmEndWeights = new List<float>();
List<float> rightArmEndWeights = new List<float>(); List<float> rightArmEndWeights = new List<float>();
@ -57,8 +56,6 @@ namespace KindRetargeting
public List<Transform> props = new List<Transform>(); public List<Transform> props = new List<Transform>();
public Transform characterRoot;
// 힙스 가중치 리스트 추가 // 힙스 가중치 리스트 추가
List<float> hipsWeights = new List<float>(); List<float> hipsWeights = new List<float>();
private float MasterHipsWeight = 1f; private float MasterHipsWeight = 1f;
@ -67,8 +64,63 @@ namespace KindRetargeting
private float currentChairSeatOffset = 0f; private float currentChairSeatOffset = 0f;
private float targetChairSeatOffset = 0f; private float targetChairSeatOffset = 0f;
void Update() private bool isInitialized;
public void Initialize(TwoBoneIKSolver ikSolver, CustomRetargetingScript crs, Transform characterRoot)
{ {
this.ikSolver = ikSolver;
this.crs = crs;
this.characterRoot = characterRoot;
if (ikSolver == null || crs == null) return;
InitWeightLayers();
//프랍 오브젝트 찾기
props = Object.FindObjectsByType<PropTypeController>(FindObjectsSortMode.None).Select(controller => controller.transform).ToList();
// 다른 캐릭터의 손을 props에 추가
GetHand();
//HandDistances()에서 사용을 위한 리스트 추가
//손 거리에 따른 웨이트 업데이트 인덱스 0번
leftArmEndWeights.Add(0);
rightArmEndWeights.Add(0);
// 프랍과의 거리에 따른 웨이트 업데이트 인덱스 1번
leftArmEndWeights.Add(0);
rightArmEndWeights.Add(0);
// 앉아있을 때 다리와의 거리에 따른 가중치 적용 인덱스 0번
leftLegEndWeights.Add(0);
rightLegEndWeights.Add(0);
// 다리 골 가중치 초기화
leftLegBendWeights.Add(1f); // 기본 가중치
rightLegBendWeights.Add(1f); // 기본 가중치
if (this.characterRoot == null)
{
this.characterRoot = crs.transform;
}
// 힙스 가중치 초기화 인덱스 0번
hipsWeights.Add(1f); // 의자 거리 기반 가중치
// 지면 높이 기반 가중치 초기화 인덱스 1번
hipsWeights.Add(1f); // 지면 높이 기반 가중치
// 발 높이 기반 가중치 초기화 인덱스 1번
leftLegEndWeights.Add(1f);
rightLegEndWeights.Add(1f);
isInitialized = true;
}
public void OnUpdate()
{
if (!isInitialized) return;
//손의 거리를 기반으로한 가중치 적용 //손의 거리를 기반으로한 가중치 적용
HandDistances(); HandDistances();
@ -94,69 +146,15 @@ namespace KindRetargeting
ApplyWeightsToFBIK(); ApplyWeightsToFBIK();
} }
void Start()
{
ikSolver = GetComponent<TwoBoneIKSolver>();
crs = GetComponent<CustomRetargetingScript>();
InitWeightLayers();
//프랍 오브젝트 찾기
props = FindObjectsByType<PropTypeController>(FindObjectsSortMode.None).Select(controller => controller.transform).ToList();
// 프랍 오브젝트 찾기
GetHand();
//HandDistances();에서 사용을 위한 리스트 추가
//손 거리에 따른 웨이트 업데이트 인덱스 0번
leftArmEndWeights.Add(0);
rightArmEndWeights.Add(0);
// 프랍과의 거리에 따른 웨이트 업데이트 인덱스 1번
leftArmEndWeights.Add(0);
rightArmEndWeights.Add(0);
// 앉아있을 때 다리와의 거리에 따른 가중치 적용 인덱스 0번
leftLegEndWeights.Add(0);
rightLegEndWeights.Add(0);
// 다리 골 가중치 초기화
leftLegBendWeights.Add(1f); // 기본 가중치
rightLegBendWeights.Add(1f); // 기본 가중치
// CharacterController가 있는 루트 오브젝트 찾기
if (characterRoot == null)
{
characterRoot = transform;
}
// 힙스 가중치 초기화 인덱스 0번
hipsWeights.Add(1f); // 의자 거리 기반 가중치
// 지면 높이 기반 가중치 초기화 인덱스 1번
hipsWeights.Add(1f); // 지면 높이 기반 가중치
// 발 높이 기반 가중치 초기화 인덱스 1번
leftLegEndWeights.Add(1f);
rightLegEndWeights.Add(1f);
}
private void GetHand() private void GetHand()
{ {
// 모든 LimbWeightController 찾기 // 모든 CustomRetargetingScript 찾기 (다른 캐릭터의 손을 props에 추가)
LimbWeightController[] allControllers = FindObjectsOfType<LimbWeightController>(); CustomRetargetingScript[] allCrs = Object.FindObjectsByType<CustomRetargetingScript>(FindObjectsSortMode.None);
foreach (LimbWeightController controller in allControllers) foreach (CustomRetargetingScript otherCrs in allCrs)
{ {
// 자기 자신은 제외 // 자기 자신은 제외
if (controller == this) continue; if (otherCrs == crs) continue;
// CustomRetargetingScript 가져오기
CustomRetargetingScript otherCrs = controller.GetComponent<CustomRetargetingScript>();
if (otherCrs == null) continue;
// 왼손과 오른손 Transform 가져오기 // 왼손과 오른손 Transform 가져오기
Transform leftHand = otherCrs?.sourceAnimator?.GetBoneTransform(HumanBodyBones.LeftHand); Transform leftHand = otherCrs?.sourceAnimator?.GetBoneTransform(HumanBodyBones.LeftHand);

View File

@ -4,7 +4,8 @@ using UniHumanoid;
namespace KindRetargeting namespace KindRetargeting
{ {
public class PropLocationController : MonoBehaviour [System.Serializable]
public class PropLocationController
{ {
// 캐시된 타겟과 오프셋 Transform // 캐시된 타겟과 오프셋 Transform
[System.Serializable] [System.Serializable]
@ -18,9 +19,9 @@ namespace KindRetargeting
[SerializeField] private TargetOffset rightHandTargetOffset; [SerializeField] private TargetOffset rightHandTargetOffset;
[SerializeField] private TargetOffset headTargetOffset; [SerializeField] private TargetOffset headTargetOffset;
private void Start() public void Initialize(Animator animator)
{ {
CreateTargets(); CreateTargets(animator);
} }
public void SetTPose(Animator animator) public void SetTPose(Animator animator)
@ -31,7 +32,6 @@ namespace KindRetargeting
Avatar avatar = animator.avatar; Avatar avatar = animator.avatar;
Transform transform = animator.transform; Transform transform = animator.transform;
// HumanPoseClip에 저장된 T-포즈 데이터를 로드하여 적용
var humanPoseClip = Resources.Load<HumanPoseClip>(HumanPoseClip.TPoseResourcePath); var humanPoseClip = Resources.Load<HumanPoseClip>(HumanPoseClip.TPoseResourcePath);
if (humanPoseClip != null) if (humanPoseClip != null)
{ {
@ -43,9 +43,9 @@ namespace KindRetargeting
Debug.LogWarning("T-Pose 데이터가 존재하지 않습니다."); Debug.LogWarning("T-Pose 데이터가 존재하지 않습니다.");
} }
} }
private void CreateTargets()
private void CreateTargets(Animator animator)
{ {
Animator animator = GetComponent<Animator>();
SetTPose(animator); SetTPose(animator);
// 왼손 타겟 및 오프셋 설정 // 왼손 타겟 및 오프셋 설정
@ -54,7 +54,7 @@ namespace KindRetargeting
{ {
leftHandTargetOffset = new TargetOffset(); leftHandTargetOffset = new TargetOffset();
GameObject leftTarget = new GameObject("Left_Hand_Target"); GameObject leftTarget = new GameObject("Left_Hand_Target");
leftTarget.transform.parent = leftHandBone; // 왼손 본에 직접 부모 설정 leftTarget.transform.parent = leftHandBone;
leftHandTargetOffset.target = leftTarget.transform; leftHandTargetOffset.target = leftTarget.transform;
leftTarget.transform.position = leftHandBone.position + new Vector3(-0.039f, -0.022f, 0f); leftTarget.transform.position = leftHandBone.position + new Vector3(-0.039f, -0.022f, 0f);
leftTarget.transform.rotation = Quaternion.Euler(90f, 0f, 0f); leftTarget.transform.rotation = Quaternion.Euler(90f, 0f, 0f);
@ -63,7 +63,6 @@ namespace KindRetargeting
leftOffset.transform.parent = leftTarget.transform; leftOffset.transform.parent = leftTarget.transform;
leftHandTargetOffset.offset = leftOffset.transform; leftHandTargetOffset.offset = leftOffset.transform;
// 로컬 포지션과 로테이션 설정
leftHandTargetOffset.offset.localPosition = Vector3.zero; leftHandTargetOffset.offset.localPosition = Vector3.zero;
leftHandTargetOffset.offset.localRotation = Quaternion.identity; leftHandTargetOffset.offset.localRotation = Quaternion.identity;
} }
@ -74,7 +73,7 @@ namespace KindRetargeting
{ {
rightHandTargetOffset = new TargetOffset(); rightHandTargetOffset = new TargetOffset();
GameObject rightTarget = new GameObject("Right_Hand_Target"); GameObject rightTarget = new GameObject("Right_Hand_Target");
rightTarget.transform.parent = rightHandBone; // 오른손 본에 직접 부모 설정 rightTarget.transform.parent = rightHandBone;
rightHandTargetOffset.target = rightTarget.transform; rightHandTargetOffset.target = rightTarget.transform;
rightTarget.transform.position = rightHandBone.position + new Vector3(0.039f, -0.022f, 0f); rightTarget.transform.position = rightHandBone.position + new Vector3(0.039f, -0.022f, 0f);
rightTarget.transform.rotation = Quaternion.Euler(90f, 0f, 0f); rightTarget.transform.rotation = Quaternion.Euler(90f, 0f, 0f);
@ -83,7 +82,6 @@ namespace KindRetargeting
rightOffset.transform.parent = rightTarget.transform; rightOffset.transform.parent = rightTarget.transform;
rightHandTargetOffset.offset = rightOffset.transform; rightHandTargetOffset.offset = rightOffset.transform;
// 로컬 포지션과 로테이션 설정
rightHandTargetOffset.offset.localPosition = Vector3.zero; rightHandTargetOffset.offset.localPosition = Vector3.zero;
rightHandTargetOffset.offset.localRotation = Quaternion.identity; rightHandTargetOffset.offset.localRotation = Quaternion.identity;
} }
@ -94,7 +92,7 @@ namespace KindRetargeting
{ {
headTargetOffset = new TargetOffset(); headTargetOffset = new TargetOffset();
GameObject headTarget = new GameObject("Head_Target"); GameObject headTarget = new GameObject("Head_Target");
headTarget.transform.parent = headBone; // 머리 본에 직접 부모 설정 headTarget.transform.parent = headBone;
headTargetOffset.target = headTarget.transform; headTargetOffset.target = headTarget.transform;
headTarget.transform.position = headBone.position + new Vector3(0f, 0.16f, 0f); headTarget.transform.position = headBone.position + new Vector3(0f, 0.16f, 0f);
headTarget.transform.rotation = Quaternion.Euler(0f, 0f, 0f); headTarget.transform.rotation = Quaternion.Euler(0f, 0f, 0f);
@ -103,7 +101,6 @@ namespace KindRetargeting
headOffset.transform.parent = headTarget.transform; headOffset.transform.parent = headTarget.transform;
headTargetOffset.offset = headOffset.transform; headTargetOffset.offset = headOffset.transform;
// 기본 오프셋 설정
headTargetOffset.offset.localPosition = Vector3.zero; headTargetOffset.offset.localPosition = Vector3.zero;
headTargetOffset.offset.localRotation = Quaternion.identity; headTargetOffset.offset.localRotation = Quaternion.identity;
} }
@ -172,7 +169,6 @@ namespace KindRetargeting
} }
} }
// 에디터에서 사용할 메서드들
#if UNITY_EDITOR #if UNITY_EDITOR
public void MoveToHead() public void MoveToHead()
{ {
@ -195,7 +191,6 @@ namespace KindRetargeting
} }
#endif #endif
// 오프셋 getter 메서드들 추가
public Transform GetLeftHandOffset() public Transform GetLeftHandOffset()
{ {
return leftHandTargetOffset?.offset; return leftHandTargetOffset?.offset;

View File

@ -215,9 +215,6 @@ namespace KindRetargeting.Remote
return; return;
} }
var limbWeight = script.GetComponent<LimbWeightController>();
var handPose = script.GetComponent<FingerShapedController>();
var data = new Dictionary<string, object> var data = new Dictionary<string, object>
{ {
// 힙 위치 보정 (로컬) // 힙 위치 보정 (로컬)
@ -251,37 +248,25 @@ namespace KindRetargeting.Remote
{ "fingerCopyMode", (int)GetPrivateField<EnumsList.FingerCopyMode>(script, "fingerCopyMode") }, { "fingerCopyMode", (int)GetPrivateField<EnumsList.FingerCopyMode>(script, "fingerCopyMode") },
// 캘리브레이션 상태 // 캘리브레이션 상태
{ "hasCalibrationData", script.HasCachedSettings() } { "hasCalibrationData", script.HasCachedSettings() },
// LimbWeightController 데이터
{ "limbMinDistance", script.limbWeight.minDistance },
{ "limbMaxDistance", script.limbWeight.maxDistance },
{ "weightSmoothSpeed", script.limbWeight.weightSmoothSpeed },
{ "hipsMinDistance", script.limbWeight.hipsMinDistance },
{ "hipsMaxDistance", script.limbWeight.hipsMaxDistance },
{ "groundHipsMinHeight", script.limbWeight.groundHipsMinHeight },
{ "groundHipsMaxHeight", script.limbWeight.groundHipsMaxHeight },
{ "footHeightMinThreshold", script.limbWeight.footHeightMinThreshold },
{ "footHeightMaxThreshold", script.limbWeight.footHeightMaxThreshold },
{ "chairSeatHeightOffset", script.limbWeight.chairSeatHeightOffset },
}; };
// LimbWeightController 데이터
if (limbWeight != null)
{
data["limbMinDistance"] = limbWeight.minDistance;
data["limbMaxDistance"] = limbWeight.maxDistance;
data["weightSmoothSpeed"] = limbWeight.weightSmoothSpeed;
data["hipsMinDistance"] = limbWeight.hipsMinDistance;
data["hipsMaxDistance"] = limbWeight.hipsMaxDistance;
data["groundHipsMinHeight"] = limbWeight.groundHipsMinHeight;
data["groundHipsMaxHeight"] = limbWeight.groundHipsMaxHeight;
data["footHeightMinThreshold"] = limbWeight.footHeightMinThreshold;
data["footHeightMaxThreshold"] = limbWeight.footHeightMaxThreshold;
data["chairSeatHeightOffset"] = limbWeight.chairSeatHeightOffset;
}
// FingerShapedController 데이터 // FingerShapedController 데이터
if (handPose != null) data["handPoseEnabled"] = script.fingerShaped.enabled;
{ data["leftHandEnabled"] = script.fingerShaped.leftHandEnabled;
data["handPoseEnabled"] = handPose.enabled; data["rightHandEnabled"] = script.fingerShaped.rightHandEnabled;
data["leftHandEnabled"] = handPose.leftHandEnabled;
data["rightHandEnabled"] = handPose.rightHandEnabled;
}
else
{
data["handPoseEnabled"] = false;
data["leftHandEnabled"] = false;
data["rightHandEnabled"] = false;
}
var response = new var response = new
{ {
@ -298,9 +283,6 @@ namespace KindRetargeting.Remote
var script = FindCharacter(characterId); var script = FindCharacter(characterId);
if (script == null) return; if (script == null) return;
var limbWeight = script.GetComponent<LimbWeightController>();
var handPose = script.GetComponent<FingerShapedController>();
switch (property) switch (property)
{ {
// 힙 위치 보정 // 힙 위치 보정
@ -363,56 +345,49 @@ namespace KindRetargeting.Remote
// LimbWeightController 속성 // LimbWeightController 속성
case "limbMinDistance": case "limbMinDistance":
if (limbWeight != null) limbWeight.minDistance = value; script.limbWeight.minDistance = value;
break; break;
case "limbMaxDistance": case "limbMaxDistance":
if (limbWeight != null) limbWeight.maxDistance = value; script.limbWeight.maxDistance = value;
break; break;
case "weightSmoothSpeed": case "weightSmoothSpeed":
if (limbWeight != null) limbWeight.weightSmoothSpeed = value; script.limbWeight.weightSmoothSpeed = value;
break; break;
case "hipsMinDistance": case "hipsMinDistance":
if (limbWeight != null) limbWeight.hipsMinDistance = value; script.limbWeight.hipsMinDistance = value;
break; break;
case "hipsMaxDistance": case "hipsMaxDistance":
if (limbWeight != null) limbWeight.hipsMaxDistance = value; script.limbWeight.hipsMaxDistance = value;
break; break;
case "groundHipsMinHeight": case "groundHipsMinHeight":
if (limbWeight != null) limbWeight.groundHipsMinHeight = value; script.limbWeight.groundHipsMinHeight = value;
break; break;
case "groundHipsMaxHeight": case "groundHipsMaxHeight":
if (limbWeight != null) limbWeight.groundHipsMaxHeight = value; script.limbWeight.groundHipsMaxHeight = value;
break; break;
case "footHeightMinThreshold": case "footHeightMinThreshold":
if (limbWeight != null) limbWeight.footHeightMinThreshold = value; script.limbWeight.footHeightMinThreshold = value;
break; break;
case "footHeightMaxThreshold": case "footHeightMaxThreshold":
if (limbWeight != null) limbWeight.footHeightMaxThreshold = value; script.limbWeight.footHeightMaxThreshold = value;
break; break;
case "chairSeatHeightOffset": case "chairSeatHeightOffset":
if (limbWeight != null) limbWeight.chairSeatHeightOffset = value; script.limbWeight.chairSeatHeightOffset = value;
break; break;
// FingerShapedController 속성 // FingerShapedController 속성
case "handPoseEnabled": case "handPoseEnabled":
if (handPose != null) script.fingerShaped.enabled = value > 0.5f;
handPose.enabled = value > 0.5f;
break; break;
case "leftHandEnabled": case "leftHandEnabled":
if (handPose != null) script.fingerShaped.leftHandEnabled = value > 0.5f;
{ if (script.fingerShaped.leftHandEnabled)
handPose.leftHandEnabled = value > 0.5f; script.fingerShaped.enabled = true;
if (handPose.leftHandEnabled)
handPose.enabled = true;
}
break; break;
case "rightHandEnabled": case "rightHandEnabled":
if (handPose != null) script.fingerShaped.rightHandEnabled = value > 0.5f;
{ if (script.fingerShaped.rightHandEnabled)
handPose.rightHandEnabled = value > 0.5f; script.fingerShaped.enabled = true;
if (handPose.rightHandEnabled)
handPose.enabled = true;
}
break; break;
default: default:
@ -426,8 +401,7 @@ namespace KindRetargeting.Remote
var script = FindCharacter(characterId); var script = FindCharacter(characterId);
if (script == null) return; if (script == null) return;
var handPose = script.GetComponent<FingerShapedController>(); var handPose = script.fingerShaped;
if (handPose == null) return;
// 스크립트 자동 활성화 // 스크립트 자동 활성화
handPose.enabled = true; handPose.enabled = true;

View File

@ -2,22 +2,20 @@ using UnityEngine;
namespace KindRetargeting namespace KindRetargeting
{ {
[DefaultExecutionOrder(3)] [System.Serializable]
public class ShoulderCorrectionFunction : MonoBehaviour public class ShoulderCorrectionFunction
{ {
private CustomRetargetingScript retargetingScript; // 소스 데이터를 가져올 리타게팅 스크립트
[Header("설정")] [Header("설정")]
[Range(0f, 5f)] [Range(0f, 5f)]
public float blendStrength = 2f; // 전체적인 보정 강도 public float blendStrength = 2f;
[Range(0f, 1f)] [Range(0f, 1f)]
public float maxShoulderBlend = 0.7f; // 어깨에 최대로 전달될 수 있는 회전 비율 public float maxShoulderBlend = 0.7f;
public bool reverseLeftRotation = false; // 왼쪽 어깨 회전 방향 반전 설정 public bool reverseLeftRotation = false;
public bool reverseRightRotation = false; // 오른쪽 어깨 회전 방향 반전 설정 public bool reverseRightRotation = false;
[Header("높이 제한 설정")] [Header("높이 제한 설정")]
public float maxHeightDifference = 0.8f; // 최대 높이 차이 public float maxHeightDifference = 0.8f;
public float minHeightDifference = -0.1f; // 최소 높이 차이 (이 값 이하에서는 보정하지 않음) public float minHeightDifference = -0.1f;
[Header("보정 커브 설정")] [Header("보정 커브 설정")]
public AnimationCurve shoulderCorrectionCurve = AnimationCurve.Linear(0f, 0f, 1f, 1f); public AnimationCurve shoulderCorrectionCurve = AnimationCurve.Linear(0f, 0f, 1f, 1f);
@ -29,26 +27,24 @@ namespace KindRetargeting
private Transform rightShoulder; private Transform rightShoulder;
private Transform leftUpperArm; private Transform leftUpperArm;
private Transform rightUpperArm; private Transform rightUpperArm;
// 최적화: 팔꿈치(LowerArm) Transform 캐싱 추가
private Transform leftLowerArm; private Transform leftLowerArm;
private Transform rightLowerArm; private Transform rightLowerArm;
private void Start() public void Initialize(Animator targetAnimator)
{ {
retargetingScript = GetComponent<CustomRetargetingScript>(); leftShoulder = targetAnimator.GetBoneTransform(HumanBodyBones.LeftShoulder);
rightShoulder = targetAnimator.GetBoneTransform(HumanBodyBones.RightShoulder);
leftShoulder = retargetingScript.targetAnimator.GetBoneTransform(HumanBodyBones.LeftShoulder); leftUpperArm = targetAnimator.GetBoneTransform(HumanBodyBones.LeftUpperArm);
rightShoulder = retargetingScript.targetAnimator.GetBoneTransform(HumanBodyBones.RightShoulder); rightUpperArm = targetAnimator.GetBoneTransform(HumanBodyBones.RightUpperArm);
leftUpperArm = retargetingScript.targetAnimator.GetBoneTransform(HumanBodyBones.LeftUpperArm); leftLowerArm = targetAnimator.GetBoneTransform(HumanBodyBones.LeftLowerArm);
rightUpperArm = retargetingScript.targetAnimator.GetBoneTransform(HumanBodyBones.RightUpperArm); rightLowerArm = targetAnimator.GetBoneTransform(HumanBodyBones.RightLowerArm);
// 최적화: 팔꿈치 Transform도 Start에서 캐싱
leftLowerArm = retargetingScript.targetAnimator.GetBoneTransform(HumanBodyBones.LeftLowerArm);
rightLowerArm = retargetingScript.targetAnimator.GetBoneTransform(HumanBodyBones.RightLowerArm);
} }
private void Update() public void OnUpdate()
{ {
// 왼쪽 어깨 보정 (최적화: 캐싱된 Transform 사용) if (leftShoulder == null || rightShoulder == null) return;
// 왼쪽 어깨 보정
Vector3 leftElbowPos = leftLowerArm.position; Vector3 leftElbowPos = leftLowerArm.position;
float leftHeightDiff = leftElbowPos.y - leftShoulder.position.y; float leftHeightDiff = leftElbowPos.y - leftShoulder.position.y;
float leftRawBlend = Mathf.Clamp01( float leftRawBlend = Mathf.Clamp01(
@ -56,7 +52,7 @@ namespace KindRetargeting
); );
leftBlendWeight = shoulderCorrectionCurve.Evaluate(leftRawBlend) * maxShoulderBlend; leftBlendWeight = shoulderCorrectionCurve.Evaluate(leftRawBlend) * maxShoulderBlend;
// 오른쪽 어깨 보정 (최적화: 캐싱된 Transform 사용) // 오른쪽 어깨 보정
Vector3 rightElbowPos = rightLowerArm.position; Vector3 rightElbowPos = rightLowerArm.position;
float rightHeightDiff = rightElbowPos.y - rightShoulder.position.y; float rightHeightDiff = rightElbowPos.y - rightShoulder.position.y;
float rightRawBlend = Mathf.Clamp01( float rightRawBlend = Mathf.Clamp01(

View File

@ -7,8 +7,8 @@ namespace KindRetargeting
/// FinalIK IKSolverTrigonometric.Solve()를 사용하는 IK 래퍼. /// FinalIK IKSolverTrigonometric.Solve()를 사용하는 IK 래퍼.
/// 4개 사지(양팔, 양다리)에 대해 FinalIK의 검증된 코사인 법칙 솔버를 호출합니다. /// 4개 사지(양팔, 양다리)에 대해 FinalIK의 검증된 코사인 법칙 솔버를 호출합니다.
/// </summary> /// </summary>
[DefaultExecutionOrder(6)] [System.Serializable]
public class TwoBoneIKSolver : MonoBehaviour public class TwoBoneIKSolver
{ {
[System.Serializable] [System.Serializable]
public class LimbIK public class LimbIK
@ -27,7 +27,6 @@ namespace KindRetargeting
[HideInInspector] public float upperLength; [HideInInspector] public float upperLength;
[HideInInspector] public float lowerLength; [HideInInspector] public float lowerLength;
// 초기 벤드 법선 (upper 본 로컬 공간 — FinalIK 방식)
[HideInInspector] public Vector3 localBendNormal; [HideInInspector] public Vector3 localBendNormal;
} }
@ -43,15 +42,9 @@ namespace KindRetargeting
private bool isInitialized; private bool isInitialized;
private void Start() public void Initialize(Animator targetAnimator)
{ {
Initialize(); animator = targetAnimator;
}
public void Initialize()
{
if (animator == null)
animator = GetComponent<Animator>();
if (animator == null || !animator.isHuman) return; if (animator == null || !animator.isHuman) return;
CacheLimb(leftArm, HumanBodyBones.LeftUpperArm, HumanBodyBones.LeftLowerArm, HumanBodyBones.LeftHand); CacheLimb(leftArm, HumanBodyBones.LeftUpperArm, HumanBodyBones.LeftLowerArm, HumanBodyBones.LeftHand);
@ -73,8 +66,6 @@ namespace KindRetargeting
limb.upperLength = Vector3.Distance(limb.upper.position, limb.lower.position); limb.upperLength = Vector3.Distance(limb.upper.position, limb.lower.position);
limb.lowerLength = Vector3.Distance(limb.lower.position, limb.end.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 ab = limb.lower.position - limb.upper.position;
Vector3 bc = limb.end.position - limb.lower.position; Vector3 bc = limb.end.position - limb.lower.position;
Vector3 bendNormal = Vector3.Cross(ab, bc); Vector3 bendNormal = Vector3.Cross(ab, bc);
@ -90,7 +81,7 @@ namespace KindRetargeting
limb.localBendNormal = Quaternion.Inverse(limb.upper.rotation) * bendNormal; limb.localBendNormal = Quaternion.Inverse(limb.upper.rotation) * bendNormal;
} }
private void Update() public void OnUpdate()
{ {
if (!isInitialized) return; if (!isInitialized) return;
@ -105,10 +96,8 @@ namespace KindRetargeting
if (!limb.enabled || limb.target == null || limb.positionWeight < 0.001f) return; if (!limb.enabled || limb.target == null || limb.positionWeight < 0.001f) return;
if (limb.upper == null || limb.lower == null || limb.end == null) return; if (limb.upper == null || limb.lower == null || limb.end == null) return;
// 벤드 법선 계산
Vector3 bendNormal = GetBendNormal(limb); Vector3 bendNormal = GetBendNormal(limb);
// bendGoal 적용
if (limb.bendGoal != null && limb.bendGoalWeight > 0.001f) if (limb.bendGoal != null && limb.bendGoalWeight > 0.001f)
{ {
Vector3 goalNormal = Vector3.Cross( Vector3 goalNormal = Vector3.Cross(
@ -121,7 +110,6 @@ namespace KindRetargeting
} }
} }
// FinalIK 정적 솔버 호출
IKSolverTrigonometric.Solve( IKSolverTrigonometric.Solve(
limb.upper, limb.upper,
limb.lower, limb.lower,
@ -131,7 +119,6 @@ namespace KindRetargeting
limb.positionWeight limb.positionWeight
); );
// 끝단 회전
if (limb.rotationWeight > 0.001f) if (limb.rotationWeight > 0.001f)
{ {
limb.end.rotation = Quaternion.Slerp( limb.end.rotation = Quaternion.Slerp(
@ -142,19 +129,11 @@ namespace KindRetargeting
} }
} }
/// <summary>
/// 벤드 법선을 upper 본의 회전에서 유도합니다 (FinalIK 방식).
/// 위치 기반 Cross(ab, bc)는 직선 근처에서 불안정하지만,
/// 회전 기반은 본의 회전을 그대로 따르므로 안정적입니다.
/// </summary>
private Vector3 GetBendNormal(LimbIK limb) private Vector3 GetBendNormal(LimbIK limb)
{ {
return limb.upper.rotation * limb.localBendNormal; return limb.upper.rotation * limb.localBendNormal;
} }
/// <summary>
/// 히프 높이 자동 보정값을 계산합니다.
/// </summary>
public float CalculateAutoFloorHeight(float comfortRatio = 0.98f) public float CalculateAutoFloorHeight(float comfortRatio = 0.98f)
{ {
if (animator == null || leftLeg.upper == null || leftLeg.lower == null || leftLeg.end == null) return 0f; if (animator == null || leftLeg.upper == null || leftLeg.lower == null || leftLeg.end == null) return 0f;