Attention: Here be dragons
This is the latest
(unstable) version of this documentation, which may document features
not available in or compatible with released stable versions of Godot.
Checking the stable version of the documentation...
MultiMeshInstance3D로 수천 개의 물고기 애니메이션화하기
이 튜토리얼에서는 ABZU 게임에서 정점 애니메이션과 정적 메쉬 인스턴싱을 사용하여 수천 마리의 물고기를 렌더링하고 애니메이션화하는 데 사용되는 기술을 살펴봅니다.
Godot에서 이는 사용자 정의 셰이더 및 :ref:`MultiMeshInstance3D <class_MultiMeshInstance3D>`을 사용하여 수행할 수 있습니다. 다음 기술을 사용하면 저사양 하드웨어에서도 수천 개의 애니메이션 객체를 렌더링할 수 있습니다.
물고기 한 마리를 애니메이션화하는 것부터 시작하겠습니다. 그런 다음 해당 애니메이션을 수천 마리의 물고기로 확장하는 방법을 살펴보겠습니다.
물고기 한 마리 애니메이션화하기
물고기 한 마리부터 시작하겠습니다. 물고기 모델을 :ref:`MeshInstance3D <class_MeshInstance3D>`에 로드하고 새로운 :ref:`ShaderMaterial <class_ShaderMaterial>`을 추가합니다.
예시 이미지에 사용할 물고기는 다음과 같습니다. 원하는 물고기 모델을 사용할 수 있습니다.
참고
이 튜토리얼의 물고기 모델은 `QuaterniusDev <https://quaternius.com>`_에 의해 제작되었으며 크리에이티브 커먼즈 라이선스로 공유됩니다. CC0 1.0 범용(CC0 1.0) 공개 도메인 전용 https://creativecommons.org/publicdomain/zero/1.0/
일반적으로 뼈대와 :ref:`Skeleton3D <class_Skeleton3D>`을 사용하여 객체에 애니메이션을 적용합니다. 그러나 뼈는 CPU에서 애니메이션화되므로 매 프레임마다 수천 번의 작업을 계산해야 하고 수천 개의 개체를 갖는 것이 불가능해집니다. 정점 셰이더에서 정점 애니메이션을 사용하면 뼈대 사용을 피하고 대신 몇 줄의 코드로 완전히 GPU에서 전체 애니메이션을 계산할 수 있습니다.
애니메이션은 네 가지 주요 동작으로 구성됩니다.
좌우 모션
물고기 중심을 중심으로 하는 피벗 동작
패닝 웨이브 모션
패닝 트위스트 모션
애니메이션의 모든 코드는 모션 양을 제어하는 유니폼과 함께 정점 셰이더에 있습니다. 우리는 유니폼을 사용하여 모션의 강도를 제어하므로 셰이더를 다시 컴파일할 필요 없이 편집기에서 애니메이션을 조정하고 결과를 실시간으로 확인할 수 있습니다.
모든 모션은 모델 공간의 VERTEX``에 적용된 코사인파를 사용하여 만들어집니다. 우리는 모션이 항상 물고기의 방향을 기준으로 하도록 정점이 모델 공간에 있기를 원합니다. 예를 들어 좌우는 항상 물고기를 표준 방향의 ``x 축 대신 왼쪽에서 오른쪽 방향으로 앞뒤로 이동합니다.
애니메이션 속도를 제어하기 위해 ``TIME``를 사용하여 자체 시간 변수를 정의하는 것부터 시작하겠습니다.
//time_scale is a uniform float
float time = TIME * time_scale;
우리가 구현할 첫 번째 모션은 좌우 모션입니다. ``TIME``의 ``cos``로 ``VERTEX.x``를 오프셋하여 만들 수 있습니다. 메쉬가 렌더링될 때마다 모든 정점이 ``cos(time)``만큼 측면으로 이동합니다.
//side_to_side is a uniform float
VERTEX.x += cos(time) * side_to_side;
결과 애니메이션은 이렇게 보여야 합니다:
다음으로 피벗을 추가합니다. 물고기의 중심은 (0, 0)에 있으므로 우리가 해야 할 일은 물고기의 중심을 중심으로 회전하도록 회전 행렬을 ``VERTEX``에 곱하는 것뿐입니다.
다음과 같이 회전 행렬을 구성합니다.
//angle is scaled by 0.1 so that the fish only pivots and doesn't rotate all the way around
//pivot is a uniform float
float pivot_angle = cos(time) * 0.1 * pivot;
mat2 rotation_matrix = mat2(vec2(cos(pivot_angle), -sin(pivot_angle)), vec2(sin(pivot_angle), cos(pivot_angle)));
그런 다음 x 및 z 축에 ``VERTEX.xz``를 곱하여 적용합니다.
VERTEX.xz = rotation_matrix * VERTEX.xz;
피벗만 적용하면 이렇게 보여야 합니다:
다음 두 동작은 물고기의 척추를 따라 이동해야 합니다. 이를 위해서는 새로운 변수 body``가 필요합니다. ``body``는 물고기 꼬리 부분에 ``0, 머리 부분에 ``1``가 있는 플로트입니다.
float body = (VERTEX.z + 1.0) / 2.0; //for a fish centered at (0, 0) with a length of 2
다음 동작은 물고기의 길이를 따라 움직이는 코사인파입니다. 물고기의 척추를 따라 움직이게 하기 위해 ``cos``에 대한 입력을 척추를 따른 위치로 오프셋합니다. 이는 위에서 정의한 변수 ``body``입니다.
//wave is a uniform float
VERTEX.x += cos(time + body) * wave;
이것은 위에서 정의한 좌우 모션과 매우 유사해 보이지만, 여기서는 ``body``를 사용하여 ``cos``를 오프셋함으로써 척추를 따라 있는 각 정점이 파도에서 다른 위치를 갖게 되므로 파도가 물고기를 따라 움직이는 것처럼 보입니다.
마지막 동작은 척추를 따라 패닝 롤을 하는 트위스트입니다. 피벗과 마찬가지로 먼저 회전 행렬을 구성합니다.
//twist is a uniform float
float twist_angle = cos(time + body) * 0.3 * twist;
mat2 twist_matrix = mat2(vec2(cos(twist_angle), -sin(twist_angle)), vec2(sin(twist_angle), cos(twist_angle)));
물고기가 척추 주위를 굴러다니는 것처럼 보이도록 xy 축에 회전을 적용합니다. 이것이 작동하려면 물고기의 척추가 z 축 중심에 있어야 합니다.
VERTEX.xy = twist_matrix * VERTEX.xy;
GDScript 샘플 코드입니다:
이 모든 동작을 차례로 적용하면 유동적인 젤리 같은 동작을 얻게 됩니다.
일반 물고기는 대부분 몸의 뒷부분으로 헤엄칩니다. 따라서 패닝 동작을 물고기의 뒤쪽 절반으로 제한해야 합니다. 이를 위해 새 변수 ``mask``를 만듭니다.
``mask``는 ``0``에서 ``1``로 전환되는 지점을 제어하기 위해 ``smoothstep``를 사용하여 물고기 앞쪽의 ``0``에서 끝의 ``1``로 이동하는 플로트입니다.
//mask_black and mask_white are uniforms
float mask = smoothstep(mask_black, mask_white, 1.0 - body);
아래는 ``COLOR``로 사용된 ``mask``가 있는 물고기의 이미지입니다.
웨이브의 경우 모션에 ``mask``를 곱하여 뒤쪽 절반으로 제한합니다.
//wave motion with mask
VERTEX.x += cos(time + body) * mask * wave;
트위스트에 마스크를 적용하기 위해 ``mix``를 사용합니다. ``mix``를 사용하면 완전히 회전된 정점과 회전되지 않은 정점 사이의 정점 위치를 혼합할 수 있습니다. ``mask``에 회전된 ``VERTEX``를 곱하는 대신 ``mix``를 사용해야 합니다. 왜냐하면 ``VERTEX``에 모션을 추가하지 않고 ``VERTEX``를 회전된 버전으로 대체하기 때문입니다. 여기에 ``mask``를 곱하면 물고기가 줄어들 것입니다.
//twist motion with mask
VERTEX.xy = mix(VERTEX.xy, twist_matrix * VERTEX.xy, mask);
네 가지 모션을 합치면 최종 애니메이션이 생성됩니다.
물고기의 수영 주기를 바꾸기 위해 유니폼을 가지고 놀아보세요. 이 네 가지 동작을 사용하여 다양한 수영 스타일을 만들 수 있다는 것을 알게 될 것입니다.
물고기 떼 만들기
Godot에서는 MultiMeshInstance3D 노드를 사용하여 수천 개의 같은 오브젝트를 쉽게 렌더링할 수 있습니다.
MultiMeshInstance3D 노드는 MeshInstance3D 노드를 만드는 것과 동일한 방식으로 생성되고 사용됩니다. 이 튜토리얼에서는 물고기 떼가 포함되므로 MultiMeshInstance3D 노드 ``School``라는 이름을 지정합니다.
MultiMeshInstance3D가 있으면 :ref:`MultiMesh <class_MultiMesh>`을 추가하고 해당 MultiMesh에 위의 셰이더를 사용하여 :ref:`Mesh <class_Mesh>`를 추가합니다.
MultiMeshes는 변환(회전, 이동, 배율), 색상 및 사용자 정의라는 세 가지 추가 인스턴스별 속성을 사용하여 메시를 그립니다. 사용자 정의는 :ref:`Color <class_Color>`을 사용하여 4개의 다용도 변수를 전달하는 데 사용됩니다.
``instance_count``는 그리려는 메시 인스턴스 수를 지정합니다. ``instance_count``가 ``0``보다 큰 동안에는 다른 매개변수를 변경할 수 없으므로 지금은 ``instance_count``를 ``0``에 그대로 둡니다. 나중에 GDScript에서 ``instance count``를 설정하겠습니다.
``transform_format``는 사용된 변환이 3D인지 2D인지 지정합니다. 이 튜토리얼에서는 3D를 선택합니다.
color_format 및 custom_data_format``의 경우 ``None, Byte 및 Float 중에서 선택할 수 있습니다. None``는 해당 데이터(인스턴스별 ``COLOR 변수 또는 INSTANCE_CUSTOM)를 셰이더에 전달하지 않는다는 것을 의미합니다. ``Byte``는 전달한 색상을 구성하는 각 숫자가 8비트로 저장된다는 것을 의미하고, ``Float``는 각 숫자가 부동 소수점 숫자(32비트)로 저장된다는 것을 의미합니다. ``Float``는 느리지만 더 정확합니다. ``Byte``는 메모리를 덜 사용하고 더 빠르지만 일부 시각적 아티팩트가 나타날 수 있습니다.
이제 ``instance_count``를 갖고 싶은 물고기 수로 설정하세요.
다음으로 인스턴스별 변환을 설정해야 합니다.
멀티메시에 대한 인스턴스별 변환을 설정하는 방법에는 두 가지가 있습니다. 첫 번째는 완전히 편집기에 있으며 :ref:`MultiMeshInstance3D 튜토리얼 <doc_using_multi_mesh_instance>`에 설명되어 있습니다.
두 번째는 모든 인스턴스를 반복하고 코드에서 변환을 설정하는 것입니다. 아래에서는 GDScript를 사용하여 모든 인스턴스를 반복하고 변환을 임의의 위치로 설정합니다.
for i in range($School.multimesh.instance_count):
var position = Transform3D()
position = position.translated(Vector3(randf() * 100 - 50, randf() * 50 - 25, randf() * 50 - 25))
$School.multimesh.set_instance_transform(i, position)
이 스크립트를 실행하면 물고기가 MultiMeshInstance3D 위치 주변 상자의 임의 위치에 배치됩니다.
참고
성능이 문제인 경우 더 적은 수의 물고기를 사용하여 씬을 실행해 보십시오.
수영 주기에서 모든 물고기가 모두 같은 위치에 있다는 점에 주목하세요. 그것은 그들을 매우 로봇처럼 보이게 만듭니다. 다음 단계는 수영 주기에서 각 물고기에게 서로 다른 위치를 부여하여 전체 학교가 더욱 유기적으로 보이도록 하는 것입니다.
물고기 떼 애니메이션
cos 함수를 사용하여 물고기를 애니메이션화할 때의 이점 중 하나는 ``time``라는 하나의 매개변수를 사용하여 애니메이션화된다는 것입니다. 수영 주기에서 각 물고기에게 고유한 위치를 부여하려면 ``time``만 오프셋하면 됩니다.
인스턴스별 사용자 지정 값 ``INSTANCE_CUSTOM``를 ``time``에 추가하면 됩니다.
float time = (TIME * time_scale) + (6.28318 * INSTANCE_CUSTOM.x);
다음으로 INSTANCE_CUSTOM``에 값을 전달해야 합니다. 위에서 ``for 루프에 한 줄을 추가하면 됩니다. for 루프에서는 각 인스턴스에 사용할 4개의 무작위 부동 소수점 세트를 할당합니다.
$School.multimesh.set_instance_custom_data(i, Color(randf(), randf(), randf(), randf()))
이제 물고기는 모두 수영 주기에서 고유한 위치를 갖습니다. ``INSTANCE_CUSTOM``를 사용하여 ``TIME``를 곱하여 더 빠르게 또는 느리게 수영하도록 하여 좀 더 개성을 부여할 수 있습니다.
//set speed from 50% - 150% of regular speed
float time = (TIME * (0.5 + INSTANCE_CUSTOM.y) * time_scale) + (6.28318 * INSTANCE_CUSTOM.x);
인스턴스별 사용자 정의 값을 변경한 것과 동일한 방식으로 인스턴스별 색상을 변경하여 실험할 수도 있습니다.
이 시점에서 직면하게 될 한 가지 문제는 물고기가 애니메이션되지만 움직이지 않는다는 것입니다. 매 프레임마다 각 물고기에 대한 인스턴스별 변환을 업데이트하여 물고기를 이동할 수 있습니다. 이렇게 하면 프레임당 수천 개의 MeshInstance3D를 이동하는 것보다 빠르지만 여전히 느릴 수 있습니다.
다음 튜토리얼에서는 :ref:`GPUParticles3D <class_GPUParticles3D>`를 사용하여 GPU를 활용하고 인스턴싱의 이점을 유지하면서 각 물고기를 개별적으로 이동하는 방법을 다룰 것입니다.