ํฐ์คํ ๋ฆฌ ๋ทฐ
Github
LukasFratzl/TurboSequence: Skeletal Based GPU Crowds for UE5 ๐
๊ฐ์
TurboSequence๋ Niagara Mesh Particles๋ฅผ ์ฌ์ฉํ๋, Skeletal Mesh๊ฐ ์๋ Static Mesh๋ฅผ ๋ ๋๋งํฉ๋๋ค. ํต์ฌ ํธ๋ฆญ์ ์ ๋๋ฉ์ด์ ๋ฐ์ดํฐ๋ฅผ ํ ์ค์ฒ์ ๋ฒ ์ดํฌํ๊ณ , GPU Compute Shader + Vertex Shader์์ ์ค์๊ฐ ์คํค๋์ ์ํํ๋ ๊ฒ์ ๋๋ค.
์ Niagara SkeletalMesh Instance๊ฐ ์๋๊ฐ?
Niagara์ SkeletalMesh ๋ ๋๋ฌ๋ ๋ด๋ถ์ ์ผ๋ก ๊ฐ ์ธ์คํด์ค๋ง๋ค ๊ฐ๋ณ draw call์ ๋ฐ์์ํต๋๋ค. TurboSequence๋ ์ด๋ฅผ ์ฐํํ์ฌ:
- Static Mesh๋ฅผ Niagara Mesh Particles๋ก ๋ ๋๋ง (๋จ์ผ instanced draw call)
- ์ ๋๋ฉ์ด์ ์ GPU์์ ๊ณ์ฐํ์ฌ CPU ์ค๋ฒํค๋ ์ ๊ฑฐ
ํ์ดํ๋ผ์ธ ์ ์ฒด ๊ตฌ์กฐ
1๋จ๊ณ: ์คํ๋ผ์ธ ๋ณํ (Content Pipeline)
// TurboSequence_ControlPanelLibrary_Lf.cpp - ConvertSkeletalMeshToTurboSequence_BlueprintThreadSafe
1.1 Static Mesh ์์ฑ
// ์๋ณธ Skeletal Mesh์ ๊ฐ LOD๋ฅผ Static Mesh๋ก ๋ณํ
for (int32 i = 0; i < SkeletalMesh->GetLODNum(); ++i)
{
UStaticMesh* StaticMesh = NewObject<UStaticMesh>();
// Skeletal Mesh์ ์ ์ ๋ฐ์ดํฐ๋ฅผ Static Mesh๋ก ๋ณต์ฌ
// ๋ณธ ์จ์ดํธ๋ ์ ๊ฑฐ๋๊ณ Bind Pose ์ ์ ๋ง ์ ์ฅ
}
1.2 ์ ๋๋ฉ์ด์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ํ ์ค์ฒ ์์ฑ
// TurboSequence_Helper_Lf.cpp - CreateAnimationLibraryTexture2D
ํต์ฌ์ ๋ชจ๋ ์ ๋๋ฉ์ด์ ํ๋ ์์ ๋ณธ Transform์ ํ ์ค์ฒ์ ๋ฒ ์ดํฌ:
ํ
์ค์ฒ ๊ตฌ์กฐ:
- Width: NumBones * 3 (๊ฐ ๋ณธ๋น 3ํฝ์
= Rotation Quat + Translation + Scale)
- Height: TotalAnimationFrames (๋ชจ๋ ์ ๋๋ฉ์ด์
์ ๋ชจ๋ ํ๋ ์)
- Format: RGBA16F (๊ณ ์ ๋ฐ๋)
์์:
- ๋ณธ 50๊ฐ, ์ ๋๋ฉ์ด์
10๊ฐ (๊ฐ 100ํ๋ ์) = 150x1000 ํ
์ค์ฒ
- Pixel[0-2, FrameY] = Bone0์ Rotation(RGBA), Translation(RGB), Scale(RGB)
- Pixel[3-5, FrameY] = Bone1์ ๋ฐ์ดํฐ...
ํ ์ค์ฒ ํฝ์ ์ธ์ฝ๋ฉ:
// ๊ฐ ๋ณธ์ Transform์ 3๊ฐ ํฝ์
์ ์ ์ฅ
FLinearColor RotationPixel = FLinearColor(Quat.X, Quat.Y, Quat.Z, Quat.W);
FLinearColor TranslationPixel = FLinearColor(Pos.X, Pos.Y, Pos.Z, 0);
FLinearColor ScalePixel = FLinearColor(Scale.X, Scale.Y, Scale.Z, 0);
1.3 ๋ณธ ์จ์ดํธ ํ ์ค์ฒ ์์ฑ
// ๊ฐ ์ ์ ์ ๋ณธ ์ธ๋ฑ์ค์ ์จ์ดํธ๋ฅผ ํ
์ค์ฒ์ ์ ์ฅ
// Width: NumVertices
// Height: 1
// RGBA: (BoneIndex0, Weight0, BoneIndex1, Weight1)
2๋จ๊ณ: ๋ฐํ์ CPU (Game Thread)
// TurboSequence_Manager_Lf.cpp
2.1 ์ธ์คํด์ค ์คํฐ
FTurboSequence_MinimalMeshData_Lf AddSkinnedMeshInstance_GameThread(...)
{
// 1. MeshAsset์์ Static Mesh + ํ
์ค์ฒ ์ฐธ์กฐ ๊ฐ์ ธ์ค๊ธฐ
// 2. Niagara System์ ์ธ์คํด์ค ์ถ๊ฐ ์ค๋น
// 3. GPU์ ์ ๋ฌํ ๋ฉํ๋ฐ์ดํฐ ๊ตฌ์ฑ
FTurboSequence_MinimalMeshData_Lf Data;
Data.AnimationStartFrame = 0;
Data.AnimationEndFrame = 100;
Data.CustomData = FVector4f(...); // Niagara User Parameter๋ก ์ ๋ฌ
return Data;
}
2.2 ์ ๋๋ฉ์ด์ ์ ๋ฐ์ดํธ (Concurrent)
void UpdateMeshAnimation_Concurrent(...)
{
// CPU์์๋ ๋ฉํ๋ฐ์ดํฐ๋ง ์
๋ฐ์ดํธ (์ด๋ค ์ ๋๋ฉ์ด์
, ์ด๋ค ํ๋ ์)
Instance.AnimationStartFrame = NewAnimStartFrame;
Instance.AnimationTime += DeltaTime;
// ์ค์ ๋ณธ ๊ณ์ฐ์ GPU์์ ์ํ
}
2.3 Solve (Game Thread)
void SolveMeshes_GameThread(float DeltaTime, ...)
{
// 1. CPU์์ ์
๋ฐ์ดํธ๋ ๋ชจ๋ ์ธ์คํด์ค ๋ฐ์ดํฐ๋ฅผ ๋ชจ์
// 2. Niagara System์ ์ธ์คํด์ค ๋ฐ์ดํฐ ์ ๋ฌ
// 3. GPU Compute Shader ๋์คํจ์น ์ค์ผ์ค๋ง
for (UpdateGroup : UpdateGroups)
{
NiagaraComponent->SetCustomData(InstanceData); // GPU๋ก ์ ์ก
DispatchComputeShaders(UpdateGroup);
}
}
3๋จ๊ณ: ๋ฐํ์ GPU - Compute Shader
// Shaders/Private/BoneSettings_CS_Lf.usf
3.1 ๋ณธ ๋ฐ์ดํฐ ์ค๋น Compute Shader
[numthreads(64, 1, 1)]
void BoneSettings_CS_Lf(uint3 ThreadId : SV_DispatchThreadID)
{
uint InstanceID = ThreadId.x;
// 1. ์ธ์คํด์ค ๋ฉํ๋ฐ์ดํฐ ๋ก๋ (Niagara User Parameters)
float AnimTime = InstanceAnimTimes[InstanceID];
int StartFrame = InstanceStartFrames[InstanceID];
int EndFrame = InstanceEndFrames[InstanceID];
// 2. ํ์ฌ ํ๋ ์ ๊ณ์ฐ
float FrameFloat = StartFrame + (AnimTime * FPS);
int Frame0 = floor(FrameFloat);
int Frame1 = ceil(FrameFloat);
float BlendAlpha = frac(FrameFloat);
// 3. ์ ๋๋ฉ์ด์
๋ผ์ด๋ธ๋ฌ๋ฆฌ ํ
์ค์ฒ์์ ๋ณธ Transform ์ํ๋ง
for (int BoneIdx = 0; BoneIdx < NumBones; BoneIdx++)
{
// ํ
์ค์ฒ UV ๊ณ์ฐ
float U0 = (BoneIdx * 3.0f) / TextureWidth;
float V0 = Frame0 / TextureHeight;
float V1 = Frame1 / TextureHeight;
// ๋ณธ Transform ๋ก๋ (3ํฝ์
= Rotation, Translation, Scale)
float4 Rot0 = AnimLibraryTexture.SampleLevel(Sampler, float2(U0, V0), 0);
float4 Trans0 = AnimLibraryTexture.SampleLevel(Sampler, float2(U0 + 1.0/Width, V0), 0);
float4 Scale0 = AnimLibraryTexture.SampleLevel(Sampler, float2(U0 + 2.0/Width, V0), 0);
float4 Rot1 = AnimLibraryTexture.SampleLevel(Sampler, float2(U0, V1), 0);
float4 Trans1 = AnimLibraryTexture.SampleLevel(Sampler, float2(U0 + 1.0/Width, V1), 0);
float4 Scale1 = AnimLibraryTexture.SampleLevel(Sampler, float2(U0 + 2.0/Width, V1), 0);
// 4. ํ๋ ์ ๋ณด๊ฐ
float4 FinalRot = QuatSlerp(Rot0, Rot1, BlendAlpha);
float3 FinalTrans = lerp(Trans0.xyz, Trans1.xyz, BlendAlpha);
float3 FinalScale = lerp(Scale0.xyz, Scale1.xyz, BlendAlpha);
// 5. ๊ฒฐ๊ณผ๋ฅผ Per-Instance ๋ณธ ๋ฒํผ์ ์ ์ฅ (๋ค์ ๋จ๊ณ์์ ์ฌ์ฉ)
OutBoneTransforms[InstanceID * NumBones + BoneIdx] =
ComposeTransform(FinalRot, FinalTrans, FinalScale);
}
}
3.2 ๋ฉ์ ๋จ์ Compute Shader
// Shaders/Private/MeshUnit_CS_Lf.usf
[numthreads(64, 1, 1)]
void MeshUnit_CS_Lf(uint3 ThreadId : SV_DispatchThreadID)
{
uint InstanceID = ThreadId.x;
// ์ถ๊ฐ ๋ฉ์๋ณ ๋ก์ง (LOD ์ ํ, ๊ฐ์์ฑ ๋ฑ)
// ์ค์ ์ ์ ์คํค๋์ Vertex Shader์์ ์ํ
}
4๋จ๊ณ: ๋ฐํ์ GPU - Vertex Shader (Material)
// Shaders/Private/GPU_VertexSkinning_VS_Lf.ush
// Material Function: MF_TurboSequence_PositionOffset_Lf์์ ํธ์ถ
์ ์ ์คํค๋ ์ํ
float3 TurboSequence_VertexSkinning(
float3 LocalPosition,
uint VertexID,
uint InstanceID)
{
// 1. ๋ณธ ์จ์ดํธ ํ
์ค์ฒ์์ ํด๋น ์ ์ ์ ์ํฅ๋ฐ๋ ๋ณธ ์ ๋ณด ๋ก๋
float4 BoneWeightData = BoneWeightTexture.Load(int3(VertexID, 0, 0));
int BoneIndex0 = int(BoneWeightData.x);
float Weight0 = BoneWeightData.y;
int BoneIndex1 = int(BoneWeightData.z);
float Weight1 = BoneWeightData.w;
// 2. Compute Shader์์ ๊ณ์ฐํ ๋ณธ Transform ๋ก๋
float4x4 BoneMatrix0 = BoneTransforms[InstanceID * NumBones + BoneIndex0];
float4x4 BoneMatrix1 = BoneTransforms[InstanceID * NumBones + BoneIndex1];
// 3. ์ ์ ์ ๋ณธ Transform ์ ์ฉ (์คํค๋)
float3 SkinnedPos0 = mul(float4(LocalPosition, 1.0), BoneMatrix0).xyz;
float3 SkinnedPos1 = mul(float4(LocalPosition, 1.0), BoneMatrix1).xyz;
// 4. ์จ์ดํธ ๋ธ๋ ๋ฉ
float3 FinalPosition = SkinnedPos0 * Weight0 + SkinnedPos1 * Weight1;
// 5. World Position Offset์ผ๋ก ์ถ๋ ฅ
return FinalPosition - LocalPosition; // Offset ๋ฐํ
}
Material Graph ์ฐ๊ฒฐ
Material:
World Position Offset Pin <- MF_TurboSequence_PositionOffset_Lf ์ถ๋ ฅ
โโ VertexID (Vertex Interpolator)
โโ InstanceID (Niagara Mesh Particles ์๋ ์ ๊ณต)
โโ Bone Transform Buffer (Compute Shader ์ถ๋ ฅ)
5๋จ๊ณ: Niagara Instanced Rendering
// Niagara System์ด ์ต์ข
๋ ๋๋ง ์ํ
NiagaraRenderer->DrawInstances(
StaticMesh, // ๋ณํ๋ Static Mesh
Material, // ์ปค์คํ
์คํค๋ Material
InstanceCount, // 10k-50k ์ธ์คํด์ค
PerInstanceData // Compute Shader ๊ฒฐ๊ณผ
);
ํต์ฌ: Niagara๋ ํ๋์ DrawIndexedInstanced ํธ์ถ๋ก ๋ชจ๋ ์ธ์คํด์ค๋ฅผ ๋ ๋๋ง. GPU๋ ๊ฐ ์ธ์คํด์ค๋ง๋ค Vertex Shader๋ฅผ ์คํํ๋ฉฐ, ๊ทธ ์์์ ๋ณธ ์คํค๋์ด ๋ฐ์.
GPU ๋ฉ๋ชจ๋ฆฌ ๋ ์ด์์
GPU Memory:
โโ Animation Library Texture (VRAM)
โ โโ 150x1000x8bytes = ~1.2MB (๋ณธ 50๊ฐ, ํ๋ ์ 1000๊ฐ ๊ธฐ์ค)
โโ Bone Weight Texture (VRAM)
โ โโ NumVertices x 16bytes (์ ์ ๋น 4๊ฐ float)
โโ Bone Transform Buffer (Compute Shader ์ถ๋ ฅ)
โ โโ NumInstances x NumBones x 64bytes (4x4 ํ๋ ฌ)
โ ์: 10k instances x 50 bones = ~30MB
โโ Static Mesh Vertex/Index Buffers (VRAM)
โโ ํ์ค Static Mesh ๋ฐ์ดํฐ
์ฑ๋ฅ ์ต์ ํ ์๋ฆฌ
์ ๋น ๋ฅธ๊ฐ?
- Draw Call ๊ทน์ํ
- ๊ธฐ์กด: 10k ์ธ์คํด์ค = 10k draw calls
- TurboSequence: 10k ์ธ์คํด์ค = 1 draw call (์ํคํ์ ๋น)
- CPU ๋ถํ ์ด๋
- ๊ธฐ์กด: CPU์์ ๋ณธ ํ๋ ฌ ๊ณ์ฐ → GPU๋ก ์ ์ก
- TurboSequence: CPU๋ ๋ฉํ๋ฐ์ดํฐ๋ง ์ ๋ฐ์ดํธ, GPU๊ฐ ๋ชจ๋ ๊ณ์ฐ ์ํ
- ๋ฉ๋ชจ๋ฆฌ ๋์ญํญ ์ต์ ํ
- ์ ๋๋ฉ์ด์ ๋ฐ์ดํฐ๋ฅผ ํ ์ค์ฒ ์์ถ์ผ๋ก ์ ์ก
- ๋ณธ๋น 3ํฝ์ (48๋ฐ์ดํธ) vs ๊ธฐ์กด ๋ณธ ํ๋ ฌ(64๋ฐ์ดํธ)
ํธ๋ ์ด๋์คํ
์ฅ์ :
- ์๋ง ๊ฐ ์ธ์คํด์ค๋ฅผ ๋จ์ผ draw call๋ก ๋ ๋๋ง
- CPU ์ค๋ฒํค๋ ๊ฑฐ์ ์์
- ์ ๋๋ฉ์ด์ ๋ธ๋ ๋ฉ๋ GPU์์ ๊ฐ๋ฅ
๋จ์ :
- Static Mesh ๋ณํ ํ์ (์คํ๋ผ์ธ ์์ )
- ํ ์ค์ฒ ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ (์ ๋๋ฉ์ด์ ๋ง๋ค ํ ์ค์ฒ ์์ฑ)
- ๋ณต์กํ ์ ๋๋ฉ์ด์ ๋ธ๋ ๋ฉ ์ ํ (CPU ๋ก์ง ํ์ ์ ๋ณ๋ชฉ)
- ๋ณธ ๊ฐ์ ์ ํ (30-75๊ฐ ๊ถ์ฅ, ํ ์ค์ฒ ํฌ๊ธฐ ์ด์)
์ฝ๋ ํ๋ฆ ์์ฝ
1. [Editor] Skeletal Mesh → Static Mesh + Animation Textures
2. [CPU GameThread] Spawn Instances → Update Animation Meta
3. [CPU GameThread] SolveMeshes → Dispatch Compute Shaders
4. [GPU Compute] BoneSettings_CS → Calculate Bone Transforms per Instance
5. [GPU Compute] MeshUnit_CS → LOD/Visibility processing
6. [GPU Vertex] Material Vertex Shader → Skinning per Vertex
7. [GPU Raster] Niagara Instanced Draw Call → Final Render
์ค์ ๊ตฌํ ํ์ธ ํ์ผ
- Compute Shader: Shaders/Private/BoneSettings_CS_Lf.usf, MeshUnit_CS_Lf.usf
- Vertex Shader: Shaders/Private/GPU_VertexSkinning_VS_Lf.ush
- Material Functions: MF_TurboSequence_PositionOffset_Lf.uasset
- Texture ์์ฑ: Source/TurboSequence_Lf/Private/TurboSequence_Helper_Lf.cpp::CreateAnimationLibraryTexture2D
- Compute Dispatch: Source/TurboSequence_Lf/Private/TurboSequence_Manager_Lf.cpp::SolveMeshes_GameThread
์ด ๊ตฌ์กฐ๋ก Niagara์ Static Mesh Instancing + GPU Compute Skinning์ ๊ฒฐํฉํ์ฌ ๋๊ท๋ชจ ํฌ๋ผ์ฐ๋ ๋ ๋๋ง์ ๋ฌ์ฑํฉ๋๋ค.
'๊ฐ๋ฐ > UE5' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ
| [UE5/Interchange] FBX ์ํฌํธ LOD ์ฅ์ Guide (0) | 2025.12.08 |
|---|---|
| [UE5] Mutable ํ ์คํธ ๋ถ์ (0) | 2025.09.11 |
| [UE5/Networking] IRIS ๋คํธ์ํฌ ๋ชจ๋ธ ๋ถ์ (1) | 2025.09.05 |
| [Rendering] First Person Rendering (0) | 2025.09.03 |
| [ํ๋ฌ๊ทธ์ธ ๋ถ์] ์ธ๋ฆฌ์ผ Mass Entity Framework์ Unity DOTS ๋น๊ต, ๊ทธ๋ฆฌ๊ณ ํ์ฉ๋ฒ (0) | 2025.09.02 |
- Total
- Today
- Yesterday
- ์ ๋ฆฌ
- HLSL
- Substance
- ์ปดํจํฐ๋คํธ์ํฌ
- ๋ชจ์ ๋ธ๋ฌ
- ๋ ธ์ํ
- ๋คํธ์ํฌ
- unity
- ์ํํธ์จ์ด๊ณตํ
- ๋ฒ์
- ์ ๋ํฐ
- ImageEffect
- ๋ธ๋ฌํจ๊ณผ
- ์ด๋ฏธ์ง ํจ๊ณผ
- ์ปด๋ค
- #Shader #์ ฐ์ด๋ #Tessellator #๋๋ฐ์๊ตญ #๋ฐ์๊ตญ
- MotionBlur
- designer
- ue4
- ์ปดํจํฐ๊ตฌ์กฐ๋ก
- Unreal
- ์ธํ๋
- ์ด๋ฏธ์ง์ดํํธ
- Substance Designer
- shader
- ์ ๋ํฐ ์ ฐ์ด๋
- ์ด์ข ์
- ์๊ณต
- Noise
- normal
| ์ผ | ์ | ํ | ์ | ๋ชฉ | ๊ธ | ํ |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |