- AvatarOutfitController: 의상 변경 시 NiloToon renderCharacter 토글 + VFX/SFX/컬러 플래시 연출 - TransformEffectPreset ScriptableObject 로 프리셋 공유 - 아바타별 isTransforming 플래그로 동시 변신 지원 - 의상 리스트 순서 변경 (▲▼) 기능 - Piloto Studio 파티클 번들 추가 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
230 lines
6.8 KiB
C#
230 lines
6.8 KiB
C#
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
using KindRetargeting;
|
|
|
|
[DefaultExecutionOrder(16001)]
|
|
public class SimplePoseTransfer : MonoBehaviour
|
|
{
|
|
[System.Serializable]
|
|
public struct TargetEntry
|
|
{
|
|
public Animator animator;
|
|
[Tooltip("월드 공간 힙 오프셋")]
|
|
public Vector3 hipOffset;
|
|
}
|
|
|
|
[Header("Pose Transfer Settings")]
|
|
public Animator sourceBone;
|
|
public List<TargetEntry> targets = new List<TargetEntry>();
|
|
|
|
[Header("Scale Transfer")]
|
|
[Tooltip("소스의 머리 스케일을 타겟에도 적용")]
|
|
public bool transferHeadScale = true;
|
|
|
|
// 캐싱된 Transform들
|
|
private Transform[] cachedSourceBones;
|
|
private Transform[,] cachedTargetBones;
|
|
|
|
// 각 target의 초기 회전 차이를 저장
|
|
private Quaternion[,] boneRotationDifferences;
|
|
|
|
// 머리 스케일 관련
|
|
private CustomRetargetingScript sourceRetargetingScript;
|
|
private Vector3[] originalTargetHeadScales;
|
|
|
|
private void Start()
|
|
{
|
|
Init();
|
|
}
|
|
|
|
public void Init()
|
|
{
|
|
if (targets == null || targets.Count == 0)
|
|
{
|
|
Debug.LogError("Targets are null or empty");
|
|
return;
|
|
}
|
|
|
|
if (sourceBone == null)
|
|
{
|
|
Debug.LogError("Source bone is null");
|
|
return;
|
|
}
|
|
|
|
InitializeTargetBones();
|
|
CacheAllBoneTransforms();
|
|
CacheHeadScales();
|
|
|
|
Debug.Log($"SimplePoseTransfer initialized with {targets.Count} targets");
|
|
}
|
|
|
|
public void SetHipOffset(int targetIndex, Vector3 offset)
|
|
{
|
|
if (targets == null || targetIndex < 0 || targetIndex >= targets.Count)
|
|
{
|
|
return;
|
|
}
|
|
TargetEntry entry = targets[targetIndex];
|
|
entry.hipOffset = offset;
|
|
targets[targetIndex] = entry;
|
|
}
|
|
|
|
public Vector3 GetHipOffset(int targetIndex)
|
|
{
|
|
if (targets == null || targetIndex < 0 || targetIndex >= targets.Count)
|
|
{
|
|
return Vector3.zero;
|
|
}
|
|
return targets[targetIndex].hipOffset;
|
|
}
|
|
|
|
private void CacheHeadScales()
|
|
{
|
|
// 소스에서 CustomRetargetingScript 찾기
|
|
sourceRetargetingScript = sourceBone.GetComponent<CustomRetargetingScript>();
|
|
|
|
// 타겟들의 원본 머리 스케일 저장
|
|
originalTargetHeadScales = new Vector3[targets.Count];
|
|
for (int i = 0; i < targets.Count; i++)
|
|
{
|
|
Animator animator = targets[i].animator;
|
|
if (animator != null)
|
|
{
|
|
Transform headBone = animator.GetBoneTransform(HumanBodyBones.Head);
|
|
if (headBone != null)
|
|
{
|
|
originalTargetHeadScales[i] = headBone.localScale;
|
|
}
|
|
else
|
|
{
|
|
originalTargetHeadScales[i] = Vector3.one;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void InitializeTargetBones()
|
|
{
|
|
boneRotationDifferences = new Quaternion[targets.Count, 55];
|
|
|
|
for (int i = 0; i < targets.Count; i++)
|
|
{
|
|
Animator animator = targets[i].animator;
|
|
if (animator == null)
|
|
{
|
|
Debug.LogError($"targets[{i}].animator is null");
|
|
continue;
|
|
}
|
|
|
|
// 55개의 휴머노이드 본에 대해 회전 차이 계산
|
|
for (int j = 0; j < 55; j++)
|
|
{
|
|
Transform sourceBoneTransform = sourceBone.GetBoneTransform((HumanBodyBones)j);
|
|
Transform targetBoneTransform = animator.GetBoneTransform((HumanBodyBones)j);
|
|
|
|
if (sourceBoneTransform != null && targetBoneTransform != null)
|
|
{
|
|
boneRotationDifferences[i, j] = Quaternion.Inverse(sourceBoneTransform.rotation) * targetBoneTransform.rotation;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void CacheAllBoneTransforms()
|
|
{
|
|
// 소스 본 캐싱
|
|
cachedSourceBones = new Transform[55];
|
|
for (int i = 0; i < 55; i++)
|
|
{
|
|
cachedSourceBones[i] = sourceBone.GetBoneTransform((HumanBodyBones)i);
|
|
}
|
|
|
|
// 타겟 본 캐싱
|
|
cachedTargetBones = new Transform[targets.Count, 55];
|
|
for (int t = 0; t < targets.Count; t++)
|
|
{
|
|
Animator animator = targets[t].animator;
|
|
if (animator == null) continue;
|
|
for (int i = 0; i < 55; i++)
|
|
{
|
|
cachedTargetBones[t, i] = animator.GetBoneTransform((HumanBodyBones)i);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void LateUpdate()
|
|
{
|
|
TransferPoses();
|
|
}
|
|
|
|
private void TransferPoses()
|
|
{
|
|
if (sourceBone == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (int i = 0; i < targets.Count; i++)
|
|
{
|
|
Animator animator = targets[i].animator;
|
|
if (animator != null && animator.gameObject.activeInHierarchy)
|
|
{
|
|
TransferPoseToTarget(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void TransferPoseToTarget(int targetIndex)
|
|
{
|
|
TargetEntry entry = targets[targetIndex];
|
|
Animator targetAnimator = entry.animator;
|
|
|
|
// 루트 회전 동기화
|
|
targetAnimator.transform.rotation = sourceBone.transform.rotation;
|
|
|
|
// 모든 본에 대해 포즈 전송
|
|
for (int i = 0; i < 55; i++)
|
|
{
|
|
Transform targetBoneTransform = cachedTargetBones[targetIndex, i];
|
|
Transform sourceBoneTransform = cachedSourceBones[i];
|
|
|
|
if (targetBoneTransform != null && sourceBoneTransform != null)
|
|
{
|
|
// 회전 적용
|
|
targetBoneTransform.rotation = sourceBoneTransform.rotation * boneRotationDifferences[targetIndex, i];
|
|
|
|
// 펠비스 위치 동기화 (HumanBodyBones.Hips = 0) + 월드 공간 힙 오프셋
|
|
if (i == 0)
|
|
{
|
|
targetBoneTransform.position = sourceBoneTransform.position + entry.hipOffset;
|
|
}
|
|
|
|
// 머리 스케일 적용 (HumanBodyBones.Head = 10)
|
|
if (transferHeadScale && i == 10)
|
|
{
|
|
ApplyHeadScale(targetIndex, targetBoneTransform);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ApplyHeadScale(int targetIndex, Transform targetHeadBone)
|
|
{
|
|
if (sourceRetargetingScript != null)
|
|
{
|
|
float headScale = sourceRetargetingScript.GetHeadScale();
|
|
targetHeadBone.localScale = originalTargetHeadScales[targetIndex] * headScale;
|
|
}
|
|
else
|
|
{
|
|
// CustomRetargetingScript가 없으면 소스 머리 스케일 직접 복사
|
|
Transform sourceHead = cachedSourceBones[10]; // HumanBodyBones.Head
|
|
if (sourceHead != null)
|
|
{
|
|
targetHeadBone.localScale = sourceHead.localScale;
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|