Streamingle_URP/Assets/Scripts/KindRetargeting/Editor/CustomRetargetingScriptEditor.cs

571 lines
28 KiB
C#

using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
namespace KindRetargeting
{
[CustomEditor(typeof(CustomRetargetingScript))]
public class CustomRetargetingScriptEditor : BaseRetargetingEditor
{
private const string CommonUssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/StreamingleCommon.uss";
protected override void OnDisable()
{
base.OnDisable();
}
public override VisualElement CreateInspectorGUI()
{
var root = new VisualElement();
var commonUss = AssetDatabase.LoadAssetAtPath<StyleSheet>(CommonUssPath);
if (commonUss != null) root.styleSheets.Add(commonUss);
// ── 기본 설정 ──
root.Add(new PropertyField(serializedObject.FindProperty("optitrackSource"), "원본 OptiTrack"));
// ── 아바타 크기 ──
var scaleFoldout = new Foldout { text = "아바타 크기 설정", value = true };
scaleFoldout.Add(new PropertyField(serializedObject.FindProperty("avatarScale"), "아바타 크기"));
scaleFoldout.Add(new PropertyField(serializedObject.FindProperty("headScale"), "머리 크기"));
root.Add(scaleFoldout);
// ── 힙 위치 보정 ──
root.Add(BuildHipsSection());
// ── 무릎 위치 조정 ──
var kneeFoldout = new Foldout { text = "무릎 위치 조정", value = false };
var kneeFB = new Slider("무릎 앞/뒤", -1f, 1f) { showInputField = true };
kneeFB.BindProperty(serializedObject.FindProperty("kneeFrontBackWeight"));
kneeFoldout.Add(kneeFB);
var kneeIO = new Slider("무릎 안/밖", -1f, 1f) { showInputField = true };
kneeIO.BindProperty(serializedObject.FindProperty("kneeInOutWeight"));
kneeFoldout.Add(kneeIO);
root.Add(kneeFoldout);
// ── 발 IK 위치 조정 ──
var footFoldout = new Foldout { text = "발 IK 위치 조정", value = false };
var footFB = new Slider("발 앞/뒤", -1f, 1f) { showInputField = true };
footFB.BindProperty(serializedObject.FindProperty("footFrontBackOffset"));
footFoldout.Add(footFB);
var footIO = new Slider("발 벌리기/모으기", -1f, 1f) { showInputField = true };
footIO.BindProperty(serializedObject.FindProperty("footInOutOffset"));
footFoldout.Add(footIO);
root.Add(footFoldout);
// ── 바닥 높이 ──
var floorFoldout = new Foldout { text = "바닥 높이 조정", value = false };
floorFoldout.Add(new PropertyField(serializedObject.FindProperty("floorHeight"), "바닥 높이 (-1 ~ 1)"));
root.Add(floorFoldout);
// ── 머리 회전 오프셋 ──
root.Add(BuildHeadRotationSection());
// ── 어깨 보정 (ShoulderCorrection) ──
root.Add(BuildShoulderSection());
// ── 사지 가중치 (LimbWeight) ──
root.Add(BuildLimbWeightSection());
// ── 손가락 셰이핑 (FingerShaped) ──
root.Add(BuildFingerShapedSection());
// ── 캘리브레이션 ──
root.Add(BuildCacheSection());
// ── 변경 시 저장 ──
root.TrackSerializedObjectValue(serializedObject, so =>
{
if (target == null) return;
var script = (CustomRetargetingScript)target;
if (script.targetAnimator != null)
script.SaveSettings();
});
return root;
}
// ========== 힙 위치 보정 ==========
private VisualElement BuildHipsSection()
{
var foldout = new Foldout { text = "힙 위치 보정 (로컬 좌표계)", value = true };
var axisInfo = new HelpBox("플레이 모드에서 축 매핑 정보가 표시됩니다.", HelpBoxMessageType.Info);
foldout.Add(axisInfo);
foldout.schedule.Execute(() =>
{
if (target == null) return;
serializedObject.Update();
var axisProp = serializedObject.FindProperty("debugAxisNormalizer");
if (axisProp == null || !Application.isPlaying) { axisInfo.text = "플레이 모드에서 축 매핑 정보가 표시됩니다."; return; }
Vector3 m = axisProp.vector3Value;
if (m == Vector3.one) { axisInfo.text = "플레이 모드에서 축 매핑 정보가 표시됩니다."; return; }
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)}";
}).Every(500);
var hx = new Slider("← 좌우 →", -1f, 1f) { showInputField = true };
hx.BindProperty(serializedObject.FindProperty("hipsOffsetX"));
foldout.Add(hx);
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(() =>
{
if (!Application.isPlaying) { Debug.LogWarning("플레이 모드에서만 사용 가능합니다."); return; }
var script = (CustomRetargetingScript)target;
float offset = CalculateHipsOffsetFromLegDifference(script);
serializedObject.FindProperty("hipsOffsetY").floatValue = offset;
serializedObject.ApplyModifiedProperties();
script.SaveSettings();
}) { text = "다리 길이 자동 보정", tooltip = "소스/타겟 다리 길이 차이로 힙 상하 오프셋을 자동 계산합니다." };
autoHipsBtn.style.marginTop = 4; autoHipsBtn.style.height = 25;
foldout.Add(autoHipsBtn);
return foldout;
}
// ========== 머리 회전 오프셋 ==========
private VisualElement BuildHeadRotationSection()
{
var foldout = new Foldout { text = "머리 회전 오프셋", value = false };
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;
}
// ========== 어깨 보정 ==========
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 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()
{
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 cacheLabel = new Label();
cacheLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
box.Add(cacheLabel);
var btnRow = new VisualElement { style = { flexDirection = FlexDirection.Row, marginTop = 4 } };
var calibBtn = new Button(() => { ((CustomRetargetingScript)target).I_PoseCalibration(); }) { text = "I-포즈 캘리브레이션" };
calibBtn.style.flexGrow = 1; calibBtn.style.marginRight = 2;
btnRow.Add(calibBtn);
var resetBtn = new Button(() => { ((CustomRetargetingScript)target).ResetPoseAndCache(); }) { text = "캘리브레이션 초기화" };
resetBtn.style.flexGrow = 1;
btnRow.Add(resetBtn);
box.Add(btnRow);
// 전체 자동 보정
var autoBtn = new Button(() =>
{
if (!Application.isPlaying) { Debug.LogWarning("플레이 모드에서만 사용 가능합니다."); return; }
AutoCalibrateAll((CustomRetargetingScript)target, serializedObject);
}) { text = "전체 자동 보정 (크기 + 힙 높이 + 머리 정면)", tooltip = "아바타 크기, 힙 높이, 머리 정면을 자동 보정합니다." };
autoBtn.style.marginTop = 4; autoBtn.style.height = 28;
box.Add(autoBtn);
box.schedule.Execute(() =>
{
if (target == null) return;
bool cached = ((CustomRetargetingScript)target).HasCachedSettings();
cacheLabel.text = cached ? "캘리브레이션 데이터가 저장되어 있습니다." : "저장된 캘리브레이션 데이터가 없습니다.";
resetBtn.style.display = cached ? DisplayStyle.Flex : DisplayStyle.None;
}).Every(1000);
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;
}
// ========== 자동 보정 ==========
private void AutoCalibrateAll(CustomRetargetingScript script, SerializedObject so)
{
var source = script.optitrackSource;
Animator targetAnim = script.targetAnimator;
if (source == null || targetAnim == null || !targetAnim.isHuman)
{
Debug.LogWarning("소스 OptiTrack 또는 타겟 Animator가 설정되지 않았습니다.");
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}");
};
};
}
// ========== 유틸리티 ==========
private float CalculateHipsOffsetFromLegDifference(CustomRetargetingScript script)
{
var source = script.optitrackSource;
Animator targetAnim = script.targetAnimator;
if (source == null || targetAnim == null) return 0f;
float sourceLeg = GetSourceLegLength(source);
float targetLeg = GetLegLength(targetAnim);
if (sourceLeg < 0.01f || targetLeg < 0.01f) return 0f;
return targetLeg - sourceLeg;
}
private float GetLegLength(Animator animator)
{
Transform upper = animator.GetBoneTransform(HumanBodyBones.LeftUpperLeg);
Transform lower = animator.GetBoneTransform(HumanBodyBones.LeftLowerLeg);
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);
}
private float GetSourceLegLength(OptitrackSkeletonAnimator_Mingle source)
{
Transform upper = source.GetBoneTransform(HumanBodyBones.LeftUpperLeg);
Transform lower = source.GetBoneTransform(HumanBodyBones.LeftLowerLeg);
Transform foot = source.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);
}
private void CalibrateHeadToForward(SerializedObject so, SerializedProperty xProp, SerializedProperty yProp, SerializedProperty zProp)
{
CustomRetargetingScript script = so.targetObject as CustomRetargetingScript;
if (script == null) return;
Animator targetAnimator = script.GetComponent<Animator>();
if (targetAnimator == null) return;
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);
}
}
}