Merge remote-tracking branch 'origin/retargeting-update'
This commit is contained in:
commit
64ad1eec3a
BIN
Assets/Resources/StreamingleDashboard/dashboard_script.txt
(Stored with Git LFS)
BIN
Assets/Resources/StreamingleDashboard/dashboard_script.txt
(Stored with Git LFS)
Binary file not shown.
@ -9,12 +9,6 @@ namespace KindRetargeting
|
|||||||
/// 이 스크립트는 원본 아바타(Source)의 포즈 손가락 움직임을 대상 아바타(Target)에 리타게팅(Retargeting)합니다.
|
/// 이 스크립트는 원본 아바타(Source)의 포즈 손가락 움직임을 대상 아바타(Target)에 리타게팅(Retargeting)합니다.
|
||||||
/// 또한 IK 타겟을 생성하여 대상 아바타의 관절 움직임을 자연스럽게 조정합니다.
|
/// 또한 IK 타겟을 생성하여 대상 아바타의 관절 움직임을 자연스럽게 조정합니다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[RequireComponent(typeof(LimbWeightController))]
|
|
||||||
[RequireComponent(typeof(ShoulderCorrectionFunction))]
|
|
||||||
[RequireComponent(typeof(TwoBoneIKSolver))]
|
|
||||||
[RequireComponent(typeof(FootGroundingController))]
|
|
||||||
[RequireComponent(typeof(PropLocationController))]
|
|
||||||
[RequireComponent(typeof(FingerShapedController))]
|
|
||||||
[DefaultExecutionOrder(1)]
|
[DefaultExecutionOrder(1)]
|
||||||
public class CustomRetargetingScript : MonoBehaviour
|
public class CustomRetargetingScript : MonoBehaviour
|
||||||
{
|
{
|
||||||
@ -25,7 +19,7 @@ namespace KindRetargeting
|
|||||||
[HideInInspector] public Animator targetAnimator; // 대상 아바타의 Animator
|
[HideInInspector] public Animator targetAnimator; // 대상 아바타의 Animator
|
||||||
|
|
||||||
// IK 컴포넌트 참조
|
// IK 컴포넌트 참조
|
||||||
private TwoBoneIKSolver ikSolver;
|
[SerializeField] public TwoBoneIKSolver ikSolver = new TwoBoneIKSolver();
|
||||||
|
|
||||||
[Header("힙 위치 보정 (로컬 좌표계 기반)")]
|
[Header("힙 위치 보정 (로컬 좌표계 기반)")]
|
||||||
[SerializeField, Range(-1, 1)]
|
[SerializeField, Range(-1, 1)]
|
||||||
@ -101,6 +95,21 @@ namespace KindRetargeting
|
|||||||
[HideInInspector] public Vector3 tPoseHeadForward = Vector3.forward;
|
[HideInInspector] public Vector3 tPoseHeadForward = Vector3.forward;
|
||||||
[HideInInspector] public Vector3 tPoseHeadUp = Vector3.up;
|
[HideInInspector] public Vector3 tPoseHeadUp = Vector3.up;
|
||||||
|
|
||||||
|
[Header("어깨 보정")]
|
||||||
|
[SerializeField] public ShoulderCorrectionFunction shoulderCorrection = new ShoulderCorrectionFunction();
|
||||||
|
|
||||||
|
[Header("발 접지")]
|
||||||
|
[SerializeField] public FootGroundingController footGrounding = new FootGroundingController();
|
||||||
|
|
||||||
|
[Header("프랍 부착")]
|
||||||
|
[SerializeField] public PropLocationController propLocation = new PropLocationController();
|
||||||
|
|
||||||
|
[Header("사지 가중치")]
|
||||||
|
[SerializeField] public LimbWeightController limbWeight = new LimbWeightController();
|
||||||
|
|
||||||
|
[Header("손가락 셰이핑")]
|
||||||
|
[SerializeField] public FingerShapedController fingerShaped = new FingerShapedController();
|
||||||
|
|
||||||
[Header("아바타 크기 조정")]
|
[Header("아바타 크기 조정")]
|
||||||
[SerializeField, Range(0.1f, 3f)] private float avatarScale = 1f;
|
[SerializeField, Range(0.1f, 3f)] private float avatarScale = 1f;
|
||||||
private float previousScale = 1f;
|
private float previousScale = 1f;
|
||||||
@ -269,8 +278,7 @@ namespace KindRetargeting
|
|||||||
// 설정 로드
|
// 설정 로드
|
||||||
LoadSettings();
|
LoadSettings();
|
||||||
|
|
||||||
// IK 컴포넌트 참조 가져오기
|
// IK 모듈은 InitializeIKJoints에서 초기화
|
||||||
ikSolver = GetComponent<TwoBoneIKSolver>();
|
|
||||||
|
|
||||||
// IK 타겟 생성 (무릎 시각화 오브젝트 포함)
|
// IK 타겟 생성 (무릎 시각화 오브젝트 포함)
|
||||||
CreateIKTargets();
|
CreateIKTargets();
|
||||||
@ -324,6 +332,24 @@ namespace KindRetargeting
|
|||||||
Debug.LogWarning("[CustomRetargetingScript] 머리 본을 찾을 수 없습니다!");
|
Debug.LogWarning("[CustomRetargetingScript] 머리 본을 찾을 수 없습니다!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 어깨 보정 모듈 초기화
|
||||||
|
if (targetAnimator != null)
|
||||||
|
shoulderCorrection.Initialize(targetAnimator);
|
||||||
|
|
||||||
|
// 발 접지 모듈 초기화
|
||||||
|
footGrounding.Initialize(ikSolver, targetAnimator);
|
||||||
|
|
||||||
|
// 프랍 부착 모듈 초기화
|
||||||
|
if (targetAnimator != null)
|
||||||
|
propLocation.Initialize(targetAnimator);
|
||||||
|
|
||||||
|
// 사지 가중치 모듈 초기화
|
||||||
|
limbWeight.Initialize(ikSolver, this, transform);
|
||||||
|
|
||||||
|
// 손가락 셰이핑 모듈 초기화
|
||||||
|
if (targetAnimator != null)
|
||||||
|
fingerShaped.Initialize(targetAnimator);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -521,8 +547,7 @@ namespace KindRetargeting
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LimbWeightController에서 의자 높이 오프셋 가져오기
|
// LimbWeightController에서 의자 높이 오프셋 가져오기
|
||||||
var limbController = GetComponent<LimbWeightController>();
|
float chairOffset = limbWeight.chairSeatHeightOffset;
|
||||||
float chairOffset = limbController != null ? limbController.chairSeatHeightOffset : 0.05f;
|
|
||||||
|
|
||||||
var settings = new RetargetingSettings
|
var settings = new RetargetingSettings
|
||||||
{
|
{
|
||||||
@ -600,11 +625,7 @@ namespace KindRetargeting
|
|||||||
headScale = settings.headScale;
|
headScale = settings.headScale;
|
||||||
|
|
||||||
// LimbWeightController에 의자 높이 오프셋 적용
|
// LimbWeightController에 의자 높이 오프셋 적용
|
||||||
var limbController = GetComponent<LimbWeightController>();
|
limbWeight.chairSeatHeightOffset = settings.chairSeatHeightOffset;
|
||||||
if (limbController != null)
|
|
||||||
{
|
|
||||||
limbController.chairSeatHeightOffset = settings.chairSeatHeightOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 머리 회전 오프셋 로드
|
// 머리 회전 오프셋 로드
|
||||||
headRotationOffsetX = settings.headRotationOffsetX;
|
headRotationOffsetX = settings.headRotationOffsetX;
|
||||||
@ -821,6 +842,21 @@ namespace KindRetargeting
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 손가락 셰이핑 (기존 ExecutionOrder 2)
|
||||||
|
fingerShaped.OnUpdate();
|
||||||
|
|
||||||
|
// 어깨 보정 (기존 ExecutionOrder 3)
|
||||||
|
shoulderCorrection.OnUpdate();
|
||||||
|
|
||||||
|
// 사지 가중치 (기존 ExecutionOrder 4)
|
||||||
|
limbWeight.OnUpdate();
|
||||||
|
|
||||||
|
// 발 접지 Pre-IK (기존 ExecutionOrder 5)
|
||||||
|
footGrounding.OnUpdate();
|
||||||
|
|
||||||
|
// IK 솔버 (기존 ExecutionOrder 6)
|
||||||
|
ikSolver.OnUpdate();
|
||||||
|
|
||||||
// 스케일 변경 확인 및 적용
|
// 스케일 변경 확인 및 적용
|
||||||
if (!Mathf.Approximately(previousScale, avatarScale))
|
if (!Mathf.Approximately(previousScale, avatarScale))
|
||||||
{
|
{
|
||||||
@ -834,6 +870,9 @@ namespace KindRetargeting
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
void LateUpdate()
|
void LateUpdate()
|
||||||
{
|
{
|
||||||
|
// 발 접지 Post-IK (기존 FootGroundingController LateUpdate)
|
||||||
|
footGrounding.OnLateUpdate();
|
||||||
|
|
||||||
ApplyHeadRotationOffset();
|
ApplyHeadRotationOffset();
|
||||||
ApplyHeadScale();
|
ApplyHeadScale();
|
||||||
}
|
}
|
||||||
@ -1147,11 +1186,6 @@ namespace KindRetargeting
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void CreateIKTargets()
|
private void CreateIKTargets()
|
||||||
{
|
{
|
||||||
// IK 컴포넌트 가져오기 또는 새로 추가
|
|
||||||
ikSolver = GetComponent<TwoBoneIKSolver>();
|
|
||||||
if (ikSolver == null)
|
|
||||||
ikSolver = gameObject.AddComponent<TwoBoneIKSolver>();
|
|
||||||
|
|
||||||
ikSolver.animator = targetAnimator;
|
ikSolver.animator = targetAnimator;
|
||||||
|
|
||||||
// IK 타겟들을 담을 부모 오브젝트 생성
|
// IK 타겟들을 담을 부모 오브젝트 생성
|
||||||
@ -1183,7 +1217,7 @@ namespace KindRetargeting
|
|||||||
ikSolver.rightLeg.bendGoal = rightLegGoal.transform;
|
ikSolver.rightLeg.bendGoal = rightLegGoal.transform;
|
||||||
|
|
||||||
// TwoBoneIKSolver 본 캐싱 초기화
|
// TwoBoneIKSolver 본 캐싱 초기화
|
||||||
ikSolver.Initialize();
|
ikSolver.Initialize(targetAnimator);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -1384,6 +1418,7 @@ namespace KindRetargeting
|
|||||||
{
|
{
|
||||||
sourcePoseHandler?.Dispose();
|
sourcePoseHandler?.Dispose();
|
||||||
targetPoseHandler?.Dispose();
|
targetPoseHandler?.Dispose();
|
||||||
|
fingerShaped.Cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -10,48 +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";
|
||||||
|
|
||||||
private SerializedObject groundingSO;
|
|
||||||
|
|
||||||
// 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 = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
||||||
@ -61,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;
|
||||||
@ -118,225 +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의 SerializedObject를 직접 바인딩
|
foldout.Add(new PropertyField(serializedObject.FindProperty("footGrounding.groundHeight"), "바닥 높이"));
|
||||||
var script = (CustomRetargetingScript)target;
|
|
||||||
var grounding = script.GetComponent<FootGroundingController>();
|
|
||||||
if (grounding != null)
|
|
||||||
{
|
|
||||||
groundingSO = new SerializedObject(grounding);
|
|
||||||
|
|
||||||
var groundHeightField = new PropertyField(groundingSO.FindProperty("groundHeight"), "바닥 높이");
|
|
||||||
foldout.Add(groundHeightField);
|
|
||||||
|
|
||||||
var weightSlider = new Slider("접지 강도", 0f, 1f) { showInputField = true };
|
var weightSlider = new Slider("접지 강도", 0f, 1f) { showInputField = true };
|
||||||
weightSlider.BindProperty(groundingSO.FindProperty("groundingWeight"));
|
weightSlider.BindProperty(serializedObject.FindProperty("footGrounding.groundingWeight"));
|
||||||
foldout.Add(weightSlider);
|
foldout.Add(weightSlider);
|
||||||
|
|
||||||
var activationField = new PropertyField(groundingSO.FindProperty("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(groundingSO.FindProperty("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(groundingSO.FindProperty("smoothSpeed"), "보정 스무딩 속도");
|
|
||||||
smoothField.tooltip = "보정량 변화 속도 (높을수록 빠른 반응, 낮으면 부드러운 전환)";
|
|
||||||
foldout.Add(smoothField);
|
foldout.Add(smoothField);
|
||||||
|
|
||||||
foldout.Add(new HelpBox("힙 높이 보정은 '바닥 높이 조정' 섹션의 floorHeight로 제어합니다.", HelpBoxMessageType.Info));
|
return foldout;
|
||||||
|
|
||||||
foldout.TrackSerializedObjectValue(groundingSO, so => so.ApplyModifiedProperties());
|
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
// ========== 손가락 셰이핑 ==========
|
||||||
|
|
||||||
|
private VisualElement BuildFingerShapedSection()
|
||||||
{
|
{
|
||||||
foldout.Add(new HelpBox("FootGroundingController 컴포넌트를 찾을 수 없습니다.", HelpBoxMessageType.Warning));
|
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;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
script.ResetScale();
|
||||||
/// 소스/타겟 다리 길이 차이로 힙 상하 오프셋을 계산합니다.
|
so.FindProperty("avatarScale").floatValue = 1f;
|
||||||
/// 소스 다리가 더 길면 → 음수 (힙을 내려서 타겟이 뜨지 않게)
|
so.FindProperty("hipsOffsetY").floatValue = CalculateHipsOffsetFromLegDifference(script);
|
||||||
/// 소스 다리가 더 짧으면 → 양수 (힙을 올려서 타겟 다리를 펴줌)
|
so.ApplyModifiedProperties();
|
||||||
/// </summary>
|
|
||||||
|
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)
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,193 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
using UnityEditor;
|
|
||||||
using UnityEngine.UIElements;
|
|
||||||
using UnityEditor.UIElements;
|
|
||||||
|
|
||||||
[CustomEditor(typeof(FingerShapedController))]
|
|
||||||
public class FingerShapedControllerEditor : Editor
|
|
||||||
{
|
|
||||||
private const string CommonUssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/StreamingleCommon.uss";
|
|
||||||
private static readonly string[] fingerPropNames = { "ThumbCurl", "IndexCurl", "MiddleCurl", "RingCurl", "PinkyCurl" };
|
|
||||||
private static readonly string[] fingerKoreanNames = { "엄지", "검지", "중지", "약지", "새끼" };
|
|
||||||
|
|
||||||
private FingerShapedController controller;
|
|
||||||
|
|
||||||
public override VisualElement CreateInspectorGUI()
|
|
||||||
{
|
|
||||||
controller = target as FingerShapedController;
|
|
||||||
if (controller == null) return new VisualElement();
|
|
||||||
var root = new VisualElement();
|
|
||||||
|
|
||||||
var commonUss = AssetDatabase.LoadAssetAtPath<StyleSheet>(CommonUssPath);
|
|
||||||
if (commonUss != null) root.styleSheets.Add(commonUss);
|
|
||||||
|
|
||||||
// 활성화 토글
|
|
||||||
var enabledProp = serializedObject.FindProperty("m_Enabled");
|
|
||||||
var enableToggle = new PropertyField(enabledProp, "손가락 제어 활성화");
|
|
||||||
root.Add(enableToggle);
|
|
||||||
|
|
||||||
// 왼손
|
|
||||||
var leftFoldout = new Foldout { text = "왼손", value = true };
|
|
||||||
leftFoldout.Add(BuildHandControls("left"));
|
|
||||||
root.Add(leftFoldout);
|
|
||||||
|
|
||||||
// 오른손
|
|
||||||
var rightFoldout = new Foldout { text = "오른손", value = true };
|
|
||||||
rightFoldout.Add(BuildHandControls("right"));
|
|
||||||
root.Add(rightFoldout);
|
|
||||||
|
|
||||||
// 프리셋 버튼
|
|
||||||
root.Add(BuildPresetButtons());
|
|
||||||
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
private VisualElement BuildHandControls(string prefix)
|
|
||||||
{
|
|
||||||
var container = new VisualElement();
|
|
||||||
container.style.backgroundColor = new Color(0, 0, 0, 0.08f);
|
|
||||||
container.style.borderTopLeftRadius = container.style.borderTopRightRadius =
|
|
||||||
container.style.borderBottomLeftRadius = container.style.borderBottomRightRadius = 4;
|
|
||||||
container.style.paddingTop = container.style.paddingBottom =
|
|
||||||
container.style.paddingLeft = container.style.paddingRight = 6;
|
|
||||||
|
|
||||||
// 헤더: 활성화 토글 + 초기화 버튼
|
|
||||||
var header = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center, marginBottom = 4 } };
|
|
||||||
var handEnabledProp = serializedObject.FindProperty($"{prefix}HandEnabled");
|
|
||||||
header.Add(new PropertyField(handEnabledProp, "제어 활성화") { style = { flexGrow = 1 } });
|
|
||||||
var resetBtn = new Button(() => ResetHandValues(prefix)) { text = "초기화" };
|
|
||||||
resetBtn.style.width = 60;
|
|
||||||
header.Add(resetBtn);
|
|
||||||
container.Add(header);
|
|
||||||
|
|
||||||
// 손가락 슬라이더 (가로 배치)
|
|
||||||
var slidersRow = new VisualElement { style = { flexDirection = FlexDirection.Row, justifyContent = Justify.Center, marginTop = 4 } };
|
|
||||||
|
|
||||||
for (int i = 0; i < fingerPropNames.Length; i++)
|
|
||||||
{
|
|
||||||
var fingerProp = serializedObject.FindProperty($"{prefix}{fingerPropNames[i]}");
|
|
||||||
slidersRow.Add(BuildVerticalSlider(fingerKoreanNames[i], fingerProp));
|
|
||||||
}
|
|
||||||
container.Add(slidersRow);
|
|
||||||
|
|
||||||
// 벌리기 슬라이더
|
|
||||||
var spreadProp = serializedObject.FindProperty($"{prefix}SpreadFingers");
|
|
||||||
var spreadSlider = new Slider("벌리기", -1f, 1f) { showInputField = true };
|
|
||||||
spreadSlider.BindProperty(spreadProp);
|
|
||||||
spreadSlider.style.marginTop = 8;
|
|
||||||
container.Add(spreadSlider);
|
|
||||||
|
|
||||||
// 비활성 시 숨기기
|
|
||||||
container.TrackPropertyValue(handEnabledProp, prop =>
|
|
||||||
{
|
|
||||||
slidersRow.style.display = prop.boolValue ? DisplayStyle.Flex : DisplayStyle.None;
|
|
||||||
spreadSlider.style.display = prop.boolValue ? DisplayStyle.Flex : DisplayStyle.None;
|
|
||||||
});
|
|
||||||
slidersRow.style.display = handEnabledProp.boolValue ? DisplayStyle.Flex : DisplayStyle.None;
|
|
||||||
spreadSlider.style.display = handEnabledProp.boolValue ? DisplayStyle.Flex : DisplayStyle.None;
|
|
||||||
|
|
||||||
return container;
|
|
||||||
}
|
|
||||||
|
|
||||||
private VisualElement BuildVerticalSlider(string label, SerializedProperty prop)
|
|
||||||
{
|
|
||||||
var column = new VisualElement { style = { alignItems = Align.Center, width = 45, marginLeft = 2, marginRight = 2 } };
|
|
||||||
|
|
||||||
column.Add(new Label(label) { style = { fontSize = 10, color = new Color(0.6f, 0.6f, 0.6f) } });
|
|
||||||
|
|
||||||
var valueLabel = new Label(prop.floatValue.ToString("F1")) { style = { fontSize = 10, color = new Color(0.6f, 0.6f, 0.6f) } };
|
|
||||||
column.Add(valueLabel);
|
|
||||||
|
|
||||||
// UI Toolkit Slider in vertical mode (direction: Vertical 대신 세로용 Slider 사용)
|
|
||||||
// SliderDirection.Vertical 사용
|
|
||||||
var slider = new Slider(-1f, 1f) { direction = SliderDirection.Vertical };
|
|
||||||
slider.style.height = 100;
|
|
||||||
slider.style.width = 25;
|
|
||||||
slider.BindProperty(prop);
|
|
||||||
slider.RegisterValueChangedCallback(evt => valueLabel.text = evt.newValue.ToString("F1"));
|
|
||||||
|
|
||||||
// 초기값 동기화
|
|
||||||
column.TrackPropertyValue(prop, p => valueLabel.text = p.floatValue.ToString("F1"));
|
|
||||||
|
|
||||||
column.Add(slider);
|
|
||||||
return column;
|
|
||||||
}
|
|
||||||
|
|
||||||
private VisualElement BuildPresetButtons()
|
|
||||||
{
|
|
||||||
var container = new VisualElement { style = { marginTop = 10 } };
|
|
||||||
container.Add(new Label("손 모양 프리셋") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginBottom = 4 } });
|
|
||||||
|
|
||||||
string[,] presets = {
|
|
||||||
{ "가위", "바위", "보" },
|
|
||||||
{ "브이", "검지", "초기화" }
|
|
||||||
};
|
|
||||||
|
|
||||||
for (int row = 0; row < 2; row++)
|
|
||||||
{
|
|
||||||
var btnRow = new VisualElement { style = { flexDirection = FlexDirection.Row, justifyContent = Justify.Center, marginBottom = 4 } };
|
|
||||||
for (int col = 0; col < 3; col++)
|
|
||||||
{
|
|
||||||
string presetName = presets[row, col];
|
|
||||||
var btn = new Button(() => ApplyPreset(presetName)) { text = presetName };
|
|
||||||
btn.style.height = 30;
|
|
||||||
btn.style.width = 100;
|
|
||||||
btn.style.marginLeft = btn.style.marginRight = 4;
|
|
||||||
btnRow.Add(btn);
|
|
||||||
}
|
|
||||||
container.Add(btnRow);
|
|
||||||
}
|
|
||||||
|
|
||||||
return container;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ResetHandValues(string prefix)
|
|
||||||
{
|
|
||||||
serializedObject.Update();
|
|
||||||
string[] props = { "ThumbCurl", "IndexCurl", "MiddleCurl", "RingCurl", "PinkyCurl", "SpreadFingers" };
|
|
||||||
foreach (var prop in props)
|
|
||||||
serializedObject.FindProperty($"{prefix}{prop}").floatValue = 0f;
|
|
||||||
serializedObject.ApplyModifiedProperties();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ApplyPreset(string presetName)
|
|
||||||
{
|
|
||||||
if (!controller.enabled)
|
|
||||||
controller.enabled = true;
|
|
||||||
|
|
||||||
switch (presetName)
|
|
||||||
{
|
|
||||||
case "가위": SetPreset(1f, 1f, -1f, -1f, -1f, 0.3f); break;
|
|
||||||
case "바위": SetPreset(-1f, -1f, -1f, -1f, -1f, 0f); break;
|
|
||||||
case "보": SetPreset(1f, 1f, 1f, 1f, 1f, 1f); break;
|
|
||||||
case "브이": SetPreset(-1f, 1f, 1f, -1f, -1f, 1f); break;
|
|
||||||
case "검지": SetPreset(-1f, 1f, -1f, -1f, -1f, 0f); break;
|
|
||||||
case "초기화": SetPreset(0.8f, 0.8f, 0.8f, 0.8f, 0.8f, 0.8f); break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetPreset(float thumb, float index, float middle, float ring, float pinky, float spread)
|
|
||||||
{
|
|
||||||
if (controller.leftHandEnabled)
|
|
||||||
{
|
|
||||||
controller.leftThumbCurl = thumb;
|
|
||||||
controller.leftIndexCurl = index;
|
|
||||||
controller.leftMiddleCurl = middle;
|
|
||||||
controller.leftRingCurl = ring;
|
|
||||||
controller.leftPinkyCurl = pinky;
|
|
||||||
controller.leftSpreadFingers = spread;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (controller.rightHandEnabled)
|
|
||||||
{
|
|
||||||
controller.rightThumbCurl = thumb;
|
|
||||||
controller.rightIndexCurl = index;
|
|
||||||
controller.rightMiddleCurl = middle;
|
|
||||||
controller.rightRingCurl = ring;
|
|
||||||
controller.rightPinkyCurl = pinky;
|
|
||||||
controller.rightSpreadFingers = spread;
|
|
||||||
}
|
|
||||||
|
|
||||||
EditorUtility.SetDirty(controller);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 291a583b9a953e041a119ba6c332d187
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@ -1,185 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
using UnityEditor;
|
|
||||||
using UnityEngine.UIElements;
|
|
||||||
using UnityEditor.UIElements;
|
|
||||||
|
|
||||||
namespace KindRetargeting
|
|
||||||
{
|
|
||||||
[CustomEditor(typeof(LimbWeightController))]
|
|
||||||
public class LimbWeightControllerEditor : BaseRetargetingEditor
|
|
||||||
{
|
|
||||||
private const string CommonUssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/StreamingleCommon.uss";
|
|
||||||
|
|
||||||
SerializedProperty maxDistance;
|
|
||||||
SerializedProperty minDistance;
|
|
||||||
SerializedProperty weightSmoothSpeed;
|
|
||||||
SerializedProperty middleWeightMultiplier;
|
|
||||||
SerializedProperty hipsMinDistance;
|
|
||||||
SerializedProperty hipsMaxDistance;
|
|
||||||
SerializedProperty props;
|
|
||||||
SerializedProperty characterRoot;
|
|
||||||
SerializedProperty groundHipsMinHeight;
|
|
||||||
SerializedProperty groundHipsMaxHeight;
|
|
||||||
SerializedProperty footHeightMinThreshold;
|
|
||||||
SerializedProperty footHeightMaxThreshold;
|
|
||||||
SerializedProperty enableLeftArmIK;
|
|
||||||
SerializedProperty enableRightArmIK;
|
|
||||||
SerializedProperty chairSeatHeightOffset;
|
|
||||||
|
|
||||||
protected override void OnEnable()
|
|
||||||
{
|
|
||||||
base.OnEnable();
|
|
||||||
if (serializedObject == null) return;
|
|
||||||
|
|
||||||
maxDistance = serializedObject.FindProperty("maxDistance");
|
|
||||||
minDistance = serializedObject.FindProperty("minDistance");
|
|
||||||
weightSmoothSpeed = serializedObject.FindProperty("weightSmoothSpeed");
|
|
||||||
middleWeightMultiplier = serializedObject.FindProperty("middleWeightMultiplier");
|
|
||||||
hipsMinDistance = serializedObject.FindProperty("hipsMinDistance");
|
|
||||||
hipsMaxDistance = serializedObject.FindProperty("hipsMaxDistance");
|
|
||||||
props = serializedObject.FindProperty("props");
|
|
||||||
characterRoot = serializedObject.FindProperty("characterRoot");
|
|
||||||
groundHipsMinHeight = serializedObject.FindProperty("groundHipsMinHeight");
|
|
||||||
groundHipsMaxHeight = serializedObject.FindProperty("groundHipsMaxHeight");
|
|
||||||
footHeightMinThreshold = serializedObject.FindProperty("footHeightMinThreshold");
|
|
||||||
footHeightMaxThreshold = serializedObject.FindProperty("footHeightMaxThreshold");
|
|
||||||
enableLeftArmIK = serializedObject.FindProperty("enableLeftArmIK");
|
|
||||||
enableRightArmIK = serializedObject.FindProperty("enableRightArmIK");
|
|
||||||
chairSeatHeightOffset = serializedObject.FindProperty("chairSeatHeightOffset");
|
|
||||||
}
|
|
||||||
|
|
||||||
public override VisualElement CreateInspectorGUI()
|
|
||||||
{
|
|
||||||
if (serializedObject == null || target == null)
|
|
||||||
return new VisualElement();
|
|
||||||
|
|
||||||
var root = new VisualElement();
|
|
||||||
|
|
||||||
var commonUss = AssetDatabase.LoadAssetAtPath<StyleSheet>(CommonUssPath);
|
|
||||||
if (commonUss != null) root.styleSheets.Add(commonUss);
|
|
||||||
|
|
||||||
// IK 활성화 설정
|
|
||||||
var ikFoldout = new Foldout { text = "IK 활성화 설정", value = true };
|
|
||||||
ikFoldout.Add(new PropertyField(enableLeftArmIK, "왼팔 IK 활성화"));
|
|
||||||
ikFoldout.Add(new PropertyField(enableRightArmIK, "오른팔 IK 활성화"));
|
|
||||||
root.Add(ikFoldout);
|
|
||||||
|
|
||||||
// 거리 기반 가중치 설정
|
|
||||||
var distFoldout = new Foldout { text = "거리 기반 가중치 설정", value = true };
|
|
||||||
distFoldout.Add(BuildMinMaxRange("거리 범위 (가중치 1 → 0)", minDistance, maxDistance, 0f, 1f));
|
|
||||||
root.Add(distFoldout);
|
|
||||||
|
|
||||||
// 가중치 변화 설정
|
|
||||||
var weightFoldout = new Foldout { text = "가중치 변화 설정", value = true };
|
|
||||||
weightFoldout.Add(new PropertyField(weightSmoothSpeed, "가중치 변화 속도"));
|
|
||||||
root.Add(weightFoldout);
|
|
||||||
|
|
||||||
// 허리 가중치 설정
|
|
||||||
var hipsFoldout = new Foldout { text = "허리 가중치 설정", value = true };
|
|
||||||
hipsFoldout.Add(BuildMinMaxRange("허리 거리 범위 (가중치 1 → 0)", hipsMinDistance, hipsMaxDistance, 0f, 1f));
|
|
||||||
root.Add(hipsFoldout);
|
|
||||||
|
|
||||||
// 바닥 기준 히프 보정 설정
|
|
||||||
var groundFoldout = new Foldout { text = "바닥 기준 히프 보정 설정", value = true };
|
|
||||||
groundFoldout.Add(BuildMinMaxRange("바닥 기준 히프 높이 범위 (가중치 0 → 1)", groundHipsMinHeight, groundHipsMaxHeight, 0f, 2f));
|
|
||||||
root.Add(groundFoldout);
|
|
||||||
|
|
||||||
// 발 높이 기반 가중치 설정
|
|
||||||
var footFoldout = new Foldout { text = "발 높이 기반 가중치 설정", value = true };
|
|
||||||
footFoldout.Add(BuildMinMaxRange("발 높이 범위 (가중치 1 → 0)", footHeightMinThreshold, footHeightMaxThreshold, 0.1f, 1f));
|
|
||||||
root.Add(footFoldout);
|
|
||||||
|
|
||||||
// 의자 앉기 높이 설정
|
|
||||||
var chairFoldout = new Foldout { text = "의자 앉기 높이 설정", value = true };
|
|
||||||
chairFoldout.Add(new PropertyField(chairSeatHeightOffset, "좌석 높이 오프셋"));
|
|
||||||
root.Add(chairFoldout);
|
|
||||||
|
|
||||||
// 참조 설정
|
|
||||||
var refFoldout = new Foldout { text = "참조 설정", value = true };
|
|
||||||
refFoldout.Add(new PropertyField(props, "프랍 오브젝트"));
|
|
||||||
refFoldout.Add(new PropertyField(characterRoot, "캐릭터 루트"));
|
|
||||||
root.Add(refFoldout);
|
|
||||||
|
|
||||||
// 변경 감지
|
|
||||||
root.TrackSerializedObjectValue(serializedObject, so =>
|
|
||||||
{
|
|
||||||
if (target == null) return;
|
|
||||||
MarkDirty();
|
|
||||||
var script = (LimbWeightController)target;
|
|
||||||
var retargeting = script.GetComponent<CustomRetargetingScript>();
|
|
||||||
if (retargeting != null)
|
|
||||||
EditorUtility.SetDirty(retargeting);
|
|
||||||
});
|
|
||||||
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
private VisualElement BuildMinMaxRange(string label, SerializedProperty minProp, SerializedProperty maxProp, float limitMin, float limitMax)
|
|
||||||
{
|
|
||||||
var container = new VisualElement { style = { marginBottom = 4 } };
|
|
||||||
if (minProp == null || maxProp == null)
|
|
||||||
{
|
|
||||||
container.Add(new HelpBox($"'{label}' 프로퍼티를 찾을 수 없습니다.", HelpBoxMessageType.Warning));
|
|
||||||
return container;
|
|
||||||
}
|
|
||||||
container.Add(new Label(label) { style = { marginBottom = 2 } });
|
|
||||||
|
|
||||||
var row = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center } };
|
|
||||||
|
|
||||||
var minField = new FloatField { value = minProp.floatValue, style = { width = 50 } };
|
|
||||||
var slider = new MinMaxSlider(minProp.floatValue, maxProp.floatValue, limitMin, limitMax);
|
|
||||||
slider.style.flexGrow = 1;
|
|
||||||
slider.style.marginLeft = slider.style.marginRight = 4;
|
|
||||||
var maxField = new FloatField { value = maxProp.floatValue, style = { width = 50 } };
|
|
||||||
|
|
||||||
// MinMaxSlider → sync fields + properties
|
|
||||||
slider.RegisterValueChangedCallback(evt =>
|
|
||||||
{
|
|
||||||
minProp.floatValue = evt.newValue.x;
|
|
||||||
maxProp.floatValue = evt.newValue.y;
|
|
||||||
serializedObject.ApplyModifiedProperties();
|
|
||||||
minField.SetValueWithoutNotify(evt.newValue.x);
|
|
||||||
maxField.SetValueWithoutNotify(evt.newValue.y);
|
|
||||||
});
|
|
||||||
|
|
||||||
// FloatField min → sync slider + property
|
|
||||||
minField.RegisterValueChangedCallback(evt =>
|
|
||||||
{
|
|
||||||
float clamped = Mathf.Clamp(evt.newValue, limitMin, maxProp.floatValue);
|
|
||||||
minProp.floatValue = clamped;
|
|
||||||
serializedObject.ApplyModifiedProperties();
|
|
||||||
slider.SetValueWithoutNotify(new Vector2(clamped, maxProp.floatValue));
|
|
||||||
minField.SetValueWithoutNotify(clamped);
|
|
||||||
});
|
|
||||||
|
|
||||||
// FloatField max → sync slider + property
|
|
||||||
maxField.RegisterValueChangedCallback(evt =>
|
|
||||||
{
|
|
||||||
float clamped = Mathf.Clamp(evt.newValue, minProp.floatValue, limitMax);
|
|
||||||
maxProp.floatValue = clamped;
|
|
||||||
serializedObject.ApplyModifiedProperties();
|
|
||||||
slider.SetValueWithoutNotify(new Vector2(minProp.floatValue, clamped));
|
|
||||||
maxField.SetValueWithoutNotify(clamped);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Track external changes (undo, etc.)
|
|
||||||
container.TrackPropertyValue(minProp, p =>
|
|
||||||
{
|
|
||||||
minField.SetValueWithoutNotify(p.floatValue);
|
|
||||||
slider.SetValueWithoutNotify(new Vector2(p.floatValue, maxProp.floatValue));
|
|
||||||
});
|
|
||||||
container.TrackPropertyValue(maxProp, p =>
|
|
||||||
{
|
|
||||||
maxField.SetValueWithoutNotify(p.floatValue);
|
|
||||||
slider.SetValueWithoutNotify(new Vector2(minProp.floatValue, p.floatValue));
|
|
||||||
});
|
|
||||||
|
|
||||||
row.Add(minField);
|
|
||||||
row.Add(slider);
|
|
||||||
row.Add(maxField);
|
|
||||||
container.Add(row);
|
|
||||||
|
|
||||||
return container;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: 199539b34f08aac41a86f4767bc49def
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@ -1,167 +0,0 @@
|
|||||||
using UnityEngine;
|
|
||||||
using UnityEditor;
|
|
||||||
using UnityEngine.UIElements;
|
|
||||||
using UnityEditor.UIElements;
|
|
||||||
|
|
||||||
namespace KindRetargeting
|
|
||||||
{
|
|
||||||
[CustomEditor(typeof(PropLocationController))]
|
|
||||||
public class PropLocationControllerEditor : BaseRetargetingEditor
|
|
||||||
{
|
|
||||||
private const string CommonUssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/StreamingleCommon.uss";
|
|
||||||
|
|
||||||
private VisualElement offsetContainer;
|
|
||||||
private VisualElement propListContainer;
|
|
||||||
private PropLocationController controller;
|
|
||||||
|
|
||||||
public override VisualElement CreateInspectorGUI()
|
|
||||||
{
|
|
||||||
controller = target as PropLocationController;
|
|
||||||
if (controller == null) return new VisualElement();
|
|
||||||
var root = new VisualElement();
|
|
||||||
|
|
||||||
var commonUss = AssetDatabase.LoadAssetAtPath<StyleSheet>(CommonUssPath);
|
|
||||||
if (commonUss != null) root.styleSheets.Add(commonUss);
|
|
||||||
|
|
||||||
// 기본 프로퍼티
|
|
||||||
var iterator = serializedObject.GetIterator();
|
|
||||||
iterator.NextVisible(true); // skip m_Script
|
|
||||||
while (iterator.NextVisible(false))
|
|
||||||
{
|
|
||||||
var field = new PropertyField(iterator.Copy());
|
|
||||||
root.Add(field);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 오프셋 조정 섹션
|
|
||||||
root.Add(new Label("오프셋 조정") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginTop = 10 } });
|
|
||||||
offsetContainer = new VisualElement();
|
|
||||||
root.Add(offsetContainer);
|
|
||||||
|
|
||||||
// 부착된 프랍 목록 섹션
|
|
||||||
root.Add(new Label("부착된 프랍 목록") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginTop = 10 } });
|
|
||||||
propListContainer = new VisualElement();
|
|
||||||
root.Add(propListContainer);
|
|
||||||
|
|
||||||
// 프랍 위치 이동 버튼
|
|
||||||
root.Add(new Label("프랍 위치 이동") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginTop = 10 } });
|
|
||||||
var btnRow = new VisualElement { style = { flexDirection = FlexDirection.Row, marginTop = 4 } };
|
|
||||||
|
|
||||||
var headBtn = new Button(() => controller.MoveToHead()) { text = "머리로 이동" };
|
|
||||||
headBtn.style.flexGrow = 1; headBtn.style.height = 30; headBtn.style.marginRight = 2;
|
|
||||||
btnRow.Add(headBtn);
|
|
||||||
|
|
||||||
var leftBtn = new Button(() => controller.MoveToLeftHand()) { text = "왼손으로 이동" };
|
|
||||||
leftBtn.style.flexGrow = 1; leftBtn.style.height = 30; leftBtn.style.marginRight = 2;
|
|
||||||
btnRow.Add(leftBtn);
|
|
||||||
|
|
||||||
var rightBtn = new Button(() => controller.MoveToRightHand()) { text = "오른손으로 이동" };
|
|
||||||
rightBtn.style.flexGrow = 1; rightBtn.style.height = 30;
|
|
||||||
btnRow.Add(rightBtn);
|
|
||||||
|
|
||||||
root.Add(btnRow);
|
|
||||||
|
|
||||||
var detachBtn = new Button(() =>
|
|
||||||
{
|
|
||||||
if (Selection.activeGameObject != null)
|
|
||||||
Undo.RecordObject(Selection.activeGameObject.transform, "Detach Prop");
|
|
||||||
controller.DetachProp();
|
|
||||||
}) { text = "프랍 해제" };
|
|
||||||
detachBtn.style.height = 30;
|
|
||||||
detachBtn.style.marginTop = 4;
|
|
||||||
root.Add(detachBtn);
|
|
||||||
|
|
||||||
// 주기적으로 동적 UI 갱신
|
|
||||||
root.schedule.Execute(() => RebuildDynamicUI()).Every(500);
|
|
||||||
RebuildDynamicUI();
|
|
||||||
|
|
||||||
return root;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RebuildDynamicUI()
|
|
||||||
{
|
|
||||||
if (controller == null) return;
|
|
||||||
RebuildOffsets();
|
|
||||||
RebuildPropLists();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RebuildOffsets()
|
|
||||||
{
|
|
||||||
if (offsetContainer == null) return;
|
|
||||||
offsetContainer.Clear();
|
|
||||||
|
|
||||||
BuildOffsetSection(offsetContainer, "왼손 오프셋", controller.GetLeftHandOffset());
|
|
||||||
BuildOffsetSection(offsetContainer, "오른손 오프셋", controller.GetRightHandOffset());
|
|
||||||
BuildOffsetSection(offsetContainer, "머리 오프셋", controller.GetHeadOffset());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void BuildOffsetSection(VisualElement parent, string label, Transform offset)
|
|
||||||
{
|
|
||||||
if (offset == null) return;
|
|
||||||
|
|
||||||
var foldout = new Foldout { text = label, value = true };
|
|
||||||
|
|
||||||
var posField = new Vector3Field("위치") { value = offset.localPosition };
|
|
||||||
posField.RegisterValueChangedCallback(evt =>
|
|
||||||
{
|
|
||||||
Undo.RecordObject(offset, $"Update {label}");
|
|
||||||
offset.localPosition = evt.newValue;
|
|
||||||
EditorUtility.SetDirty(offset);
|
|
||||||
});
|
|
||||||
foldout.Add(posField);
|
|
||||||
|
|
||||||
var rotField = new Vector3Field("회전") { value = offset.localRotation.eulerAngles };
|
|
||||||
rotField.RegisterValueChangedCallback(evt =>
|
|
||||||
{
|
|
||||||
Undo.RecordObject(offset, $"Update {label}");
|
|
||||||
offset.localRotation = Quaternion.Euler(evt.newValue);
|
|
||||||
EditorUtility.SetDirty(offset);
|
|
||||||
});
|
|
||||||
foldout.Add(rotField);
|
|
||||||
|
|
||||||
parent.Add(foldout);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RebuildPropLists()
|
|
||||||
{
|
|
||||||
if (propListContainer == null) return;
|
|
||||||
propListContainer.Clear();
|
|
||||||
|
|
||||||
BuildPropListSection(propListContainer, "머리 프랍", controller.GetHeadProps());
|
|
||||||
BuildPropListSection(propListContainer, "왼손 프랍", controller.GetLeftHandProps());
|
|
||||||
BuildPropListSection(propListContainer, "오른손 프랍", controller.GetRightHandProps());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void BuildPropListSection(VisualElement parent, string title, GameObject[] propList)
|
|
||||||
{
|
|
||||||
var box = new VisualElement();
|
|
||||||
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 = 4;
|
|
||||||
box.style.marginBottom = 4;
|
|
||||||
|
|
||||||
box.Add(new Label(title) { style = { unityFontStyleAndWeight = FontStyle.Bold } });
|
|
||||||
|
|
||||||
if (propList.Length > 0)
|
|
||||||
{
|
|
||||||
foreach (var prop in propList)
|
|
||||||
{
|
|
||||||
if (prop == null) continue;
|
|
||||||
var row = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center, marginTop = 2 } };
|
|
||||||
row.Add(new Label(prop.name) { style = { flexGrow = 1 } });
|
|
||||||
var selectBtn = new Button(() => Selection.activeGameObject = prop) { text = "선택" };
|
|
||||||
selectBtn.style.width = 60;
|
|
||||||
row.Add(selectBtn);
|
|
||||||
box.Add(row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
box.Add(new Label("부착된 프랍 없음") { style = { color = new Color(0.6f, 0.6f, 0.6f) } });
|
|
||||||
}
|
|
||||||
|
|
||||||
parent.Add(box);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
fileFormatVersion: 2
|
|
||||||
guid: a49ee1ae55b970e4c8ca00ccae5d6f97
|
|
||||||
MonoImporter:
|
|
||||||
externalObjects: {}
|
|
||||||
serializedVersion: 2
|
|
||||||
defaultReferences: []
|
|
||||||
executionOrder: 0
|
|
||||||
icon: {instanceID: 0}
|
|
||||||
userData:
|
|
||||||
assetBundleName:
|
|
||||||
assetBundleVariant:
|
|
||||||
@ -144,12 +144,7 @@ public class RetargetingControlWindow : EditorWindow
|
|||||||
foreach (var s in retargetingScripts)
|
foreach (var s in retargetingScripts)
|
||||||
{
|
{
|
||||||
if (s == null) continue;
|
if (s == null) continue;
|
||||||
if (s.GetComponent<LimbWeightController>() == null)
|
// 모든 컴포넌트는 CRS 내부 모듈로 이동됨
|
||||||
s.gameObject.AddComponent<LimbWeightController>();
|
|
||||||
if (s.GetComponent<FingerShapedController>() == null)
|
|
||||||
s.gameObject.AddComponent<FingerShapedController>();
|
|
||||||
if (s.GetComponent<PropLocationController>() == null)
|
|
||||||
s.gameObject.AddComponent<PropLocationController>();
|
|
||||||
EditorUtility.SetDirty(s.gameObject);
|
EditorUtility.SetDirty(s.gameObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -198,7 +193,7 @@ public class RetargetingControlWindow : EditorWindow
|
|||||||
panel.Add(BuildHeader(script));
|
panel.Add(BuildHeader(script));
|
||||||
|
|
||||||
// 가중치 설정
|
// 가중치 설정
|
||||||
panel.Add(BuildWeightSection(script));
|
panel.Add(BuildWeightSection(script, so));
|
||||||
|
|
||||||
// 힙 위치 보정
|
// 힙 위치 보정
|
||||||
panel.Add(BuildHipsSection(script, so));
|
panel.Add(BuildHipsSection(script, so));
|
||||||
@ -229,17 +224,25 @@ public class RetargetingControlWindow : EditorWindow
|
|||||||
footContainer.Bind(so);
|
footContainer.Bind(so);
|
||||||
panel.Add(footFoldout);
|
panel.Add(footFoldout);
|
||||||
|
|
||||||
|
// 어깨 보정
|
||||||
|
panel.Add(BuildShoulderSection(script, so));
|
||||||
|
|
||||||
|
// 접지 설정
|
||||||
|
panel.Add(BuildGroundingSection(script, so));
|
||||||
|
|
||||||
// 손가락 제어 설정
|
// 손가락 제어 설정
|
||||||
panel.Add(BuildFingerControlSection(script));
|
panel.Add(BuildFingerControlSection(script, so));
|
||||||
|
|
||||||
// 손가락 복제 설정
|
// 손가락 복제 설정
|
||||||
panel.Add(BuildFingerCopySection(script, so));
|
panel.Add(BuildFingerCopySection(script, so));
|
||||||
|
|
||||||
// 바닥 높이 설정
|
// 바닥 높이 설정
|
||||||
var floorFoldout = new Foldout { text = "바닥 높이 설정", value = false };
|
var floorFoldout = new Foldout { text = "바닥 높이 설정", value = false };
|
||||||
var floorField = new PropertyField(so.FindProperty("floorHeight"), "바닥 높이 (-1 ~ 1)");
|
var floorContainer = new VisualElement();
|
||||||
floorFoldout.Add(floorField);
|
floorContainer.Add(new PropertyField(so.FindProperty("floorHeight"), "바닥 높이 (-1 ~ 1)"));
|
||||||
floorField.Bind(so);
|
floorContainer.Add(new PropertyField(so.FindProperty("minimumAnkleHeight"), "최소 발목 높이"));
|
||||||
|
floorContainer.Bind(so);
|
||||||
|
floorFoldout.Add(floorContainer);
|
||||||
panel.Add(floorFoldout);
|
panel.Add(floorFoldout);
|
||||||
|
|
||||||
// 아바타 크기 설정
|
// 아바타 크기 설정
|
||||||
@ -304,27 +307,26 @@ public class RetargetingControlWindow : EditorWindow
|
|||||||
|
|
||||||
// ========== Weight Settings ==========
|
// ========== Weight Settings ==========
|
||||||
|
|
||||||
private VisualElement BuildWeightSection(CustomRetargetingScript script)
|
private VisualElement BuildWeightSection(CustomRetargetingScript script, SerializedObject so)
|
||||||
{
|
{
|
||||||
var foldout = new Foldout { text = "가중치 설정", value = false };
|
var foldout = new Foldout { text = "가중치 설정", value = false };
|
||||||
var limb = script.GetComponent<LimbWeightController>();
|
|
||||||
if (limb == null) { foldout.Add(new HelpBox("LimbWeightController가 없습니다.", HelpBoxMessageType.Warning)); return foldout; }
|
|
||||||
|
|
||||||
var limbSO = CreateTrackedSO(limb);
|
|
||||||
var container = new VisualElement();
|
var container = new VisualElement();
|
||||||
|
|
||||||
container.Add(BuildMinMaxRange("손과 프랍과의 범위 (가중치 1 → 0)",
|
// IK 활성화 토글
|
||||||
limbSO.FindProperty("minDistance"), limbSO.FindProperty("maxDistance"), 0f, 1f, limbSO));
|
container.Add(new PropertyField(so.FindProperty("limbWeight.enableLeftArmIK"), "왼팔 IK 활성화"));
|
||||||
container.Add(BuildMinMaxRange("의자와 허리 거리 범위 (가중치 1 → 0)",
|
container.Add(new PropertyField(so.FindProperty("limbWeight.enableRightArmIK"), "오른팔 IK 활성화"));
|
||||||
limbSO.FindProperty("hipsMinDistance"), limbSO.FindProperty("hipsMaxDistance"), 0f, 1f, limbSO));
|
|
||||||
container.Add(BuildMinMaxRange("바닥과 허리 높이에 의한 블렌딩 (가중치 0 → 1)",
|
|
||||||
limbSO.FindProperty("groundHipsMinHeight"), limbSO.FindProperty("groundHipsMaxHeight"), 0f, 2f, limbSO));
|
|
||||||
container.Add(BuildMinMaxRange("지면으로부터 발의 범위에 의한 IK 블렌딩 (가중치 1 → 0)",
|
|
||||||
limbSO.FindProperty("footHeightMinThreshold"), limbSO.FindProperty("footHeightMaxThreshold"), 0.1f, 1f, limbSO));
|
|
||||||
|
|
||||||
var smoothField = new PropertyField(limbSO.FindProperty("weightSmoothSpeed"), "가중치 변화 속도");
|
container.Add(BuildMinMaxRange("손과 프랍과의 범위 (가중치 1 → 0)",
|
||||||
|
so.FindProperty("limbWeight.minDistance"), so.FindProperty("limbWeight.maxDistance"), 0f, 1f, so));
|
||||||
|
container.Add(BuildMinMaxRange("의자와 허리 거리 범위 (가중치 1 → 0)",
|
||||||
|
so.FindProperty("limbWeight.hipsMinDistance"), so.FindProperty("limbWeight.hipsMaxDistance"), 0f, 1f, so));
|
||||||
|
container.Add(BuildMinMaxRange("바닥과 허리 높이에 의한 블렌딩 (가중치 0 → 1)",
|
||||||
|
so.FindProperty("limbWeight.groundHipsMinHeight"), so.FindProperty("limbWeight.groundHipsMaxHeight"), 0f, 2f, so));
|
||||||
|
container.Add(BuildMinMaxRange("지면으로부터 발의 범위에 의한 IK 블렌딩 (가중치 1 → 0)",
|
||||||
|
so.FindProperty("limbWeight.footHeightMinThreshold"), so.FindProperty("limbWeight.footHeightMaxThreshold"), 0.1f, 1f, so));
|
||||||
|
|
||||||
|
var smoothField = new PropertyField(so.FindProperty("limbWeight.weightSmoothSpeed"), "가중치 변화 속도");
|
||||||
container.Add(smoothField);
|
container.Add(smoothField);
|
||||||
container.Bind(limbSO);
|
|
||||||
|
|
||||||
foldout.Add(container);
|
foldout.Add(container);
|
||||||
return foldout;
|
return foldout;
|
||||||
@ -367,15 +369,9 @@ public class RetargetingControlWindow : EditorWindow
|
|||||||
container.Add(hz);
|
container.Add(hz);
|
||||||
|
|
||||||
// 의자 앉기 높이
|
// 의자 앉기 높이
|
||||||
var limb = script.GetComponent<LimbWeightController>();
|
|
||||||
if (limb != null)
|
|
||||||
{
|
|
||||||
var limbSO = CreateTrackedSO(limb);
|
|
||||||
var chairSlider = new Slider("의자 앉기 높이", -1f, 1f) { showInputField = true, tooltip = "의자에 앉을 때 엉덩이 높이 조정 (월드 Y 기준)" };
|
var chairSlider = new Slider("의자 앉기 높이", -1f, 1f) { showInputField = true, tooltip = "의자에 앉을 때 엉덩이 높이 조정 (월드 Y 기준)" };
|
||||||
chairSlider.BindProperty(limbSO.FindProperty("chairSeatHeightOffset"));
|
chairSlider.BindProperty(so.FindProperty("limbWeight.chairSeatHeightOffset"));
|
||||||
container.Add(chairSlider);
|
container.Add(chairSlider);
|
||||||
chairSlider.Bind(limbSO);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 다리 길이 자동 보정 버튼
|
// 다리 길이 자동 보정 버튼
|
||||||
var autoHipsBtn = new Button(() =>
|
var autoHipsBtn = new Button(() =>
|
||||||
@ -403,48 +399,28 @@ public class RetargetingControlWindow : EditorWindow
|
|||||||
|
|
||||||
// ========== Finger Control ==========
|
// ========== Finger Control ==========
|
||||||
|
|
||||||
private VisualElement BuildFingerControlSection(CustomRetargetingScript script)
|
private VisualElement BuildFingerControlSection(CustomRetargetingScript script, SerializedObject so)
|
||||||
{
|
{
|
||||||
var foldout = new Foldout { text = "손가락 제어 설정", value = false };
|
var foldout = new Foldout { text = "손가락 제어 설정", value = false };
|
||||||
var fingerController = script.GetComponent<FingerShapedController>();
|
|
||||||
if (fingerController == null)
|
|
||||||
{
|
|
||||||
foldout.Add(new HelpBox("FingerShapedController가 없습니다.", HelpBoxMessageType.Warning));
|
|
||||||
var addBtn = new Button(() =>
|
|
||||||
{
|
|
||||||
script.gameObject.AddComponent<FingerShapedController>();
|
|
||||||
EditorUtility.SetDirty(script.gameObject);
|
|
||||||
RebuildCharacterPanels();
|
|
||||||
}) { text = "FingerShapedController 추가" };
|
|
||||||
foldout.Add(addBtn);
|
|
||||||
return foldout;
|
|
||||||
}
|
|
||||||
|
|
||||||
var fso = CreateTrackedSO(fingerController);
|
|
||||||
var container = new VisualElement();
|
var container = new VisualElement();
|
||||||
|
|
||||||
// 활성화 토글
|
// 활성화 토글
|
||||||
var enableToggle = new Toggle("손가락 제어 활성화") { value = fingerController.enabled };
|
var enabledProp = so.FindProperty("fingerShaped.enabled");
|
||||||
enableToggle.RegisterValueChangedCallback(evt =>
|
var enableToggle = new PropertyField(enabledProp, "손가락 제어 활성화");
|
||||||
{
|
|
||||||
fingerController.enabled = evt.newValue;
|
|
||||||
EditorUtility.SetDirty(fingerController);
|
|
||||||
});
|
|
||||||
container.Add(enableToggle);
|
container.Add(enableToggle);
|
||||||
|
|
||||||
// 왼손
|
// 왼손
|
||||||
container.Add(BuildHandSection("왼손", "left", fso, fingerController));
|
container.Add(BuildHandSection("왼손", "left", so, script.fingerShaped));
|
||||||
// 오른손
|
// 오른손
|
||||||
container.Add(BuildHandSection("오른손", "right", fso, fingerController));
|
container.Add(BuildHandSection("오른손", "right", so, script.fingerShaped));
|
||||||
// 프리셋
|
// 프리셋
|
||||||
container.Add(BuildFingerPresets(fingerController));
|
container.Add(BuildFingerPresets(script, script.fingerShaped));
|
||||||
|
|
||||||
container.Bind(fso);
|
|
||||||
foldout.Add(container);
|
foldout.Add(container);
|
||||||
return foldout;
|
return foldout;
|
||||||
}
|
}
|
||||||
|
|
||||||
private VisualElement BuildHandSection(string label, string prefix, SerializedObject fso, FingerShapedController fc)
|
private VisualElement BuildHandSection(string label, string prefix, SerializedObject so, FingerShapedController fc)
|
||||||
{
|
{
|
||||||
var box = new VisualElement();
|
var box = new VisualElement();
|
||||||
box.style.backgroundColor = new Color(0, 0, 0, 0.08f);
|
box.style.backgroundColor = new Color(0, 0, 0, 0.08f);
|
||||||
@ -456,13 +432,13 @@ public class RetargetingControlWindow : EditorWindow
|
|||||||
|
|
||||||
var header = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center } };
|
var header = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center } };
|
||||||
var handFoldout = new Foldout { text = label, value = false };
|
var handFoldout = new Foldout { text = label, value = false };
|
||||||
var handEnabledProp = fso.FindProperty($"{prefix}HandEnabled");
|
var handEnabledProp = so.FindProperty($"fingerShaped.{prefix}HandEnabled");
|
||||||
header.Add(new PropertyField(handEnabledProp, "활성화") { style = { flexGrow = 1 } });
|
header.Add(new PropertyField(handEnabledProp, "활성화") { style = { flexGrow = 1 } });
|
||||||
var resetBtn = new Button(() =>
|
var resetBtn = new Button(() =>
|
||||||
{
|
{
|
||||||
string[] props = { "ThumbCurl", "IndexCurl", "MiddleCurl", "RingCurl", "PinkyCurl", "SpreadFingers" };
|
string[] props = { "ThumbCurl", "IndexCurl", "MiddleCurl", "RingCurl", "PinkyCurl", "SpreadFingers" };
|
||||||
foreach (var p in props) fso.FindProperty($"{prefix}{p}").floatValue = 0f;
|
foreach (var p in props) so.FindProperty($"fingerShaped.{prefix}{p}").floatValue = 0f;
|
||||||
fso.ApplyModifiedProperties();
|
so.ApplyModifiedProperties();
|
||||||
}) { text = "초기화" };
|
}) { text = "초기화" };
|
||||||
resetBtn.style.width = 60;
|
resetBtn.style.width = 60;
|
||||||
header.Add(resetBtn);
|
header.Add(resetBtn);
|
||||||
@ -478,7 +454,7 @@ public class RetargetingControlWindow : EditorWindow
|
|||||||
var col = new VisualElement { style = { alignItems = Align.Center, width = 45, marginLeft = 2, marginRight = 2 } };
|
var col = new VisualElement { style = { alignItems = Align.Center, width = 45, marginLeft = 2, marginRight = 2 } };
|
||||||
col.Add(new Label(korNames[i]) { style = { fontSize = 10, color = new Color(0.6f, 0.6f, 0.6f) } });
|
col.Add(new Label(korNames[i]) { style = { fontSize = 10, color = new Color(0.6f, 0.6f, 0.6f) } });
|
||||||
|
|
||||||
var prop = fso.FindProperty($"{prefix}{fingerNames[i]}Curl");
|
var prop = so.FindProperty($"fingerShaped.{prefix}{fingerNames[i]}Curl");
|
||||||
var valLabel = new Label(prop.floatValue.ToString("F1")) { style = { fontSize = 10, color = new Color(0.6f, 0.6f, 0.6f) } };
|
var valLabel = new Label(prop.floatValue.ToString("F1")) { style = { fontSize = 10, color = new Color(0.6f, 0.6f, 0.6f) } };
|
||||||
col.Add(valLabel);
|
col.Add(valLabel);
|
||||||
|
|
||||||
@ -495,16 +471,18 @@ public class RetargetingControlWindow : EditorWindow
|
|||||||
|
|
||||||
// 벌리기
|
// 벌리기
|
||||||
var spreadSlider = new Slider("벌리기", -1f, 1f) { showInputField = true };
|
var spreadSlider = new Slider("벌리기", -1f, 1f) { showInputField = true };
|
||||||
spreadSlider.BindProperty(fso.FindProperty($"{prefix}SpreadFingers"));
|
spreadSlider.BindProperty(so.FindProperty($"fingerShaped.{prefix}SpreadFingers"));
|
||||||
handFoldout.Add(spreadSlider);
|
handFoldout.Add(spreadSlider);
|
||||||
|
|
||||||
// 비활성 시 숨김
|
// 비활성 시 숨김
|
||||||
handFoldout.schedule.Execute(() =>
|
handFoldout.schedule.Execute(() =>
|
||||||
{
|
{
|
||||||
try { if (fso == null || fso.targetObject == null) return; }
|
try { if (so == null || so.targetObject == null) return; }
|
||||||
catch (System.Exception) { return; }
|
catch (System.Exception) { return; }
|
||||||
fso.Update();
|
so.Update();
|
||||||
bool enabled = fso.FindProperty($"{prefix}HandEnabled").boolValue;
|
var enabledProp = so.FindProperty($"fingerShaped.{prefix}HandEnabled");
|
||||||
|
if (enabledProp == null) return;
|
||||||
|
bool enabled = enabledProp.boolValue;
|
||||||
slidersRow.style.display = enabled ? DisplayStyle.Flex : DisplayStyle.None;
|
slidersRow.style.display = enabled ? DisplayStyle.Flex : DisplayStyle.None;
|
||||||
spreadSlider.style.display = enabled ? DisplayStyle.Flex : DisplayStyle.None;
|
spreadSlider.style.display = enabled ? DisplayStyle.Flex : DisplayStyle.None;
|
||||||
}).Every(300);
|
}).Every(300);
|
||||||
@ -513,7 +491,7 @@ public class RetargetingControlWindow : EditorWindow
|
|||||||
return box;
|
return box;
|
||||||
}
|
}
|
||||||
|
|
||||||
private VisualElement BuildFingerPresets(FingerShapedController controller)
|
private VisualElement BuildFingerPresets(CustomRetargetingScript script, FingerShapedController controller)
|
||||||
{
|
{
|
||||||
var container = new VisualElement { style = { marginTop = 6 } };
|
var container = new VisualElement { style = { marginTop = 6 } };
|
||||||
container.Add(new Label("손 모양 프리셋") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginBottom = 4 } });
|
container.Add(new Label("손 모양 프리셋") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginBottom = 4 } });
|
||||||
@ -525,7 +503,7 @@ public class RetargetingControlWindow : EditorWindow
|
|||||||
for (int col = 0; col < 3; col++)
|
for (int col = 0; col < 3; col++)
|
||||||
{
|
{
|
||||||
string name = presets[row, col];
|
string name = presets[row, col];
|
||||||
var btn = new Button(() => ApplyFingerPreset(controller, name)) { text = name };
|
var btn = new Button(() => ApplyFingerPreset(script, controller, name)) { text = name };
|
||||||
btn.style.height = 30; btn.style.width = 100; btn.style.marginLeft = btn.style.marginRight = 4;
|
btn.style.height = 30; btn.style.width = 100; btn.style.marginLeft = btn.style.marginRight = 4;
|
||||||
btnRow.Add(btn);
|
btnRow.Add(btn);
|
||||||
}
|
}
|
||||||
@ -606,19 +584,7 @@ public class RetargetingControlWindow : EditorWindow
|
|||||||
private VisualElement BuildPropSection(CustomRetargetingScript script)
|
private VisualElement BuildPropSection(CustomRetargetingScript script)
|
||||||
{
|
{
|
||||||
var foldout = new Foldout { text = "프랍 설정", value = false };
|
var foldout = new Foldout { text = "프랍 설정", value = false };
|
||||||
var propController = script.GetComponent<PropLocationController>();
|
var propController = script.propLocation;
|
||||||
if (propController == null)
|
|
||||||
{
|
|
||||||
foldout.Add(new HelpBox("PropLocationController가 없습니다.", HelpBoxMessageType.Warning));
|
|
||||||
var addBtn = new Button(() =>
|
|
||||||
{
|
|
||||||
script.gameObject.AddComponent<PropLocationController>();
|
|
||||||
EditorUtility.SetDirty(script.gameObject);
|
|
||||||
RebuildCharacterPanels();
|
|
||||||
}) { text = "PropLocationController 추가" };
|
|
||||||
foldout.Add(addBtn);
|
|
||||||
return foldout;
|
|
||||||
}
|
|
||||||
|
|
||||||
var dynamicContainer = new VisualElement();
|
var dynamicContainer = new VisualElement();
|
||||||
foldout.Add(dynamicContainer);
|
foldout.Add(dynamicContainer);
|
||||||
@ -782,6 +748,61 @@ public class RetargetingControlWindow : EditorWindow
|
|||||||
return box;
|
return box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Shoulder Correction ==========
|
||||||
|
|
||||||
|
private VisualElement BuildShoulderSection(CustomRetargetingScript script, SerializedObject so)
|
||||||
|
{
|
||||||
|
var foldout = new Foldout { text = "어깨 보정", value = false };
|
||||||
|
var container = new VisualElement();
|
||||||
|
|
||||||
|
var strength = new Slider("블렌드 강도", 0f, 5f) { showInputField = true };
|
||||||
|
strength.BindProperty(so.FindProperty("shoulderCorrection.blendStrength"));
|
||||||
|
container.Add(strength);
|
||||||
|
|
||||||
|
var maxBlend = new Slider("최대 블렌드", 0f, 1f) { showInputField = true };
|
||||||
|
maxBlend.BindProperty(so.FindProperty("shoulderCorrection.maxShoulderBlend"));
|
||||||
|
container.Add(maxBlend);
|
||||||
|
|
||||||
|
container.Add(new PropertyField(so.FindProperty("shoulderCorrection.reverseLeftRotation"), "왼쪽 회전 반전"));
|
||||||
|
container.Add(new PropertyField(so.FindProperty("shoulderCorrection.reverseRightRotation"), "오른쪽 회전 반전"));
|
||||||
|
|
||||||
|
container.Add(BuildMinMaxRange("높이 차이 범위",
|
||||||
|
so.FindProperty("shoulderCorrection.minHeightDifference"),
|
||||||
|
so.FindProperty("shoulderCorrection.maxHeightDifference"),
|
||||||
|
-0.5f, 2f, so));
|
||||||
|
|
||||||
|
container.Add(new PropertyField(so.FindProperty("shoulderCorrection.shoulderCorrectionCurve"), "보정 커브"));
|
||||||
|
|
||||||
|
container.Bind(so);
|
||||||
|
foldout.Add(container);
|
||||||
|
return foldout;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Foot Grounding ==========
|
||||||
|
|
||||||
|
private VisualElement BuildGroundingSection(CustomRetargetingScript script, SerializedObject so)
|
||||||
|
{
|
||||||
|
var foldout = new Foldout { text = "접지 설정", value = false };
|
||||||
|
var container = new VisualElement();
|
||||||
|
|
||||||
|
container.Add(new PropertyField(so.FindProperty("footGrounding.groundHeight"), "바닥 높이"));
|
||||||
|
|
||||||
|
var weightSlider = new Slider("접지 강도", 0f, 1f) { showInputField = true };
|
||||||
|
weightSlider.BindProperty(so.FindProperty("footGrounding.groundingWeight"));
|
||||||
|
container.Add(weightSlider);
|
||||||
|
|
||||||
|
container.Add(new PropertyField(so.FindProperty("footGrounding.activationHeight"), "활성화 높이") { tooltip = "발목이 이 높이 이상이면 보정 비활성화" });
|
||||||
|
container.Add(new PropertyField(so.FindProperty("footGrounding.plantThreshold"), "접지 판정 범위"));
|
||||||
|
|
||||||
|
var smoothField = new Slider("스무딩 속도", 1f, 30f) { showInputField = true };
|
||||||
|
smoothField.BindProperty(so.FindProperty("footGrounding.smoothSpeed"));
|
||||||
|
container.Add(smoothField);
|
||||||
|
|
||||||
|
container.Bind(so);
|
||||||
|
foldout.Add(container);
|
||||||
|
return foldout;
|
||||||
|
}
|
||||||
|
|
||||||
// ========== Helpers ==========
|
// ========== Helpers ==========
|
||||||
|
|
||||||
private VisualElement BuildMinMaxRange(string label, SerializedProperty minProp, SerializedProperty maxProp, float limitMin, float limitMax, SerializedObject so)
|
private VisualElement BuildMinMaxRange(string label, SerializedProperty minProp, SerializedProperty maxProp, float limitMin, float limitMax, SerializedObject so)
|
||||||
@ -848,7 +869,7 @@ public class RetargetingControlWindow : EditorWindow
|
|||||||
return container;
|
return container;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyFingerPreset(FingerShapedController controller, string presetName)
|
private void ApplyFingerPreset(CustomRetargetingScript script, FingerShapedController controller, string presetName)
|
||||||
{
|
{
|
||||||
if (!controller.enabled) controller.enabled = true;
|
if (!controller.enabled) controller.enabled = true;
|
||||||
|
|
||||||
@ -873,7 +894,7 @@ public class RetargetingControlWindow : EditorWindow
|
|||||||
controller.rightThumbCurl = t; controller.rightIndexCurl = i; controller.rightMiddleCurl = m;
|
controller.rightThumbCurl = t; controller.rightIndexCurl = i; controller.rightMiddleCurl = m;
|
||||||
controller.rightRingCurl = r; controller.rightPinkyCurl = p; controller.rightSpreadFingers = s;
|
controller.rightRingCurl = r; controller.rightPinkyCurl = p; controller.rightSpreadFingers = s;
|
||||||
}
|
}
|
||||||
EditorUtility.SetDirty(controller);
|
EditorUtility.SetDirty(script);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== Head Calibration ==========
|
// ========== Head Calibration ==========
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
|
||||||
[DefaultExecutionOrder(2)]
|
namespace KindRetargeting
|
||||||
public class FingerShapedController : MonoBehaviour
|
|
||||||
{
|
{
|
||||||
|
[System.Serializable]
|
||||||
|
public class FingerShapedController
|
||||||
|
{
|
||||||
private Animator animator;
|
private Animator animator;
|
||||||
private HumanPoseHandler humanPoseHandler;
|
private HumanPoseHandler humanPoseHandler;
|
||||||
|
|
||||||
@ -40,6 +42,8 @@ public class FingerShapedController : MonoBehaviour
|
|||||||
HumanBodyBones.Jaw
|
HumanBodyBones.Jaw
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public bool enabled = false;
|
||||||
|
|
||||||
[Header("왼손 제어 값")]
|
[Header("왼손 제어 값")]
|
||||||
[Range(-1, 1)] public float leftPinkyCurl; // 새끼손가락 구부리기
|
[Range(-1, 1)] public float leftPinkyCurl; // 새끼손가락 구부리기
|
||||||
[Range(-1, 1)] public float leftRingCurl; // 약지 구부리기
|
[Range(-1, 1)] public float leftRingCurl; // 약지 구부리기
|
||||||
@ -59,30 +63,30 @@ public class FingerShapedController : MonoBehaviour
|
|||||||
public bool leftHandEnabled = false; // 왼손 제어 활성화 상태
|
public bool leftHandEnabled = false; // 왼손 제어 활성화 상태
|
||||||
public bool rightHandEnabled = false; // 오른손 제어 활성화 상태
|
public bool rightHandEnabled = false; // 오른손 제어 활성화 상태
|
||||||
|
|
||||||
private void Reset()
|
private bool isInitialized;
|
||||||
{
|
|
||||||
// 컴포넌트가 처음 추가될 때 자동으로 비활성화
|
|
||||||
enabled = false;
|
|
||||||
leftHandEnabled = false;
|
|
||||||
rightHandEnabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Awake()
|
public void Initialize(Animator targetAnimator)
|
||||||
{
|
{
|
||||||
animator = GetComponent<Animator>();
|
animator = targetAnimator;
|
||||||
|
if (animator == null || !animator.isHuman) return;
|
||||||
|
|
||||||
humanPoseHandler = new HumanPoseHandler(animator.avatar, animator.transform);
|
humanPoseHandler = new HumanPoseHandler(animator.avatar, animator.transform);
|
||||||
|
isInitialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnDestroy()
|
public void Cleanup()
|
||||||
{
|
{
|
||||||
if (humanPoseHandler != null)
|
if (humanPoseHandler != null)
|
||||||
{
|
{
|
||||||
humanPoseHandler.Dispose();
|
humanPoseHandler.Dispose();
|
||||||
|
humanPoseHandler = null;
|
||||||
}
|
}
|
||||||
|
isInitialized = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Update()
|
public void OnUpdate()
|
||||||
{
|
{
|
||||||
|
if (!isInitialized || !enabled) return;
|
||||||
UpdateMuscleValues();
|
UpdateMuscleValues();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -165,4 +169,5 @@ public class FingerShapedController : MonoBehaviour
|
|||||||
if (baseOffset + 18 < muscleCount) humanPose.muscles[baseOffset + 18] = pinky; // Little 2
|
if (baseOffset + 18 < muscleCount) humanPose.muscles[baseOffset + 18] = pinky; // Little 2
|
||||||
if (baseOffset + 19 < muscleCount) humanPose.muscles[baseOffset + 19] = pinky; // Little 3
|
if (baseOffset + 19 < muscleCount) humanPose.muscles[baseOffset + 19] = pinky; // Little 3
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,19 +5,14 @@ namespace KindRetargeting
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// HIK 스타일 2-Pass 접지 시스템.
|
/// HIK 스타일 2-Pass 접지 시스템.
|
||||||
///
|
///
|
||||||
/// Pass 1 (Update, Order 5 → IK 전):
|
/// Pass 1 (OnUpdate, Order 5 → IK 전):
|
||||||
/// IK 타겟 위치를 수정하여 Toes가 바닥을 뚫지 않도록 보정.
|
/// IK 타겟 위치를 수정하여 Toes가 바닥을 뚫지 않도록 보정.
|
||||||
/// Toe Pivot 감지: 발끝이 바닥에 있고 발목이 올라가면
|
|
||||||
/// 발목 타겟을 역산하여 Toes가 groundHeight에 고정.
|
|
||||||
///
|
///
|
||||||
/// Pass 2 (LateUpdate → IK 후):
|
/// Pass 2 (OnLateUpdate → IK 후):
|
||||||
/// IK 결과의 잔차를 Foot 회전으로 미세 보정.
|
/// IK 결과의 잔차를 Foot 회전으로 미세 보정.
|
||||||
/// 위치 변경 없음 — 본 길이 보존.
|
|
||||||
///
|
|
||||||
/// 힙 높이 보정은 CRS의 floorHeight가 담당합니다 (이중 보정 방지).
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DefaultExecutionOrder(5)]
|
[System.Serializable]
|
||||||
public class FootGroundingController : MonoBehaviour
|
public class FootGroundingController
|
||||||
{
|
{
|
||||||
[Header("접지 설정")]
|
[Header("접지 설정")]
|
||||||
[Tooltip("바닥 Y 좌표 (월드 공간)")]
|
[Tooltip("바닥 Y 좌표 (월드 공간)")]
|
||||||
@ -41,37 +36,31 @@ namespace KindRetargeting
|
|||||||
private TwoBoneIKSolver ikSolver;
|
private TwoBoneIKSolver ikSolver;
|
||||||
private Animator animator;
|
private Animator animator;
|
||||||
|
|
||||||
// 타겟 아바타 캐싱
|
|
||||||
private Transform leftFoot;
|
private Transform leftFoot;
|
||||||
private Transform rightFoot;
|
private Transform rightFoot;
|
||||||
private Transform leftToes;
|
private Transform leftToes;
|
||||||
private Transform rightToes;
|
private Transform rightToes;
|
||||||
|
|
||||||
// Toes의 Foot 로컬 오프셋 (T-pose에서 캐싱)
|
|
||||||
private Vector3 leftLocalToesOffset;
|
private Vector3 leftLocalToesOffset;
|
||||||
private Vector3 rightLocalToesOffset;
|
private Vector3 rightLocalToesOffset;
|
||||||
|
|
||||||
// flat 상태에서 발목 최소 높이 (Foot.y - Toes.y)
|
|
||||||
private float leftFootHeight;
|
private float leftFootHeight;
|
||||||
private float rightFootHeight;
|
private float rightFootHeight;
|
||||||
|
|
||||||
// Toes 본 존재 여부
|
|
||||||
private bool leftHasToes;
|
private bool leftHasToes;
|
||||||
private bool rightHasToes;
|
private bool rightHasToes;
|
||||||
|
|
||||||
// 스무딩용: 이전 프레임 보정량
|
|
||||||
private float leftPrevAdj;
|
private float leftPrevAdj;
|
||||||
private float rightPrevAdj;
|
private float rightPrevAdj;
|
||||||
|
|
||||||
private bool isInitialized;
|
private bool isInitialized;
|
||||||
|
|
||||||
private void Start()
|
public void Initialize(TwoBoneIKSolver ikSolver, Animator animator)
|
||||||
{
|
{
|
||||||
ikSolver = GetComponent<TwoBoneIKSolver>();
|
this.ikSolver = ikSolver;
|
||||||
animator = GetComponent<Animator>();
|
this.animator = animator;
|
||||||
|
|
||||||
if (animator == null || !animator.isHuman || ikSolver == null) return;
|
if (animator == null || !animator.isHuman || ikSolver == null) return;
|
||||||
if (leftFoot == null && rightFoot == null) return;
|
|
||||||
|
|
||||||
leftFoot = animator.GetBoneTransform(HumanBodyBones.LeftFoot);
|
leftFoot = animator.GetBoneTransform(HumanBodyBones.LeftFoot);
|
||||||
rightFoot = animator.GetBoneTransform(HumanBodyBones.RightFoot);
|
rightFoot = animator.GetBoneTransform(HumanBodyBones.RightFoot);
|
||||||
@ -80,7 +69,6 @@ namespace KindRetargeting
|
|||||||
|
|
||||||
if (leftFoot == null || rightFoot == null) return;
|
if (leftFoot == null || rightFoot == null) return;
|
||||||
|
|
||||||
// Toes 존재 여부 + 캐싱
|
|
||||||
leftHasToes = leftToes != null;
|
leftHasToes = leftToes != null;
|
||||||
rightHasToes = rightToes != null;
|
rightHasToes = rightToes != null;
|
||||||
|
|
||||||
@ -91,7 +79,7 @@ namespace KindRetargeting
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
leftFootHeight = 0.05f; // Toes 없을 때 기본 발목 높이
|
leftFootHeight = 0.05f;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rightHasToes)
|
if (rightHasToes)
|
||||||
@ -110,7 +98,7 @@ namespace KindRetargeting
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Pass 1: Pre-IK — IK 타겟 위치를 수정합니다.
|
/// Pass 1: Pre-IK — IK 타겟 위치를 수정합니다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void Update()
|
public void OnUpdate()
|
||||||
{
|
{
|
||||||
if (!isInitialized || groundingWeight < 0.001f) return;
|
if (!isInitialized || groundingWeight < 0.001f) return;
|
||||||
|
|
||||||
@ -121,16 +109,11 @@ namespace KindRetargeting
|
|||||||
ikSolver.rightLeg, rightLocalToesOffset, rightFootHeight,
|
ikSolver.rightLeg, rightLocalToesOffset, rightFootHeight,
|
||||||
rightHasToes, ikSolver.rightLeg.positionWeight);
|
rightHasToes, ikSolver.rightLeg.positionWeight);
|
||||||
|
|
||||||
// 스무딩: 보정량 급변 방지
|
|
||||||
float dt = Time.deltaTime * smoothSpeed;
|
float dt = Time.deltaTime * smoothSpeed;
|
||||||
leftPrevAdj = Mathf.Lerp(leftPrevAdj, leftAdj, Mathf.Clamp01(dt));
|
leftPrevAdj = Mathf.Lerp(leftPrevAdj, leftAdj, Mathf.Clamp01(dt));
|
||||||
rightPrevAdj = Mathf.Lerp(rightPrevAdj, rightAdj, Mathf.Clamp01(dt));
|
rightPrevAdj = Mathf.Lerp(rightPrevAdj, rightAdj, Mathf.Clamp01(dt));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 발 IK 타겟을 접지 모드에 따라 보정합니다.
|
|
||||||
/// Toes가 없는 아바타는 발목 Y 클램프만 수행합니다.
|
|
||||||
/// </summary>
|
|
||||||
private float AdjustFootTarget(TwoBoneIKSolver.LimbIK limb, Vector3 localToesOffset,
|
private float AdjustFootTarget(TwoBoneIKSolver.LimbIK limb, Vector3 localToesOffset,
|
||||||
float footHeight, bool hasToes, float ikWeight)
|
float footHeight, bool hasToes, float ikWeight)
|
||||||
{
|
{
|
||||||
@ -140,12 +123,10 @@ namespace KindRetargeting
|
|||||||
Vector3 anklePos = ankleTarget.position;
|
Vector3 anklePos = ankleTarget.position;
|
||||||
float ankleY = anklePos.y;
|
float ankleY = anklePos.y;
|
||||||
|
|
||||||
// AIRBORNE 체크
|
|
||||||
if (ankleY - groundHeight > activationHeight) return 0f;
|
if (ankleY - groundHeight > activationHeight) return 0f;
|
||||||
|
|
||||||
float weight = groundingWeight * ikWeight;
|
float weight = groundingWeight * ikWeight;
|
||||||
|
|
||||||
// === Toes 없는 아바타: 단순 Y 클램프 ===
|
|
||||||
if (!hasToes)
|
if (!hasToes)
|
||||||
{
|
{
|
||||||
float minAnkleY = groundHeight + footHeight;
|
float minAnkleY = groundHeight + footHeight;
|
||||||
@ -159,7 +140,6 @@ namespace KindRetargeting
|
|||||||
return 0f;
|
return 0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Toes 있는 아바타: 예측 기반 보정 ===
|
|
||||||
Vector3 predictedToesWorld = anklePos + ankleTarget.rotation * localToesOffset;
|
Vector3 predictedToesWorld = anklePos + ankleTarget.rotation * localToesOffset;
|
||||||
float predictedToesY = predictedToesWorld.y;
|
float predictedToesY = predictedToesWorld.y;
|
||||||
|
|
||||||
@ -171,7 +151,6 @@ namespace KindRetargeting
|
|||||||
|
|
||||||
if (ankleY < groundHeight + footHeight + plantThreshold)
|
if (ankleY < groundHeight + footHeight + plantThreshold)
|
||||||
{
|
{
|
||||||
// PLANTED: 발 전체가 바닥 근처
|
|
||||||
float minAnkleY = groundHeight + footHeight;
|
float minAnkleY = groundHeight + footHeight;
|
||||||
if (ankleY < minAnkleY)
|
if (ankleY < minAnkleY)
|
||||||
{
|
{
|
||||||
@ -188,7 +167,6 @@ namespace KindRetargeting
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// TOE_PIVOT: 발끝 고정, 발목 올라감
|
|
||||||
if (toesError > 0f)
|
if (toesError > 0f)
|
||||||
{
|
{
|
||||||
adjustment = toesError * weight;
|
adjustment = toesError * weight;
|
||||||
@ -200,7 +178,6 @@ namespace KindRetargeting
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Toes 충분히 위 → 발목만 바닥 아래 방지
|
|
||||||
float minAnkleY = groundHeight + footHeight;
|
float minAnkleY = groundHeight + footHeight;
|
||||||
if (ankleY < minAnkleY)
|
if (ankleY < minAnkleY)
|
||||||
{
|
{
|
||||||
@ -216,7 +193,7 @@ namespace KindRetargeting
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Pass 2: Post-IK — 잔차를 Foot 회전으로 미세 보정합니다.
|
/// Pass 2: Post-IK — 잔차를 Foot 회전으로 미세 보정합니다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void LateUpdate()
|
public void OnLateUpdate()
|
||||||
{
|
{
|
||||||
if (!isInitialized || groundingWeight < 0.001f) return;
|
if (!isInitialized || groundingWeight < 0.001f) return;
|
||||||
|
|
||||||
@ -226,10 +203,6 @@ namespace KindRetargeting
|
|||||||
AlignFootToGround(rightFoot, rightToes, ikSolver.rightLeg.positionWeight);
|
AlignFootToGround(rightFoot, rightToes, ikSolver.rightLeg.positionWeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// IK 후 실제 Toes 위치를 확인하고, Foot 본을 pitch 회전하여 잔차 보정.
|
|
||||||
/// 바닥 아래로 뚫린 경우만 보정합니다.
|
|
||||||
/// </summary>
|
|
||||||
private void AlignFootToGround(Transform foot, Transform toes, float ikWeight)
|
private void AlignFootToGround(Transform foot, Transform toes, float ikWeight)
|
||||||
{
|
{
|
||||||
if (foot == null || toes == null) return;
|
if (foot == null || toes == null) return;
|
||||||
@ -244,7 +217,6 @@ namespace KindRetargeting
|
|||||||
|
|
||||||
if (Mathf.Abs(error) < 0.001f) return;
|
if (Mathf.Abs(error) < 0.001f) return;
|
||||||
|
|
||||||
// 바닥 아래로 뚫린 경우만 보정
|
|
||||||
if (error > plantThreshold) return;
|
if (error > plantThreshold) return;
|
||||||
|
|
||||||
Vector3 footToToes = toes.position - foot.position;
|
Vector3 footToToes = toes.position - foot.position;
|
||||||
|
|||||||
@ -4,8 +4,8 @@ using UnityEngine;
|
|||||||
|
|
||||||
namespace KindRetargeting
|
namespace KindRetargeting
|
||||||
{
|
{
|
||||||
[DefaultExecutionOrder(4)]
|
[System.Serializable]
|
||||||
public class LimbWeightController : MonoBehaviour
|
public class LimbWeightController
|
||||||
{
|
{
|
||||||
[Header("거리 기반 가중치 설정")]
|
[Header("거리 기반 가중치 설정")]
|
||||||
[SerializeField, Range(0.3f, 1f)] public float maxDistance = 0.5f; // 가중치가 0이 되는 최대 거리
|
[SerializeField, Range(0.3f, 1f)] public float maxDistance = 0.5f; // 가중치가 0이 되는 최대 거리
|
||||||
@ -36,9 +36,8 @@ namespace KindRetargeting
|
|||||||
public float chairSeatHeightOffset = 0.05f;
|
public float chairSeatHeightOffset = 0.05f;
|
||||||
|
|
||||||
private TwoBoneIKSolver ikSolver;
|
private TwoBoneIKSolver ikSolver;
|
||||||
|
|
||||||
private CustomRetargetingScript crs;
|
private CustomRetargetingScript crs;
|
||||||
private Dictionary<string, Dictionary<int, float>> weightLayers = new Dictionary<string, Dictionary<int, float>>();
|
private Transform characterRoot;
|
||||||
|
|
||||||
List<float> leftArmEndWeights = new List<float>();
|
List<float> leftArmEndWeights = new List<float>();
|
||||||
List<float> rightArmEndWeights = new List<float>();
|
List<float> rightArmEndWeights = new List<float>();
|
||||||
@ -57,8 +56,6 @@ namespace KindRetargeting
|
|||||||
|
|
||||||
public List<Transform> props = new List<Transform>();
|
public List<Transform> props = new List<Transform>();
|
||||||
|
|
||||||
public Transform characterRoot;
|
|
||||||
|
|
||||||
// 힙스 가중치 리스트 추가
|
// 힙스 가중치 리스트 추가
|
||||||
List<float> hipsWeights = new List<float>();
|
List<float> hipsWeights = new List<float>();
|
||||||
private float MasterHipsWeight = 1f;
|
private float MasterHipsWeight = 1f;
|
||||||
@ -67,8 +64,63 @@ namespace KindRetargeting
|
|||||||
private float currentChairSeatOffset = 0f;
|
private float currentChairSeatOffset = 0f;
|
||||||
private float targetChairSeatOffset = 0f;
|
private float targetChairSeatOffset = 0f;
|
||||||
|
|
||||||
void Update()
|
private bool isInitialized;
|
||||||
|
|
||||||
|
public void Initialize(TwoBoneIKSolver ikSolver, CustomRetargetingScript crs, Transform characterRoot)
|
||||||
{
|
{
|
||||||
|
this.ikSolver = ikSolver;
|
||||||
|
this.crs = crs;
|
||||||
|
this.characterRoot = characterRoot;
|
||||||
|
|
||||||
|
if (ikSolver == null || crs == null) return;
|
||||||
|
|
||||||
|
InitWeightLayers();
|
||||||
|
|
||||||
|
//프랍 오브젝트 찾기
|
||||||
|
props = Object.FindObjectsByType<PropTypeController>(FindObjectsSortMode.None).Select(controller => controller.transform).ToList();
|
||||||
|
|
||||||
|
// 다른 캐릭터의 손을 props에 추가
|
||||||
|
GetHand();
|
||||||
|
|
||||||
|
//HandDistances()에서 사용을 위한 리스트 추가
|
||||||
|
//손 거리에 따른 웨이트 업데이트 인덱스 0번
|
||||||
|
leftArmEndWeights.Add(0);
|
||||||
|
rightArmEndWeights.Add(0);
|
||||||
|
|
||||||
|
// 프랍과의 거리에 따른 웨이트 업데이트 인덱스 1번
|
||||||
|
leftArmEndWeights.Add(0);
|
||||||
|
rightArmEndWeights.Add(0);
|
||||||
|
|
||||||
|
// 앉아있을 때 다리와의 거리에 따른 가중치 적용 인덱스 0번
|
||||||
|
leftLegEndWeights.Add(0);
|
||||||
|
rightLegEndWeights.Add(0);
|
||||||
|
|
||||||
|
// 다리 골 가중치 초기화
|
||||||
|
leftLegBendWeights.Add(1f); // 기본 가중치
|
||||||
|
rightLegBendWeights.Add(1f); // 기본 가중치
|
||||||
|
|
||||||
|
if (this.characterRoot == null)
|
||||||
|
{
|
||||||
|
this.characterRoot = crs.transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 힙스 가중치 초기화 인덱스 0번
|
||||||
|
hipsWeights.Add(1f); // 의자 거리 기반 가중치
|
||||||
|
|
||||||
|
// 지면 높이 기반 가중치 초기화 인덱스 1번
|
||||||
|
hipsWeights.Add(1f); // 지면 높이 기반 가중치
|
||||||
|
|
||||||
|
// 발 높이 기반 가중치 초기화 인덱스 1번
|
||||||
|
leftLegEndWeights.Add(1f);
|
||||||
|
rightLegEndWeights.Add(1f);
|
||||||
|
|
||||||
|
isInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnUpdate()
|
||||||
|
{
|
||||||
|
if (!isInitialized) return;
|
||||||
|
|
||||||
//손의 거리를 기반으로한 가중치 적용
|
//손의 거리를 기반으로한 가중치 적용
|
||||||
HandDistances();
|
HandDistances();
|
||||||
|
|
||||||
@ -94,69 +146,15 @@ namespace KindRetargeting
|
|||||||
ApplyWeightsToFBIK();
|
ApplyWeightsToFBIK();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Start()
|
|
||||||
{
|
|
||||||
ikSolver = GetComponent<TwoBoneIKSolver>();
|
|
||||||
|
|
||||||
crs = GetComponent<CustomRetargetingScript>();
|
|
||||||
|
|
||||||
InitWeightLayers();
|
|
||||||
|
|
||||||
//프랍 오브젝트 찾기
|
|
||||||
props = FindObjectsByType<PropTypeController>(FindObjectsSortMode.None).Select(controller => controller.transform).ToList();
|
|
||||||
|
|
||||||
// 프랍 오브젝트 찾기
|
|
||||||
GetHand();
|
|
||||||
|
|
||||||
//HandDistances();에서 사용을 위한 리스트 추가
|
|
||||||
//손 거리에 따른 웨이트 업데이트 인덱스 0번
|
|
||||||
leftArmEndWeights.Add(0);
|
|
||||||
rightArmEndWeights.Add(0);
|
|
||||||
|
|
||||||
// 프랍과의 거리에 따른 웨이트 업데이트 인덱스 1번
|
|
||||||
leftArmEndWeights.Add(0);
|
|
||||||
rightArmEndWeights.Add(0);
|
|
||||||
|
|
||||||
// 앉아있을 때 다리와의 거리에 따른 가중치 적용 인덱스 0번
|
|
||||||
leftLegEndWeights.Add(0);
|
|
||||||
rightLegEndWeights.Add(0);
|
|
||||||
|
|
||||||
// 다리 골 가중치 초기화
|
|
||||||
leftLegBendWeights.Add(1f); // 기본 가중치
|
|
||||||
rightLegBendWeights.Add(1f); // 기본 가중치
|
|
||||||
|
|
||||||
// CharacterController가 있는 루트 오브젝트 찾기
|
|
||||||
|
|
||||||
|
|
||||||
if (characterRoot == null)
|
|
||||||
{
|
|
||||||
characterRoot = transform;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 힙스 가중치 초기화 인덱스 0번
|
|
||||||
hipsWeights.Add(1f); // 의자 거리 기반 가중치
|
|
||||||
|
|
||||||
// 지면 높이 기반 가중치 초기화 인덱스 1번
|
|
||||||
hipsWeights.Add(1f); // 지면 높이 기반 가중치
|
|
||||||
|
|
||||||
// 발 높이 기반 가중치 초기화 인덱스 1번
|
|
||||||
leftLegEndWeights.Add(1f);
|
|
||||||
rightLegEndWeights.Add(1f);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void GetHand()
|
private void GetHand()
|
||||||
{
|
{
|
||||||
// 모든 LimbWeightController 찾기
|
// 모든 CustomRetargetingScript 찾기 (다른 캐릭터의 손을 props에 추가)
|
||||||
LimbWeightController[] allControllers = FindObjectsOfType<LimbWeightController>();
|
CustomRetargetingScript[] allCrs = Object.FindObjectsByType<CustomRetargetingScript>(FindObjectsSortMode.None);
|
||||||
|
|
||||||
foreach (LimbWeightController controller in allControllers)
|
foreach (CustomRetargetingScript otherCrs in allCrs)
|
||||||
{
|
{
|
||||||
// 자기 자신은 제외
|
// 자기 자신은 제외
|
||||||
if (controller == this) continue;
|
if (otherCrs == crs) continue;
|
||||||
|
|
||||||
// CustomRetargetingScript 가져오기
|
|
||||||
CustomRetargetingScript otherCrs = controller.GetComponent<CustomRetargetingScript>();
|
|
||||||
if (otherCrs == null) continue;
|
|
||||||
|
|
||||||
// 왼손과 오른손 Transform 가져오기
|
// 왼손과 오른손 Transform 가져오기
|
||||||
Transform leftHand = otherCrs?.sourceAnimator?.GetBoneTransform(HumanBodyBones.LeftHand);
|
Transform leftHand = otherCrs?.sourceAnimator?.GetBoneTransform(HumanBodyBones.LeftHand);
|
||||||
|
|||||||
@ -4,7 +4,8 @@ using UniHumanoid;
|
|||||||
|
|
||||||
namespace KindRetargeting
|
namespace KindRetargeting
|
||||||
{
|
{
|
||||||
public class PropLocationController : MonoBehaviour
|
[System.Serializable]
|
||||||
|
public class PropLocationController
|
||||||
{
|
{
|
||||||
// 캐시된 타겟과 오프셋 Transform
|
// 캐시된 타겟과 오프셋 Transform
|
||||||
[System.Serializable]
|
[System.Serializable]
|
||||||
@ -18,9 +19,9 @@ namespace KindRetargeting
|
|||||||
[SerializeField] private TargetOffset rightHandTargetOffset;
|
[SerializeField] private TargetOffset rightHandTargetOffset;
|
||||||
[SerializeField] private TargetOffset headTargetOffset;
|
[SerializeField] private TargetOffset headTargetOffset;
|
||||||
|
|
||||||
private void Start()
|
public void Initialize(Animator animator)
|
||||||
{
|
{
|
||||||
CreateTargets();
|
CreateTargets(animator);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetTPose(Animator animator)
|
public void SetTPose(Animator animator)
|
||||||
@ -31,7 +32,6 @@ namespace KindRetargeting
|
|||||||
Avatar avatar = animator.avatar;
|
Avatar avatar = animator.avatar;
|
||||||
Transform transform = animator.transform;
|
Transform transform = animator.transform;
|
||||||
|
|
||||||
// HumanPoseClip에 저장된 T-포즈 데이터를 로드하여 적용
|
|
||||||
var humanPoseClip = Resources.Load<HumanPoseClip>(HumanPoseClip.TPoseResourcePath);
|
var humanPoseClip = Resources.Load<HumanPoseClip>(HumanPoseClip.TPoseResourcePath);
|
||||||
if (humanPoseClip != null)
|
if (humanPoseClip != null)
|
||||||
{
|
{
|
||||||
@ -43,9 +43,9 @@ namespace KindRetargeting
|
|||||||
Debug.LogWarning("T-Pose 데이터가 존재하지 않습니다.");
|
Debug.LogWarning("T-Pose 데이터가 존재하지 않습니다.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private void CreateTargets()
|
|
||||||
|
private void CreateTargets(Animator animator)
|
||||||
{
|
{
|
||||||
Animator animator = GetComponent<Animator>();
|
|
||||||
SetTPose(animator);
|
SetTPose(animator);
|
||||||
|
|
||||||
// 왼손 타겟 및 오프셋 설정
|
// 왼손 타겟 및 오프셋 설정
|
||||||
@ -54,7 +54,7 @@ namespace KindRetargeting
|
|||||||
{
|
{
|
||||||
leftHandTargetOffset = new TargetOffset();
|
leftHandTargetOffset = new TargetOffset();
|
||||||
GameObject leftTarget = new GameObject("Left_Hand_Target");
|
GameObject leftTarget = new GameObject("Left_Hand_Target");
|
||||||
leftTarget.transform.parent = leftHandBone; // 왼손 본에 직접 부모 설정
|
leftTarget.transform.parent = leftHandBone;
|
||||||
leftHandTargetOffset.target = leftTarget.transform;
|
leftHandTargetOffset.target = leftTarget.transform;
|
||||||
leftTarget.transform.position = leftHandBone.position + new Vector3(-0.039f, -0.022f, 0f);
|
leftTarget.transform.position = leftHandBone.position + new Vector3(-0.039f, -0.022f, 0f);
|
||||||
leftTarget.transform.rotation = Quaternion.Euler(90f, 0f, 0f);
|
leftTarget.transform.rotation = Quaternion.Euler(90f, 0f, 0f);
|
||||||
@ -63,7 +63,6 @@ namespace KindRetargeting
|
|||||||
leftOffset.transform.parent = leftTarget.transform;
|
leftOffset.transform.parent = leftTarget.transform;
|
||||||
leftHandTargetOffset.offset = leftOffset.transform;
|
leftHandTargetOffset.offset = leftOffset.transform;
|
||||||
|
|
||||||
// 로컬 포지션과 로테이션 설정
|
|
||||||
leftHandTargetOffset.offset.localPosition = Vector3.zero;
|
leftHandTargetOffset.offset.localPosition = Vector3.zero;
|
||||||
leftHandTargetOffset.offset.localRotation = Quaternion.identity;
|
leftHandTargetOffset.offset.localRotation = Quaternion.identity;
|
||||||
}
|
}
|
||||||
@ -74,7 +73,7 @@ namespace KindRetargeting
|
|||||||
{
|
{
|
||||||
rightHandTargetOffset = new TargetOffset();
|
rightHandTargetOffset = new TargetOffset();
|
||||||
GameObject rightTarget = new GameObject("Right_Hand_Target");
|
GameObject rightTarget = new GameObject("Right_Hand_Target");
|
||||||
rightTarget.transform.parent = rightHandBone; // 오른손 본에 직접 부모 설정
|
rightTarget.transform.parent = rightHandBone;
|
||||||
rightHandTargetOffset.target = rightTarget.transform;
|
rightHandTargetOffset.target = rightTarget.transform;
|
||||||
rightTarget.transform.position = rightHandBone.position + new Vector3(0.039f, -0.022f, 0f);
|
rightTarget.transform.position = rightHandBone.position + new Vector3(0.039f, -0.022f, 0f);
|
||||||
rightTarget.transform.rotation = Quaternion.Euler(90f, 0f, 0f);
|
rightTarget.transform.rotation = Quaternion.Euler(90f, 0f, 0f);
|
||||||
@ -83,7 +82,6 @@ namespace KindRetargeting
|
|||||||
rightOffset.transform.parent = rightTarget.transform;
|
rightOffset.transform.parent = rightTarget.transform;
|
||||||
rightHandTargetOffset.offset = rightOffset.transform;
|
rightHandTargetOffset.offset = rightOffset.transform;
|
||||||
|
|
||||||
// 로컬 포지션과 로테이션 설정
|
|
||||||
rightHandTargetOffset.offset.localPosition = Vector3.zero;
|
rightHandTargetOffset.offset.localPosition = Vector3.zero;
|
||||||
rightHandTargetOffset.offset.localRotation = Quaternion.identity;
|
rightHandTargetOffset.offset.localRotation = Quaternion.identity;
|
||||||
}
|
}
|
||||||
@ -94,7 +92,7 @@ namespace KindRetargeting
|
|||||||
{
|
{
|
||||||
headTargetOffset = new TargetOffset();
|
headTargetOffset = new TargetOffset();
|
||||||
GameObject headTarget = new GameObject("Head_Target");
|
GameObject headTarget = new GameObject("Head_Target");
|
||||||
headTarget.transform.parent = headBone; // 머리 본에 직접 부모 설정
|
headTarget.transform.parent = headBone;
|
||||||
headTargetOffset.target = headTarget.transform;
|
headTargetOffset.target = headTarget.transform;
|
||||||
headTarget.transform.position = headBone.position + new Vector3(0f, 0.16f, 0f);
|
headTarget.transform.position = headBone.position + new Vector3(0f, 0.16f, 0f);
|
||||||
headTarget.transform.rotation = Quaternion.Euler(0f, 0f, 0f);
|
headTarget.transform.rotation = Quaternion.Euler(0f, 0f, 0f);
|
||||||
@ -103,7 +101,6 @@ namespace KindRetargeting
|
|||||||
headOffset.transform.parent = headTarget.transform;
|
headOffset.transform.parent = headTarget.transform;
|
||||||
headTargetOffset.offset = headOffset.transform;
|
headTargetOffset.offset = headOffset.transform;
|
||||||
|
|
||||||
// 기본 오프셋 설정
|
|
||||||
headTargetOffset.offset.localPosition = Vector3.zero;
|
headTargetOffset.offset.localPosition = Vector3.zero;
|
||||||
headTargetOffset.offset.localRotation = Quaternion.identity;
|
headTargetOffset.offset.localRotation = Quaternion.identity;
|
||||||
}
|
}
|
||||||
@ -172,7 +169,6 @@ namespace KindRetargeting
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 에디터에서 사용할 메서드들
|
|
||||||
#if UNITY_EDITOR
|
#if UNITY_EDITOR
|
||||||
public void MoveToHead()
|
public void MoveToHead()
|
||||||
{
|
{
|
||||||
@ -195,7 +191,6 @@ namespace KindRetargeting
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// 오프셋 getter 메서드들 추가
|
|
||||||
public Transform GetLeftHandOffset()
|
public Transform GetLeftHandOffset()
|
||||||
{
|
{
|
||||||
return leftHandTargetOffset?.offset;
|
return leftHandTargetOffset?.offset;
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
@ -131,6 +132,7 @@ namespace KindRetargeting.Remote
|
|||||||
string property = json["property"]?.ToString();
|
string property = json["property"]?.ToString();
|
||||||
float value = json["value"]?.Value<float>() ?? 0f;
|
float value = json["value"]?.Value<float>() ?? 0f;
|
||||||
UpdateValue(charId, property, value);
|
UpdateValue(charId, property, value);
|
||||||
|
BroadcastValueChanged(charId, property, value);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@ -164,6 +166,20 @@ namespace KindRetargeting.Remote
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "autoHipsOffset":
|
||||||
|
{
|
||||||
|
string charId = json["characterId"]?.ToString();
|
||||||
|
AutoHipsOffset(charId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "autoCalibrateAll":
|
||||||
|
{
|
||||||
|
string charId = json["characterId"]?.ToString();
|
||||||
|
AutoCalibrateAll(charId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
Debug.LogWarning($"[RetargetingRemote] 알 수 없는 액션: {action}");
|
Debug.LogWarning($"[RetargetingRemote] 알 수 없는 액션: {action}");
|
||||||
break;
|
break;
|
||||||
@ -215,9 +231,6 @@ namespace KindRetargeting.Remote
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var limbWeight = script.GetComponent<LimbWeightController>();
|
|
||||||
var handPose = script.GetComponent<FingerShapedController>();
|
|
||||||
|
|
||||||
var data = new Dictionary<string, object>
|
var data = new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
// 힙 위치 보정 (로컬)
|
// 힙 위치 보정 (로컬)
|
||||||
@ -251,37 +264,57 @@ namespace KindRetargeting.Remote
|
|||||||
{ "fingerCopyMode", (int)GetPrivateField<EnumsList.FingerCopyMode>(script, "fingerCopyMode") },
|
{ "fingerCopyMode", (int)GetPrivateField<EnumsList.FingerCopyMode>(script, "fingerCopyMode") },
|
||||||
|
|
||||||
// 캘리브레이션 상태
|
// 캘리브레이션 상태
|
||||||
{ "hasCalibrationData", script.HasCachedSettings() }
|
{ "hasCalibrationData", script.HasCachedSettings() },
|
||||||
};
|
|
||||||
|
|
||||||
// LimbWeightController 데이터
|
// LimbWeightController 데이터
|
||||||
if (limbWeight != null)
|
{ "limbMinDistance", script.limbWeight.minDistance },
|
||||||
{
|
{ "limbMaxDistance", script.limbWeight.maxDistance },
|
||||||
data["limbMinDistance"] = limbWeight.minDistance;
|
{ "weightSmoothSpeed", script.limbWeight.weightSmoothSpeed },
|
||||||
data["limbMaxDistance"] = limbWeight.maxDistance;
|
{ "hipsMinDistance", script.limbWeight.hipsMinDistance },
|
||||||
data["weightSmoothSpeed"] = limbWeight.weightSmoothSpeed;
|
{ "hipsMaxDistance", script.limbWeight.hipsMaxDistance },
|
||||||
data["hipsMinDistance"] = limbWeight.hipsMinDistance;
|
{ "groundHipsMinHeight", script.limbWeight.groundHipsMinHeight },
|
||||||
data["hipsMaxDistance"] = limbWeight.hipsMaxDistance;
|
{ "groundHipsMaxHeight", script.limbWeight.groundHipsMaxHeight },
|
||||||
data["groundHipsMinHeight"] = limbWeight.groundHipsMinHeight;
|
{ "footHeightMinThreshold", script.limbWeight.footHeightMinThreshold },
|
||||||
data["groundHipsMaxHeight"] = limbWeight.groundHipsMaxHeight;
|
{ "footHeightMaxThreshold", script.limbWeight.footHeightMaxThreshold },
|
||||||
data["footHeightMinThreshold"] = limbWeight.footHeightMinThreshold;
|
{ "chairSeatHeightOffset", script.limbWeight.chairSeatHeightOffset },
|
||||||
data["footHeightMaxThreshold"] = limbWeight.footHeightMaxThreshold;
|
{ "enableLeftArmIK", script.limbWeight.enableLeftArmIK },
|
||||||
data["chairSeatHeightOffset"] = limbWeight.chairSeatHeightOffset;
|
{ "enableRightArmIK", script.limbWeight.enableRightArmIK },
|
||||||
}
|
|
||||||
|
// ShoulderCorrection 데이터
|
||||||
|
{ "shoulderBlendStrength", script.shoulderCorrection.blendStrength },
|
||||||
|
{ "shoulderMaxBlend", script.shoulderCorrection.maxShoulderBlend },
|
||||||
|
{ "shoulderMaxHeightDiff", script.shoulderCorrection.maxHeightDifference },
|
||||||
|
{ "shoulderMinHeightDiff", script.shoulderCorrection.minHeightDifference },
|
||||||
|
{ "shoulderReverseLeft", script.shoulderCorrection.reverseLeftRotation },
|
||||||
|
{ "shoulderReverseRight", script.shoulderCorrection.reverseRightRotation },
|
||||||
|
|
||||||
|
// FootGrounding 데이터
|
||||||
|
{ "groundingWeight", script.footGrounding.groundingWeight },
|
||||||
|
{ "groundingGroundHeight", script.footGrounding.groundHeight },
|
||||||
|
{ "groundingActivationHeight", script.footGrounding.activationHeight },
|
||||||
|
{ "groundingPlantThreshold", script.footGrounding.plantThreshold },
|
||||||
|
{ "groundingSmoothSpeed", script.footGrounding.smoothSpeed },
|
||||||
|
|
||||||
// FingerShapedController 데이터
|
// FingerShapedController 데이터
|
||||||
if (handPose != null)
|
{ "handPoseEnabled", script.fingerShaped.enabled },
|
||||||
{
|
{ "leftHandEnabled", script.fingerShaped.leftHandEnabled },
|
||||||
data["handPoseEnabled"] = handPose.enabled;
|
{ "rightHandEnabled", script.fingerShaped.rightHandEnabled },
|
||||||
data["leftHandEnabled"] = handPose.leftHandEnabled;
|
{ "leftThumbCurl", script.fingerShaped.leftThumbCurl },
|
||||||
data["rightHandEnabled"] = handPose.rightHandEnabled;
|
{ "leftIndexCurl", script.fingerShaped.leftIndexCurl },
|
||||||
}
|
{ "leftMiddleCurl", script.fingerShaped.leftMiddleCurl },
|
||||||
else
|
{ "leftRingCurl", script.fingerShaped.leftRingCurl },
|
||||||
{
|
{ "leftPinkyCurl", script.fingerShaped.leftPinkyCurl },
|
||||||
data["handPoseEnabled"] = false;
|
{ "leftSpreadFingers", script.fingerShaped.leftSpreadFingers },
|
||||||
data["leftHandEnabled"] = false;
|
{ "rightThumbCurl", script.fingerShaped.rightThumbCurl },
|
||||||
data["rightHandEnabled"] = false;
|
{ "rightIndexCurl", script.fingerShaped.rightIndexCurl },
|
||||||
}
|
{ "rightMiddleCurl", script.fingerShaped.rightMiddleCurl },
|
||||||
|
{ "rightRingCurl", script.fingerShaped.rightRingCurl },
|
||||||
|
{ "rightPinkyCurl", script.fingerShaped.rightPinkyCurl },
|
||||||
|
{ "rightSpreadFingers", script.fingerShaped.rightSpreadFingers },
|
||||||
|
|
||||||
|
// 최소 발목 높이
|
||||||
|
{ "minimumAnkleHeight", GetPrivateField<float>(script, "minimumAnkleHeight") },
|
||||||
|
};
|
||||||
|
|
||||||
var response = new
|
var response = new
|
||||||
{
|
{
|
||||||
@ -298,9 +331,6 @@ namespace KindRetargeting.Remote
|
|||||||
var script = FindCharacter(characterId);
|
var script = FindCharacter(characterId);
|
||||||
if (script == null) return;
|
if (script == null) return;
|
||||||
|
|
||||||
var limbWeight = script.GetComponent<LimbWeightController>();
|
|
||||||
var handPose = script.GetComponent<FingerShapedController>();
|
|
||||||
|
|
||||||
switch (property)
|
switch (property)
|
||||||
{
|
{
|
||||||
// 힙 위치 보정
|
// 힙 위치 보정
|
||||||
@ -363,56 +393,135 @@ namespace KindRetargeting.Remote
|
|||||||
|
|
||||||
// LimbWeightController 속성
|
// LimbWeightController 속성
|
||||||
case "limbMinDistance":
|
case "limbMinDistance":
|
||||||
if (limbWeight != null) limbWeight.minDistance = value;
|
script.limbWeight.minDistance = value;
|
||||||
break;
|
break;
|
||||||
case "limbMaxDistance":
|
case "limbMaxDistance":
|
||||||
if (limbWeight != null) limbWeight.maxDistance = value;
|
script.limbWeight.maxDistance = value;
|
||||||
break;
|
break;
|
||||||
case "weightSmoothSpeed":
|
case "weightSmoothSpeed":
|
||||||
if (limbWeight != null) limbWeight.weightSmoothSpeed = value;
|
script.limbWeight.weightSmoothSpeed = value;
|
||||||
break;
|
break;
|
||||||
case "hipsMinDistance":
|
case "hipsMinDistance":
|
||||||
if (limbWeight != null) limbWeight.hipsMinDistance = value;
|
script.limbWeight.hipsMinDistance = value;
|
||||||
break;
|
break;
|
||||||
case "hipsMaxDistance":
|
case "hipsMaxDistance":
|
||||||
if (limbWeight != null) limbWeight.hipsMaxDistance = value;
|
script.limbWeight.hipsMaxDistance = value;
|
||||||
break;
|
break;
|
||||||
case "groundHipsMinHeight":
|
case "groundHipsMinHeight":
|
||||||
if (limbWeight != null) limbWeight.groundHipsMinHeight = value;
|
script.limbWeight.groundHipsMinHeight = value;
|
||||||
break;
|
break;
|
||||||
case "groundHipsMaxHeight":
|
case "groundHipsMaxHeight":
|
||||||
if (limbWeight != null) limbWeight.groundHipsMaxHeight = value;
|
script.limbWeight.groundHipsMaxHeight = value;
|
||||||
break;
|
break;
|
||||||
case "footHeightMinThreshold":
|
case "footHeightMinThreshold":
|
||||||
if (limbWeight != null) limbWeight.footHeightMinThreshold = value;
|
script.limbWeight.footHeightMinThreshold = value;
|
||||||
break;
|
break;
|
||||||
case "footHeightMaxThreshold":
|
case "footHeightMaxThreshold":
|
||||||
if (limbWeight != null) limbWeight.footHeightMaxThreshold = value;
|
script.limbWeight.footHeightMaxThreshold = value;
|
||||||
break;
|
break;
|
||||||
case "chairSeatHeightOffset":
|
case "chairSeatHeightOffset":
|
||||||
if (limbWeight != null) limbWeight.chairSeatHeightOffset = value;
|
script.limbWeight.chairSeatHeightOffset = value;
|
||||||
|
break;
|
||||||
|
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;
|
||||||
|
case "shoulderReverseLeft":
|
||||||
|
script.shoulderCorrection.reverseLeftRotation = value > 0.5f;
|
||||||
|
break;
|
||||||
|
case "shoulderReverseRight":
|
||||||
|
script.shoulderCorrection.reverseRightRotation = value > 0.5f;
|
||||||
|
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;
|
break;
|
||||||
|
|
||||||
// FingerShapedController 속성
|
// FingerShapedController 속성
|
||||||
case "handPoseEnabled":
|
case "handPoseEnabled":
|
||||||
if (handPose != null)
|
script.fingerShaped.enabled = value > 0.5f;
|
||||||
handPose.enabled = value > 0.5f;
|
|
||||||
break;
|
break;
|
||||||
case "leftHandEnabled":
|
case "leftHandEnabled":
|
||||||
if (handPose != null)
|
script.fingerShaped.leftHandEnabled = value > 0.5f;
|
||||||
{
|
if (script.fingerShaped.leftHandEnabled)
|
||||||
handPose.leftHandEnabled = value > 0.5f;
|
script.fingerShaped.enabled = true;
|
||||||
if (handPose.leftHandEnabled)
|
|
||||||
handPose.enabled = true;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case "rightHandEnabled":
|
case "rightHandEnabled":
|
||||||
if (handPose != null)
|
script.fingerShaped.rightHandEnabled = value > 0.5f;
|
||||||
{
|
if (script.fingerShaped.rightHandEnabled)
|
||||||
handPose.rightHandEnabled = value > 0.5f;
|
script.fingerShaped.enabled = true;
|
||||||
if (handPose.rightHandEnabled)
|
break;
|
||||||
handPose.enabled = true;
|
|
||||||
}
|
// 개별 손가락 curl 값
|
||||||
|
case "leftThumbCurl":
|
||||||
|
script.fingerShaped.leftThumbCurl = value;
|
||||||
|
break;
|
||||||
|
case "leftIndexCurl":
|
||||||
|
script.fingerShaped.leftIndexCurl = value;
|
||||||
|
break;
|
||||||
|
case "leftMiddleCurl":
|
||||||
|
script.fingerShaped.leftMiddleCurl = value;
|
||||||
|
break;
|
||||||
|
case "leftRingCurl":
|
||||||
|
script.fingerShaped.leftRingCurl = value;
|
||||||
|
break;
|
||||||
|
case "leftPinkyCurl":
|
||||||
|
script.fingerShaped.leftPinkyCurl = value;
|
||||||
|
break;
|
||||||
|
case "leftSpreadFingers":
|
||||||
|
script.fingerShaped.leftSpreadFingers = value;
|
||||||
|
break;
|
||||||
|
case "rightThumbCurl":
|
||||||
|
script.fingerShaped.rightThumbCurl = value;
|
||||||
|
break;
|
||||||
|
case "rightIndexCurl":
|
||||||
|
script.fingerShaped.rightIndexCurl = value;
|
||||||
|
break;
|
||||||
|
case "rightMiddleCurl":
|
||||||
|
script.fingerShaped.rightMiddleCurl = value;
|
||||||
|
break;
|
||||||
|
case "rightRingCurl":
|
||||||
|
script.fingerShaped.rightRingCurl = value;
|
||||||
|
break;
|
||||||
|
case "rightPinkyCurl":
|
||||||
|
script.fingerShaped.rightPinkyCurl = value;
|
||||||
|
break;
|
||||||
|
case "rightSpreadFingers":
|
||||||
|
script.fingerShaped.rightSpreadFingers = value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
// 최소 발목 높이
|
||||||
|
case "minimumAnkleHeight":
|
||||||
|
SetPrivateField(script, "minimumAnkleHeight", value);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -426,8 +535,7 @@ namespace KindRetargeting.Remote
|
|||||||
var script = FindCharacter(characterId);
|
var script = FindCharacter(characterId);
|
||||||
if (script == null) return;
|
if (script == null) return;
|
||||||
|
|
||||||
var handPose = script.GetComponent<FingerShapedController>();
|
var handPose = script.fingerShaped;
|
||||||
if (handPose == null) return;
|
|
||||||
|
|
||||||
// 스크립트 자동 활성화
|
// 스크립트 자동 활성화
|
||||||
handPose.enabled = true;
|
handPose.enabled = true;
|
||||||
@ -482,6 +590,7 @@ namespace KindRetargeting.Remote
|
|||||||
handPose.rightSpreadFingers = spread;
|
handPose.rightSpreadFingers = spread;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SendCharacterData(characterId);
|
||||||
SendStatus(true, $"{presetName} 프리셋 적용됨");
|
SendStatus(true, $"{presetName} 프리셋 적용됨");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -520,6 +629,93 @@ namespace KindRetargeting.Remote
|
|||||||
SendStatus(true, "정면 캘리브레이션 완료");
|
SendStatus(true, "정면 캘리브레이션 완료");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void AutoHipsOffset(string characterId)
|
||||||
|
{
|
||||||
|
var script = FindCharacter(characterId);
|
||||||
|
if (script == null) return;
|
||||||
|
|
||||||
|
float offset = CalculateHipsOffsetFromLegDifference(script);
|
||||||
|
SetPrivateField(script, "hipsOffsetY", offset);
|
||||||
|
script.SaveSettings();
|
||||||
|
|
||||||
|
SendCharacterData(characterId);
|
||||||
|
SendStatus(true, $"다리 길이 자동 보정 완료: hipsOffsetY={offset:F4}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AutoCalibrateAll(string characterId)
|
||||||
|
{
|
||||||
|
var script = FindCharacter(characterId);
|
||||||
|
if (script == null) return;
|
||||||
|
|
||||||
|
Animator source = script.sourceAnimator;
|
||||||
|
Animator targetAnim = script.targetAnimator;
|
||||||
|
if (source == null || targetAnim == null || !source.isHuman || !targetAnim.isHuman)
|
||||||
|
{
|
||||||
|
SendStatus(false, "소스/타겟 Animator가 없거나 Humanoid가 아닙니다.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1: 크기 초기화 + 힙 오프셋 계산
|
||||||
|
script.ResetScale();
|
||||||
|
SetPrivateField(script, "avatarScale", 1f);
|
||||||
|
SetPrivateField(script, "hipsOffsetY", CalculateHipsOffsetFromLegDifference(script));
|
||||||
|
|
||||||
|
// Step 2: 1프레임 후 목 높이 비율로 크기 조정
|
||||||
|
StartCoroutine(AutoCalibrateCoroutine(script, characterId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerator AutoCalibrateCoroutine(CustomRetargetingScript script, string characterId)
|
||||||
|
{
|
||||||
|
yield return null; // 1프레임 대기
|
||||||
|
|
||||||
|
Animator source = script.sourceAnimator;
|
||||||
|
Animator targetAnim = script.targetAnimator;
|
||||||
|
|
||||||
|
Transform sourceNeck = source.GetBoneTransform(HumanBodyBones.Neck);
|
||||||
|
Transform targetNeck = targetAnim.GetBoneTransform(HumanBodyBones.Neck);
|
||||||
|
if (sourceNeck == null || targetNeck == null)
|
||||||
|
{
|
||||||
|
SendStatus(false, "목 본을 찾을 수 없습니다.");
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
float scaleRatio = Mathf.Clamp(sourceNeck.position.y / Mathf.Max(targetNeck.position.y, 0.01f), 0.1f, 3f);
|
||||||
|
script.SetAvatarScale(scaleRatio);
|
||||||
|
SetPrivateField(script, "avatarScale", scaleRatio);
|
||||||
|
|
||||||
|
yield return null; // 1프레임 대기
|
||||||
|
|
||||||
|
// Step 3: 힙 오프셋 재계산 + 머리 정면 캘리브레이션
|
||||||
|
SetPrivateField(script, "hipsOffsetY", CalculateHipsOffsetFromLegDifference(script));
|
||||||
|
script.CalibrateHeadToForward();
|
||||||
|
script.SaveSettings();
|
||||||
|
|
||||||
|
SendCharacterData(characterId);
|
||||||
|
SendStatus(true, $"전체 자동 보정 완료: avatarScale={scaleRatio:F3}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private float CalculateHipsOffsetFromLegDifference(CustomRetargetingScript script)
|
||||||
|
{
|
||||||
|
Animator source = script.sourceAnimator;
|
||||||
|
Animator targetAnim = script.targetAnimator;
|
||||||
|
if (source == null || targetAnim == null) return 0f;
|
||||||
|
|
||||||
|
float sourceLeg = GetLegLength(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 CustomRetargetingScript FindCharacter(string characterId)
|
private CustomRetargetingScript FindCharacter(string characterId)
|
||||||
{
|
{
|
||||||
foreach (var script in registeredCharacters)
|
foreach (var script in registeredCharacters)
|
||||||
@ -532,6 +728,19 @@ namespace KindRetargeting.Remote
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void BroadcastValueChanged(string characterId, string property, float value)
|
||||||
|
{
|
||||||
|
var response = new
|
||||||
|
{
|
||||||
|
type = "valueChanged",
|
||||||
|
characterId = characterId,
|
||||||
|
property = property,
|
||||||
|
value = value
|
||||||
|
};
|
||||||
|
|
||||||
|
wsServer?.Broadcast(JsonConvert.SerializeObject(response));
|
||||||
|
}
|
||||||
|
|
||||||
private void SendStatus(bool success, string message)
|
private void SendStatus(bool success, string message)
|
||||||
{
|
{
|
||||||
var response = new
|
var response = new
|
||||||
|
|||||||
@ -2,22 +2,20 @@ using UnityEngine;
|
|||||||
|
|
||||||
namespace KindRetargeting
|
namespace KindRetargeting
|
||||||
{
|
{
|
||||||
[DefaultExecutionOrder(3)]
|
[System.Serializable]
|
||||||
public class ShoulderCorrectionFunction : MonoBehaviour
|
public class ShoulderCorrectionFunction
|
||||||
{
|
{
|
||||||
private CustomRetargetingScript retargetingScript; // 소스 데이터를 가져올 리타게팅 스크립트
|
|
||||||
|
|
||||||
[Header("설정")]
|
[Header("설정")]
|
||||||
[Range(0f, 5f)]
|
[Range(0f, 5f)]
|
||||||
public float blendStrength = 2f; // 전체적인 보정 강도
|
public float blendStrength = 2f;
|
||||||
[Range(0f, 1f)]
|
[Range(0f, 1f)]
|
||||||
public float maxShoulderBlend = 0.7f; // 어깨에 최대로 전달될 수 있는 회전 비율
|
public float maxShoulderBlend = 0.7f;
|
||||||
public bool reverseLeftRotation = false; // 왼쪽 어깨 회전 방향 반전 설정
|
public bool reverseLeftRotation = false;
|
||||||
public bool reverseRightRotation = false; // 오른쪽 어깨 회전 방향 반전 설정
|
public bool reverseRightRotation = false;
|
||||||
|
|
||||||
[Header("높이 제한 설정")]
|
[Header("높이 제한 설정")]
|
||||||
public float maxHeightDifference = 0.8f; // 최대 높이 차이
|
public float maxHeightDifference = 0.8f;
|
||||||
public float minHeightDifference = -0.1f; // 최소 높이 차이 (이 값 이하에서는 보정하지 않음)
|
public float minHeightDifference = -0.1f;
|
||||||
|
|
||||||
[Header("보정 커브 설정")]
|
[Header("보정 커브 설정")]
|
||||||
public AnimationCurve shoulderCorrectionCurve = AnimationCurve.Linear(0f, 0f, 1f, 1f);
|
public AnimationCurve shoulderCorrectionCurve = AnimationCurve.Linear(0f, 0f, 1f, 1f);
|
||||||
@ -29,26 +27,24 @@ namespace KindRetargeting
|
|||||||
private Transform rightShoulder;
|
private Transform rightShoulder;
|
||||||
private Transform leftUpperArm;
|
private Transform leftUpperArm;
|
||||||
private Transform rightUpperArm;
|
private Transform rightUpperArm;
|
||||||
// 최적화: 팔꿈치(LowerArm) Transform 캐싱 추가
|
|
||||||
private Transform leftLowerArm;
|
private Transform leftLowerArm;
|
||||||
private Transform rightLowerArm;
|
private Transform rightLowerArm;
|
||||||
|
|
||||||
private void Start()
|
public void Initialize(Animator targetAnimator)
|
||||||
{
|
{
|
||||||
retargetingScript = GetComponent<CustomRetargetingScript>();
|
leftShoulder = targetAnimator.GetBoneTransform(HumanBodyBones.LeftShoulder);
|
||||||
|
rightShoulder = targetAnimator.GetBoneTransform(HumanBodyBones.RightShoulder);
|
||||||
leftShoulder = retargetingScript.targetAnimator.GetBoneTransform(HumanBodyBones.LeftShoulder);
|
leftUpperArm = targetAnimator.GetBoneTransform(HumanBodyBones.LeftUpperArm);
|
||||||
rightShoulder = retargetingScript.targetAnimator.GetBoneTransform(HumanBodyBones.RightShoulder);
|
rightUpperArm = targetAnimator.GetBoneTransform(HumanBodyBones.RightUpperArm);
|
||||||
leftUpperArm = retargetingScript.targetAnimator.GetBoneTransform(HumanBodyBones.LeftUpperArm);
|
leftLowerArm = targetAnimator.GetBoneTransform(HumanBodyBones.LeftLowerArm);
|
||||||
rightUpperArm = retargetingScript.targetAnimator.GetBoneTransform(HumanBodyBones.RightUpperArm);
|
rightLowerArm = targetAnimator.GetBoneTransform(HumanBodyBones.RightLowerArm);
|
||||||
// 최적화: 팔꿈치 Transform도 Start에서 캐싱
|
|
||||||
leftLowerArm = retargetingScript.targetAnimator.GetBoneTransform(HumanBodyBones.LeftLowerArm);
|
|
||||||
rightLowerArm = retargetingScript.targetAnimator.GetBoneTransform(HumanBodyBones.RightLowerArm);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Update()
|
public void OnUpdate()
|
||||||
{
|
{
|
||||||
// 왼쪽 어깨 보정 (최적화: 캐싱된 Transform 사용)
|
if (leftShoulder == null || rightShoulder == null) return;
|
||||||
|
|
||||||
|
// 왼쪽 어깨 보정
|
||||||
Vector3 leftElbowPos = leftLowerArm.position;
|
Vector3 leftElbowPos = leftLowerArm.position;
|
||||||
float leftHeightDiff = leftElbowPos.y - leftShoulder.position.y;
|
float leftHeightDiff = leftElbowPos.y - leftShoulder.position.y;
|
||||||
float leftRawBlend = Mathf.Clamp01(
|
float leftRawBlend = Mathf.Clamp01(
|
||||||
@ -56,7 +52,7 @@ namespace KindRetargeting
|
|||||||
);
|
);
|
||||||
leftBlendWeight = shoulderCorrectionCurve.Evaluate(leftRawBlend) * maxShoulderBlend;
|
leftBlendWeight = shoulderCorrectionCurve.Evaluate(leftRawBlend) * maxShoulderBlend;
|
||||||
|
|
||||||
// 오른쪽 어깨 보정 (최적화: 캐싱된 Transform 사용)
|
// 오른쪽 어깨 보정
|
||||||
Vector3 rightElbowPos = rightLowerArm.position;
|
Vector3 rightElbowPos = rightLowerArm.position;
|
||||||
float rightHeightDiff = rightElbowPos.y - rightShoulder.position.y;
|
float rightHeightDiff = rightElbowPos.y - rightShoulder.position.y;
|
||||||
float rightRawBlend = Mathf.Clamp01(
|
float rightRawBlend = Mathf.Clamp01(
|
||||||
|
|||||||
@ -7,8 +7,8 @@ namespace KindRetargeting
|
|||||||
/// FinalIK IKSolverTrigonometric.Solve()를 사용하는 IK 래퍼.
|
/// FinalIK IKSolverTrigonometric.Solve()를 사용하는 IK 래퍼.
|
||||||
/// 4개 사지(양팔, 양다리)에 대해 FinalIK의 검증된 코사인 법칙 솔버를 호출합니다.
|
/// 4개 사지(양팔, 양다리)에 대해 FinalIK의 검증된 코사인 법칙 솔버를 호출합니다.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DefaultExecutionOrder(6)]
|
[System.Serializable]
|
||||||
public class TwoBoneIKSolver : MonoBehaviour
|
public class TwoBoneIKSolver
|
||||||
{
|
{
|
||||||
[System.Serializable]
|
[System.Serializable]
|
||||||
public class LimbIK
|
public class LimbIK
|
||||||
@ -27,7 +27,6 @@ namespace KindRetargeting
|
|||||||
[HideInInspector] public float upperLength;
|
[HideInInspector] public float upperLength;
|
||||||
[HideInInspector] public float lowerLength;
|
[HideInInspector] public float lowerLength;
|
||||||
|
|
||||||
// 초기 벤드 법선 (upper 본 로컬 공간 — FinalIK 방식)
|
|
||||||
[HideInInspector] public Vector3 localBendNormal;
|
[HideInInspector] public Vector3 localBendNormal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,15 +42,9 @@ namespace KindRetargeting
|
|||||||
|
|
||||||
private bool isInitialized;
|
private bool isInitialized;
|
||||||
|
|
||||||
private void Start()
|
public void Initialize(Animator targetAnimator)
|
||||||
{
|
{
|
||||||
Initialize();
|
animator = targetAnimator;
|
||||||
}
|
|
||||||
|
|
||||||
public void Initialize()
|
|
||||||
{
|
|
||||||
if (animator == null)
|
|
||||||
animator = GetComponent<Animator>();
|
|
||||||
if (animator == null || !animator.isHuman) return;
|
if (animator == null || !animator.isHuman) return;
|
||||||
|
|
||||||
CacheLimb(leftArm, HumanBodyBones.LeftUpperArm, HumanBodyBones.LeftLowerArm, HumanBodyBones.LeftHand);
|
CacheLimb(leftArm, HumanBodyBones.LeftUpperArm, HumanBodyBones.LeftLowerArm, HumanBodyBones.LeftHand);
|
||||||
@ -73,8 +66,6 @@ namespace KindRetargeting
|
|||||||
limb.upperLength = Vector3.Distance(limb.upper.position, limb.lower.position);
|
limb.upperLength = Vector3.Distance(limb.upper.position, limb.lower.position);
|
||||||
limb.lowerLength = Vector3.Distance(limb.lower.position, limb.end.position);
|
limb.lowerLength = Vector3.Distance(limb.lower.position, limb.end.position);
|
||||||
|
|
||||||
// 초기 벤드 법선을 upper 본의 로컬 공간에 캐싱 (FinalIK TrigonometricBone 방식)
|
|
||||||
// 런타임에 upper.rotation * localBendNormal로 안정적인 월드 법선 획득
|
|
||||||
Vector3 ab = limb.lower.position - limb.upper.position;
|
Vector3 ab = limb.lower.position - limb.upper.position;
|
||||||
Vector3 bc = limb.end.position - limb.lower.position;
|
Vector3 bc = limb.end.position - limb.lower.position;
|
||||||
Vector3 bendNormal = Vector3.Cross(ab, bc);
|
Vector3 bendNormal = Vector3.Cross(ab, bc);
|
||||||
@ -90,7 +81,7 @@ namespace KindRetargeting
|
|||||||
limb.localBendNormal = Quaternion.Inverse(limb.upper.rotation) * bendNormal;
|
limb.localBendNormal = Quaternion.Inverse(limb.upper.rotation) * bendNormal;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Update()
|
public void OnUpdate()
|
||||||
{
|
{
|
||||||
if (!isInitialized) return;
|
if (!isInitialized) return;
|
||||||
|
|
||||||
@ -105,10 +96,8 @@ namespace KindRetargeting
|
|||||||
if (!limb.enabled || limb.target == null || limb.positionWeight < 0.001f) return;
|
if (!limb.enabled || limb.target == null || limb.positionWeight < 0.001f) return;
|
||||||
if (limb.upper == null || limb.lower == null || limb.end == null) return;
|
if (limb.upper == null || limb.lower == null || limb.end == null) return;
|
||||||
|
|
||||||
// 벤드 법선 계산
|
|
||||||
Vector3 bendNormal = GetBendNormal(limb);
|
Vector3 bendNormal = GetBendNormal(limb);
|
||||||
|
|
||||||
// bendGoal 적용
|
|
||||||
if (limb.bendGoal != null && limb.bendGoalWeight > 0.001f)
|
if (limb.bendGoal != null && limb.bendGoalWeight > 0.001f)
|
||||||
{
|
{
|
||||||
Vector3 goalNormal = Vector3.Cross(
|
Vector3 goalNormal = Vector3.Cross(
|
||||||
@ -121,7 +110,6 @@ namespace KindRetargeting
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// FinalIK 정적 솔버 호출
|
|
||||||
IKSolverTrigonometric.Solve(
|
IKSolverTrigonometric.Solve(
|
||||||
limb.upper,
|
limb.upper,
|
||||||
limb.lower,
|
limb.lower,
|
||||||
@ -131,7 +119,6 @@ namespace KindRetargeting
|
|||||||
limb.positionWeight
|
limb.positionWeight
|
||||||
);
|
);
|
||||||
|
|
||||||
// 끝단 회전
|
|
||||||
if (limb.rotationWeight > 0.001f)
|
if (limb.rotationWeight > 0.001f)
|
||||||
{
|
{
|
||||||
limb.end.rotation = Quaternion.Slerp(
|
limb.end.rotation = Quaternion.Slerp(
|
||||||
@ -142,19 +129,11 @@ namespace KindRetargeting
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 벤드 법선을 upper 본의 회전에서 유도합니다 (FinalIK 방식).
|
|
||||||
/// 위치 기반 Cross(ab, bc)는 직선 근처에서 불안정하지만,
|
|
||||||
/// 회전 기반은 본의 회전을 그대로 따르므로 안정적입니다.
|
|
||||||
/// </summary>
|
|
||||||
private Vector3 GetBendNormal(LimbIK limb)
|
private Vector3 GetBendNormal(LimbIK limb)
|
||||||
{
|
{
|
||||||
return limb.upper.rotation * limb.localBendNormal;
|
return limb.upper.rotation * limb.localBendNormal;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 히프 높이 자동 보정값을 계산합니다.
|
|
||||||
/// </summary>
|
|
||||||
public float CalculateAutoFloorHeight(float comfortRatio = 0.98f)
|
public float CalculateAutoFloorHeight(float comfortRatio = 0.98f)
|
||||||
{
|
{
|
||||||
if (animator == null || leftLeg.upper == null || leftLeg.lower == null || leftLeg.end == null) return 0f;
|
if (animator == null || leftLeg.upper == null || leftLeg.lower == null || leftLeg.end == null) return 0f;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user