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("프랍 부착")] [Header("프랍 부착")]
[SerializeField] public PropLocationController propLocation = new PropLocationController(); [SerializeField] public PropLocationController propLocation = new PropLocationController();
[Header("사지 가중치")]
[SerializeField] public LimbWeightController limbWeight = new LimbWeightController();
[Header("손가락 셰이핑")]
[SerializeField] public FingerShapedController fingerShaped = new FingerShapedController();
[Header("아바타 크기 조정")] [Header("아바타 크기 조정")]
[SerializeField, Range(0.1f, 3f)] private float avatarScale = 1f; [SerializeField, Range(0.1f, 3f)] private float avatarScale = 1f;
private float previousScale = 1f; private float previousScale = 1f;
@ -337,6 +343,13 @@ namespace KindRetargeting
// 프랍 부착 모듈 초기화 // 프랍 부착 모듈 초기화
if (targetAnimator != null) if (targetAnimator != null)
propLocation.Initialize(targetAnimator); propLocation.Initialize(targetAnimator);
// 사지 가중치 모듈 초기화
limbWeight.Initialize(ikSolver, this, transform);
// 손가락 셰이핑 모듈 초기화
if (targetAnimator != null)
fingerShaped.Initialize(targetAnimator);
} }
/// <summary> /// <summary>
@ -534,8 +547,7 @@ namespace KindRetargeting
} }
// LimbWeightController에서 의자 높이 오프셋 가져오기 // LimbWeightController에서 의자 높이 오프셋 가져오기
var limbController = GetComponent<LimbWeightController>(); float chairOffset = limbWeight.chairSeatHeightOffset;
float chairOffset = limbController != null ? limbController.chairSeatHeightOffset : 0.05f;
var settings = new RetargetingSettings var settings = new RetargetingSettings
{ {
@ -613,11 +625,7 @@ namespace KindRetargeting
headScale = settings.headScale; headScale = settings.headScale;
// LimbWeightController에 의자 높이 오프셋 적용 // LimbWeightController에 의자 높이 오프셋 적용
var limbController = GetComponent<LimbWeightController>(); limbWeight.chairSeatHeightOffset = settings.chairSeatHeightOffset;
if (limbController != null)
{
limbController.chairSeatHeightOffset = settings.chairSeatHeightOffset;
}
// 머리 회전 오프셋 로드 // 머리 회전 오프셋 로드
headRotationOffsetX = settings.headRotationOffsetX; headRotationOffsetX = settings.headRotationOffsetX;
@ -834,9 +842,15 @@ namespace KindRetargeting
break; break;
} }
// 손가락 셰이핑 (기존 ExecutionOrder 2)
fingerShaped.OnUpdate();
// 어깨 보정 (기존 ExecutionOrder 3) // 어깨 보정 (기존 ExecutionOrder 3)
shoulderCorrection.OnUpdate(); shoulderCorrection.OnUpdate();
// 사지 가중치 (기존 ExecutionOrder 4)
limbWeight.OnUpdate();
// 발 접지 Pre-IK (기존 ExecutionOrder 5) // 발 접지 Pre-IK (기존 ExecutionOrder 5)
footGrounding.OnUpdate(); footGrounding.OnUpdate();
@ -1404,6 +1418,7 @@ namespace KindRetargeting
{ {
sourcePoseHandler?.Dispose(); sourcePoseHandler?.Dispose();
targetPoseHandler?.Dispose(); targetPoseHandler?.Dispose();
fingerShaped.Cleanup();
} }
/// <summary> /// <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) foreach (var s in retargetingScripts)
{ {
if (s == null) continue; if (s == null) continue;
if (s.GetComponent<LimbWeightController>() == null) // 모든 컴포넌트는 CRS 내부 모듈로 이동됨
s.gameObject.AddComponent<LimbWeightController>();
if (s.GetComponent<FingerShapedController>() == null)
s.gameObject.AddComponent<FingerShapedController>();
// PropLocationController는 CRS 내부 모듈로 이동됨
EditorUtility.SetDirty(s.gameObject); EditorUtility.SetDirty(s.gameObject);
} }
@ -197,7 +193,7 @@ public class RetargetingControlWindow : EditorWindow
panel.Add(BuildHeader(script)); panel.Add(BuildHeader(script));
// 가중치 설정 // 가중치 설정
panel.Add(BuildWeightSection(script)); panel.Add(BuildWeightSection(script, so));
// 힙 위치 보정 // 힙 위치 보정
panel.Add(BuildHipsSection(script, so)); panel.Add(BuildHipsSection(script, so));
@ -229,7 +225,7 @@ public class RetargetingControlWindow : EditorWindow
panel.Add(footFoldout); panel.Add(footFoldout);
// 손가락 제어 설정 // 손가락 제어 설정
panel.Add(BuildFingerControlSection(script)); panel.Add(BuildFingerControlSection(script, so));
// 손가락 복제 설정 // 손가락 복제 설정
panel.Add(BuildFingerCopySection(script, so)); panel.Add(BuildFingerCopySection(script, so));
@ -303,27 +299,22 @@ public class RetargetingControlWindow : EditorWindow
// ========== Weight Settings ========== // ========== Weight Settings ==========
private VisualElement BuildWeightSection(CustomRetargetingScript script) private VisualElement BuildWeightSection(CustomRetargetingScript script, SerializedObject so)
{ {
var foldout = new Foldout { text = "가중치 설정", value = false }; var foldout = new Foldout { text = "가중치 설정", value = false };
var limb = script.GetComponent<LimbWeightController>();
if (limb == null) { foldout.Add(new HelpBox("LimbWeightController가 없습니다.", HelpBoxMessageType.Warning)); return foldout; }
var limbSO = CreateTrackedSO(limb);
var container = new VisualElement(); var container = new VisualElement();
container.Add(BuildMinMaxRange("손과 프랍과의 범위 (가중치 1 → 0)", container.Add(BuildMinMaxRange("손과 프랍과의 범위 (가중치 1 → 0)",
limbSO.FindProperty("minDistance"), limbSO.FindProperty("maxDistance"), 0f, 1f, limbSO)); so.FindProperty("limbWeight.minDistance"), so.FindProperty("limbWeight.maxDistance"), 0f, 1f, so));
container.Add(BuildMinMaxRange("의자와 허리 거리 범위 (가중치 1 → 0)", container.Add(BuildMinMaxRange("의자와 허리 거리 범위 (가중치 1 → 0)",
limbSO.FindProperty("hipsMinDistance"), limbSO.FindProperty("hipsMaxDistance"), 0f, 1f, limbSO)); so.FindProperty("limbWeight.hipsMinDistance"), so.FindProperty("limbWeight.hipsMaxDistance"), 0f, 1f, so));
container.Add(BuildMinMaxRange("바닥과 허리 높이에 의한 블렌딩 (가중치 0 → 1)", container.Add(BuildMinMaxRange("바닥과 허리 높이에 의한 블렌딩 (가중치 0 → 1)",
limbSO.FindProperty("groundHipsMinHeight"), limbSO.FindProperty("groundHipsMaxHeight"), 0f, 2f, limbSO)); so.FindProperty("limbWeight.groundHipsMinHeight"), so.FindProperty("limbWeight.groundHipsMaxHeight"), 0f, 2f, so));
container.Add(BuildMinMaxRange("지면으로부터 발의 범위에 의한 IK 블렌딩 (가중치 1 → 0)", container.Add(BuildMinMaxRange("지면으로부터 발의 범위에 의한 IK 블렌딩 (가중치 1 → 0)",
limbSO.FindProperty("footHeightMinThreshold"), limbSO.FindProperty("footHeightMaxThreshold"), 0.1f, 1f, limbSO)); so.FindProperty("limbWeight.footHeightMinThreshold"), so.FindProperty("limbWeight.footHeightMaxThreshold"), 0.1f, 1f, so));
var smoothField = new PropertyField(limbSO.FindProperty("weightSmoothSpeed"), "가중치 변화 속도"); var smoothField = new PropertyField(so.FindProperty("limbWeight.weightSmoothSpeed"), "가중치 변화 속도");
container.Add(smoothField); container.Add(smoothField);
container.Bind(limbSO);
foldout.Add(container); foldout.Add(container);
return foldout; return foldout;
@ -366,15 +357,9 @@ public class RetargetingControlWindow : EditorWindow
container.Add(hz); 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 기준)" }; var chairSlider = new Slider("의자 앉기 높이", -1f, 1f) { showInputField = true, tooltip = "의자에 앉을 때 엉덩이 높이 조정 (월드 Y 기준)" };
chairSlider.BindProperty(limbSO.FindProperty("chairSeatHeightOffset")); chairSlider.BindProperty(so.FindProperty("limbWeight.chairSeatHeightOffset"));
container.Add(chairSlider); container.Add(chairSlider);
chairSlider.Bind(limbSO);
}
// 다리 길이 자동 보정 버튼 // 다리 길이 자동 보정 버튼
var autoHipsBtn = new Button(() => var autoHipsBtn = new Button(() =>
@ -402,48 +387,28 @@ public class RetargetingControlWindow : EditorWindow
// ========== Finger Control ========== // ========== Finger Control ==========
private VisualElement BuildFingerControlSection(CustomRetargetingScript script) private VisualElement BuildFingerControlSection(CustomRetargetingScript script, SerializedObject so)
{ {
var foldout = new Foldout { text = "손가락 제어 설정", value = false }; var foldout = new Foldout { text = "손가락 제어 설정", value = false };
var fingerController = script.GetComponent<FingerShapedController>();
if (fingerController == null)
{
foldout.Add(new HelpBox("FingerShapedController가 없습니다.", HelpBoxMessageType.Warning));
var addBtn = new Button(() =>
{
script.gameObject.AddComponent<FingerShapedController>();
EditorUtility.SetDirty(script.gameObject);
RebuildCharacterPanels();
}) { text = "FingerShapedController 추가" };
foldout.Add(addBtn);
return foldout;
}
var fso = CreateTrackedSO(fingerController);
var container = new VisualElement(); var container = new VisualElement();
// 활성화 토글 // 활성화 토글
var enableToggle = new Toggle("손가락 제어 활성화") { value = fingerController.enabled }; var enabledProp = so.FindProperty("fingerShaped.enabled");
enableToggle.RegisterValueChangedCallback(evt => var enableToggle = new PropertyField(enabledProp, "손가락 제어 활성화");
{
fingerController.enabled = evt.newValue;
EditorUtility.SetDirty(fingerController);
});
container.Add(enableToggle); container.Add(enableToggle);
// 왼손 // 왼손
container.Add(BuildHandSection("왼손", "left", fso, fingerController)); container.Add(BuildHandSection("왼손", "left", so, script.fingerShaped));
// 오른손 // 오른손
container.Add(BuildHandSection("오른손", "right", fso, fingerController)); container.Add(BuildHandSection("오른손", "right", so, script.fingerShaped));
// 프리셋 // 프리셋
container.Add(BuildFingerPresets(fingerController)); container.Add(BuildFingerPresets(script, script.fingerShaped));
container.Bind(fso);
foldout.Add(container); foldout.Add(container);
return foldout; return foldout;
} }
private VisualElement BuildHandSection(string label, string prefix, SerializedObject fso, FingerShapedController fc) private VisualElement BuildHandSection(string label, string prefix, SerializedObject so, FingerShapedController fc)
{ {
var box = new VisualElement(); var box = new VisualElement();
box.style.backgroundColor = new Color(0, 0, 0, 0.08f); box.style.backgroundColor = new Color(0, 0, 0, 0.08f);
@ -455,13 +420,13 @@ public class RetargetingControlWindow : EditorWindow
var header = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center } }; var header = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center } };
var handFoldout = new Foldout { text = label, value = false }; var handFoldout = new Foldout { text = label, value = false };
var handEnabledProp = fso.FindProperty($"{prefix}HandEnabled"); var handEnabledProp = so.FindProperty($"fingerShaped.{prefix}HandEnabled");
header.Add(new PropertyField(handEnabledProp, "활성화") { style = { flexGrow = 1 } }); header.Add(new PropertyField(handEnabledProp, "활성화") { style = { flexGrow = 1 } });
var resetBtn = new Button(() => var resetBtn = new Button(() =>
{ {
string[] props = { "ThumbCurl", "IndexCurl", "MiddleCurl", "RingCurl", "PinkyCurl", "SpreadFingers" }; string[] props = { "ThumbCurl", "IndexCurl", "MiddleCurl", "RingCurl", "PinkyCurl", "SpreadFingers" };
foreach (var p in props) fso.FindProperty($"{prefix}{p}").floatValue = 0f; foreach (var p in props) so.FindProperty($"fingerShaped.{prefix}{p}").floatValue = 0f;
fso.ApplyModifiedProperties(); so.ApplyModifiedProperties();
}) { text = "초기화" }; }) { text = "초기화" };
resetBtn.style.width = 60; resetBtn.style.width = 60;
header.Add(resetBtn); header.Add(resetBtn);
@ -477,7 +442,7 @@ public class RetargetingControlWindow : EditorWindow
var col = new VisualElement { style = { alignItems = Align.Center, width = 45, marginLeft = 2, marginRight = 2 } }; var col = new VisualElement { style = { alignItems = Align.Center, width = 45, marginLeft = 2, marginRight = 2 } };
col.Add(new Label(korNames[i]) { style = { fontSize = 10, color = new Color(0.6f, 0.6f, 0.6f) } }); col.Add(new Label(korNames[i]) { style = { fontSize = 10, color = new Color(0.6f, 0.6f, 0.6f) } });
var prop = fso.FindProperty($"{prefix}{fingerNames[i]}Curl"); var prop = so.FindProperty($"fingerShaped.{prefix}{fingerNames[i]}Curl");
var valLabel = new Label(prop.floatValue.ToString("F1")) { style = { fontSize = 10, color = new Color(0.6f, 0.6f, 0.6f) } }; var valLabel = new Label(prop.floatValue.ToString("F1")) { style = { fontSize = 10, color = new Color(0.6f, 0.6f, 0.6f) } };
col.Add(valLabel); col.Add(valLabel);
@ -494,16 +459,18 @@ public class RetargetingControlWindow : EditorWindow
// 벌리기 // 벌리기
var spreadSlider = new Slider("벌리기", -1f, 1f) { showInputField = true }; var spreadSlider = new Slider("벌리기", -1f, 1f) { showInputField = true };
spreadSlider.BindProperty(fso.FindProperty($"{prefix}SpreadFingers")); spreadSlider.BindProperty(so.FindProperty($"fingerShaped.{prefix}SpreadFingers"));
handFoldout.Add(spreadSlider); handFoldout.Add(spreadSlider);
// 비활성 시 숨김 // 비활성 시 숨김
handFoldout.schedule.Execute(() => handFoldout.schedule.Execute(() =>
{ {
try { if (fso == null || fso.targetObject == null) return; } try { if (so == null || so.targetObject == null) return; }
catch (System.Exception) { return; } catch (System.Exception) { return; }
fso.Update(); so.Update();
bool enabled = fso.FindProperty($"{prefix}HandEnabled").boolValue; var enabledProp = so.FindProperty($"fingerShaped.{prefix}HandEnabled");
if (enabledProp == null) return;
bool enabled = enabledProp.boolValue;
slidersRow.style.display = enabled ? DisplayStyle.Flex : DisplayStyle.None; slidersRow.style.display = enabled ? DisplayStyle.Flex : DisplayStyle.None;
spreadSlider.style.display = enabled ? DisplayStyle.Flex : DisplayStyle.None; spreadSlider.style.display = enabled ? DisplayStyle.Flex : DisplayStyle.None;
}).Every(300); }).Every(300);
@ -512,7 +479,7 @@ public class RetargetingControlWindow : EditorWindow
return box; return box;
} }
private VisualElement BuildFingerPresets(FingerShapedController controller) private VisualElement BuildFingerPresets(CustomRetargetingScript script, FingerShapedController controller)
{ {
var container = new VisualElement { style = { marginTop = 6 } }; var container = new VisualElement { style = { marginTop = 6 } };
container.Add(new Label("손 모양 프리셋") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginBottom = 4 } }); container.Add(new Label("손 모양 프리셋") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginBottom = 4 } });
@ -524,7 +491,7 @@ public class RetargetingControlWindow : EditorWindow
for (int col = 0; col < 3; col++) for (int col = 0; col < 3; col++)
{ {
string name = presets[row, col]; string name = presets[row, col];
var btn = new Button(() => ApplyFingerPreset(controller, name)) { text = name }; var btn = new Button(() => ApplyFingerPreset(script, controller, name)) { text = name };
btn.style.height = 30; btn.style.width = 100; btn.style.marginLeft = btn.style.marginRight = 4; btn.style.height = 30; btn.style.width = 100; btn.style.marginLeft = btn.style.marginRight = 4;
btnRow.Add(btn); btnRow.Add(btn);
} }
@ -835,7 +802,7 @@ public class RetargetingControlWindow : EditorWindow
return container; return container;
} }
private void ApplyFingerPreset(FingerShapedController controller, string presetName) private void ApplyFingerPreset(CustomRetargetingScript script, FingerShapedController controller, string presetName)
{ {
if (!controller.enabled) controller.enabled = true; if (!controller.enabled) controller.enabled = true;
@ -860,7 +827,7 @@ public class RetargetingControlWindow : EditorWindow
controller.rightThumbCurl = t; controller.rightIndexCurl = i; controller.rightMiddleCurl = m; controller.rightThumbCurl = t; controller.rightIndexCurl = i; controller.rightMiddleCurl = m;
controller.rightRingCurl = r; controller.rightPinkyCurl = p; controller.rightSpreadFingers = s; controller.rightRingCurl = r; controller.rightPinkyCurl = p; controller.rightSpreadFingers = s;
} }
EditorUtility.SetDirty(controller); EditorUtility.SetDirty(script);
} }
// ========== Head Calibration ========== // ========== Head Calibration ==========

View File

@ -1,9 +1,11 @@
using UnityEngine; using UnityEngine;
using System.Collections.Generic; using System.Collections.Generic;
[DefaultExecutionOrder(2)] namespace KindRetargeting
public class FingerShapedController : MonoBehaviour
{ {
[System.Serializable]
public class FingerShapedController
{
private Animator animator; private Animator animator;
private HumanPoseHandler humanPoseHandler; private HumanPoseHandler humanPoseHandler;
@ -40,6 +42,8 @@ public class FingerShapedController : MonoBehaviour
HumanBodyBones.Jaw HumanBodyBones.Jaw
}; };
public bool enabled = false;
[Header("왼손 제어 값")] [Header("왼손 제어 값")]
[Range(-1, 1)] public float leftPinkyCurl; // 새끼손가락 구부리기 [Range(-1, 1)] public float leftPinkyCurl; // 새끼손가락 구부리기
[Range(-1, 1)] public float leftRingCurl; // 약지 구부리기 [Range(-1, 1)] public float leftRingCurl; // 약지 구부리기
@ -59,30 +63,30 @@ public class FingerShapedController : MonoBehaviour
public bool leftHandEnabled = false; // 왼손 제어 활성화 상태 public bool leftHandEnabled = false; // 왼손 제어 활성화 상태
public bool rightHandEnabled = false; // 오른손 제어 활성화 상태 public bool rightHandEnabled = false; // 오른손 제어 활성화 상태
private void Reset() private bool isInitialized;
{
// 컴포넌트가 처음 추가될 때 자동으로 비활성화
enabled = false;
leftHandEnabled = false;
rightHandEnabled = false;
}
private void Awake() public void Initialize(Animator targetAnimator)
{ {
animator = GetComponent<Animator>(); animator = targetAnimator;
if (animator == null || !animator.isHuman) return;
humanPoseHandler = new HumanPoseHandler(animator.avatar, animator.transform); humanPoseHandler = new HumanPoseHandler(animator.avatar, animator.transform);
isInitialized = true;
} }
private void OnDestroy() public void Cleanup()
{ {
if (humanPoseHandler != null) if (humanPoseHandler != null)
{ {
humanPoseHandler.Dispose(); humanPoseHandler.Dispose();
humanPoseHandler = null;
} }
isInitialized = false;
} }
private void Update() public void OnUpdate()
{ {
if (!isInitialized || !enabled) return;
UpdateMuscleValues(); UpdateMuscleValues();
} }
@ -165,4 +169,5 @@ public class FingerShapedController : MonoBehaviour
if (baseOffset + 18 < muscleCount) humanPose.muscles[baseOffset + 18] = pinky; // Little 2 if (baseOffset + 18 < muscleCount) humanPose.muscles[baseOffset + 18] = pinky; // Little 2
if (baseOffset + 19 < muscleCount) humanPose.muscles[baseOffset + 19] = pinky; // Little 3 if (baseOffset + 19 < muscleCount) humanPose.muscles[baseOffset + 19] = pinky; // Little 3
} }
}
} }

View File

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

View File

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