캐릭터 로드
This commit is contained in:
134
Assets/02_Scripts/Character/ProceduralIdle.cs
Normal file
134
Assets/02_Scripts/Character/ProceduralIdle.cs
Normal file
@@ -0,0 +1,134 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// 애니메이션 클립 없이 T포즈를 자연스러운 대기 자세로 바꾼다.
|
||||
///
|
||||
/// 왜 필요한가: VRM 은 모델만 담고 애니메이션은 담지 않는다. 사용자가 임의의 VRM 을
|
||||
/// 넣는 구조에서는 클립이 딸려 온다는 보장이 없으므로, 최소한의 대기 자세는
|
||||
/// 코드로 만들어 두어야 한다. 나중에 실제 클립을 넣으면 이 컴포넌트를 끄면 된다.
|
||||
///
|
||||
/// 구현 노트 1: 본의 로컬 축은 리그마다 제각각이라 로컬 오일러로 돌리면 모델에 따라
|
||||
/// 엉뚱하게 꺾인다. 캐릭터 루트의 축을 기준으로 월드 회전을 덧씌워 리그에 무관하게 만든다.
|
||||
///
|
||||
/// 구현 노트 2: VRM 컨트롤 리그는 Vrm10Instance.LateUpdate 에서 실제 본으로 반영되므로
|
||||
/// 그보다 먼저 실행돼야 한다. HeadLookAt 과 같은 이유로 실행 순서를 앞당긴다.
|
||||
/// </summary>
|
||||
[DefaultExecutionOrder(-100)]
|
||||
public class ProceduralIdle : MonoBehaviour
|
||||
{
|
||||
[Header("참조")]
|
||||
[Tooltip("비우면 자식에서 자동 탐색")]
|
||||
[SerializeField] Animator animator;
|
||||
|
||||
[Header("팔 내리기 (T포즈 해소)")]
|
||||
[Tooltip("위팔을 몸쪽으로 내리는 각도. 70 전후가 자연스럽다")]
|
||||
[Range(0f, 90f)]
|
||||
[SerializeField] float upperArmDown = 70f;
|
||||
|
||||
[Tooltip("아래팔을 조금 더 내려 팔이 늘어지게 한다")]
|
||||
[Range(0f, 40f)]
|
||||
[SerializeField] float lowerArmDown = 10f;
|
||||
|
||||
[Tooltip("팔을 몸에서 살짝 띄운다. 0 이면 몸에 붙는다")]
|
||||
[Range(0f, 20f)]
|
||||
[SerializeField] float armOutward = 5f;
|
||||
|
||||
[Header("호흡")]
|
||||
[SerializeField] bool breathing = true;
|
||||
|
||||
[Tooltip("가슴이 오르내리는 각도")]
|
||||
[Range(0f, 5f)]
|
||||
[SerializeField] float breathAmplitude = 1.2f;
|
||||
|
||||
[Tooltip("한 번 호흡하는 데 걸리는 시간(초)")]
|
||||
[SerializeField] float breathPeriod = 4f;
|
||||
|
||||
[Header("체중 이동")]
|
||||
[SerializeField] bool weightShift = true;
|
||||
|
||||
[Tooltip("좌우로 기우는 각도")]
|
||||
[Range(0f, 5f)]
|
||||
[SerializeField] float swayAmplitude = 1.2f;
|
||||
|
||||
[Tooltip("한 번 왕복하는 데 걸리는 시간(초). 호흡과 주기를 다르게 해야 기계적으로 안 보인다")]
|
||||
[SerializeField] float swayPeriod = 7f;
|
||||
|
||||
Transform root;
|
||||
Transform hips, chest, leftUpperArm, rightUpperArm, leftLowerArm, rightLowerArm;
|
||||
|
||||
float phaseOffset;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
root = transform;
|
||||
if (animator == null) animator = GetComponentInChildren<Animator>();
|
||||
|
||||
if (animator == null || !animator.isHuman)
|
||||
{
|
||||
Debug.LogWarning("[ProceduralIdle] 휴머노이드 Animator 가 없어 비활성화합니다.");
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
hips = animator.GetBoneTransform(HumanBodyBones.Hips);
|
||||
chest = animator.GetBoneTransform(HumanBodyBones.Chest)
|
||||
?? animator.GetBoneTransform(HumanBodyBones.Spine);
|
||||
|
||||
leftUpperArm = animator.GetBoneTransform(HumanBodyBones.LeftUpperArm);
|
||||
rightUpperArm = animator.GetBoneTransform(HumanBodyBones.RightUpperArm);
|
||||
leftLowerArm = animator.GetBoneTransform(HumanBodyBones.LeftLowerArm);
|
||||
rightLowerArm = animator.GetBoneTransform(HumanBodyBones.RightLowerArm);
|
||||
|
||||
// 여러 캐릭터가 있어도 동시에 같은 박자로 숨쉬지 않게 한다.
|
||||
phaseOffset = Random.Range(0f, 100f);
|
||||
}
|
||||
|
||||
void LateUpdate()
|
||||
{
|
||||
ApplyArms();
|
||||
ApplyBody();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// T포즈에서 팔은 좌우로 뻗어 있다. 캐릭터의 forward 축을 중심으로 돌려 내린다.
|
||||
/// 왼팔은 -right 방향이라 +각도, 오른팔은 +right 방향이라 -각도가 아래로 향한다.
|
||||
/// </summary>
|
||||
void ApplyArms()
|
||||
{
|
||||
RotateArm(leftUpperArm, +upperArmDown, +armOutward);
|
||||
RotateArm(rightUpperArm, -upperArmDown, -armOutward);
|
||||
|
||||
// 위팔을 돌리면 아래팔도 따라오므로, 여기서는 추가분만 얹는다.
|
||||
RotateArm(leftLowerArm, +lowerArmDown, 0f);
|
||||
RotateArm(rightLowerArm, -lowerArmDown, 0f);
|
||||
}
|
||||
|
||||
void RotateArm(Transform bone, float downAngle, float outwardAngle)
|
||||
{
|
||||
if (bone == null) return;
|
||||
|
||||
Quaternion delta = Quaternion.AngleAxis(downAngle, root.forward);
|
||||
if (!Mathf.Approximately(outwardAngle, 0f))
|
||||
{
|
||||
delta = Quaternion.AngleAxis(outwardAngle, root.up) * delta;
|
||||
}
|
||||
bone.rotation = delta * bone.rotation;
|
||||
}
|
||||
|
||||
void ApplyBody()
|
||||
{
|
||||
float t = Time.time + phaseOffset;
|
||||
|
||||
if (breathing && chest != null && breathPeriod > 0.01f)
|
||||
{
|
||||
float breath = Mathf.Sin(t * Mathf.PI * 2f / breathPeriod);
|
||||
chest.rotation = Quaternion.AngleAxis(-breath * breathAmplitude, root.right) * chest.rotation;
|
||||
}
|
||||
|
||||
if (weightShift && hips != null && swayPeriod > 0.01f)
|
||||
{
|
||||
float sway = Mathf.Sin(t * Mathf.PI * 2f / swayPeriod);
|
||||
hips.rotation = Quaternion.AngleAxis(sway * swayAmplitude, root.forward) * hips.rotation;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user