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>
This commit is contained in:
user 2026-03-07 23:21:12 +09:00
parent 62a5a9bbb5
commit e4ca30b98a
9 changed files with 309 additions and 749 deletions

View File

@ -104,6 +104,12 @@ namespace KindRetargeting
[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;
@ -337,6 +343,13 @@ namespace KindRetargeting
// 프랍 부착 모듈 초기화
if (targetAnimator != null)
propLocation.Initialize(targetAnimator);
// 사지 가중치 모듈 초기화
limbWeight.Initialize(ikSolver, this, transform);
// 손가락 셰이핑 모듈 초기화
if (targetAnimator != null)
fingerShaped.Initialize(targetAnimator);
}
/// <summary>
@ -534,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
{
@ -613,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;
@ -834,9 +842,15 @@ namespace KindRetargeting
break;
}
// 손가락 셰이핑 (기존 ExecutionOrder 2)
fingerShaped.OnUpdate();
// 어깨 보정 (기존 ExecutionOrder 3)
shoulderCorrection.OnUpdate();
// 사지 가중치 (기존 ExecutionOrder 4)
limbWeight.OnUpdate();
// 발 접지 Pre-IK (기존 ExecutionOrder 5)
footGrounding.OnUpdate();
@ -1404,6 +1418,7 @@ namespace KindRetargeting
{
sourcePoseHandler?.Dispose();
targetPoseHandler?.Dispose();
fingerShaped.Cleanup();
}
/// <summary>

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

@ -144,11 +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>();
// PropLocationController는 CRS 내부 모듈로 이동됨
// 모든 컴포넌트는 CRS 내부 모듈로 이동됨
EditorUtility.SetDirty(s.gameObject);
}
@ -197,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));
@ -229,7 +225,7 @@ public class RetargetingControlWindow : EditorWindow
panel.Add(footFoldout);
// 손가락 제어 설정
panel.Add(BuildFingerControlSection(script));
panel.Add(BuildFingerControlSection(script, so));
// 손가락 복제 설정
panel.Add(BuildFingerCopySection(script, so));
@ -303,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;
@ -366,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(() =>
@ -402,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);
@ -455,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);
@ -477,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);
@ -494,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);
@ -512,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 } });
@ -524,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);
}
@ -835,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;
@ -860,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

@ -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,68 +146,15 @@ namespace KindRetargeting
ApplyWeightsToFBIK();
}
void Start()
{
crs = GetComponent<CustomRetargetingScript>();
if (crs != null) ikSolver = crs.ikSolver;
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);
@ -195,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);
@ -254,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;
@ -278,7 +277,7 @@ namespace KindRetargeting
minLeftDistance = Mathf.Min(minLeftDistance, distance);
}
// 오른손과 프랍 사이의 최소 거리 계산
// 오른손과 프랍 사이의 최소 거리 계산
float minRightDistance = float.MaxValue;
foreach (Transform prop in props)
{

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;