Cast Away 프로젝트에서 중립 동물 NPC를 구현하는 과정에서 동물 NPC가 경사를 올라가는 도중 동물 NPC의 발과 그림자에서 위화감을 느꼈다.
Scene 뷰를 통하여 관찰한 결과 Box Collider 컴포넌트의 한 모서리만 붙어있는 채로 공중에 떠 있는 동물 NPC를 발견할 수 있었다.

이를 해결하기 위해 간단히 Rigidbody 컴포넌트의 X축 회전 고정을 풀면 경사면에 맞춰 밑면이 들리거나 가라 앉을 줄 알았으나 동물이 뒤집혀 버둥버둥 대는 꼴만 보게 되었다..
위에서 얻은 실패 덕분에 많은 고민을 하게 되었다. 내가 생각한 방법은 지면의 법선 벡터에 동물의 업 벡터를 일치시키는 방법이었다.
지면의 법선 벡터에 동물의 업 벡터를 일치시키게 되면 동물은 자연스럽게 경사면 위에 있다는 느낌을 받게 되지 않을까 싶었다.

물론 동물의 업 벡터를 지면의 법선 벡터에 단순히 보간하는 작업이기 때문에 저렇게 딱 알맞게 붙는다기보다 툭 하고 떨어지는 느낌이지만 그 경사가 높지 않아서 자연스러워 보인다.
생각은 단순했지만 지면의 법선 벡터를 어떻게 하면 얻을 수 있을까?에 대해서 고민을 했다. 면의 법선 벡터는 레이캐스팅을 통해서 구할 수 있었다.
레이캐스팅의 결과 값으로 사용하는 RaycastHit에는 normal이라는 속성이 존재한다.
normal은 충돌한 지점에서 충돌체 면에 수직인 벡터다. 즉, 법선 벡터다.
동물의 업 벡터를 지면의 법선 벡터와 일치시키는 회전을 만들기 위해서 사용했다.
Quaternion.FromToRotation(from, to)는 from 벡터를 to 벡터로 만들기 위한 회전을 생성한다.
예를 들어, 을 로 회전시킨다고 하면 다음과 같다.

Y축을 X축으로 만드는 것과 같기 때문에 Z축으로 90˚ 회전이 발생한다. 회전 쿼터니언을 생성한 것이다.
이처럼 동물 NPC의 업 벡터를 지면의 법선 벡터에 일치시켜 회전 쿼터니언을 생성할 수 있다.
Quaternion.FromToRotation(transform.up, hit.normal);
그리고 원하는 회전 값을 얻기 위해 현재 회전 값을 곱한다.
var target_rotation = Quaternion.FromToRotation(transform.up, hit.normal) * transform.rotation;
두 벡터의 방향이 너무 다르면 자연스럽고 부드럽게 보간되지 못하기 때문에 Quaternion.Slerp를 이용하여 구면 보간을 한다.
구면 보간의 강도를 조절할 수 있도록 강도를 변수로 둔다.
transform.rotation = Quaternion.Slerp(transform.rotation, target_rotation, Time.deltaTime * m_smoothness);
// AnimalMovement.cs의 일부 발췌
public class AnimalMovement
{
[Header("레이의 길이")]
[SerializeField] private float m_ray_distance;
[Header("레이가 감지할 레이어")]
[SerializeField] private LayerMask m_ground_mask;
[Header("보간 강도")]
[SerializeField] private float m_smoothness;
public InclineInterpolation()
{
if(Physics.Raycast(transform.position + Vector3.up,
Vector3.down,
out var hit,
m_ray_distance,
m_ground_layer))
{
var target_rotation = Quaternion.FromToRotation(transform.up, hit.normal) * tranform.rotation;
transform.rotation = Quaternion.Slerp(transform.rotation, target_rotation, Time.deltaTime * m_smoothness);
}
}
}
나름 만족스럽게 경사면에 하단이 붙은 채로 움직이는 것을 확인할 수 있다.