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

View File

@ -10,8 +10,6 @@ namespace KindRetargeting
{
private const string CommonUssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/StreamingleCommon.uss";
private SerializedObject groundingSO;
// SerializedProperty
private SerializedProperty sourceAnimatorProp;
private SerializedProperty targetAnimatorProp;
@ -33,7 +31,7 @@ namespace KindRetargeting
protected override void OnDisable()
{
base.OnDisable();
groundingSO = null;
// groundingSO 삭제됨 — footGrounding은 CRS 내부 모듈
}
protected override void OnEnable()
@ -197,36 +195,27 @@ namespace KindRetargeting
"• Toe Pivot: 발끝 고정 + 발목 회전 자동 감지",
HelpBoxMessageType.Info));
// FootGroundingController의 SerializedObject를 직접 바인딩
var script = (CustomRetargetingScript)target;
var grounding = script.GetComponent<FootGroundingController>();
if (grounding != null)
{
groundingSO = new SerializedObject(grounding);
// FootGroundingController는 CRS 내부 모듈 — serializedObject의 프로퍼티 경로로 접근
var groundHeightField = new PropertyField(serializedObject.FindProperty("footGrounding.groundHeight"), "바닥 높이");
foldout.Add(groundHeightField);
var groundHeightField = new PropertyField(groundingSO.FindProperty("groundHeight"), "바닥 높이");
foldout.Add(groundHeightField);
var weightSlider = new Slider("접지 강도", 0f, 1f) { showInputField = true };
weightSlider.BindProperty(serializedObject.FindProperty("footGrounding.groundingWeight"));
foldout.Add(weightSlider);
var weightSlider = new Slider("접지 강도", 0f, 1f) { showInputField = true };
weightSlider.BindProperty(groundingSO.FindProperty("groundingWeight"));
foldout.Add(weightSlider);
var activationField = new PropertyField(serializedObject.FindProperty("footGrounding.activationHeight"), "활성화 높이");
activationField.tooltip = "발목이 이 높이 이상이면 접지 보정 비활성화 (점프 등)";
foldout.Add(activationField);
var activationField = new PropertyField(groundingSO.FindProperty("activationHeight"), "활성화 높이");
activationField.tooltip = "발목이 이 높이 이상이면 접지 보정 비활성화 (점프 등)";
foldout.Add(activationField);
var thresholdField = new PropertyField(serializedObject.FindProperty("footGrounding.plantThreshold"), "접지 판정 범위");
thresholdField.tooltip = "Toes가 이 범위 안이면 접지 중으로 판정";
foldout.Add(thresholdField);
var thresholdField = new PropertyField(groundingSO.FindProperty("plantThreshold"), "접지 판정 범위");
thresholdField.tooltip = "Toes가 이 범위 안이면 접지 중으로 판정";
foldout.Add(thresholdField);
var smoothField = new PropertyField(serializedObject.FindProperty("footGrounding.smoothSpeed"), "보정 스무딩 속도");
smoothField.tooltip = "보정량 변화 속도 (높을수록 빠른 반응, 낮으면 부드러운 전환)";
foldout.Add(smoothField);
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());
}
foldout.Add(new HelpBox("힙 높이 보정은 '바닥 높이 조정' 섹션의 floorHeight로 제어합니다.", HelpBoxMessageType.Info));
else
{
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)
{
if (s == null) continue;
if (s.GetComponent<LimbWeightController>() == null)
s.gameObject.AddComponent<LimbWeightController>();
if (s.GetComponent<FingerShapedController>() == null)
s.gameObject.AddComponent<FingerShapedController>();
if (s.GetComponent<PropLocationController>() == null)
s.gameObject.AddComponent<PropLocationController>();
// 모든 컴포넌트는 CRS 내부 모듈로 이동됨
EditorUtility.SetDirty(s.gameObject);
}
@ -198,7 +193,7 @@ public class RetargetingControlWindow : EditorWindow
panel.Add(BuildHeader(script));
// 가중치 설정
panel.Add(BuildWeightSection(script));
panel.Add(BuildWeightSection(script, so));
// 힙 위치 보정
panel.Add(BuildHipsSection(script, so));
@ -230,7 +225,7 @@ public class RetargetingControlWindow : EditorWindow
panel.Add(footFoldout);
// 손가락 제어 설정
panel.Add(BuildFingerControlSection(script));
panel.Add(BuildFingerControlSection(script, so));
// 손가락 복제 설정
panel.Add(BuildFingerCopySection(script, so));
@ -304,27 +299,22 @@ public class RetargetingControlWindow : EditorWindow
// ========== Weight Settings ==========
private VisualElement BuildWeightSection(CustomRetargetingScript script)
private VisualElement BuildWeightSection(CustomRetargetingScript script, SerializedObject so)
{
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();
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)",
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)",
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)",
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.Bind(limbSO);
foldout.Add(container);
return foldout;
@ -367,15 +357,9 @@ public class RetargetingControlWindow : EditorWindow
container.Add(hz);
// 의자 앉기 높이
var limb = script.GetComponent<LimbWeightController>();
if (limb != null)
{
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 chairSlider = new Slider("의자 앉기 높이", -1f, 1f) { showInputField = true, tooltip = "의자에 앉을 때 엉덩이 높이 조정 (월드 Y 기준)" };
chairSlider.BindProperty(so.FindProperty("limbWeight.chairSeatHeightOffset"));
container.Add(chairSlider);
// 다리 길이 자동 보정 버튼
var autoHipsBtn = new Button(() =>
@ -403,48 +387,28 @@ public class RetargetingControlWindow : EditorWindow
// ========== Finger Control ==========
private VisualElement BuildFingerControlSection(CustomRetargetingScript script)
private VisualElement BuildFingerControlSection(CustomRetargetingScript script, SerializedObject so)
{
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 enableToggle = new Toggle("손가락 제어 활성화") { value = fingerController.enabled };
enableToggle.RegisterValueChangedCallback(evt =>
{
fingerController.enabled = evt.newValue;
EditorUtility.SetDirty(fingerController);
});
var enabledProp = so.FindProperty("fingerShaped.enabled");
var enableToggle = new PropertyField(enabledProp, "손가락 제어 활성화");
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);
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();
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 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 } });
var resetBtn = new Button(() =>
{
string[] props = { "ThumbCurl", "IndexCurl", "MiddleCurl", "RingCurl", "PinkyCurl", "SpreadFingers" };
foreach (var p in props) fso.FindProperty($"{prefix}{p}").floatValue = 0f;
fso.ApplyModifiedProperties();
foreach (var p in props) so.FindProperty($"fingerShaped.{prefix}{p}").floatValue = 0f;
so.ApplyModifiedProperties();
}) { text = "초기화" };
resetBtn.style.width = 60;
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 } };
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) } };
col.Add(valLabel);
@ -495,16 +459,18 @@ public class RetargetingControlWindow : EditorWindow
// 벌리기
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.schedule.Execute(() =>
{
try { if (fso == null || fso.targetObject == null) return; }
try { if (so == null || so.targetObject == null) return; }
catch (System.Exception) { return; }
fso.Update();
bool enabled = fso.FindProperty($"{prefix}HandEnabled").boolValue;
so.Update();
var enabledProp = so.FindProperty($"fingerShaped.{prefix}HandEnabled");
if (enabledProp == null) return;
bool enabled = enabledProp.boolValue;
slidersRow.style.display = enabled ? DisplayStyle.Flex : DisplayStyle.None;
spreadSlider.style.display = enabled ? DisplayStyle.Flex : DisplayStyle.None;
}).Every(300);
@ -513,7 +479,7 @@ public class RetargetingControlWindow : EditorWindow
return box;
}
private VisualElement BuildFingerPresets(FingerShapedController controller)
private VisualElement BuildFingerPresets(CustomRetargetingScript script, FingerShapedController controller)
{
var container = new VisualElement { style = { marginTop = 6 } };
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++)
{
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;
btnRow.Add(btn);
}
@ -606,19 +572,7 @@ public class RetargetingControlWindow : EditorWindow
private VisualElement BuildPropSection(CustomRetargetingScript script)
{
var foldout = new Foldout { text = "프랍 설정", value = false };
var propController = script.GetComponent<PropLocationController>();
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 propController = script.propLocation;
var dynamicContainer = new VisualElement();
foldout.Add(dynamicContainer);
@ -848,7 +802,7 @@ public class RetargetingControlWindow : EditorWindow
return container;
}
private void ApplyFingerPreset(FingerShapedController controller, string presetName)
private void ApplyFingerPreset(CustomRetargetingScript script, FingerShapedController controller, string presetName)
{
if (!controller.enabled) controller.enabled = true;
@ -873,7 +827,7 @@ public class RetargetingControlWindow : EditorWindow
controller.rightThumbCurl = t; controller.rightIndexCurl = i; controller.rightMiddleCurl = m;
controller.rightRingCurl = r; controller.rightPinkyCurl = p; controller.rightSpreadFingers = s;
}
EditorUtility.SetDirty(controller);
EditorUtility.SetDirty(script);
}
// ========== Head Calibration ==========

View File

@ -1,168 +1,173 @@
using UnityEngine;
using System.Collections.Generic;
[DefaultExecutionOrder(2)]
public class FingerShapedController : MonoBehaviour
namespace KindRetargeting
{
private Animator animator;
private HumanPoseHandler humanPoseHandler;
// 손가락을 제외한 모든 본의 로컬 회전 저장용 (SetHumanPose 호출 시 몸 복원용)
private Dictionary<HumanBodyBones, Quaternion> savedBoneLocalRotations = new Dictionary<HumanBodyBones, Quaternion>();
// 손가락을 제외한 모든 휴먼본 목록
private static readonly HumanBodyBones[] nonFingerBones = new HumanBodyBones[]
[System.Serializable]
public class FingerShapedController
{
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
};
private Animator animator;
private HumanPoseHandler humanPoseHandler;
[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; // 손가락 벌리기
// 손가락을 제외한 모든 본의 로컬 회전 저장용 (SetHumanPose 호출 시 몸 복원용)
private Dictionary<HumanBodyBones, Quaternion> savedBoneLocalRotations = new Dictionary<HumanBodyBones, Quaternion>();
[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 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)
// 손가락을 제외한 모든 휴먼본 목록
private static readonly HumanBodyBones[] nonFingerBones = new HumanBodyBones[]
{
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()
{
UpdateMuscleValues();
}
private void UpdateMuscleValues()
{
// 1. 손가락을 제외한 모든 본의 로컬 회전 저장 (SetHumanPose 호출 전)
savedBoneLocalRotations.Clear();
for (int i = 0; i < nonFingerBones.Length; i++)
public void Cleanup()
{
Transform bone = animator.GetBoneTransform(nonFingerBones[i]);
if (bone != null)
if (humanPoseHandler != 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 가져오기 및 손가락 머슬 설정
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)
private void SetHandMuscles(bool isLeft, float thumb, float index, float middle, float ring,
float pinky, float spread, ref HumanPose humanPose)
{
Transform bone = animator.GetBoneTransform(kvp.Key);
if (bone != null)
{
bone.localRotation = kvp.Value;
}
// 해당 손이 비활성화 상태면 건너뛰기
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
}
}
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>
/// HIK 스타일 2-Pass 접지 시스템.
///
/// Pass 1 (Update, Order 5 → IK 전):
/// Pass 1 (OnUpdate, Order 5 → IK 전):
/// IK 타겟 위치를 수정하여 Toes가 바닥을 뚫지 않도록 보정.
/// Toe Pivot 감지: 발끝이 바닥에 있고 발목이 올라가면
/// 발목 타겟을 역산하여 Toes가 groundHeight에 고정.
///
/// Pass 2 (LateUpdate → IK 후):
/// Pass 2 (OnLateUpdate → IK 후):
/// IK 결과의 잔차를 Foot 회전으로 미세 보정.
/// 위치 변경 없음 — 본 길이 보존.
///
/// 힙 높이 보정은 CRS의 floorHeight가 담당합니다 (이중 보정 방지).
/// </summary>
[DefaultExecutionOrder(5)]
public class FootGroundingController : MonoBehaviour
[System.Serializable]
public class FootGroundingController
{
[Header("접지 설정")]
[Tooltip("바닥 Y 좌표 (월드 공간)")]
@ -41,37 +36,31 @@ namespace KindRetargeting
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()
public void Initialize(TwoBoneIKSolver ikSolver, Animator animator)
{
ikSolver = GetComponent<TwoBoneIKSolver>();
animator = GetComponent<Animator>();
this.ikSolver = ikSolver;
this.animator = 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);
@ -80,7 +69,6 @@ namespace KindRetargeting
if (leftFoot == null || rightFoot == null) return;
// Toes 존재 여부 + 캐싱
leftHasToes = leftToes != null;
rightHasToes = rightToes != null;
@ -91,7 +79,7 @@ namespace KindRetargeting
}
else
{
leftFootHeight = 0.05f; // Toes 없을 때 기본 발목 높이
leftFootHeight = 0.05f;
}
if (rightHasToes)
@ -110,7 +98,7 @@ namespace KindRetargeting
/// <summary>
/// Pass 1: Pre-IK — IK 타겟 위치를 수정합니다.
/// </summary>
private void Update()
public void OnUpdate()
{
if (!isInitialized || groundingWeight < 0.001f) return;
@ -121,16 +109,11 @@ namespace KindRetargeting
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)
{
@ -140,12 +123,10 @@ namespace KindRetargeting
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;
@ -159,7 +140,6 @@ namespace KindRetargeting
return 0f;
}
// === Toes 있는 아바타: 예측 기반 보정 ===
Vector3 predictedToesWorld = anklePos + ankleTarget.rotation * localToesOffset;
float predictedToesY = predictedToesWorld.y;
@ -171,7 +151,6 @@ namespace KindRetargeting
if (ankleY < groundHeight + footHeight + plantThreshold)
{
// PLANTED: 발 전체가 바닥 근처
float minAnkleY = groundHeight + footHeight;
if (ankleY < minAnkleY)
{
@ -188,7 +167,6 @@ namespace KindRetargeting
}
else
{
// TOE_PIVOT: 발끝 고정, 발목 올라감
if (toesError > 0f)
{
adjustment = toesError * weight;
@ -200,7 +178,6 @@ namespace KindRetargeting
}
else
{
// Toes 충분히 위 → 발목만 바닥 아래 방지
float minAnkleY = groundHeight + footHeight;
if (ankleY < minAnkleY)
{
@ -216,7 +193,7 @@ namespace KindRetargeting
/// <summary>
/// Pass 2: Post-IK — 잔차를 Foot 회전으로 미세 보정합니다.
/// </summary>
private void LateUpdate()
public void OnLateUpdate()
{
if (!isInitialized || groundingWeight < 0.001f) return;
@ -226,10 +203,6 @@ namespace KindRetargeting
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;
@ -244,7 +217,6 @@ namespace KindRetargeting
if (Mathf.Abs(error) < 0.001f) return;
// 바닥 아래로 뚫린 경우만 보정
if (error > plantThreshold) return;
Vector3 footToToes = toes.position - foot.position;

View File

@ -4,8 +4,8 @@ using UnityEngine;
namespace KindRetargeting
{
[DefaultExecutionOrder(4)]
public class LimbWeightController : MonoBehaviour
[System.Serializable]
public class LimbWeightController
{
[Header("거리 기반 가중치 설정")]
[SerializeField, Range(0.3f, 1f)] public float maxDistance = 0.5f; // 가중치가 0이 되는 최대 거리
@ -36,9 +36,8 @@ namespace KindRetargeting
public float chairSeatHeightOffset = 0.05f;
private TwoBoneIKSolver ikSolver;
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> rightArmEndWeights = new List<float>();
@ -57,8 +56,6 @@ namespace KindRetargeting
public List<Transform> props = new List<Transform>();
public Transform characterRoot;
// 힙스 가중치 리스트 추가
List<float> hipsWeights = new List<float>();
private float MasterHipsWeight = 1f;
@ -67,8 +64,63 @@ namespace KindRetargeting
private float currentChairSeatOffset = 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();
@ -94,69 +146,15 @@ namespace KindRetargeting
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()
{
// 모든 LimbWeightController 찾기
LimbWeightController[] allControllers = FindObjectsOfType<LimbWeightController>();
// 모든 CustomRetargetingScript 찾기 (다른 캐릭터의 손을 props에 추가)
CustomRetargetingScript[] allCrs = Object.FindObjectsByType<CustomRetargetingScript>(FindObjectsSortMode.None);
foreach (LimbWeightController controller in allControllers)
foreach (CustomRetargetingScript otherCrs in allCrs)
{
// 자기 자신은 제외
if (controller == this) continue;
// CustomRetargetingScript 가져오기
CustomRetargetingScript otherCrs = controller.GetComponent<CustomRetargetingScript>();
if (otherCrs == null) continue;
if (otherCrs == crs) continue;
// 왼손과 오른손 Transform 가져오기
Transform leftHand = otherCrs?.sourceAnimator?.GetBoneTransform(HumanBodyBones.LeftHand);
@ -196,7 +194,7 @@ namespace KindRetargeting
{
Transform leftHandTransform = crs?.sourceAnimator?.GetBoneTransform(HumanBodyBones.LeftHand);
Transform rightHandTransform = crs?.sourceAnimator?.GetBoneTransform(HumanBodyBones.RightHand);
if (leftHandTransform != null && rightHandTransform != null &&
if (leftHandTransform != null && rightHandTransform != null &&
props != null && props.Count > 0)
{
float Distance = Vector3.Distance(leftHandTransform.position, rightHandTransform.position);
@ -255,7 +253,7 @@ namespace KindRetargeting
const float MIN_LEG_DISTANCE_RATIO = 0.3f; // 다리 길이의 30%
// 거리가 멀수록 가중치 감소
float weight = 1f - Mathf.Clamp01((horizontalDistance - MIN_LEG_DISTANCE_RATIO) /
float weight = 1f - Mathf.Clamp01((horizontalDistance - MIN_LEG_DISTANCE_RATIO) /
(MAX_LEG_DISTANCE_RATIO - MIN_LEG_DISTANCE_RATIO));
weightList[weightIndex] = weight;
@ -279,7 +277,7 @@ namespace KindRetargeting
minLeftDistance = Mathf.Min(minLeftDistance, distance);
}
// 오른손과 프랍 사이의 최소 거리 계산
// 오른손과 프랍 사이의 최소 거리 계산
float minRightDistance = float.MaxValue;
foreach (Transform prop in props)
{

View File

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

View File

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

View File

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

View File

@ -7,8 +7,8 @@ namespace KindRetargeting
/// FinalIK IKSolverTrigonometric.Solve()를 사용하는 IK 래퍼.
/// 4개 사지(양팔, 양다리)에 대해 FinalIK의 검증된 코사인 법칙 솔버를 호출합니다.
/// </summary>
[DefaultExecutionOrder(6)]
public class TwoBoneIKSolver : MonoBehaviour
[System.Serializable]
public class TwoBoneIKSolver
{
[System.Serializable]
public class LimbIK
@ -27,7 +27,6 @@ namespace KindRetargeting
[HideInInspector] public float upperLength;
[HideInInspector] public float lowerLength;
// 초기 벤드 법선 (upper 본 로컬 공간 — FinalIK 방식)
[HideInInspector] public Vector3 localBendNormal;
}
@ -43,15 +42,9 @@ namespace KindRetargeting
private bool isInitialized;
private void Start()
public void Initialize(Animator targetAnimator)
{
Initialize();
}
public void Initialize()
{
if (animator == null)
animator = GetComponent<Animator>();
animator = targetAnimator;
if (animator == null || !animator.isHuman) return;
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.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);
@ -90,7 +81,7 @@ namespace KindRetargeting
limb.localBendNormal = Quaternion.Inverse(limb.upper.rotation) * bendNormal;
}
private void Update()
public void OnUpdate()
{
if (!isInitialized) return;
@ -105,10 +96,8 @@ namespace KindRetargeting
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(
@ -121,7 +110,6 @@ namespace KindRetargeting
}
}
// FinalIK 정적 솔버 호출
IKSolverTrigonometric.Solve(
limb.upper,
limb.lower,
@ -131,7 +119,6 @@ namespace KindRetargeting
limb.positionWeight
);
// 끝단 회전
if (limb.rotationWeight > 0.001f)
{
limb.end.rotation = Quaternion.Slerp(
@ -142,19 +129,11 @@ namespace KindRetargeting
}
}
/// <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;