1
$\begingroup$

I've recently been working on a particle system in my renderer and I've encountered a situation where my particle class on the C++ side has data I need to maintain my particle on that end, but that I don't need in my shader (velocity, acceleration, life span, start colour, end colour, gravity toggle, etc.).

So presently I've created a second class that just contains my necessary rendering data and I have a function that will copy the needed data from my "full" particle to my "rendering" particle.

class HParticle {
public:
    vec3 m_pos = vec3(0, 0, 0);
    float m_life = 0; // Remaining life of the particle. if < 0 : dead and unused.
    vec4 m_color = vec4(1, 1, 1, 1); // Color
    float m_texture = -1;
    float m_scale = 1;
    vec3 m_orientation;
    float m_featureValue = 0;
    float m_featureSelected = 0;
    float m_emission = 0;
    float m_scaleRate = 0;
    vec4 m_startColor = vec4(1, 1, 1, 1);
    vec4 m_endColor = vec4(1, 1, 1, 1);
    vec3 m_velocity = vec3(0, 0, 0);
    vec3 m_acceleration = vec3(0, 0, 0);
    float m_featureValueRateOfChange = 0;
    float m_totalLife = 0;
    float m_colorLife = 0;
}

struct HStrippedParticle {
    mat4 m_transform;
    vec4 m_color = vec4(1, 1, 1, 1); // Color
    float m_texture = -1;
    float m_featureValue = 0;
    float m_featureSelected = 0;
    float m_emission = 0;
};

I know I'm speaking specifically to particles here but I've encountered this situation a couple of times before and I'm wondering if there's a better general approach than maintaining two lists. One for C++ and one for my shader. And having to update the shader one every time I go to render (I know I could be cleverer and avoid updating data that hasn't changed but still). Maybe something fancy with C++ memory to copy desired chunks from the full class to the stripped class?

Thanks in advance

$\endgroup$
1
  • $\begingroup$ You would think the shaderwiter would be happy to have all the data? But yeah two lists should work $\endgroup$
    – joojaa
    Sep 11 at 18:55
2
$\begingroup$

I typically like to define a "GPUData" struct containing the fields that are going to the shader, and keep the GPUDatas for all the particles in an array. Then, when it's time to update the GPU buffer, you can just memcpy out of that array to your mapped buffer (or you can pass the array to glBufferSubData, etc).

For the CPU state, rather than duplicating the fields that are already in the GPUData, you can just keep the non-GPU fields there. Then your particle simulation code just reads and write the GPUData fields directly.

Pseudocode example:

struct ParticleGPUData
{
    mat4 m_transform;
    vec4 m_color;
    // ...
};
std::vector<ParticleGPUData> particleGPUData;

struct ParticleCPUData
{
    float m_life;
    vec3 m_velocity;
    vec3 m_acceleration;
    // ...
};
std::vector<ParticleCPUData> particleCPUData;

// In your simulation loop
for (int i = 0; i < countParticles; ++i)
{
    ParticleCPUData* cpuData = &particleCPUData[i];
    ParticleGPUData* gpuData = &particleGPUData[i];

    // update the particle...
    cpuData->m_velocity += cpuData->m_acceleration * dT;
    gpuData->m_color = CalculateColor(cpuData);
    // ... etc.
}

// When you go to update the buffer to render
glBufferSubData(
    GL_SHADER_STORAGE_BUFFER, 0,
    particleGPUData.size() * sizeof(ParticleGPUData),
    particleGPUData.data());

Also, it can be handy to keep the GPUData struct definition in a header file that is included by both C++ and GLSL, which ensures there's one definition of it and it will always be in sync between application code and shader code (although you have to watch out for potential alignment differences).

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.