Efficiently making a particle system without textures
Asked Answered
C

1

0

I am trying to make a particle system where instead of a texture, a quad is rendered by a fragment shader such as below.

uniform vec3 color;
uniform float radius;
uniform float edge;
uniform vec2 position;
uniform float alpha;

void main()
{
    float dist = distance(gl_FragCoord.xy, position);
    float intensity = smoothstep(dist-edge, dist+edge, radius);

    gl_FragColor = vec4(color, intensity*alpha);
}

Each particle is an object of a c++ class that wraps this shader and all the variables together and draws it. I use openFrameworks so the exact openGL calls are hidden from me.

I've read that usually particle systems are done with textures, however, I prefer to do it like this because this way I can add more functionality to the particles. The problem is that after only 30 particles, the framerate drops dramatically. Is there a more efficient way of doing this? I was thinking of maybe putting the variables for each particle into an array and sending these arrays into one fragment shader that then renders all particles in one go. But this would mean that the amount of particles would be fixed because the uniform arrays in the shader would have to be declared beforehand.

Are non-texture-based particle systems simply too inefficient to be realistic, or is there a way of designing this that I'm overlooking?

Cassock answered 15/9, 2014 at 19:24 Comment(2)
I don't know openFrameworks, but I assume that for every particle you draw an OpenGL draw call is emitted and probably also some bindings. While this shouldn't be a huge problem for just 30 particles, the performance is not the best with that approach. The problem is not that you use a shader and not a texture, but that at least 2 API calls are done for every particle. A better way would be to store all particles in one VBO. You could either store their full geometry or use glDrawArraysInstanced and/or Geometry-Shader.Sphygmograph
Actually, not accessing a texture in the shader should be slightly faster, so the bottleneck is surely somewhere else. Most likely in the way you are drawing the particles. Like @Sphygmograph commented, you should batch particles together and draw a bunch at a time.Overburdensome
U
2

The reason textures are used is because you can move the particles using the GPU, which is very fast. You'd double buffer a texture which stores particle attributes (like position) per texel, and ping-pong data between them, using a framebuffer object to draw to them and the fragment shader to do the computation, rendering a full screen polygon. Then you'd draw an array of quads and read the texture to get the positions.

Instead of a texture storing attributes you could pack them directly into your VBO data. This gets complicated because you have multiple vertices per particle, but can still be done a number of ways. glVertexBindingDivisor (requires instancing), drawing points, or using the geometry shader come to mind. Transform feedback or image_load_store could be used to update VBOs with the GPU instead of textures.

If you move particles with the CPU, you also need to copy the data to the GPU every frame. This is quite slow, but nothing like 30 particles being a problem slow. This is probably to do with the number of draw calls. Each time you draw something there's a tonne of stuff GL does to set up the operation. Setting uniform values per-primitive (nearly) is very expensive for the same reason. Particles work well if you have arrays of data that gets processed by a manager all at once. They parallelize very well in such cases. Generally their computation is cheap and it all comes down to minimizing memory and keeping good locality.

If you want to keep particle updating CPU side, I'd go with this:

  1. Create a VBO full of -1 to 1 quads (two triangles, 6 verts) and element array buffer to draw them. This buffer will remain static in GPU memory and is what you use to draw the particles all at once with a single draw call.
  2. Create a texture (could be 1D) or VBO (if you choose one of the above methods) that contains positions and particle attributes that update pretty much every frame (using glTexImage1D/glBufferData/glMapBuffer).
  3. Create another texture with particle attributes that rarely update (e.g. only when you spawn them). You can send updates with glTexSubImage1D/glBufferSubData/glMapBufferRange.

When you draw the particles, read position and other attributes from the texture (or attributes if you used VBOs) and use the -1 to 1 quads in the main geometry VBO as offsets to your position.

Un answered 16/9, 2014 at 8:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.