- BackgroundSceneLoaderWindow: OnGUI → CreateGUI (Toolbar + ToolbarSearchField) - PropBrowserWindow: OnGUI → CreateGUI (Toolbar + ToolbarSearchField) - StreamingleCommon.uss: 브라우저 공통 스타일 추가 (그리드/리스트/뷰토글/액션바/상태바) - excludeFromWeb 상태 새로고침 시 보존 수정 - 삭제된 배경 리소스 정리 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
378 lines
20 KiB
C#
378 lines
20 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";
|
|
|
|
// SerializedProperty
|
|
private SerializedProperty sourceAnimatorProp;
|
|
private SerializedProperty targetAnimatorProp;
|
|
private SerializedProperty hipsOffsetXProp;
|
|
private SerializedProperty hipsOffsetYProp;
|
|
private SerializedProperty hipsOffsetZProp;
|
|
private SerializedProperty debugAxisNormalizerProp;
|
|
private SerializedProperty fingerCopyModeProp;
|
|
private SerializedProperty useMotionFilterProp;
|
|
private SerializedProperty filterBufferSizeProp;
|
|
private SerializedProperty useBodyRoughMotionProp;
|
|
private SerializedProperty useFingerRoughMotionProp;
|
|
private SerializedProperty bodyRoughnessProp;
|
|
private SerializedProperty fingerRoughnessProp;
|
|
private SerializedProperty kneeInOutWeightProp;
|
|
private SerializedProperty kneeFrontBackWeightProp;
|
|
private SerializedProperty footFrontBackOffsetProp;
|
|
private SerializedProperty footInOutOffsetProp;
|
|
private SerializedProperty floorHeightProp;
|
|
private SerializedProperty avatarScaleProp;
|
|
|
|
// Dynamic UI
|
|
private VisualElement calibrationContainer;
|
|
private Label cacheStatusLabel;
|
|
|
|
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");
|
|
useMotionFilterProp = serializedObject.FindProperty("useMotionFilter");
|
|
filterBufferSizeProp = serializedObject.FindProperty("filterBufferSize");
|
|
useBodyRoughMotionProp = serializedObject.FindProperty("useBodyRoughMotion");
|
|
useFingerRoughMotionProp = serializedObject.FindProperty("useFingerRoughMotion");
|
|
bodyRoughnessProp = serializedObject.FindProperty("bodyRoughness");
|
|
fingerRoughnessProp = serializedObject.FindProperty("fingerRoughness");
|
|
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()
|
|
{
|
|
var root = new VisualElement();
|
|
|
|
var commonUss = AssetDatabase.LoadAssetAtPath<StyleSheet>(CommonUssPath);
|
|
if (commonUss != null) root.styleSheets.Add(commonUss);
|
|
|
|
// 원본 Animator
|
|
root.Add(new PropertyField(sourceAnimatorProp, "원본 Animator"));
|
|
|
|
// 아바타 크기 설정
|
|
var scaleFoldout = new Foldout { text = "아바타 크기 설정", value = true };
|
|
scaleFoldout.Add(new PropertyField(avatarScaleProp, "아바타 크기"));
|
|
root.Add(scaleFoldout);
|
|
|
|
// 힙 위치 보정
|
|
root.Add(BuildHipsSection());
|
|
|
|
// 무릎 위치 조정
|
|
var kneeFoldout = new Foldout { text = "무릎 위치 조정", value = true };
|
|
kneeFoldout.Add(new Slider("무릎 앞/뒤 가중치", -1f, 1f) { showInputField = true, tooltip = "음수: 뒤로, 양수: 앞으로" });
|
|
kneeFoldout.Q<Slider>().BindProperty(kneeFrontBackWeightProp);
|
|
var kneeInOut = new Slider("무릎 안/밖 가중치", -1f, 1f) { showInputField = true, tooltip = "음수: 안쪽, 양수: 바깥쪽" };
|
|
kneeInOut.BindProperty(kneeInOutWeightProp);
|
|
kneeFoldout.Add(kneeInOut);
|
|
root.Add(kneeFoldout);
|
|
|
|
// 발 IK 위치 조정
|
|
var footFoldout = new Foldout { text = "발 IK 위치 조정", value = true };
|
|
var footFB = new Slider("발 앞/뒤 오프셋", -1f, 1f) { showInputField = true, tooltip = "+: 앞으로, -: 뒤로" };
|
|
footFB.BindProperty(footFrontBackOffsetProp);
|
|
footFoldout.Add(footFB);
|
|
var footIO = new Slider("발 벌리기/모으기", -1f, 1f) { showInputField = true, tooltip = "+: 벌리기, -: 모으기" };
|
|
footIO.BindProperty(footInOutOffsetProp);
|
|
footFoldout.Add(footIO);
|
|
root.Add(footFoldout);
|
|
|
|
// 손가락 복제 설정
|
|
root.Add(BuildFingerCopySection());
|
|
|
|
// 모션 필터링 설정
|
|
root.Add(BuildMotionFilterSection());
|
|
|
|
// 러프 모션 설정
|
|
root.Add(BuildRoughMotionSection());
|
|
|
|
// 바닥 높이 조정
|
|
var floorFoldout = new Foldout { text = "바닥 높이 조정", value = false };
|
|
floorFoldout.Add(new PropertyField(floorHeightProp, "바닥 높이 (-1 ~ 1)"));
|
|
root.Add(floorFoldout);
|
|
|
|
// 캐시 상태 + 캘리브레이션 버튼
|
|
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 = "힙 위치 보정 (로컬 좌표계)" };
|
|
|
|
// 축 매핑 정보
|
|
var axisInfo = new HelpBox("플레이 모드에서 T-포즈 분석 후 축 매핑 정보가 표시됩니다.\n이 매핑은 각 아바타의 힙 로컬 축 방향에 맞춰 자동 계산됩니다.", HelpBoxMessageType.Info);
|
|
foldout.Add(axisInfo);
|
|
|
|
// 주기적으로 축 매핑 정보 갱신
|
|
foldout.schedule.Execute(() =>
|
|
{
|
|
if (target == null || debugAxisNormalizerProp == null) return;
|
|
serializedObject.Update();
|
|
Vector3 axisMapping = debugAxisNormalizerProp.vector3Value;
|
|
|
|
if (Application.isPlaying && axisMapping != Vector3.one)
|
|
{
|
|
string GetAxisName(float value)
|
|
{
|
|
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);
|
|
|
|
foldout.Add(new PropertyField(hipsOffsetXProp, "좌우 오프셋 (←-/+→)") { tooltip = "캐릭터 기준 왼쪽(-) / 오른쪽(+)" });
|
|
foldout.Add(new PropertyField(hipsOffsetYProp, "상하 오프셋 (↓-/+↑)") { tooltip = "캐릭터 기준 아래(-) / 위(+)" });
|
|
foldout.Add(new PropertyField(hipsOffsetZProp, "앞뒤 오프셋 (←-/+→)") { tooltip = "캐릭터 기준 뒤(-) / 앞(+)" });
|
|
foldout.Add(new HelpBox("로컬 좌표계 기반: 캐릭터의 회전 상태와 관계없이 항상 캐릭터 기준으로 이동합니다.", HelpBoxMessageType.Info));
|
|
|
|
return foldout;
|
|
}
|
|
|
|
private VisualElement BuildFingerCopySection()
|
|
{
|
|
var foldout = new Foldout { text = "손가락 복제 설정" };
|
|
foldout.Add(new PropertyField(fingerCopyModeProp, "복제 방식") { tooltip = "손가락 포즈를 복제하는 방식을 선택합니다." });
|
|
|
|
// Mingle 모드 캘리브레이션 컨테이너
|
|
calibrationContainer = new VisualElement();
|
|
calibrationContainer.style.backgroundColor = new Color(0, 0, 0, 0.1f);
|
|
calibrationContainer.style.borderTopLeftRadius = calibrationContainer.style.borderTopRightRadius =
|
|
calibrationContainer.style.borderBottomLeftRadius = calibrationContainer.style.borderBottomRightRadius = 4;
|
|
calibrationContainer.style.paddingTop = calibrationContainer.style.paddingBottom =
|
|
calibrationContainer.style.paddingLeft = calibrationContainer.style.paddingRight = 6;
|
|
calibrationContainer.style.marginTop = 4;
|
|
|
|
calibrationContainer.Add(new Label("Mingle 캘리브레이션") { style = { unityFontStyleAndWeight = FontStyle.Bold } });
|
|
calibrationContainer.Add(new HelpBox(
|
|
"Mingle 모드는 소스 아바타의 손가락 회전 범위를 캘리브레이션하여 타겟에 적용합니다.\n" +
|
|
"1. 손가락을 완전히 펼친 상태에서 '펼침 기록' 클릭\n" +
|
|
"2. 손가락을 완전히 모은(주먹) 상태에서 '모음 기록' 클릭",
|
|
HelpBoxMessageType.Info));
|
|
|
|
// 수동 캘리브레이션 버튼
|
|
var manualRow = new VisualElement { style = { flexDirection = FlexDirection.Row, marginTop = 4 } };
|
|
var openBtn = new Button(() => ((CustomRetargetingScript)target).CalibrateMingleOpen()) { text = "펼침 기록 (Open)" };
|
|
openBtn.style.flexGrow = 1; openBtn.style.marginRight = 2;
|
|
manualRow.Add(openBtn);
|
|
var closeBtn = new Button(() => ((CustomRetargetingScript)target).CalibrateMingleClose()) { text = "모음 기록 (Close)" };
|
|
closeBtn.style.flexGrow = 1;
|
|
manualRow.Add(closeBtn);
|
|
calibrationContainer.Add(manualRow);
|
|
|
|
// 자동 캘리브레이션 버튼
|
|
var autoBtn = new Button(() => ((CustomRetargetingScript)target).StartAutoCalibration())
|
|
{ text = "자동 캘리브레이션 (3초 펼침 → 3초 모음)" };
|
|
autoBtn.style.marginTop = 4;
|
|
calibrationContainer.Add(autoBtn);
|
|
|
|
// 플레이 모드 경고
|
|
var playWarning = new HelpBox("캘리브레이션은 플레이 모드에서만 가능합니다.", HelpBoxMessageType.Warning);
|
|
calibrationContainer.Add(playWarning);
|
|
|
|
// 자동 캘리브레이션 진행 상태
|
|
var autoCalibStatus = new VisualElement();
|
|
autoCalibStatus.style.backgroundColor = new Color(0, 0, 0, 0.15f);
|
|
autoCalibStatus.style.borderTopLeftRadius = autoCalibStatus.style.borderTopRightRadius =
|
|
autoCalibStatus.style.borderBottomLeftRadius = autoCalibStatus.style.borderBottomRightRadius = 4;
|
|
autoCalibStatus.style.paddingTop = autoCalibStatus.style.paddingBottom =
|
|
autoCalibStatus.style.paddingLeft = autoCalibStatus.style.paddingRight = 4;
|
|
autoCalibStatus.style.marginTop = 4;
|
|
var statusLabel = new Label("자동 캘리브레이션 진행 중");
|
|
statusLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
|
autoCalibStatus.Add(statusLabel);
|
|
var statusDetailLabel = new Label();
|
|
autoCalibStatus.Add(statusDetailLabel);
|
|
var timeLabel = new Label();
|
|
autoCalibStatus.Add(timeLabel);
|
|
var cancelBtn = new Button(() => ((CustomRetargetingScript)target).StopAutoCalibration()) { text = "취소" };
|
|
autoCalibStatus.Add(cancelBtn);
|
|
calibrationContainer.Add(autoCalibStatus);
|
|
|
|
foldout.Add(calibrationContainer);
|
|
|
|
// 주기적으로 Mingle 모드 표시/숨김 + 캘리브레이션 상태 갱신
|
|
foldout.schedule.Execute(() =>
|
|
{
|
|
if (target == null) return;
|
|
serializedObject.Update();
|
|
bool isMingle = fingerCopyModeProp.enumValueIndex == (int)EnumsList.FingerCopyMode.Mingle;
|
|
calibrationContainer.style.display = isMingle ? DisplayStyle.Flex : DisplayStyle.None;
|
|
|
|
if (isMingle)
|
|
{
|
|
bool isPlaying = Application.isPlaying;
|
|
var script = (CustomRetargetingScript)target;
|
|
bool isAutoCalib = isPlaying && script.IsAutoCalibrating;
|
|
|
|
openBtn.SetEnabled(isPlaying && !isAutoCalib);
|
|
closeBtn.SetEnabled(isPlaying && !isAutoCalib);
|
|
autoBtn.SetEnabled(isPlaying && !isAutoCalib);
|
|
playWarning.style.display = isPlaying ? DisplayStyle.None : DisplayStyle.Flex;
|
|
|
|
manualRow.style.display = isAutoCalib ? DisplayStyle.None : DisplayStyle.Flex;
|
|
autoBtn.style.display = isAutoCalib ? DisplayStyle.None : DisplayStyle.Flex;
|
|
autoCalibStatus.style.display = isAutoCalib ? DisplayStyle.Flex : DisplayStyle.None;
|
|
|
|
if (isAutoCalib)
|
|
{
|
|
statusDetailLabel.text = $"상태: {script.AutoCalibrationStatus}";
|
|
timeLabel.text = $"남은 시간: {script.AutoCalibrationTimeRemaining:F1}초";
|
|
}
|
|
}
|
|
}).Every(200);
|
|
|
|
// 초기 상태
|
|
bool initMingle = fingerCopyModeProp.enumValueIndex == (int)EnumsList.FingerCopyMode.Mingle;
|
|
calibrationContainer.style.display = initMingle ? DisplayStyle.Flex : DisplayStyle.None;
|
|
autoCalibStatus.style.display = DisplayStyle.None;
|
|
|
|
return foldout;
|
|
}
|
|
|
|
private VisualElement BuildMotionFilterSection()
|
|
{
|
|
var foldout = new Foldout { text = "모션 필터링 설정" };
|
|
foldout.Add(new PropertyField(useMotionFilterProp, "모션 필터 사용") { tooltip = "모션 필터링을 적용할지 여부를 설정합니다." });
|
|
|
|
var bufferField = new PropertyField(filterBufferSizeProp, "필터 버퍼 크기") { tooltip = "모션 필터링에 사용할 버퍼의 크기를 설정합니다. (2-10)" };
|
|
foldout.Add(bufferField);
|
|
|
|
foldout.TrackPropertyValue(useMotionFilterProp, prop =>
|
|
{
|
|
bufferField.style.display = prop.boolValue ? DisplayStyle.Flex : DisplayStyle.None;
|
|
});
|
|
bufferField.style.display = useMotionFilterProp.boolValue ? DisplayStyle.Flex : DisplayStyle.None;
|
|
|
|
return foldout;
|
|
}
|
|
|
|
private VisualElement BuildRoughMotionSection()
|
|
{
|
|
var foldout = new Foldout { text = "러프 모션 설정" };
|
|
|
|
// 몸
|
|
foldout.Add(new PropertyField(useBodyRoughMotionProp, "몸 러프 모션 사용") { tooltip = "몸의 러프한 움직임을 적용할지 여부를 설정합니다." });
|
|
var bodyRoughField = new PropertyField(bodyRoughnessProp, "몸 러프니스") { tooltip = "몸 전체의 러프한 정도 (0: 없음, 1: 최대)" };
|
|
foldout.Add(bodyRoughField);
|
|
|
|
foldout.TrackPropertyValue(useBodyRoughMotionProp, prop =>
|
|
{
|
|
bodyRoughField.style.display = prop.boolValue ? DisplayStyle.Flex : DisplayStyle.None;
|
|
});
|
|
bodyRoughField.style.display = useBodyRoughMotionProp.boolValue ? DisplayStyle.Flex : DisplayStyle.None;
|
|
|
|
// 손가락
|
|
foldout.Add(new PropertyField(useFingerRoughMotionProp, "손가락 러프 모션 사용") { tooltip = "손가락의 러프한 움직임을 적용할지 여부를 설정합니다." });
|
|
var fingerRoughField = new PropertyField(fingerRoughnessProp, "손가락 러프니스") { tooltip = "손가락의 러프한 정도 (0: 없음, 1: 최대)" };
|
|
foldout.Add(fingerRoughField);
|
|
|
|
foldout.TrackPropertyValue(useFingerRoughMotionProp, prop =>
|
|
{
|
|
fingerRoughField.style.display = prop.boolValue ? DisplayStyle.Flex : DisplayStyle.None;
|
|
});
|
|
fingerRoughField.style.display = useFingerRoughMotionProp.boolValue ? DisplayStyle.Flex : DisplayStyle.None;
|
|
|
|
return foldout;
|
|
}
|
|
|
|
private VisualElement BuildCacheSection()
|
|
{
|
|
var container = new VisualElement { style = { marginTop = 8 } };
|
|
|
|
var script = (CustomRetargetingScript)target;
|
|
bool hasCached = script.HasCachedSettings();
|
|
|
|
cacheStatusLabel = new Label(hasCached ?
|
|
"캘리브레이션 데이터가 저장되어 있습니다." :
|
|
"저장된 캘리브레이션 데이터가 없습니다.");
|
|
cacheStatusLabel.style.unityFontStyleAndWeight = FontStyle.Bold;
|
|
container.Add(cacheStatusLabel);
|
|
|
|
var btnRow = new VisualElement { style = { flexDirection = FlexDirection.Row, marginTop = 4 } };
|
|
|
|
var calibBtn = new Button(() =>
|
|
{
|
|
((CustomRetargetingScript)target).I_PoseCalibration();
|
|
UpdateCacheStatus();
|
|
}) { text = "I-포즈 캘리브레이션" };
|
|
calibBtn.style.flexGrow = 1;
|
|
calibBtn.style.marginRight = 2;
|
|
btnRow.Add(calibBtn);
|
|
|
|
var deleteCacheBtn = new Button(() =>
|
|
{
|
|
((CustomRetargetingScript)target).ResetPoseAndCache();
|
|
UpdateCacheStatus();
|
|
}) { text = "캐시 데이터 삭제" };
|
|
deleteCacheBtn.style.flexGrow = 1;
|
|
btnRow.Add(deleteCacheBtn);
|
|
|
|
container.Add(btnRow);
|
|
|
|
// 주기적으로 캐시 상태 갱신
|
|
container.schedule.Execute(() =>
|
|
{
|
|
if (target == null) return;
|
|
bool cached = ((CustomRetargetingScript)target).HasCachedSettings();
|
|
deleteCacheBtn.style.display = cached ? DisplayStyle.Flex : DisplayStyle.None;
|
|
cacheStatusLabel.text = cached ?
|
|
"캘리브레이션 데이터가 저장되어 있습니다." :
|
|
"저장된 캘리브레이션 데이터가 없습니다.";
|
|
}).Every(1000);
|
|
|
|
deleteCacheBtn.style.display = hasCached ? DisplayStyle.Flex : DisplayStyle.None;
|
|
|
|
return container;
|
|
}
|
|
|
|
private void UpdateCacheStatus()
|
|
{
|
|
if (cacheStatusLabel == null || target == null) return;
|
|
bool hasCached = ((CustomRetargetingScript)target).HasCachedSettings();
|
|
cacheStatusLabel.text = hasCached ?
|
|
"캘리브레이션 데이터가 저장되어 있습니다." :
|
|
"저장된 캘리브레이션 데이터가 없습니다.";
|
|
}
|
|
}
|
|
}
|