Particles WIP

This commit is contained in:
Brandon Mak 2023-03-13 11:41:23 +08:00
parent eeaa642f30
commit 2f4a316a09
25 changed files with 314 additions and 42 deletions

View File

@ -7,7 +7,7 @@ struct EmitterParameters
vec4 angularMin;
vec4 angularMax;
vec4 lifeAndSizeRange; // min life, max life, min size, max size
}
};
struct ParticleData
{
@ -18,7 +18,7 @@ struct ParticleData
vec4 scaleAndDecay;
float life;
uint textureIndex;
}
};
struct GenericData
{
@ -96,9 +96,15 @@ float rand(inout uint state)
return float(x)*uintBitsToFloat(0x2f800004u);
}
float map(float value, float inMin, float inMax, float outMin, float outMax)
{
return outMin + (outMax - outMin) * (value - inMin) / (inMax - inMin);
}
void main()
{
uint emitterInvocationIndex = gl_GlobalInvocationID.x;
vec4 emitterPosition = emitterPushConstant.emitterPosition;
if (emitterInvocationIndex >= emitterPushConstant.emissionCount)
return;
@ -109,9 +115,18 @@ void main()
ParticleData particle;
int index = freelist.freeIndices[freelistIndex];
particle.position = emitterPosition;
particle.life = emitterParams.10.0f;
// Get seed for randomization
uint pixel_index = uint (emitterPosition.x + emitterPosition.y + floatBitsToUint(genericDataBuffer.data.elapsedTime) * (gl_GlobalInvocationID.x + 1));
uint seed = pcg_hash (pixel_index);
particles[index] = particle;
int index = freelist.freeIndices[freelistIndex];
// emit particle from emitter position
particle.position = vec4 (emitterPosition.xyz, 1.0f);
// randomize life value that ranges from minLife to maxLife
particle.life = map (rand(seed), -1.0f, 1.0f, emitterParams.data.lifeAndSizeRange.x, emitterParams.data.lifeAndSizeRange.y);
inputParticles.data[index] = particle;
}

Binary file not shown.

View File

@ -0,0 +1,3 @@
Name: ParticleEmit_CS
ID: 49959611
Type: 2

View File

@ -19,7 +19,7 @@ struct ParticleData
vec4 scaleAndDecay;
float life;
uint textureIndex;
}
};
struct GenericData
{
@ -73,7 +73,7 @@ layout (std430, set = 2, binding = 4) coherent restrict buffer IndicesData
uint indices[];
};
layout (std140, set = 2, binding = 5) coherent restrict uniform IndirectDrawArgs
layout (std140, set = 2, binding = 5) coherent restrict buffer IndirectDrawArgs
{
DrawArraysIndirectArgs indirectArgs;
};
@ -116,17 +116,12 @@ void main()
ParticleData particle = inputParticles.data[index];
if (particle.lifetime > 0.0f)
if (particle.life > 0.0f)
{
// particle.position += particle.velocity * dt;
// particle.lifetime -= dt;
// particle.size -= 1.2f * dt;
// particle.color += 1.0f * dt;
if (particle.lifetime < 0.0f || particle.size < 0.0f)
if (particle.life < 0.0f || particle.scaleAndDecay.x < 0.0f || particle.scaleAndDecay.y < 0.0f)
{
particle.lifetime = 0.0f;
particle.life = 0.0f;
particle.position.x = 99999.0f;
outputParticles.data[index] = particle;

Binary file not shown.

View File

@ -0,0 +1,3 @@
Name: ParticleUpdate_CS
ID: 36260925
Type: 2

View File

@ -0,0 +1,15 @@
#version 460 core
layout (location = 0) out vec4 fragColor;
// between shader stages
layout(location = 0) in struct
{
vec2 uv; // location = 0
} In;
void main ()
{
fragColor = vec4 (1.0f);
}

Binary file not shown.

View File

@ -0,0 +1,3 @@
Name: Particle_FS
ID: 42509714
Type: 2

View File

@ -0,0 +1,93 @@
#version 460 core
struct GenericData
{
//! Delta time
float dt;
//! Elapsed time of the application
float elapsedTime;
//! Viewport width of the scene (excluding imgui, that means smaller than window)
uint viewportWidth;
//! Ditto but for height
uint viewportHeight;
};
struct ParticleData
{
vec4 position;
vec4 rotation;
vec4 velocity;
vec4 acceleration;
vec4 scaleAndDecay;
float life;
uint textureIndex;
};
layout (set = 0, binding = 0) uniform GenericDataBuffer
{
GenericData data;
} genericDataBuffer;
layout(set = 1, binding = 0) uniform CameraData
{
vec4 position;
mat4 vpMat;
mat4 viewMat;
mat4 projMat;
} cameraData;
// output buffer not needed
layout (std430, set = 2, binding = 2) coherent restrict buffer ParticlesOutputBuffer
{
ParticleData data[];
} outputParticles;
layout (std430, set = 2, binding = 4) coherent restrict buffer IndicesData
{
uint indices[];
};
// between shader stages
layout(location = 0) out struct
{
vec2 uv; // location = 0
} Out;
vec2 CreateQuad (in uint vertexID)
{
uint b = 1 << vertexID;
return vec2 ((0x3 & b) != 0, (0x9 & b) != 0);
}
void main()
{
// Create a quad and its texture coordinates
Out.uv = CreateQuad (gl_VertexIndex);
vec3 vertexPos = vec3 (Out.uv - vec2(0.5f), 0.0f);
ParticleData particle = outputParticles.data[indices[gl_InstanceIndex]];
vec3 normalized = normalize (particle.velocity.xyz);
float angle = atan (normalized.y, normalized.x);
vec2 particleScaleData = particle.scaleAndDecay.xz; // x and y
mat4 localModel = mat4 (1.0f);
localModel[0][0] = particleScaleData.x;
localModel[1][1] = particleScaleData.y;
mat4 rotate = mat4(1.0f);
rotate[0][0] = cos(angle);
rotate[0][1] = sin(angle);
rotate[1][0] = -sin(angle);
rotate[1][1] = cos(angle);
localModel = rotate * localModel;
localModel[3] = vec4 (particle.position.xyz, 1.0f);
gl_Position = cameraData.vpMat * localModel * vec4(vertexPos, 1.0f);
}

Binary file not shown.

View File

@ -0,0 +1,3 @@
Name: Particle_VS
ID: 35035037
Type: 2

View File

@ -30,6 +30,7 @@
#include "../SHEditorWindowManager.h"
#include "../AssetBrowser/SHAssetBrowser.h"
#include "Graphics/MiddleEnd/TrajectoryRendering/SHTrajectoryRenderableComponent.h"
#include "Graphics/MiddleEnd/Particles/SHParticleEmitterComponent.h"
#include "Animation/SHAnimationClip.h"
namespace SHADE
@ -804,4 +805,32 @@ namespace SHADE
ImGui::PopID();
}
template<>
static void DrawComponent(SHParticleEmitterComponent* component)
{
if (!component)
return;
ImGui::PushID(SHFamilyID<SHComponent>::GetID<SHParticleEmitterComponent>());
const auto componentType = rttr::type::get(*component);
SHEditorWidgets::CheckBox("##IsActive", [component]() {return component->isActive; }, [component](bool const& active) {component->isActive = active; }, "Is Component Active");
ImGui::SameLine();
if (ImGui::CollapsingHeader(componentType.get_name().data(), ImGuiTreeNodeFlags_DefaultOpen))
{
DrawContextMenu(component);
SHEditorWidgets::DragFloat("Emission Count", [comp = component]() {return comp->GetEmissionCount(); }, [comp = component](float count) {comp->SetEmissionCount(count);});
SHEditorWidgets::CheckBox("Is Passive", [comp = component]() {return comp->GetPassive(); }, [comp = component](bool flag) {comp->SetPassive(flag); });
}
else
{
DrawContextMenu(component);
}
ImGui::PopID();
}
}

View File

@ -450,7 +450,7 @@ namespace SHADE
*/
/***************************************************************************/
void SHVkCommandBuffer::DrawMultiIndirect(Handle<SHVkBuffer> indirectDrawData, uint32_t drawCount)
void SHVkCommandBuffer::DrawMultiIndirectIndexed(Handle<SHVkBuffer> indirectDrawData, uint32_t drawCount)
{
if (cmdBufferState != SH_CMD_BUFFER_STATE::RECORDING)
{
@ -462,6 +462,19 @@ namespace SHADE
vkCommandBuffer.drawIndexedIndirect(indirectDrawData->GetVkBuffer(), 0, drawCount, sizeof(vk::DrawIndexedIndirectCommand));
}
void SHVkCommandBuffer::DrawMultiIndirect(Handle<SHVkBuffer> indirectDrawData, uint32_t drawCount)
{
if (cmdBufferState != SH_CMD_BUFFER_STATE::RECORDING)
{
SHLOG_ERROR("Command buffer must have started recording before a pipeline can be bound.");
return;
}
if (indirectDrawData)
vkCommandBuffer.drawIndirect(indirectDrawData->GetVkBuffer(), 0, drawCount, sizeof(vk::DrawIndexedIndirectCommand));
}
void SHVkCommandBuffer::ComputeDispatch(uint32_t groupCountX, uint32_t groupCountY, uint32_t groupCountZ) noexcept
{
vkCommandBuffer.dispatch (groupCountX, groupCountY, groupCountZ);

View File

@ -128,9 +128,10 @@ namespace SHADE
void BindDescriptorSet (Handle<SHVkDescriptorSetGroup> descSetGroup, SH_PIPELINE_TYPE bindPoint, uint32_t firstSet, std::span<uint32_t> const dynamicOffsets);
// Draw Commands
void DrawArrays (uint32_t vertexCount, uint32_t instanceCount, uint32_t firstVertex, uint32_t firstInstance) const noexcept;
void DrawIndexed (uint32_t indexCount, uint32_t firstIndex, uint32_t vertexOffset) const noexcept;
void DrawMultiIndirect (Handle<SHVkBuffer> indirectDrawData, uint32_t drawCount);
void DrawArrays (uint32_t vertexCount, uint32_t instanceCount, uint32_t firstVertex, uint32_t firstInstance) const noexcept;
void DrawIndexed (uint32_t indexCount, uint32_t firstIndex, uint32_t vertexOffset) const noexcept;
void DrawMultiIndirectIndexed (Handle<SHVkBuffer> indirectDrawData, uint32_t drawCount);
void DrawMultiIndirect (Handle<SHVkBuffer> indirectDrawData, uint32_t drawCount);
// Compute Commands
void ComputeDispatch (uint32_t groupCountX, uint32_t groupCountY, uint32_t groupCountZ) noexcept;

View File

@ -755,7 +755,7 @@ namespace SHADE
}
}
cmdBuffer->DrawMultiIndirect(drawDataBuffer[frameIndex], static_cast<uint32_t>(drawData.size()));
cmdBuffer->DrawMultiIndirectIndexed(drawDataBuffer[frameIndex], static_cast<uint32_t>(drawData.size()));
cmdBuffer->EndLabeledSegment();
}

View File

@ -251,7 +251,7 @@ namespace SHADE
// particle draw call binding
SHVkDescriptorSetLayout::Binding particleDrawDataBinding
{
.Type = vk::DescriptorType::eUniformBufferDynamic, // UBO (Because lesser data), dynamic (1 set for each frame)
.Type = vk::DescriptorType::eStorageBufferDynamic, // UBO (Because lesser data), dynamic (1 set for each frame)
.Stage = vk::ShaderStageFlagBits::eCompute,
.BindPoint = SHGraphicsConstants::DescriptorSetBindings::PARTICLE_DRAW_DATA,
.DescriptorCount = 1,

View File

@ -730,7 +730,7 @@ namespace SHADE
cmdBuffer->BindVertexBuffer(COLOR_BIND_PT, batch.InstanceColorBuffer[frameIndex], 0);
// Execute draw
cmdBuffer->DrawMultiIndirect(batch.MDIBuffer[frameIndex], static_cast<uint32_t>(batch.MDIData.size()));
cmdBuffer->DrawMultiIndirectIndexed(batch.MDIBuffer[frameIndex], static_cast<uint32_t>(batch.MDIData.size()));
}
void SHDebugDrawSystem::destroyBatch(MeshBatch& batch)

View File

@ -144,6 +144,10 @@ namespace SHADE
//SHAssetManager::CompileAsset("../../Assets/Shaders/Trajectory_FS.glsl", false);
//SHAssetManager::CompileAsset("../../Assets/Shaders/ShadowMapBlur_CS.glsl", false);
//SHAssetManager::CompileAsset("../../Assets/Shaders/Anim_VS.glsl", false);
SHAssetManager::CompileAsset("../../Assets/Shaders/Particle_VS.glsl", false);
SHAssetManager::CompileAsset("../../Assets/Shaders/Particle_FS.glsl", false);
SHAssetManager::CompileAsset("../../Assets/Shaders/ParticleEmit_CS.glsl", false);
SHAssetManager::CompileAsset("../../Assets/Shaders/ParticleUpdate_CS.glsl", false);
// Load Built In Shaders
static constexpr AssetID VS_DEFAULT = 39210065; defaultVertShader = SHResourceManager::LoadOrGet<SHVkShaderModule>(VS_DEFAULT);
@ -165,6 +169,9 @@ namespace SHADE
static constexpr AssetID TRAJECTORY_VS = 41042628; trajectoryVS = SHResourceManager::LoadOrGet<SHVkShaderModule>(TRAJECTORY_VS);
static constexpr AssetID TRAJECTORY_FS = 45635685; trajectoryFS = SHResourceManager::LoadOrGet<SHVkShaderModule>(TRAJECTORY_FS);
static constexpr AssetID SHADOW_BLUR_CS = 38004013; shadowMapBlurCS = SHResourceManager::LoadOrGet<SHVkShaderModule>(SHADOW_BLUR_CS);
static constexpr AssetID PARTICLE_VS = 35035037; particleVS = SHResourceManager::LoadOrGet<SHVkShaderModule>(PARTICLE_VS);
static constexpr AssetID PARTICLE_EMIT_CS = 42509714; particleEmitCS = SHResourceManager::LoadOrGet<SHVkShaderModule>(PARTICLE_EMIT_CS);
static constexpr AssetID PARTICLE_UPDATE_CS = 36260925; particleUpdateCS = SHResourceManager::LoadOrGet<SHVkShaderModule>(PARTICLE_UPDATE_CS);
}

View File

@ -484,6 +484,10 @@ namespace SHADE
Handle<SHVkShaderModule> trajectoryVS;
Handle<SHVkShaderModule> trajectoryFS;
Handle<SHVkShaderModule> shadowMapBlurCS;
Handle<SHVkShaderModule> particleVS;
Handle<SHVkShaderModule> particleFS;
Handle<SHVkShaderModule> particleEmitCS;
Handle<SHVkShaderModule> particleUpdateCS;
// Fonts
Handle<SHFont> testFont;

View File

@ -20,4 +20,24 @@ namespace SHADE
toEmit = true;
}
void SHParticleEmitterComponent::SetEmissionCount(float count) noexcept
{
emissionCount = count;
}
bool SHParticleEmitterComponent::SetPassive(bool flag) noexcept
{
isPassive = flag;
}
float SHParticleEmitterComponent::GetEmissionCount(void) noexcept
{
return emissionCount;
}
bool SHParticleEmitterComponent::GetPassive(void) noexcept
{
return isPassive;
}
}

View File

@ -105,6 +105,12 @@ namespace SHADE
void Emit (void) noexcept;
void SetEmissionCount (float count) noexcept;
bool SetPassive (bool flag) noexcept;
float GetEmissionCount (void) noexcept;
bool GetPassive (void) noexcept;
friend class SHParticleSubSystem;
};

View File

@ -60,7 +60,7 @@ namespace SHADE
}
// buffer to store draw call data. Non-indexed, host-visible mapped, triple buffered.
comp.drawCallData = logicalDevice->CreateBuffer(SHGraphicsConstants::NUM_FRAME_BUFFERS * sizeofIndirectCmd, indirectCommands.data(), SHGraphicsConstants::NUM_FRAME_BUFFERS * sizeofIndirectCmd, vk::BufferUsageFlagBits::eUniformBuffer | vk::BufferUsageFlagBits::eIndirectBuffer, VMA_MEMORY_USAGE_AUTO, VmaAllocationCreateFlagBits::VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | VmaAllocationCreateFlagBits::VMA_ALLOCATION_CREATE_MAPPED_BIT);
comp.drawCallData = logicalDevice->CreateBuffer(SHGraphicsConstants::NUM_FRAME_BUFFERS * sizeofIndirectCmd, indirectCommands.data(), SHGraphicsConstants::NUM_FRAME_BUFFERS * sizeofIndirectCmd, vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eIndirectBuffer, VMA_MEMORY_USAGE_AUTO, VmaAllocationCreateFlagBits::VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT | VmaAllocationCreateFlagBits::VMA_ALLOCATION_CREATE_MAPPED_BIT);
}
@ -150,6 +150,59 @@ namespace SHADE
void SHParticleSubSystem::RenderComponent(Handle<SHVkCommandBuffer> cmdBuffer, SHParticleEmitterComponent& comp, uint32_t frameIndex) noexcept
{
cmdBuffer->DrawMultiIndirect(comp.drawCallData, 1);
}
void SHParticleSubSystem::PreparePrePostUpdateBarriers(std::vector<vk::BufferMemoryBarrier>& preUpdateBarriers, std::vector<vk::BufferMemoryBarrier>& postUpdateBarriers, SHParticleEmitterComponent const& emitter, uint32_t const EMITTER_INDEX, uint32_t const FRAME_INDEX) noexcept
{
// pre-update particles data barrier. Note that this is for input because we want the input to be available before we use it.
vk::BufferMemoryBarrier particleDataBarrier
{
.srcAccessMask = vk::AccessFlagBits::eShaderWrite,
.dstAccessMask = vk::AccessFlagBits::eShaderRead,
.buffer = emitter.particleData->GetVkBuffer(),
.offset = emitter.dynamicOffsets[FRAME_INDEX][DYOFF_INDEX_PARTICLE_INPUT],
.size = emitter.chunkSize
};
// pre-update free list data barrier
vk::BufferMemoryBarrier freelistDataBarrier
{
.srcAccessMask = vk::AccessFlagBits::eShaderWrite,
.dstAccessMask = vk::AccessFlagBits::eShaderRead,
.buffer = emitter.freelistData->GetVkBuffer(),
.offset = 0, // Only 1 copy of freelist data, so offset is at 0
.size = static_cast<uint32_t>(sizeof (uint32_t)) * (emitter.maxParticles + 1)
};
// ...copy assign barriers on heap
preUpdateBarriers[EMITTER_INDEX * 2] = particleDataBarrier;
preUpdateBarriers[(EMITTER_INDEX * 2) + 1] = freelistDataBarrier;
// make new barrier on stack...
vk::BufferMemoryBarrier particleDataBarrierPost
{
.srcAccessMask = vk::AccessFlagBits::eShaderWrite,
.dstAccessMask = vk::AccessFlagBits::eShaderRead,
.buffer = emitter.particleData->GetVkBuffer(),
.offset = emitter.dynamicOffsets[FRAME_INDEX][DYOFF_INDEX_PARTICLE_OUTPUT],
.size = emitter.chunkSize
};
// make new barrier on stack...
vk::BufferMemoryBarrier indicesDataBarrier
{
.srcAccessMask = vk::AccessFlagBits::eShaderWrite,
.dstAccessMask = vk::AccessFlagBits::eShaderRead,
.buffer = emitter.indicesData->GetVkBuffer(),
.offset = emitter.dynamicOffsets[FRAME_INDEX][DYOFF_INDEX_INDICES_DATA],
.size = static_cast<uint32_t>(sizeof(uint32_t)) * emitter.maxParticles
};
// ...copy assign barriers on heap
postUpdateBarriers[EMITTER_INDEX * 2] = particleDataBarrierPost;
postUpdateBarriers[(EMITTER_INDEX * 2) + 1] = indicesDataBarrier;
}
@ -203,10 +256,19 @@ namespace SHADE
// Get offset into GPU emitter data (for updating)
uint32_t emitterDataOffset = frameIndex * sizeof (SHParticleEmitterComponent::GPUEmitterStruct);
// TODO: OPTIONAL but eventually these barriers can be moved to the system as member variables. This would require additional bookkeeping
// but it will be more efficient than populating a vector every frame.
// Barriers to make sure emitting shader is done completely before update is run.
// Every emitter will have its own barrier.
// Every emitter will have its own barrier for its particle data and freelist data. Indices data is not needed since
// it's mainly used in update and rendering so a barrier for it is NOT needed here.
std::vector<vk::BufferMemoryBarrier> preUpdateBarriers{};
preUpdateBarriers.resize(emitters.size());
preUpdateBarriers.resize(emitters.size() * 2);
// After we invoke the updates for the emitters, we need to make sure all particles and indices data are done updating
// before we issue them for rendering.
std::vector<vk::BufferMemoryBarrier> postUpdateBarriers{};
postUpdateBarriers.resize(emitters.size() * 2);
// If we wanted to be VERY safe, a barrier would be good here to make sure output particles have finish reading input particles in
// the update compute. HOWEVER since a NUM_FRAME_BUFFERS frames would have passed by then, we will not insert 1 here.
@ -255,19 +317,10 @@ namespace SHADE
EmitComponent(cmdBuffer, emitter, frameIndex);
}
// make new barrier on stack...
vk::BufferMemoryBarrier barrier
{
.srcAccessMask = vk::AccessFlagBits::eShaderWrite,
.dstAccessMask = vk::AccessFlagBits::eShaderRead,
.buffer = emitter.particleData->GetVkBuffer(),
.offset = emitter.dynamicOffsets[frameIndex][DYOFF_INDEX_PARTICLE_INPUT],
.size = emitter.chunkSize
};
// ...copy assign barrier on heap
preUpdateBarriers[i] = barrier;
// prepare barriers
PreparePrePostUpdateBarriers(preUpdateBarriers, postUpdateBarriers, emitter, i, frameIndex);
// Emitter will emit once and stop emitting next frame (unless specified before reaching here to do so).
emitter.toEmit = false;
++i;
}
@ -275,6 +328,8 @@ namespace SHADE
// issue the barrier to wait
cmdBuffer->PipelineBarrier(vk::PipelineStageFlagBits::eComputeShader, vk::PipelineStageFlagBits::eComputeShader, {}, {}, preUpdateBarriers, {});
/*-----------------------------------------------------------------------*/
/* EMITTING PARTICLES DONE, BEGIN UPDATES.... */
/*-----------------------------------------------------------------------*/
@ -287,6 +342,11 @@ namespace SHADE
UpdateCompoennt(cmdBuffer, emitter, frameIndex);
}
/*-----------------------------------------------------------------------*/
/* AFTER UPDATING, RENDER PARTICLES */
/*-----------------------------------------------------------------------*/
// issue the barrier to wait for output particles to be done updating and indices data to finish being modified.
cmdBuffer->PipelineBarrier(vk::PipelineStageFlagBits::eComputeShader, vk::PipelineStageFlagBits::eComputeShader, {}, {}, postUpdateBarriers, {});
}
@ -297,7 +357,7 @@ namespace SHADE
// TODO: Issue barrier for output particle data. Semaphore should also be issued outside in SHGraphicsSystem
for (auto& emitter : emitters)
{
RenderComponent(cmdBuffer, emitter, frameIndex);
}
}

View File

@ -81,6 +81,8 @@ namespace SHADE
void UpdateCompoennt(Handle<SHVkCommandBuffer> cmdBuffer, SHParticleEmitterComponent& comp, uint32_t frameIndex) noexcept;
void RenderComponent(Handle<SHVkCommandBuffer> cmdBuffer, SHParticleEmitterComponent& comp, uint32_t frameIndex) noexcept;
void PreparePrePostUpdateBarriers (std::vector<vk::BufferMemoryBarrier>& preUpdateBarriers, std::vector<vk::BufferMemoryBarrier>& postUpdateBarriers, SHParticleEmitterComponent const& emitter, uint32_t const EMITTER_INDEX, uint32_t const FRAME_INDEX) noexcept;
public:
void Init(Handle<SHVkLogicalDevice> device, Handle<SHVkDescriptorPool> inDescPool, Handle<SHVkRenderpass> compatibleRenderpass, Handle<SHSubpass> subpass, Handle<SHVkShaderModule> VS, Handle<SHVkShaderModule> FS, Handle<SHVkShaderModule> emitCS, Handle<SHVkShaderModule> defaultUpdateCS) noexcept;

View File

@ -198,7 +198,7 @@ namespace SHADE
cmdBuffer->BindVertexBuffer(SHGraphicsConstants::VertexBufferBindings::TRAJECTORY_TRANSFORM, transformBuffer, 0);
// call draw call
cmdBuffer->DrawMultiIndirect(drawDataBuffer, drawData.size());
cmdBuffer->DrawMultiIndirectIndexed(drawDataBuffer, drawData.size());
// clear CPU transform and draw data
transformData.clear();