Particle system with compute shader

I was trying to learn compute shaders, and for this reason I decided to go for a simple particle system. And it works, but there are things I still don’t understand, marked with FIXME:

import org.openrndr.Fullscreen
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.*
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3
import org.openrndr.math.transforms.transform

fun main() = application {
  configure {
    fullscreen = Fullscreen.CURRENT_DISPLAY_MODE
  }
  program {
    val computeWidth = 1000
    val computeHeight = 100
    val particleCount = computeWidth * computeHeight
    val computeShader = ComputeShader.fromCode(
        """
#version 430
// FIXME why 16?
layout(local_size_x = 16, local_size_y = 16) in;

uniform int computeWidth;
uniform float width;
uniform float height;

struct ParticleTransform {
  mat4 transform;
};

struct ParticleProperties {
  vec2 velocity;
};

layout(binding=0) buffer transformsBuffer {
    ParticleTransform transforms[];
};

layout(binding=1) buffer propertiesBuffer {
    ParticleProperties properties[];
};

void main() {
    // FIXME is offset calculated correctly?
    const uint offset = gl_GlobalInvocationID.x + gl_GlobalInvocationID.y * computeWidth;
    ParticleTransform pt = transforms[offset];
    ParticleProperties pp = properties[offset];
    vec2 position = vec2(pt.transform[3][0], pt.transform[3][1]);
    if ((position.x < 0) || (position.x > width)) {
      properties[offset].velocity *= vec2(-1, 1);
    }
    if ((position.y < 0) || (position.y > height)) {
      properties[offset].velocity *= vec2(1, -1);
    }    
    position += properties[offset].velocity;
    transforms[offset].transform[3][0] = position.x;
    transforms[offset].transform[3][1] = position.y;
}
    """
    )
    
    // -- create the vertex buffer
    val geometry = vertexBuffer(vertexFormat {
      position(3)
    }, 4)

    // -- fill the vertex buffer with vertices for a unit quad
    geometry.put {
      write(Vector3(-1.0, -1.0, 0.0))
      write(Vector3(-1.0, 1.0, 0.0))
      write(Vector3(1.0, -1.0, 0.0))
      write(Vector3(1.0, 1.0, 0.0))
    }

    // -- create the secondary vertex buffer, which will hold particle transformations
    val transformsBuffer = vertexBuffer(vertexFormat {
      attribute("transform", VertexElementType.MATRIX44_FLOAT32)
    }, particleCount)

    // FIXME would it be possible to somehow hold it in the transformsBuffer?
    // -- create the tertiary vertex buffer, which will hold particle properties
    val propertiesBuffer = vertexBuffer(vertexFormat {
      attribute("velocity", VertexElementType.VECTOR2_FLOAT32)
    }, particleCount)

    // -- fill the initial transform buffer
    transformsBuffer.put {
      for (i in 0 until particleCount) {
        write(transform {
          translate(Math.random() * width, Math.random() * height)
          rotate(Vector3.UNIT_Z, Math.random() * 360.0)
          scale(1.0 + Math.random() * 3.0)
        })
      }
    }

    propertiesBuffer.put {
      for (i in 0 until particleCount) {
        // velocity
        write(Vector2((Math.random() * .1 - .05), Math.random() * .1 - .05))
      }
    }
    computeShader.uniform("computeWidth", computeWidth)
    computeShader.uniform("width", width.toDouble())
    computeShader.uniform("height", height.toDouble())
    computeShader.buffer("transformsBuffer", transformsBuffer)
    computeShader.buffer("propertiesBuffer", propertiesBuffer)
    extend {
      drawer.isolated {
        fill = ColorRGBa.PINK.opacify(.5)
        shadeStyle = shadeStyle {
          vertexTransform = "x_viewMatrix = x_viewMatrix * i_transform;"
          // FIXME assuming that I have my custom fragmentTransform, is there anyway to pass particle properties buffer to it?
        }
        vertexBufferInstances(
            listOf(geometry), listOf(transformsBuffer), DrawPrimitive.TRIANGLE_STRIP, particleCount
        )
      }
      computeShader.execute(computeWidth, computeHeight)
    }
  }
}

I still don’t understand what layout(local_size_x = 16, local_size_y = 16) in; stands for and why it doesn’t work with size 1. Also what are the performance implications. But I guess this I have to read on my own.

With this program I can animate up to half a million particles on my integrated intel GPU, even bigger particles than 1 pixel in size. I guess not bad. I was wondering if it is the most efficient way of doing it. Maybe rendering points instead of quads, increasing their size, and using custom fragment shader, would be even faster. I also wonder if I should use transformation matrix, or rather use compute shader to rewrite vertices buffer contents?

And also I was wondering if there is any way I can pass my particle properties buffer (velocity) to the fragment shader or fragmentTransform?

So my code just works, but I was also reading about memory consistency of buffers, should I be afraid of that?

I’ll share some resources that I’ve gathered on compute shaders:

Unfortunately I can only post 2 links atm, or I’d also share GameWorks OpenGL ES Graphics Samples Documentation from Nvidia which has open-source demos with several examples.

1 Like

Thank you, great resources. I will dive deeper into it.

This seems to deal with the purpose of the local_size variable: