Refactor : CustomRetargetingScriptEditor 리디자인 + RemoteController 모듈 데이터 추가
- Inspector에 모든 내부 모듈 섹션 추가: - 어깨 보정 (blendStrength, maxBlend, heightDiff, curve) - 사지 가중치 (IK 토글, 거리/높이 범위, 스무딩) - 접지 설정 (groundHeight, weight, activationHeight, plantThreshold, smoothSpeed) - 손가락 셰이핑 (활성화, 손가락별 슬라이더, 프리셋 버튼) - 머리 회전 오프셋 (정면 캘리브레이션 포함) - 머리/아바타 크기 - RetargetingRemoteController에 추가된 원격 제어 데이터: - enableLeftArmIK, enableRightArmIK - shoulderBlendStrength, shoulderMaxBlend, shoulderMaxHeightDiff, shoulderMinHeightDiff - groundingWeight, groundingGroundHeight, groundingActivationHeight, groundingPlantThreshold, groundingSmoothSpeed Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
88ea6a072d
commit
e17cfc003c
@ -10,46 +10,9 @@ namespace KindRetargeting
|
|||||||
{
|
{
|
||||||
private const string CommonUssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/StreamingleCommon.uss";
|
private const string CommonUssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/StreamingleCommon.uss";
|
||||||
|
|
||||||
// SerializedProperty
|
|
||||||
private SerializedProperty sourceAnimatorProp;
|
|
||||||
private SerializedProperty targetAnimatorProp;
|
|
||||||
private SerializedProperty hipsOffsetXProp;
|
|
||||||
private SerializedProperty hipsOffsetYProp;
|
|
||||||
private SerializedProperty hipsOffsetZProp;
|
|
||||||
private SerializedProperty debugAxisNormalizerProp;
|
|
||||||
private SerializedProperty fingerCopyModeProp;
|
|
||||||
private SerializedProperty kneeInOutWeightProp;
|
|
||||||
private SerializedProperty kneeFrontBackWeightProp;
|
|
||||||
private SerializedProperty footFrontBackOffsetProp;
|
|
||||||
private SerializedProperty footInOutOffsetProp;
|
|
||||||
private SerializedProperty floorHeightProp;
|
|
||||||
private SerializedProperty avatarScaleProp;
|
|
||||||
|
|
||||||
// Dynamic UI
|
|
||||||
private Label cacheStatusLabel;
|
|
||||||
|
|
||||||
protected override void OnDisable()
|
protected override void OnDisable()
|
||||||
{
|
{
|
||||||
base.OnDisable();
|
base.OnDisable();
|
||||||
// groundingSO 삭제됨 — footGrounding은 CRS 내부 모듈
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnEnable()
|
|
||||||
{
|
|
||||||
base.OnEnable();
|
|
||||||
sourceAnimatorProp = serializedObject.FindProperty("sourceAnimator");
|
|
||||||
targetAnimatorProp = serializedObject.FindProperty("targetAnimator");
|
|
||||||
hipsOffsetXProp = serializedObject.FindProperty("hipsOffsetX");
|
|
||||||
hipsOffsetYProp = serializedObject.FindProperty("hipsOffsetY");
|
|
||||||
hipsOffsetZProp = serializedObject.FindProperty("hipsOffsetZ");
|
|
||||||
debugAxisNormalizerProp = serializedObject.FindProperty("debugAxisNormalizer");
|
|
||||||
fingerCopyModeProp = serializedObject.FindProperty("fingerCopyMode");
|
|
||||||
kneeInOutWeightProp = serializedObject.FindProperty("kneeInOutWeight");
|
|
||||||
kneeFrontBackWeightProp = serializedObject.FindProperty("kneeFrontBackWeight");
|
|
||||||
footFrontBackOffsetProp = serializedObject.FindProperty("footFrontBackOffset");
|
|
||||||
footInOutOffsetProp = serializedObject.FindProperty("footInOutOffset");
|
|
||||||
floorHeightProp = serializedObject.FindProperty("floorHeight");
|
|
||||||
avatarScaleProp = serializedObject.FindProperty("avatarScale");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override VisualElement CreateInspectorGUI()
|
public override VisualElement CreateInspectorGUI()
|
||||||
@ -59,52 +22,68 @@ namespace KindRetargeting
|
|||||||
var commonUss = AssetDatabase.LoadAssetAtPath<StyleSheet>(CommonUssPath);
|
var commonUss = AssetDatabase.LoadAssetAtPath<StyleSheet>(CommonUssPath);
|
||||||
if (commonUss != null) root.styleSheets.Add(commonUss);
|
if (commonUss != null) root.styleSheets.Add(commonUss);
|
||||||
|
|
||||||
// 원본 Animator
|
// ── 기본 설정 ──
|
||||||
root.Add(new PropertyField(sourceAnimatorProp, "원본 Animator"));
|
root.Add(new PropertyField(serializedObject.FindProperty("sourceAnimator"), "원본 Animator"));
|
||||||
|
|
||||||
// 아바타 크기 설정
|
// ── 아바타 크기 ──
|
||||||
var scaleFoldout = new Foldout { text = "아바타 크기 설정", value = true };
|
var scaleFoldout = new Foldout { text = "아바타 크기 설정", value = true };
|
||||||
scaleFoldout.Add(new PropertyField(avatarScaleProp, "아바타 크기"));
|
scaleFoldout.Add(new PropertyField(serializedObject.FindProperty("avatarScale"), "아바타 크기"));
|
||||||
|
scaleFoldout.Add(new PropertyField(serializedObject.FindProperty("headScale"), "머리 크기"));
|
||||||
root.Add(scaleFoldout);
|
root.Add(scaleFoldout);
|
||||||
|
|
||||||
// 힙 위치 보정
|
// ── 힙 위치 보정 ──
|
||||||
root.Add(BuildHipsSection());
|
root.Add(BuildHipsSection());
|
||||||
|
|
||||||
// 무릎 위치 조정
|
// ── 무릎 위치 조정 ──
|
||||||
var kneeFoldout = new Foldout { text = "무릎 위치 조정", value = true };
|
var kneeFoldout = new Foldout { text = "무릎 위치 조정", value = false };
|
||||||
kneeFoldout.Add(new Slider("무릎 앞/뒤 가중치", -1f, 1f) { showInputField = true, tooltip = "음수: 뒤로, 양수: 앞으로" });
|
var kneeFB = new Slider("무릎 앞/뒤", -1f, 1f) { showInputField = true };
|
||||||
kneeFoldout.Q<Slider>().BindProperty(kneeFrontBackWeightProp);
|
kneeFB.BindProperty(serializedObject.FindProperty("kneeFrontBackWeight"));
|
||||||
var kneeInOut = new Slider("무릎 안/밖 가중치", -1f, 1f) { showInputField = true, tooltip = "음수: 안쪽, 양수: 바깥쪽" };
|
kneeFoldout.Add(kneeFB);
|
||||||
kneeInOut.BindProperty(kneeInOutWeightProp);
|
var kneeIO = new Slider("무릎 안/밖", -1f, 1f) { showInputField = true };
|
||||||
kneeFoldout.Add(kneeInOut);
|
kneeIO.BindProperty(serializedObject.FindProperty("kneeInOutWeight"));
|
||||||
|
kneeFoldout.Add(kneeIO);
|
||||||
root.Add(kneeFoldout);
|
root.Add(kneeFoldout);
|
||||||
|
|
||||||
// 발 IK 위치 조정
|
// ── 발 IK 위치 조정 ──
|
||||||
var footFoldout = new Foldout { text = "발 IK 위치 조정", value = true };
|
var footFoldout = new Foldout { text = "발 IK 위치 조정", value = false };
|
||||||
var footFB = new Slider("발 앞/뒤 오프셋", -1f, 1f) { showInputField = true, tooltip = "+: 앞으로, -: 뒤로" };
|
var footFB = new Slider("발 앞/뒤", -1f, 1f) { showInputField = true };
|
||||||
footFB.BindProperty(footFrontBackOffsetProp);
|
footFB.BindProperty(serializedObject.FindProperty("footFrontBackOffset"));
|
||||||
footFoldout.Add(footFB);
|
footFoldout.Add(footFB);
|
||||||
var footIO = new Slider("발 벌리기/모으기", -1f, 1f) { showInputField = true, tooltip = "+: 벌리기, -: 모으기" };
|
var footIO = new Slider("발 벌리기/모으기", -1f, 1f) { showInputField = true };
|
||||||
footIO.BindProperty(footInOutOffsetProp);
|
footIO.BindProperty(serializedObject.FindProperty("footInOutOffset"));
|
||||||
footFoldout.Add(footIO);
|
footFoldout.Add(footIO);
|
||||||
root.Add(footFoldout);
|
root.Add(footFoldout);
|
||||||
|
|
||||||
// 손가락 복제 설정
|
// ── 바닥 높이 ──
|
||||||
root.Add(BuildFingerCopySection());
|
|
||||||
|
|
||||||
// 바닥 높이 조정
|
|
||||||
var floorFoldout = new Foldout { text = "바닥 높이 조정", value = false };
|
var floorFoldout = new Foldout { text = "바닥 높이 조정", value = false };
|
||||||
floorFoldout.Add(new PropertyField(floorHeightProp, "바닥 높이 (-1 ~ 1)"));
|
floorFoldout.Add(new PropertyField(serializedObject.FindProperty("floorHeight"), "바닥 높이 (-1 ~ 1)"));
|
||||||
|
floorFoldout.Add(new PropertyField(serializedObject.FindProperty("minimumAnkleHeight"), "최소 발목 높이"));
|
||||||
root.Add(floorFoldout);
|
root.Add(floorFoldout);
|
||||||
|
|
||||||
// 접지 설정 (FootGroundingController)
|
// ── 머리 회전 오프셋 ──
|
||||||
|
root.Add(BuildHeadRotationSection());
|
||||||
|
|
||||||
|
// ── 어깨 보정 (ShoulderCorrection) ──
|
||||||
|
root.Add(BuildShoulderSection());
|
||||||
|
|
||||||
|
// ── 사지 가중치 (LimbWeight) ──
|
||||||
|
root.Add(BuildLimbWeightSection());
|
||||||
|
|
||||||
|
// ── 접지 설정 (FootGrounding) ──
|
||||||
root.Add(BuildGroundingSection());
|
root.Add(BuildGroundingSection());
|
||||||
|
|
||||||
// 캐시 상태 + 캘리브레이션 버튼
|
// ── 손가락 복제 설정 ──
|
||||||
|
var fingerCopyFoldout = new Foldout { text = "손가락 복제 설정", value = false };
|
||||||
|
fingerCopyFoldout.Add(new PropertyField(serializedObject.FindProperty("fingerCopyMode"), "복제 방식"));
|
||||||
|
root.Add(fingerCopyFoldout);
|
||||||
|
|
||||||
|
// ── 손가락 셰이핑 (FingerShaped) ──
|
||||||
|
root.Add(BuildFingerShapedSection());
|
||||||
|
|
||||||
|
// ── 캘리브레이션 ──
|
||||||
root.Add(BuildCacheSection());
|
root.Add(BuildCacheSection());
|
||||||
|
|
||||||
// 변경 시 저장
|
// ── 변경 시 저장 ──
|
||||||
root.TrackSerializedObjectValue(serializedObject, so =>
|
root.TrackSerializedObjectValue(serializedObject, so =>
|
||||||
{
|
{
|
||||||
if (target == null) return;
|
if (target == null) return;
|
||||||
@ -116,212 +95,504 @@ namespace KindRetargeting
|
|||||||
return root;
|
return root;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 힙 위치 보정 ==========
|
||||||
|
|
||||||
private VisualElement BuildHipsSection()
|
private VisualElement BuildHipsSection()
|
||||||
{
|
{
|
||||||
var foldout = new Foldout { text = "힙 위치 보정 (로컬 좌표계)" };
|
var foldout = new Foldout { text = "힙 위치 보정 (로컬 좌표계)", value = true };
|
||||||
|
|
||||||
// 축 매핑 정보
|
var axisInfo = new HelpBox("플레이 모드에서 축 매핑 정보가 표시됩니다.", HelpBoxMessageType.Info);
|
||||||
var axisInfo = new HelpBox("플레이 모드에서 T-포즈 분석 후 축 매핑 정보가 표시됩니다.\n이 매핑은 각 아바타의 힙 로컬 축 방향에 맞춰 자동 계산됩니다.", HelpBoxMessageType.Info);
|
|
||||||
foldout.Add(axisInfo);
|
foldout.Add(axisInfo);
|
||||||
|
|
||||||
// 주기적으로 축 매핑 정보 갱신
|
|
||||||
foldout.schedule.Execute(() =>
|
foldout.schedule.Execute(() =>
|
||||||
{
|
{
|
||||||
if (target == null || debugAxisNormalizerProp == null) return;
|
if (target == null) return;
|
||||||
serializedObject.Update();
|
serializedObject.Update();
|
||||||
Vector3 axisMapping = debugAxisNormalizerProp.vector3Value;
|
var axisProp = serializedObject.FindProperty("debugAxisNormalizer");
|
||||||
|
if (axisProp == null || !Application.isPlaying) { axisInfo.text = "플레이 모드에서 축 매핑 정보가 표시됩니다."; return; }
|
||||||
if (Application.isPlaying && axisMapping != Vector3.one)
|
Vector3 m = axisProp.vector3Value;
|
||||||
{
|
if (m == Vector3.one) { axisInfo.text = "플레이 모드에서 축 매핑 정보가 표시됩니다."; return; }
|
||||||
string GetAxisName(float value)
|
string A(float v) => Mathf.RoundToInt(Mathf.Abs(v)) switch { 1 => (v > 0 ? "+X" : "-X"), 2 => (v > 0 ? "+Y" : "-Y"), 3 => (v > 0 ? "+Z" : "-Z"), _ => "?" };
|
||||||
{
|
axisInfo.text = $"축 매핑: 좌우→{A(m.x)} 상하→{A(m.y)} 앞뒤→{A(m.z)}";
|
||||||
int axis = Mathf.RoundToInt(Mathf.Abs(value));
|
|
||||||
string sign = value > 0 ? "+" : "-";
|
|
||||||
return axis switch { 1 => $"{sign}X", 2 => $"{sign}Y", 3 => $"{sign}Z", _ => "?" };
|
|
||||||
}
|
|
||||||
axisInfo.text = "T-포즈에서 분석된 축 매핑:\n" +
|
|
||||||
$" 좌우 오프셋 → 로컬 {GetAxisName(axisMapping.x)} 축\n" +
|
|
||||||
$" 상하 오프셋 → 로컬 {GetAxisName(axisMapping.y)} 축\n" +
|
|
||||||
$" 앞뒤 오프셋 → 로컬 {GetAxisName(axisMapping.z)} 축\n\n" +
|
|
||||||
"이 매핑 덕분에 모든 아바타에서 동일하게 작동합니다.";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
axisInfo.text = "플레이 모드에서 T-포즈 분석 후 축 매핑 정보가 표시됩니다.\n이 매핑은 각 아바타의 힙 로컬 축 방향에 맞춰 자동 계산됩니다.";
|
|
||||||
}
|
|
||||||
}).Every(500);
|
}).Every(500);
|
||||||
|
|
||||||
foldout.Add(new PropertyField(hipsOffsetXProp, "좌우 오프셋 (←-/+→)") { tooltip = "캐릭터 기준 왼쪽(-) / 오른쪽(+)" });
|
var hx = new Slider("← 좌우 →", -1f, 1f) { showInputField = true };
|
||||||
foldout.Add(new PropertyField(hipsOffsetYProp, "상하 오프셋 (↓-/+↑)") { tooltip = "캐릭터 기준 아래(-) / 위(+)" });
|
hx.BindProperty(serializedObject.FindProperty("hipsOffsetX"));
|
||||||
foldout.Add(new PropertyField(hipsOffsetZProp, "앞뒤 오프셋 (←-/+→)") { tooltip = "캐릭터 기준 뒤(-) / 앞(+)" });
|
foldout.Add(hx);
|
||||||
foldout.Add(new HelpBox("로컬 좌표계 기반: 캐릭터의 회전 상태와 관계없이 항상 캐릭터 기준으로 이동합니다.", HelpBoxMessageType.Info));
|
var hy = new Slider("↓ 상하 ↑", -1f, 1f) { showInputField = true };
|
||||||
|
hy.BindProperty(serializedObject.FindProperty("hipsOffsetY"));
|
||||||
|
foldout.Add(hy);
|
||||||
|
var hz = new Slider("← 앞뒤 →", -1f, 1f) { showInputField = true };
|
||||||
|
hz.BindProperty(serializedObject.FindProperty("hipsOffsetZ"));
|
||||||
|
foldout.Add(hz);
|
||||||
|
|
||||||
|
// 의자 앉기 높이
|
||||||
|
var chairSlider = new Slider("의자 앉기 높이", -1f, 1f) { showInputField = true, tooltip = "의자에 앉을 때 엉덩이 높이 조정" };
|
||||||
|
chairSlider.BindProperty(serializedObject.FindProperty("limbWeight.chairSeatHeightOffset"));
|
||||||
|
foldout.Add(chairSlider);
|
||||||
|
|
||||||
// 다리 길이 자동 보정 버튼
|
// 다리 길이 자동 보정 버튼
|
||||||
var autoHipsBtn = new Button(() =>
|
var autoHipsBtn = new Button(() =>
|
||||||
{
|
{
|
||||||
if (!Application.isPlaying)
|
if (!Application.isPlaying) { Debug.LogWarning("플레이 모드에서만 사용 가능합니다."); return; }
|
||||||
{
|
|
||||||
Debug.LogWarning("다리 길이 자동 보정은 플레이 모드에서만 사용 가능합니다.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var script = (CustomRetargetingScript)target;
|
var script = (CustomRetargetingScript)target;
|
||||||
float offset = CalculateHipsOffsetFromLegDifference(script);
|
float offset = CalculateHipsOffsetFromLegDifference(script);
|
||||||
hipsOffsetYProp.floatValue = offset;
|
serializedObject.FindProperty("hipsOffsetY").floatValue = offset;
|
||||||
serializedObject.ApplyModifiedProperties();
|
serializedObject.ApplyModifiedProperties();
|
||||||
script.SaveSettings();
|
script.SaveSettings();
|
||||||
Debug.Log($"자동 보정 완료: hipsOffsetY = {offset:F4}");
|
}) { text = "다리 길이 자동 보정", tooltip = "소스/타겟 다리 길이 차이로 힙 상하 오프셋을 자동 계산합니다." };
|
||||||
}) { text = "다리 길이 자동 보정", tooltip = "소스/타겟 다리 길이 차이로 힙 상하 오프셋을 자동 계산합니다. (플레이 모드 전용)" };
|
autoHipsBtn.style.marginTop = 4; autoHipsBtn.style.height = 25;
|
||||||
autoHipsBtn.style.marginTop = 4;
|
|
||||||
foldout.Add(autoHipsBtn);
|
foldout.Add(autoHipsBtn);
|
||||||
|
|
||||||
return foldout;
|
return foldout;
|
||||||
}
|
}
|
||||||
|
|
||||||
private VisualElement BuildFingerCopySection()
|
// ========== 머리 회전 오프셋 ==========
|
||||||
|
|
||||||
|
private VisualElement BuildHeadRotationSection()
|
||||||
{
|
{
|
||||||
var foldout = new Foldout { text = "손가락 복제 설정" };
|
var foldout = new Foldout { text = "머리 회전 오프셋", value = false };
|
||||||
foldout.Add(new PropertyField(fingerCopyModeProp, "복제 방식") { tooltip = "손가락 포즈를 복제하는 방식을 선택합니다." });
|
|
||||||
|
var xProp = serializedObject.FindProperty("headRotationOffsetX");
|
||||||
|
var yProp = serializedObject.FindProperty("headRotationOffsetY");
|
||||||
|
var zProp = serializedObject.FindProperty("headRotationOffsetZ");
|
||||||
|
|
||||||
|
var xSlider = new Slider("X (Roll)", -180f, 180f) { showInputField = true };
|
||||||
|
xSlider.BindProperty(xProp);
|
||||||
|
foldout.Add(xSlider);
|
||||||
|
var ySlider = new Slider("Y (Yaw)", -180f, 180f) { showInputField = true };
|
||||||
|
ySlider.BindProperty(yProp);
|
||||||
|
foldout.Add(ySlider);
|
||||||
|
var zSlider = new Slider("Z (Pitch)", -180f, 180f) { showInputField = true };
|
||||||
|
zSlider.BindProperty(zProp);
|
||||||
|
foldout.Add(zSlider);
|
||||||
|
|
||||||
|
var btnRow = new VisualElement { style = { flexDirection = FlexDirection.Row, marginTop = 4 } };
|
||||||
|
var resetBtn = new Button(() =>
|
||||||
|
{
|
||||||
|
xProp.floatValue = yProp.floatValue = zProp.floatValue = 0f;
|
||||||
|
serializedObject.ApplyModifiedProperties();
|
||||||
|
EditorUtility.SetDirty(target);
|
||||||
|
}) { text = "초기화" };
|
||||||
|
resetBtn.style.flexGrow = 1; resetBtn.style.height = 25; resetBtn.style.marginRight = 2;
|
||||||
|
btnRow.Add(resetBtn);
|
||||||
|
|
||||||
|
var calibBtn = new Button(() =>
|
||||||
|
{
|
||||||
|
if (!Application.isPlaying) { Debug.LogWarning("플레이 모드에서만 사용 가능합니다."); return; }
|
||||||
|
CalibrateHeadToForward(serializedObject, xProp, yProp, zProp);
|
||||||
|
}) { text = "정면 캘리브레이션" };
|
||||||
|
calibBtn.style.flexGrow = 1; calibBtn.style.height = 25;
|
||||||
|
btnRow.Add(calibBtn);
|
||||||
|
foldout.Add(btnRow);
|
||||||
|
|
||||||
|
foldout.schedule.Execute(() =>
|
||||||
|
{
|
||||||
|
calibBtn.SetEnabled(Application.isPlaying);
|
||||||
|
}).Every(500);
|
||||||
|
|
||||||
return foldout;
|
return foldout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 어깨 보정 ==========
|
||||||
|
|
||||||
|
private VisualElement BuildShoulderSection()
|
||||||
|
{
|
||||||
|
var foldout = new Foldout { text = "어깨 보정 (ShoulderCorrection)", value = false };
|
||||||
|
|
||||||
|
var strength = new Slider("블렌드 강도", 0f, 5f) { showInputField = true };
|
||||||
|
strength.BindProperty(serializedObject.FindProperty("shoulderCorrection.blendStrength"));
|
||||||
|
foldout.Add(strength);
|
||||||
|
|
||||||
|
var maxBlend = new Slider("최대 블렌드", 0f, 1f) { showInputField = true };
|
||||||
|
maxBlend.BindProperty(serializedObject.FindProperty("shoulderCorrection.maxShoulderBlend"));
|
||||||
|
foldout.Add(maxBlend);
|
||||||
|
|
||||||
|
foldout.Add(new PropertyField(serializedObject.FindProperty("shoulderCorrection.reverseLeftRotation"), "왼쪽 회전 반전"));
|
||||||
|
foldout.Add(new PropertyField(serializedObject.FindProperty("shoulderCorrection.reverseRightRotation"), "오른쪽 회전 반전"));
|
||||||
|
|
||||||
|
foldout.Add(BuildMinMaxRange("높이 차이 범위",
|
||||||
|
serializedObject.FindProperty("shoulderCorrection.minHeightDifference"),
|
||||||
|
serializedObject.FindProperty("shoulderCorrection.maxHeightDifference"),
|
||||||
|
-0.5f, 2f));
|
||||||
|
|
||||||
|
foldout.Add(new PropertyField(serializedObject.FindProperty("shoulderCorrection.shoulderCorrectionCurve"), "보정 커브"));
|
||||||
|
|
||||||
|
return foldout;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 사지 가중치 ==========
|
||||||
|
|
||||||
|
private VisualElement BuildLimbWeightSection()
|
||||||
|
{
|
||||||
|
var foldout = new Foldout { text = "사지 가중치 (LimbWeight)", value = false };
|
||||||
|
|
||||||
|
// IK 활성화 토글
|
||||||
|
foldout.Add(new PropertyField(serializedObject.FindProperty("limbWeight.enableLeftArmIK"), "왼팔 IK 활성화"));
|
||||||
|
foldout.Add(new PropertyField(serializedObject.FindProperty("limbWeight.enableRightArmIK"), "오른팔 IK 활성화"));
|
||||||
|
|
||||||
|
foldout.Add(BuildMinMaxRange("손-프랍 거리 범위 (가중치 1→0)",
|
||||||
|
serializedObject.FindProperty("limbWeight.minDistance"),
|
||||||
|
serializedObject.FindProperty("limbWeight.maxDistance"),
|
||||||
|
0f, 1f));
|
||||||
|
|
||||||
|
var smoothField = new Slider("가중치 변화 속도", 0.1f, 20f) { showInputField = true };
|
||||||
|
smoothField.BindProperty(serializedObject.FindProperty("limbWeight.weightSmoothSpeed"));
|
||||||
|
foldout.Add(smoothField);
|
||||||
|
|
||||||
|
foldout.Add(BuildMinMaxRange("의자-허리 거리 범위 (가중치 1→0)",
|
||||||
|
serializedObject.FindProperty("limbWeight.hipsMinDistance"),
|
||||||
|
serializedObject.FindProperty("limbWeight.hipsMaxDistance"),
|
||||||
|
0f, 1f));
|
||||||
|
|
||||||
|
foldout.Add(BuildMinMaxRange("바닥-허리 높이 블렌딩 (가중치 0→1)",
|
||||||
|
serializedObject.FindProperty("limbWeight.groundHipsMinHeight"),
|
||||||
|
serializedObject.FindProperty("limbWeight.groundHipsMaxHeight"),
|
||||||
|
0f, 2f));
|
||||||
|
|
||||||
|
foldout.Add(BuildMinMaxRange("발 높이 IK 블렌딩 (가중치 1→0)",
|
||||||
|
serializedObject.FindProperty("limbWeight.footHeightMinThreshold"),
|
||||||
|
serializedObject.FindProperty("limbWeight.footHeightMaxThreshold"),
|
||||||
|
0.1f, 1f));
|
||||||
|
|
||||||
|
return foldout;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 접지 설정 ==========
|
||||||
|
|
||||||
private VisualElement BuildGroundingSection()
|
private VisualElement BuildGroundingSection()
|
||||||
{
|
{
|
||||||
var foldout = new Foldout { text = "접지 설정 (FootGrounding)", value = false };
|
var foldout = new Foldout { text = "접지 설정 (FootGrounding)", value = false };
|
||||||
|
|
||||||
foldout.Add(new HelpBox(
|
foldout.Add(new HelpBox(
|
||||||
"HIK 스타일 2-Pass 접지 시스템:\n" +
|
"HIK 스타일 2-Pass 접지 시스템:\n" +
|
||||||
"• Pre-IK: IK 타겟을 조정하여 발이 바닥을 뚫지 않도록 보정\n" +
|
"• Pre-IK: 발이 바닥을 뚫지 않도록 IK 타겟 보정\n" +
|
||||||
"• Post-IK: Foot 회전으로 Toes 접지 잔차 미세 보정\n" +
|
"• Post-IK: Foot 회전으로 Toes 접지 미세 보정",
|
||||||
"• Toe Pivot: 발끝 고정 + 발목 회전 자동 감지",
|
|
||||||
HelpBoxMessageType.Info));
|
HelpBoxMessageType.Info));
|
||||||
|
|
||||||
// FootGroundingController는 CRS 내부 모듈 — serializedObject의 프로퍼티 경로로 접근
|
foldout.Add(new PropertyField(serializedObject.FindProperty("footGrounding.groundHeight"), "바닥 높이"));
|
||||||
var groundHeightField = new PropertyField(serializedObject.FindProperty("footGrounding.groundHeight"), "바닥 높이");
|
|
||||||
foldout.Add(groundHeightField);
|
|
||||||
|
|
||||||
var weightSlider = new Slider("접지 강도", 0f, 1f) { showInputField = true };
|
var weightSlider = new Slider("접지 강도", 0f, 1f) { showInputField = true };
|
||||||
weightSlider.BindProperty(serializedObject.FindProperty("footGrounding.groundingWeight"));
|
weightSlider.BindProperty(serializedObject.FindProperty("footGrounding.groundingWeight"));
|
||||||
foldout.Add(weightSlider);
|
foldout.Add(weightSlider);
|
||||||
|
|
||||||
var activationField = new PropertyField(serializedObject.FindProperty("footGrounding.activationHeight"), "활성화 높이");
|
foldout.Add(new PropertyField(serializedObject.FindProperty("footGrounding.activationHeight"), "활성화 높이") { tooltip = "발목이 이 높이 이상이면 보정 비활성화" });
|
||||||
activationField.tooltip = "발목이 이 높이 이상이면 접지 보정 비활성화 (점프 등)";
|
foldout.Add(new PropertyField(serializedObject.FindProperty("footGrounding.plantThreshold"), "접지 판정 범위"));
|
||||||
foldout.Add(activationField);
|
|
||||||
|
|
||||||
var thresholdField = new PropertyField(serializedObject.FindProperty("footGrounding.plantThreshold"), "접지 판정 범위");
|
var smoothField = new Slider("스무딩 속도", 1f, 30f) { showInputField = true };
|
||||||
thresholdField.tooltip = "Toes가 이 범위 안이면 접지 중으로 판정";
|
smoothField.BindProperty(serializedObject.FindProperty("footGrounding.smoothSpeed"));
|
||||||
foldout.Add(thresholdField);
|
|
||||||
|
|
||||||
var smoothField = new PropertyField(serializedObject.FindProperty("footGrounding.smoothSpeed"), "보정 스무딩 속도");
|
|
||||||
smoothField.tooltip = "보정량 변화 속도 (높을수록 빠른 반응, 낮으면 부드러운 전환)";
|
|
||||||
foldout.Add(smoothField);
|
foldout.Add(smoothField);
|
||||||
|
|
||||||
foldout.Add(new HelpBox("힙 높이 보정은 '바닥 높이 조정' 섹션의 floorHeight로 제어합니다.", HelpBoxMessageType.Info));
|
|
||||||
|
|
||||||
return foldout;
|
return foldout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 손가락 셰이핑 ==========
|
||||||
|
|
||||||
|
private VisualElement BuildFingerShapedSection()
|
||||||
|
{
|
||||||
|
var foldout = new Foldout { text = "손가락 셰이핑 (FingerShaped)", value = false };
|
||||||
|
|
||||||
|
foldout.Add(new PropertyField(serializedObject.FindProperty("fingerShaped.enabled"), "셰이핑 활성화"));
|
||||||
|
foldout.Add(BuildHandSliders("왼손", "left"));
|
||||||
|
foldout.Add(BuildHandSliders("오른손", "right"));
|
||||||
|
|
||||||
|
// 프리셋 버튼
|
||||||
|
var presetLabel = new Label("프리셋") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginTop = 6 } };
|
||||||
|
foldout.Add(presetLabel);
|
||||||
|
string[,] presets = { { "가위", "바위", "보" }, { "브이", "검지", "초기화" } };
|
||||||
|
for (int row = 0; row < 2; row++)
|
||||||
|
{
|
||||||
|
var btnRow = new VisualElement { style = { flexDirection = FlexDirection.Row, justifyContent = Justify.Center, marginBottom = 2 } };
|
||||||
|
for (int col = 0; col < 3; col++)
|
||||||
|
{
|
||||||
|
string name = presets[row, col];
|
||||||
|
var btn = new Button(() => ApplyFingerPreset(name)) { text = name };
|
||||||
|
btn.style.height = 26; btn.style.width = 80; btn.style.marginLeft = btn.style.marginRight = 2;
|
||||||
|
btnRow.Add(btn);
|
||||||
|
}
|
||||||
|
foldout.Add(btnRow);
|
||||||
|
}
|
||||||
|
|
||||||
|
return foldout;
|
||||||
|
}
|
||||||
|
|
||||||
|
private VisualElement BuildHandSliders(string label, string prefix)
|
||||||
|
{
|
||||||
|
var handFoldout = new Foldout { text = label, value = false };
|
||||||
|
|
||||||
|
handFoldout.Add(new PropertyField(serializedObject.FindProperty($"fingerShaped.{prefix}HandEnabled"), "활성화"));
|
||||||
|
|
||||||
|
string[] names = { "Thumb", "Index", "Middle", "Ring", "Pinky" };
|
||||||
|
string[] korNames = { "엄지", "검지", "중지", "약지", "새끼" };
|
||||||
|
|
||||||
|
var slidersRow = new VisualElement { style = { flexDirection = FlexDirection.Row, justifyContent = Justify.Center, marginTop = 4 } };
|
||||||
|
for (int i = 0; i < names.Length; i++)
|
||||||
|
{
|
||||||
|
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 = serializedObject.FindProperty($"fingerShaped.{prefix}{names[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);
|
||||||
|
|
||||||
|
var slider = new Slider(-1f, 1f) { direction = SliderDirection.Vertical };
|
||||||
|
slider.style.height = 80; slider.style.width = 25;
|
||||||
|
slider.BindProperty(prop);
|
||||||
|
slider.RegisterValueChangedCallback(evt => valLabel.text = evt.newValue.ToString("F1"));
|
||||||
|
col.Add(slider);
|
||||||
|
slidersRow.Add(col);
|
||||||
|
}
|
||||||
|
handFoldout.Add(slidersRow);
|
||||||
|
|
||||||
|
var spreadSlider = new Slider("벌리기", -1f, 1f) { showInputField = true };
|
||||||
|
spreadSlider.BindProperty(serializedObject.FindProperty($"fingerShaped.{prefix}SpreadFingers"));
|
||||||
|
handFoldout.Add(spreadSlider);
|
||||||
|
|
||||||
|
return handFoldout;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyFingerPreset(string presetName)
|
||||||
|
{
|
||||||
|
var script = (CustomRetargetingScript)target;
|
||||||
|
var fc = script.fingerShaped;
|
||||||
|
if (!fc.enabled) fc.enabled = true;
|
||||||
|
|
||||||
|
(float t, float i, float m, float r, float p, float s) = presetName switch
|
||||||
|
{
|
||||||
|
"가위" => (1f, 1f, -1f, -1f, -1f, 0.3f),
|
||||||
|
"바위" => (-1f, -1f, -1f, -1f, -1f, 0f),
|
||||||
|
"보" => (1f, 1f, 1f, 1f, 1f, 1f),
|
||||||
|
"브이" => (-1f, 1f, 1f, -1f, -1f, 1f),
|
||||||
|
"검지" => (-1f, 1f, -1f, -1f, -1f, 0f),
|
||||||
|
"초기화" => (0.8f, 0.8f, 0.8f, 0.8f, 0.8f, 0.8f),
|
||||||
|
_ => (0f, 0f, 0f, 0f, 0f, 0f)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (fc.leftHandEnabled)
|
||||||
|
{
|
||||||
|
fc.leftThumbCurl = t; fc.leftIndexCurl = i; fc.leftMiddleCurl = m;
|
||||||
|
fc.leftRingCurl = r; fc.leftPinkyCurl = p; fc.leftSpreadFingers = s;
|
||||||
|
}
|
||||||
|
if (fc.rightHandEnabled)
|
||||||
|
{
|
||||||
|
fc.rightThumbCurl = t; fc.rightIndexCurl = i; fc.rightMiddleCurl = m;
|
||||||
|
fc.rightRingCurl = r; fc.rightPinkyCurl = p; fc.rightSpreadFingers = s;
|
||||||
|
}
|
||||||
|
EditorUtility.SetDirty(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 캘리브레이션 ==========
|
||||||
|
|
||||||
private VisualElement BuildCacheSection()
|
private VisualElement BuildCacheSection()
|
||||||
{
|
{
|
||||||
var container = new VisualElement { style = { marginTop = 8 } };
|
var box = new VisualElement { style = { marginTop = 8 } };
|
||||||
|
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 = 6;
|
||||||
|
|
||||||
var script = (CustomRetargetingScript)target;
|
var cacheLabel = new Label();
|
||||||
bool hasCached = script.HasCachedSettings();
|
cacheLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
||||||
|
box.Add(cacheLabel);
|
||||||
cacheStatusLabel = new Label(hasCached ?
|
|
||||||
"캘리브레이션 데이터가 저장되어 있습니다." :
|
|
||||||
"저장된 캘리브레이션 데이터가 없습니다.");
|
|
||||||
cacheStatusLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
|
||||||
container.Add(cacheStatusLabel);
|
|
||||||
|
|
||||||
var btnRow = new VisualElement { style = { flexDirection = FlexDirection.Row, marginTop = 4 } };
|
var btnRow = new VisualElement { style = { flexDirection = FlexDirection.Row, marginTop = 4 } };
|
||||||
|
|
||||||
var calibBtn = new Button(() =>
|
var calibBtn = new Button(() => { ((CustomRetargetingScript)target).I_PoseCalibration(); }) { text = "I-포즈 캘리브레이션" };
|
||||||
{
|
calibBtn.style.flexGrow = 1; calibBtn.style.marginRight = 2;
|
||||||
((CustomRetargetingScript)target).I_PoseCalibration();
|
|
||||||
UpdateCacheStatus();
|
|
||||||
}) { text = "I-포즈 캘리브레이션" };
|
|
||||||
calibBtn.style.flexGrow = 1;
|
|
||||||
calibBtn.style.marginRight = 2;
|
|
||||||
btnRow.Add(calibBtn);
|
btnRow.Add(calibBtn);
|
||||||
|
|
||||||
var deleteCacheBtn = new Button(() =>
|
var resetBtn = new Button(() => { ((CustomRetargetingScript)target).ResetPoseAndCache(); }) { text = "캘리브레이션 초기화" };
|
||||||
|
resetBtn.style.flexGrow = 1;
|
||||||
|
btnRow.Add(resetBtn);
|
||||||
|
box.Add(btnRow);
|
||||||
|
|
||||||
|
// 전체 자동 보정
|
||||||
|
var autoBtn = new Button(() =>
|
||||||
{
|
{
|
||||||
((CustomRetargetingScript)target).ResetPoseAndCache();
|
if (!Application.isPlaying) { Debug.LogWarning("플레이 모드에서만 사용 가능합니다."); return; }
|
||||||
UpdateCacheStatus();
|
AutoCalibrateAll((CustomRetargetingScript)target, serializedObject);
|
||||||
}) { text = "캐시 데이터 삭제" };
|
}) { text = "전체 자동 보정 (크기 + 힙 높이 + 머리 정면)", tooltip = "아바타 크기, 힙 높이, 머리 정면을 자동 보정합니다." };
|
||||||
deleteCacheBtn.style.flexGrow = 1;
|
autoBtn.style.marginTop = 4; autoBtn.style.height = 28;
|
||||||
btnRow.Add(deleteCacheBtn);
|
box.Add(autoBtn);
|
||||||
|
|
||||||
container.Add(btnRow);
|
box.schedule.Execute(() =>
|
||||||
|
|
||||||
// 주기적으로 캐시 상태 갱신
|
|
||||||
container.schedule.Execute(() =>
|
|
||||||
{
|
{
|
||||||
if (target == null) return;
|
if (target == null) return;
|
||||||
bool cached = ((CustomRetargetingScript)target).HasCachedSettings();
|
bool cached = ((CustomRetargetingScript)target).HasCachedSettings();
|
||||||
deleteCacheBtn.style.display = cached ? DisplayStyle.Flex : DisplayStyle.None;
|
cacheLabel.text = cached ? "캘리브레이션 데이터가 저장되어 있습니다." : "저장된 캘리브레이션 데이터가 없습니다.";
|
||||||
cacheStatusLabel.text = cached ?
|
resetBtn.style.display = cached ? DisplayStyle.Flex : DisplayStyle.None;
|
||||||
"캘리브레이션 데이터가 저장되어 있습니다." :
|
|
||||||
"저장된 캘리브레이션 데이터가 없습니다.";
|
|
||||||
}).Every(1000);
|
}).Every(1000);
|
||||||
|
|
||||||
deleteCacheBtn.style.display = hasCached ? DisplayStyle.Flex : DisplayStyle.None;
|
return box;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== MinMax 헬퍼 ==========
|
||||||
|
|
||||||
|
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, fontSize = 11 } });
|
||||||
|
|
||||||
|
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 } };
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
minField.RegisterValueChangedCallback(evt =>
|
||||||
|
{
|
||||||
|
float v = Mathf.Clamp(evt.newValue, limitMin, maxProp.floatValue);
|
||||||
|
minProp.floatValue = v; serializedObject.ApplyModifiedProperties();
|
||||||
|
slider.SetValueWithoutNotify(new Vector2(v, maxProp.floatValue)); minField.SetValueWithoutNotify(v);
|
||||||
|
});
|
||||||
|
maxField.RegisterValueChangedCallback(evt =>
|
||||||
|
{
|
||||||
|
float v = Mathf.Clamp(evt.newValue, minProp.floatValue, limitMax);
|
||||||
|
maxProp.floatValue = v; serializedObject.ApplyModifiedProperties();
|
||||||
|
slider.SetValueWithoutNotify(new Vector2(minProp.floatValue, v)); maxField.SetValueWithoutNotify(v);
|
||||||
|
});
|
||||||
|
|
||||||
|
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;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateCacheStatus()
|
// ========== 자동 보정 ==========
|
||||||
|
|
||||||
|
private void AutoCalibrateAll(CustomRetargetingScript script, SerializedObject so)
|
||||||
{
|
{
|
||||||
if (cacheStatusLabel == null || target == null) return;
|
Animator source = script.sourceAnimator;
|
||||||
bool hasCached = ((CustomRetargetingScript)target).HasCachedSettings();
|
Animator targetAnim = script.targetAnimator;
|
||||||
cacheStatusLabel.text = hasCached ?
|
if (source == null || targetAnim == null || !source.isHuman || !targetAnim.isHuman)
|
||||||
"캘리브레이션 데이터가 저장되어 있습니다." :
|
{
|
||||||
"저장된 캘리브레이션 데이터가 없습니다.";
|
Debug.LogWarning("소스/타겟 Animator가 없거나 Humanoid가 아닙니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
script.ResetScale();
|
||||||
|
so.FindProperty("avatarScale").floatValue = 1f;
|
||||||
|
so.FindProperty("hipsOffsetY").floatValue = CalculateHipsOffsetFromLegDifference(script);
|
||||||
|
so.ApplyModifiedProperties();
|
||||||
|
|
||||||
|
EditorApplication.delayCall += () =>
|
||||||
|
{
|
||||||
|
if (script == null) return;
|
||||||
|
Transform sourceNeck = source.GetBoneTransform(HumanBodyBones.Neck);
|
||||||
|
Transform targetNeck = targetAnim.GetBoneTransform(HumanBodyBones.Neck);
|
||||||
|
if (sourceNeck == null || targetNeck == null) return;
|
||||||
|
|
||||||
|
float scaleRatio = Mathf.Clamp(sourceNeck.position.y / Mathf.Max(targetNeck.position.y, 0.01f), 0.1f, 3f);
|
||||||
|
script.SetAvatarScale(scaleRatio);
|
||||||
|
|
||||||
|
var so2 = new SerializedObject(script);
|
||||||
|
so2.FindProperty("avatarScale").floatValue = scaleRatio;
|
||||||
|
so2.ApplyModifiedProperties();
|
||||||
|
|
||||||
|
EditorApplication.delayCall += () =>
|
||||||
|
{
|
||||||
|
if (script == null) return;
|
||||||
|
var so3 = new SerializedObject(script);
|
||||||
|
so3.FindProperty("hipsOffsetY").floatValue = CalculateHipsOffsetFromLegDifference(script);
|
||||||
|
|
||||||
|
var xP = so3.FindProperty("headRotationOffsetX");
|
||||||
|
var yP = so3.FindProperty("headRotationOffsetY");
|
||||||
|
var zP = so3.FindProperty("headRotationOffsetZ");
|
||||||
|
if (xP != null && yP != null && zP != null)
|
||||||
|
CalibrateHeadToForward(so3, xP, yP, zP);
|
||||||
|
|
||||||
|
so3.ApplyModifiedProperties();
|
||||||
|
script.SaveSettings();
|
||||||
|
Debug.Log($"전체 자동 보정 완료: avatarScale={scaleRatio:F3}");
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
// ========== 유틸리티 ==========
|
||||||
/// 소스/타겟 다리 길이 차이로 힙 상하 오프셋을 계산합니다.
|
|
||||||
/// 소스 다리가 더 길면 → 음수 (힙을 내려서 타겟이 뜨지 않게)
|
|
||||||
/// 소스 다리가 더 짧으면 → 양수 (힙을 올려서 타겟 다리를 펴줌)
|
|
||||||
/// </summary>
|
|
||||||
private float CalculateHipsOffsetFromLegDifference(CustomRetargetingScript script)
|
private float CalculateHipsOffsetFromLegDifference(CustomRetargetingScript script)
|
||||||
{
|
{
|
||||||
Animator source = script.sourceAnimator;
|
Animator source = script.sourceAnimator;
|
||||||
Animator target = script.targetAnimator;
|
Animator targetAnim = script.targetAnimator;
|
||||||
|
if (source == null || targetAnim == null) return 0f;
|
||||||
if (source == null || target == null || !source.isHuman || !target.isHuman)
|
|
||||||
{
|
|
||||||
Debug.LogWarning("소스/타겟 Animator가 없거나 Humanoid가 아닙니다.");
|
|
||||||
return 0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
float sourceLeg = GetLegLength(source);
|
float sourceLeg = GetLegLength(source);
|
||||||
float targetLeg = GetLegLength(target);
|
float targetLeg = GetLegLength(targetAnim);
|
||||||
|
if (sourceLeg < 0.01f || targetLeg < 0.01f) return 0f;
|
||||||
|
|
||||||
if (sourceLeg < 0.01f || targetLeg < 0.01f)
|
return targetLeg - sourceLeg;
|
||||||
{
|
|
||||||
Debug.LogWarning("다리 길이를 계산할 수 없습니다. 본이 올바르게 설정되어 있는지 확인해주세요.");
|
|
||||||
return 0f;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 소스 다리가 더 길면 타겟이 뜨므로 힙을 내려야 함 (음수)
|
|
||||||
// 소스 다리가 더 짧으면 타겟 다리가 구부러지므로 힙을 올려야 함 (양수)
|
|
||||||
float diff = targetLeg - sourceLeg;
|
|
||||||
Debug.Log($"소스 다리 길이: {sourceLeg:F4}, 타겟 다리 길이: {targetLeg:F4}, 힙 오프셋: {diff:F4}m");
|
|
||||||
return diff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private float GetLegLength(Animator animator)
|
private float GetLegLength(Animator animator)
|
||||||
{
|
{
|
||||||
Transform upperLeg = animator.GetBoneTransform(HumanBodyBones.LeftUpperLeg);
|
Transform upper = animator.GetBoneTransform(HumanBodyBones.LeftUpperLeg);
|
||||||
Transform lowerLeg = animator.GetBoneTransform(HumanBodyBones.LeftLowerLeg);
|
Transform lower = animator.GetBoneTransform(HumanBodyBones.LeftLowerLeg);
|
||||||
Transform foot = animator.GetBoneTransform(HumanBodyBones.LeftFoot);
|
Transform foot = animator.GetBoneTransform(HumanBodyBones.LeftFoot);
|
||||||
|
if (upper == null || lower == null || foot == null) return 0f;
|
||||||
|
return Vector3.Distance(upper.position, lower.position) + Vector3.Distance(lower.position, foot.position);
|
||||||
|
}
|
||||||
|
|
||||||
if (upperLeg == null || lowerLeg == null || foot == null) return 0f;
|
private void CalibrateHeadToForward(SerializedObject so, SerializedProperty xProp, SerializedProperty yProp, SerializedProperty zProp)
|
||||||
|
{
|
||||||
|
CustomRetargetingScript script = so.targetObject as CustomRetargetingScript;
|
||||||
|
if (script == null) return;
|
||||||
|
|
||||||
float upper = Vector3.Distance(upperLeg.position, lowerLeg.position);
|
Animator targetAnimator = script.GetComponent<Animator>();
|
||||||
float lower = Vector3.Distance(lowerLeg.position, foot.position);
|
if (targetAnimator == null) return;
|
||||||
return upper + lower;
|
|
||||||
|
Transform targetHead = targetAnimator.GetBoneTransform(HumanBodyBones.Head);
|
||||||
|
if (targetHead == null) return;
|
||||||
|
|
||||||
|
Vector3 tPoseForward = script.tPoseHeadForward;
|
||||||
|
Vector3 tPoseUp = script.tPoseHeadUp;
|
||||||
|
if (tPoseForward.sqrMagnitude < 0.001f) return;
|
||||||
|
|
||||||
|
float prevX = xProp.floatValue, prevY = yProp.floatValue, prevZ = zProp.floatValue;
|
||||||
|
Quaternion currentLocalRot = targetHead.localRotation;
|
||||||
|
Quaternion prevOffset = Quaternion.Euler(prevX, prevY, prevZ);
|
||||||
|
Quaternion baseLocalRot = currentLocalRot * Quaternion.Inverse(prevOffset);
|
||||||
|
|
||||||
|
Transform headParent = targetHead.parent;
|
||||||
|
Quaternion parentWorldRot = headParent != null ? headParent.rotation : Quaternion.identity;
|
||||||
|
Quaternion baseWorldRot = parentWorldRot * baseLocalRot;
|
||||||
|
|
||||||
|
Vector3 currentHeadForward = baseWorldRot * Vector3.forward;
|
||||||
|
Vector3 currentHeadUp = baseWorldRot * Vector3.up;
|
||||||
|
|
||||||
|
Quaternion forwardCorrection = Quaternion.FromToRotation(currentHeadForward, tPoseForward);
|
||||||
|
Vector3 correctedUp = forwardCorrection * currentHeadUp;
|
||||||
|
float rollAngle = Vector3.SignedAngle(correctedUp, tPoseUp, tPoseForward);
|
||||||
|
Quaternion rollCorrection = Quaternion.AngleAxis(rollAngle, tPoseForward);
|
||||||
|
Quaternion worldCorrection = rollCorrection * forwardCorrection;
|
||||||
|
|
||||||
|
Quaternion correctedWorldRot = worldCorrection * baseWorldRot;
|
||||||
|
Quaternion correctedLocalRot = Quaternion.Inverse(parentWorldRot) * correctedWorldRot;
|
||||||
|
Quaternion offsetQuat = Quaternion.Inverse(baseLocalRot) * correctedLocalRot;
|
||||||
|
|
||||||
|
Vector3 euler = offsetQuat.eulerAngles;
|
||||||
|
if (euler.x > 180f) euler.x -= 360f;
|
||||||
|
if (euler.y > 180f) euler.y -= 360f;
|
||||||
|
if (euler.z > 180f) euler.z -= 360f;
|
||||||
|
|
||||||
|
xProp.floatValue = Mathf.Clamp(euler.x, -180f, 180f);
|
||||||
|
yProp.floatValue = Mathf.Clamp(euler.y, -180f, 180f);
|
||||||
|
zProp.floatValue = Mathf.Clamp(euler.z, -180f, 180f);
|
||||||
|
so.ApplyModifiedProperties();
|
||||||
|
EditorUtility.SetDirty(so.targetObject);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -261,12 +261,27 @@ namespace KindRetargeting.Remote
|
|||||||
{ "footHeightMinThreshold", script.limbWeight.footHeightMinThreshold },
|
{ "footHeightMinThreshold", script.limbWeight.footHeightMinThreshold },
|
||||||
{ "footHeightMaxThreshold", script.limbWeight.footHeightMaxThreshold },
|
{ "footHeightMaxThreshold", script.limbWeight.footHeightMaxThreshold },
|
||||||
{ "chairSeatHeightOffset", script.limbWeight.chairSeatHeightOffset },
|
{ "chairSeatHeightOffset", script.limbWeight.chairSeatHeightOffset },
|
||||||
};
|
{ "enableLeftArmIK", script.limbWeight.enableLeftArmIK },
|
||||||
|
{ "enableRightArmIK", script.limbWeight.enableRightArmIK },
|
||||||
|
|
||||||
// FingerShapedController 데이터
|
// ShoulderCorrection 데이터
|
||||||
data["handPoseEnabled"] = script.fingerShaped.enabled;
|
{ "shoulderBlendStrength", script.shoulderCorrection.blendStrength },
|
||||||
data["leftHandEnabled"] = script.fingerShaped.leftHandEnabled;
|
{ "shoulderMaxBlend", script.shoulderCorrection.maxShoulderBlend },
|
||||||
data["rightHandEnabled"] = script.fingerShaped.rightHandEnabled;
|
{ "shoulderMaxHeightDiff", script.shoulderCorrection.maxHeightDifference },
|
||||||
|
{ "shoulderMinHeightDiff", script.shoulderCorrection.minHeightDifference },
|
||||||
|
|
||||||
|
// FootGrounding 데이터
|
||||||
|
{ "groundingWeight", script.footGrounding.groundingWeight },
|
||||||
|
{ "groundingGroundHeight", script.footGrounding.groundHeight },
|
||||||
|
{ "groundingActivationHeight", script.footGrounding.activationHeight },
|
||||||
|
{ "groundingPlantThreshold", script.footGrounding.plantThreshold },
|
||||||
|
{ "groundingSmoothSpeed", script.footGrounding.smoothSpeed },
|
||||||
|
|
||||||
|
// FingerShapedController 데이터
|
||||||
|
{ "handPoseEnabled", script.fingerShaped.enabled },
|
||||||
|
{ "leftHandEnabled", script.fingerShaped.leftHandEnabled },
|
||||||
|
{ "rightHandEnabled", script.fingerShaped.rightHandEnabled },
|
||||||
|
};
|
||||||
|
|
||||||
var response = new
|
var response = new
|
||||||
{
|
{
|
||||||
@ -374,6 +389,43 @@ namespace KindRetargeting.Remote
|
|||||||
case "chairSeatHeightOffset":
|
case "chairSeatHeightOffset":
|
||||||
script.limbWeight.chairSeatHeightOffset = value;
|
script.limbWeight.chairSeatHeightOffset = value;
|
||||||
break;
|
break;
|
||||||
|
case "enableLeftArmIK":
|
||||||
|
script.limbWeight.enableLeftArmIK = value > 0.5f;
|
||||||
|
break;
|
||||||
|
case "enableRightArmIK":
|
||||||
|
script.limbWeight.enableRightArmIK = value > 0.5f;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// ShoulderCorrection 속성
|
||||||
|
case "shoulderBlendStrength":
|
||||||
|
script.shoulderCorrection.blendStrength = value;
|
||||||
|
break;
|
||||||
|
case "shoulderMaxBlend":
|
||||||
|
script.shoulderCorrection.maxShoulderBlend = value;
|
||||||
|
break;
|
||||||
|
case "shoulderMaxHeightDiff":
|
||||||
|
script.shoulderCorrection.maxHeightDifference = value;
|
||||||
|
break;
|
||||||
|
case "shoulderMinHeightDiff":
|
||||||
|
script.shoulderCorrection.minHeightDifference = value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// FootGrounding 속성
|
||||||
|
case "groundingWeight":
|
||||||
|
script.footGrounding.groundingWeight = value;
|
||||||
|
break;
|
||||||
|
case "groundingGroundHeight":
|
||||||
|
script.footGrounding.groundHeight = value;
|
||||||
|
break;
|
||||||
|
case "groundingActivationHeight":
|
||||||
|
script.footGrounding.activationHeight = value;
|
||||||
|
break;
|
||||||
|
case "groundingPlantThreshold":
|
||||||
|
script.footGrounding.plantThreshold = value;
|
||||||
|
break;
|
||||||
|
case "groundingSmoothSpeed":
|
||||||
|
script.footGrounding.smoothSpeed = value;
|
||||||
|
break;
|
||||||
|
|
||||||
// FingerShapedController 속성
|
// FingerShapedController 속성
|
||||||
case "handPoseEnabled":
|
case "handPoseEnabled":
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user