Fix : 에디터 SerializedObject 바인딩 NullReferenceException 해결 및 자동 보정 개선
- SerializedObject 직접 Dispose 대신 GC 자연 수거로 변경 (바인딩 큐 충돌 방지) - 전체 자동 보정을 3프레임 순차 실행으로 변경 (스케일/리타게팅 반영 대기) - 전체 자동 보정에 머리 정면 캘리브레이션 포함 - schedule 콜백에 Dispose된 SO 접근 방어 추가 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
14874d5b6e
commit
b0c967e1fd
@ -10,6 +10,8 @@ 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";
|
||||||
|
|
||||||
|
private SerializedObject groundingSO;
|
||||||
|
|
||||||
// SerializedProperty
|
// SerializedProperty
|
||||||
private SerializedProperty sourceAnimatorProp;
|
private SerializedProperty sourceAnimatorProp;
|
||||||
private SerializedProperty targetAnimatorProp;
|
private SerializedProperty targetAnimatorProp;
|
||||||
@ -28,6 +30,12 @@ namespace KindRetargeting
|
|||||||
// Dynamic UI
|
// Dynamic UI
|
||||||
private Label cacheStatusLabel;
|
private Label cacheStatusLabel;
|
||||||
|
|
||||||
|
protected override void OnDisable()
|
||||||
|
{
|
||||||
|
base.OnDisable();
|
||||||
|
groundingSO = null;
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnEnable()
|
protected override void OnEnable()
|
||||||
{
|
{
|
||||||
base.OnEnable();
|
base.OnEnable();
|
||||||
@ -194,7 +202,7 @@ namespace KindRetargeting
|
|||||||
var grounding = script.GetComponent<FootGroundingController>();
|
var grounding = script.GetComponent<FootGroundingController>();
|
||||||
if (grounding != null)
|
if (grounding != null)
|
||||||
{
|
{
|
||||||
var groundingSO = new SerializedObject(grounding);
|
groundingSO = new SerializedObject(grounding);
|
||||||
|
|
||||||
var groundHeightField = new PropertyField(groundingSO.FindProperty("groundHeight"), "바닥 높이");
|
var groundHeightField = new PropertyField(groundingSO.FindProperty("groundHeight"), "바닥 높이");
|
||||||
foldout.Add(groundHeightField);
|
foldout.Add(groundHeightField);
|
||||||
|
|||||||
@ -87,6 +87,7 @@ public class RetargetingControlWindow : EditorWindow
|
|||||||
{
|
{
|
||||||
EditorApplication.update -= OnEditorUpdate;
|
EditorApplication.update -= OnEditorUpdate;
|
||||||
Undo.undoRedoPerformed -= OnUndoRedoPerformed;
|
Undo.undoRedoPerformed -= OnUndoRedoPerformed;
|
||||||
|
if (characterContainer != null) characterContainer.Unbind();
|
||||||
DisposeTrackedSerializedObjects();
|
DisposeTrackedSerializedObjects();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -116,8 +117,8 @@ public class RetargetingControlWindow : EditorWindow
|
|||||||
|
|
||||||
private void DisposeTrackedSerializedObjects()
|
private void DisposeTrackedSerializedObjects()
|
||||||
{
|
{
|
||||||
foreach (var so in trackedSerializedObjects)
|
// Dispose하지 않고 참조만 해제 → GC가 바인딩 시스템 정리 후 자연 수거
|
||||||
so?.Dispose();
|
// 직접 Dispose하면 UI Toolkit 바인딩 큐에 남은 pending 요청이 죽은 SO에 접근하여 NullReferenceException 발생
|
||||||
trackedSerializedObjects.Clear();
|
trackedSerializedObjects.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,8 +159,10 @@ public class RetargetingControlWindow : EditorWindow
|
|||||||
private void RebuildCharacterPanels()
|
private void RebuildCharacterPanels()
|
||||||
{
|
{
|
||||||
if (characterContainer == null) return;
|
if (characterContainer == null) return;
|
||||||
characterContainer.Clear();
|
// 바인딩 해제 후 Clear → Dispose 순서로 처리해야 NullReferenceException 방지
|
||||||
|
characterContainer.Unbind();
|
||||||
DisposeTrackedSerializedObjects();
|
DisposeTrackedSerializedObjects();
|
||||||
|
characterContainer.Clear();
|
||||||
|
|
||||||
if (retargetingScripts == null || retargetingScripts.Length == 0)
|
if (retargetingScripts == null || retargetingScripts.Length == 0)
|
||||||
{
|
{
|
||||||
@ -340,7 +343,8 @@ public class RetargetingControlWindow : EditorWindow
|
|||||||
|
|
||||||
container.schedule.Execute(() =>
|
container.schedule.Execute(() =>
|
||||||
{
|
{
|
||||||
if (so == null || so.targetObject == null) return;
|
try { if (so == null || so.targetObject == null) return; }
|
||||||
|
catch (System.Exception) { return; }
|
||||||
so.Update();
|
so.Update();
|
||||||
var normProp = so.FindProperty("debugAxisNormalizer");
|
var normProp = so.FindProperty("debugAxisNormalizer");
|
||||||
if (normProp == null || !Application.isPlaying) { axisLabel.text = ""; return; }
|
if (normProp == null || !Application.isPlaying) { axisLabel.text = ""; return; }
|
||||||
@ -497,7 +501,8 @@ public class RetargetingControlWindow : EditorWindow
|
|||||||
// 비활성 시 숨김
|
// 비활성 시 숨김
|
||||||
handFoldout.schedule.Execute(() =>
|
handFoldout.schedule.Execute(() =>
|
||||||
{
|
{
|
||||||
if (fso == null || fso.targetObject == null) return;
|
try { if (fso == null || fso.targetObject == null) return; }
|
||||||
|
catch (System.Exception) { return; }
|
||||||
fso.Update();
|
fso.Update();
|
||||||
bool enabled = fso.FindProperty($"{prefix}HandEnabled").boolValue;
|
bool enabled = fso.FindProperty($"{prefix}HandEnabled").boolValue;
|
||||||
slidersRow.style.display = enabled ? DisplayStyle.Flex : DisplayStyle.None;
|
slidersRow.style.display = enabled ? DisplayStyle.Flex : DisplayStyle.None;
|
||||||
@ -758,7 +763,7 @@ public class RetargetingControlWindow : EditorWindow
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
AutoCalibrateAll(script, so);
|
AutoCalibrateAll(script, so);
|
||||||
}) { text = "전체 자동 보정 (크기 + 힙 높이)", tooltip = "소스/타겟 목 높이 비율로 아바타 크기를 맞추고, 다리 길이 차이로 힙 높이를 자동 보정합니다. (플레이 모드 전용)" };
|
}) { text = "전체 자동 보정 (크기 + 힙 높이 + 머리 정면)", tooltip = "소스/타겟 목 높이 비율로 아바타 크기를 맞추고, 다리 길이 차이로 힙 높이를 자동 보정하고, 머리 정면 캘리브레이션을 수행합니다. (플레이 모드 전용)" };
|
||||||
autoBtn.style.marginTop = 4;
|
autoBtn.style.marginTop = 4;
|
||||||
autoBtn.style.height = 28;
|
autoBtn.style.height = 28;
|
||||||
box.Add(autoBtn);
|
box.Add(autoBtn);
|
||||||
@ -889,58 +894,57 @@ public class RetargetingControlWindow : EditorWindow
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. 아바타 크기를 1로 리셋 (즉시 적용)
|
// ── 프레임 1: 스케일 리셋 + 다리 보정 ──
|
||||||
script.ResetScale();
|
script.ResetScale();
|
||||||
var scaleProp = so.FindProperty("avatarScale");
|
var scaleProp = so.FindProperty("avatarScale");
|
||||||
scaleProp.floatValue = 1f;
|
scaleProp.floatValue = 1f;
|
||||||
|
so.FindProperty("hipsOffsetY").floatValue = CalculateHipsOffsetFromLegDifference(script);
|
||||||
so.ApplyModifiedProperties();
|
so.ApplyModifiedProperties();
|
||||||
|
|
||||||
// 2. 다리 길이 자동 보정 (스케일 1 상태)
|
// ── 프레임 2: 리타게팅 반영 후 목 높이 측정 → avatarScale 설정 ──
|
||||||
float hipsOffset0 = CalculateHipsOffsetFromLegDifference(script);
|
|
||||||
so.FindProperty("hipsOffsetY").floatValue = hipsOffset0;
|
|
||||||
so.ApplyModifiedProperties();
|
|
||||||
|
|
||||||
// 3. 목 높이 측정
|
|
||||||
Transform sourceNeck = source.GetBoneTransform(HumanBodyBones.Neck);
|
|
||||||
Transform targetNeck = target.GetBoneTransform(HumanBodyBones.Neck);
|
|
||||||
|
|
||||||
if (sourceNeck == null || targetNeck == null)
|
|
||||||
{
|
|
||||||
Debug.LogWarning("목 본을 찾을 수 없습니다.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
float sourceNeckY = sourceNeck.position.y;
|
|
||||||
float targetNeckY = targetNeck.position.y;
|
|
||||||
|
|
||||||
if (targetNeckY < 0.01f)
|
|
||||||
{
|
|
||||||
Debug.LogWarning("타겟 목 높이가 0에 가깝습니다.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 소스/타겟 비율 → avatarScale
|
|
||||||
float scaleRatio = Mathf.Clamp(sourceNeckY / targetNeckY, 0.1f, 3f);
|
|
||||||
script.SetAvatarScale(scaleRatio);
|
|
||||||
scaleProp.floatValue = scaleRatio;
|
|
||||||
so.ApplyModifiedProperties();
|
|
||||||
|
|
||||||
Debug.Log($"크기 보정: 소스 목 Y={sourceNeckY:F4}, 타겟 목 Y={targetNeckY:F4} → avatarScale = {scaleRatio:F3}");
|
|
||||||
|
|
||||||
// 5. 스케일 적용 후 다리 길이 자동 보정
|
|
||||||
EditorApplication.delayCall += () =>
|
EditorApplication.delayCall += () =>
|
||||||
{
|
{
|
||||||
if (script == null) return;
|
if (script == null) return;
|
||||||
|
|
||||||
// delayCall 시점에서 캡처된 so가 Dispose되었을 수 있으므로 새로 생성
|
Transform sourceNeck = source.GetBoneTransform(HumanBodyBones.Neck);
|
||||||
var freshSo = new SerializedObject(script);
|
Transform targetNeck = target.GetBoneTransform(HumanBodyBones.Neck);
|
||||||
float hipsOffset = CalculateHipsOffsetFromLegDifference(script);
|
if (sourceNeck == null || targetNeck == null) { Debug.LogWarning("목 본을 찾을 수 없습니다."); return; }
|
||||||
freshSo.FindProperty("hipsOffsetY").floatValue = hipsOffset;
|
|
||||||
freshSo.ApplyModifiedProperties();
|
|
||||||
freshSo.Dispose();
|
|
||||||
|
|
||||||
script.SaveSettings();
|
float sourceNeckY = sourceNeck.position.y;
|
||||||
Debug.Log($"전체 자동 보정 완료: avatarScale = {scaleRatio:F3}, hipsOffsetY = {hipsOffset:F4}m");
|
float targetNeckY = targetNeck.position.y;
|
||||||
|
if (targetNeckY < 0.01f) { Debug.LogWarning("타겟 목 높이가 0에 가깝습니다."); return; }
|
||||||
|
|
||||||
|
float scaleRatio = Mathf.Clamp(sourceNeckY / targetNeckY, 0.1f, 3f);
|
||||||
|
script.SetAvatarScale(scaleRatio);
|
||||||
|
|
||||||
|
var so2 = new SerializedObject(script);
|
||||||
|
so2.FindProperty("avatarScale").floatValue = scaleRatio;
|
||||||
|
so2.ApplyModifiedProperties();
|
||||||
|
so2.Dispose();
|
||||||
|
|
||||||
|
Debug.Log($"크기 보정: 소스 목 Y={sourceNeckY:F4}, 타겟 목 Y={targetNeckY:F4} → avatarScale = {scaleRatio:F3}");
|
||||||
|
|
||||||
|
// ── 프레임 3: 스케일 반영 후 다리 재보정 + 머리 정면 캘리 + 저장 ──
|
||||||
|
EditorApplication.delayCall += () =>
|
||||||
|
{
|
||||||
|
if (script == null) return;
|
||||||
|
|
||||||
|
var so3 = new SerializedObject(script);
|
||||||
|
float finalHipsOffset = CalculateHipsOffsetFromLegDifference(script);
|
||||||
|
so3.FindProperty("hipsOffsetY").floatValue = finalHipsOffset;
|
||||||
|
|
||||||
|
var xProp = so3.FindProperty("headRotationOffsetX");
|
||||||
|
var yProp = so3.FindProperty("headRotationOffsetY");
|
||||||
|
var zProp = so3.FindProperty("headRotationOffsetZ");
|
||||||
|
if (xProp != null && yProp != null && zProp != null)
|
||||||
|
CalibrateHeadToForward(so3, xProp, yProp, zProp);
|
||||||
|
|
||||||
|
so3.ApplyModifiedProperties();
|
||||||
|
so3.Dispose();
|
||||||
|
|
||||||
|
script.SaveSettings();
|
||||||
|
Debug.Log($"전체 자동 보정 완료: avatarScale = {scaleRatio:F3}, hipsOffsetY = {finalHipsOffset:F4}m");
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user