Skip to content

Add computeStyle demos #354

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion openrndr-demos/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ dependencies {
demoImplementation(project(":orx-jvm:orx-gui"))
demoImplementation(project(":orx-shader-phrases"))
demoImplementation(project(":orx-camera"))
demoImplementation(project(":orx-color"))
demoImplementation(project(":orx-composition"))
demoImplementation(project(":orx-shapes"))
demoImplementation(project(":orx-svg"))
demoImplementation(libs.slf4j.simple)
demoImplementation(libs.openrndr.ffmpeg)
}
}
97 changes: 97 additions & 0 deletions openrndr-demos/src/demo/kotlin/DemoComputeStyle01.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import org.openrndr.application
import org.openrndr.draw.*
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3

/**
* SSBO -> https://www.khronos.org/opengl/wiki/Shader_Storage_Buffer_Object
*
* This program demonstrates
* - how to create an SSBO
* - how to populate an SSBO with data
* - how to pass an SSBO to a compute shader
* - how to download the SSBO data to the CPU before and after executing the compute shader
*
* It prints the before/after data to observe how it was modified by the compute shader.
*
* Writing a useful compute shader that processes data faster than in the CPU is NOT a goal
* of this program, since such a simple calculation would be faster and easier if done completely in the CPU.
*
* Notice how the execute() call has a width, height and depth of 1
* (basically doing just one computation).
*
* A useful compute shader would do a large number of parallel computations.
* This will be presented in a different demo.
*
* Output: byteBuffer -> text
*/

fun main() = application {
program {
// Define SSBO format
val fmt = shaderStorageFormat {
primitive("time", BufferPrimitiveType.FLOAT32)
primitive("vertex", BufferPrimitiveType.VECTOR2_FLOAT32, 3)
struct("Particle", "particles", 5) {
primitive("pos", BufferPrimitiveType.VECTOR3_FLOAT32)
primitive("age", BufferPrimitiveType.FLOAT32)
}
}
println("Study the padding in the format:\n$fmt\n")

// Create SSBO
val ssbo = shaderStorageBuffer(fmt)

// Populate SSBO
ssbo.put {
write(3.0.toFloat()) // time
repeat(3) {
write(Vector2(1.1, 1.2)) // vertex
}
repeat(5) {
write(Vector3(2.1, 2.2, 2.3)) // pos
write(1.0.toFloat()) // age
}
}

// Create Compute Shader
val cs = computeStyle {
computeTransform = """
b_myData.time = 3.3;
b_myData.vertex[0] = vec2(7.01);
b_myData.vertex[1] = vec2(7.02);
b_myData.vertex[2] = vec2(7.03);
b_myData.particles[0].pos = vec3(112.0);
b_myData.particles[0].age = 111.0;
""".trimIndent()
}
cs.buffer("myData", ssbo)

// Download SSBO data to CPU
val byteBufferBeforeExecute = ssbo.createByteBuffer()
byteBufferBeforeExecute.rewind()
ssbo.read(byteBufferBeforeExecute)

// Execute compute shader
cs.execute(1, 1, 1)

// Download SSBO data to CPU
val byteBufferAfterExecute = ssbo.createByteBuffer()
byteBufferAfterExecute.rewind()
ssbo.read(byteBufferAfterExecute)

// Debugging

// Notice the (maybe unexpected) 0.0 padding values printed on the console.
// Depending on the variable size in bytes, padding may be added by the system
// to align them in memory. This will depend on the sizes of the involved variables,
// and their order. For instance, a vec3 and a float do not require padding, but
// a float followed by a vec3 pads the float with 3 values, and the vec3 with one.
// Run compute02.kt and study the output to observe a more inefficient layout.
byteBufferBeforeExecute.rewind()
byteBufferAfterExecute.rewind()
repeat(ssbo.format.size / 4) {
println("$it: ${byteBufferBeforeExecute.float} -> ${byteBufferAfterExecute.float}")
}
}
}
83 changes: 83 additions & 0 deletions openrndr-demos/src/demo/kotlin/DemoComputeStyle02.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import org.openrndr.application
import org.openrndr.draw.*
import org.openrndr.math.Vector2
import org.openrndr.math.Vector3

/**
* A program identical to compute01.kt, except that the order of variables
* `age` and `pos` have been swapped, resulting in a less ideal memory layout.
* In this case the SSBO requires 192 bytes instead of 112, and padding is
* inserted after the variables `time`, `age` and `pos`.
*
* Output: byteBuffer -> text
*/

fun main() = application {
program {
// Define SSBO format
val fmt = shaderStorageFormat {
primitive("time", BufferPrimitiveType.FLOAT32)
primitive("vertex", BufferPrimitiveType.VECTOR2_FLOAT32, 3)
struct("Particle", "particles", 5) {
primitive("age", BufferPrimitiveType.FLOAT32)
primitive("pos", BufferPrimitiveType.VECTOR3_FLOAT32)
}
}
println("Study the padding in the format:\n$fmt\n")

// Create SSBO
val ssbo = shaderStorageBuffer(fmt)

// Populate SSBO
ssbo.put {
write(3.0.toFloat()) // time
repeat(3) {
write(Vector2(1.1, 1.2)) // vertex
}
repeat(5) {
write(1.0.toFloat()) // age
write(Vector3(2.1, 2.2, 2.3))// pos
}
}

// Create Compute Shader
val cs = computeStyle {
computeTransform = """
b_myData.time = 3.3;
b_myData.vertex[0] = vec2(7.01);
b_myData.vertex[1] = vec2(7.02);
b_myData.vertex[2] = vec2(7.03);
b_myData.particles[0].pos = vec3(112.0);
b_myData.particles[0].age = 111.0;
""".trimIndent()
}
cs.buffer("myData", ssbo)

// Download SSBO data to CPU
val byteBufferBeforeExecute = ssbo.createByteBuffer()
byteBufferBeforeExecute.rewind()
ssbo.read(byteBufferBeforeExecute)

// Execute compute shader
cs.execute(1, 1, 1)

// Download SSBO data to CPU
val byteBufferAfterExecute = ssbo.createByteBuffer()
byteBufferAfterExecute.rewind()
ssbo.read(byteBufferAfterExecute)

// Debugging

// Notice the (maybe unexpected) 0.0 padding values printed on the console.
// Depending on the variable size in bytes, padding may be added by the system
// to align them in memory. This will depend on the sizes of the involved variables,
// and their order. For instance, a vec3 and a float do not require padding, but
// a float followed by a vec3 pads the float with 3 values, and the vec3 with one.
// Run compute02.kt and study the output to observe a more inefficient layout.
byteBufferBeforeExecute.rewind()
byteBufferAfterExecute.rewind()
repeat(ssbo.format.size / 4) {
println("$it: ${byteBufferBeforeExecute.float} -> ${byteBufferAfterExecute.float}")
}
}
}
88 changes: 88 additions & 0 deletions openrndr-demos/src/demo/kotlin/DemoComputeStyle03.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import org.openrndr.application
import org.openrndr.draw.*
import org.openrndr.math.IntVector3

/**
* This program demonstrates
* - how to use a compute shader and an SSBO to do many computations in parallel
* - how to use a compute shader to initialize an SSBO
* - how to use a different shader to update the SSBO
*
* Note the `workGroupSize` property. The GPU splits tasks
* into chunks and computes those in parallel. The ideal workGroupSize depends on
* the GPU being used. Too small of a size may be inefficient.
*
* In some cases a compute shader works with 2D images or 3D data structures, but in this
* program we are processing the elements of a 1D array. That's why we only
* increase the x value to 32, leaving y and z equal to 1.
*
* Note: this program only does the computation, but does not visualize the results
* in any way. We will do that in another program.
*
* Output: none
*/

fun main() = application {
program {
val particleCount = 4800
// Define SSBO format
val fmt = shaderStorageFormat {
struct("Particle", "particle", particleCount) {
primitive("pos", BufferPrimitiveType.VECTOR2_FLOAT32)
primitive("velocity", BufferPrimitiveType.VECTOR2_FLOAT32)
}
}
println("Study the padding in the format:\n$fmt\n")

// Create SSBO
val particlesSSBO = shaderStorageBuffer(fmt)

// Create Compute Shaders
val initCS = computeStyle {
computeTransform = """
uint id = gl_GlobalInvocationID.x;
b_particles.particle[id].pos = vec2(320.0, 240.0);
b_particles.particle[id].velocity = vec2(cos(id), sin(id));
""".trimIndent()
workGroupSize = IntVector3(32, 1, 1)
}
val updateCS = computeStyle {
computeTransform = """
// The id of the element being currently processed
uint id = gl_GlobalInvocationID.x;

// Add velocity to position
b_particles.particle[id].pos += b_particles.particle[id].velocity;

// Deal with the particle trying to escape the window
if(b_particles.particle[id].pos.x < 0.0) {
b_particles.particle[id].pos.x = 0.0;
b_particles.particle[id].velocity.x = abs(b_particles.particle[id].velocity.x);
}
if(b_particles.particle[id].pos.y < 0.0) {
b_particles.particle[id].pos.y = 0.0;
b_particles.particle[id].velocity.y = abs(b_particles.particle[id].velocity.y);
}
if(b_particles.particle[id].pos.x > p_windowSize.x) {
b_particles.particle[id].pos.x = p_windowSize.x;
b_particles.particle[id].velocity.x = -abs(b_particles.particle[id].velocity.x);
}
if(b_particles.particle[id].pos.y > p_windowSize.y) {
b_particles.particle[id].pos.y = p_windowSize.y;
b_particles.particle[id].velocity.y = -abs(b_particles.particle[id].velocity.y);
}
""".trimIndent()
workGroupSize = IntVector3(32, 1, 1)
}

// Execute initCS
initCS.buffer("particles", particlesSSBO)
initCS.execute(particleCount / initCS.workGroupSize.x)

extend {
updateCS.buffer("particles", particlesSSBO)
updateCS.parameter("windowSize", drawer.bounds.dimensions)
updateCS.execute(particleCount / updateCS.workGroupSize.x)
}
}
}
107 changes: 107 additions & 0 deletions openrndr-demos/src/demo/kotlin/DemoComputeStyle04.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import org.openrndr.application
import org.openrndr.color.ColorRGBa
import org.openrndr.draw.*
import org.openrndr.math.IntVector3

/**
* This program draws moving points to demonstrate
* how to write the resulting calculations of a compute shader into a vertex buffer.
*
* There are various ways to output the calculations and make them visible to the user:
* - Write into a vertex buffer, which can be rendered as points, lines or triangles by OPENRNDR
* (this program).
* - Update a colorBuffer (the pixels of a texture).
*
* Output: vertexBuffer -> POINTS
*/

fun main() = application {
program {
val particleCount = 4800
// Define SSBO format
val fmt = shaderStorageFormat {
struct("Particle", "particle", particleCount) {
primitive("pos", BufferPrimitiveType.VECTOR2_FLOAT32)
primitive("velocity", BufferPrimitiveType.VECTOR2_FLOAT32)
}
}
println("Study the padding in the format:\n$fmt\n")

// Create SSBO
val particleSSBO = shaderStorageBuffer(fmt)

// Create a vertex buffer.
// Padding is required if position has less than 4 dimensions.
// val vb = vertexBuffer(vertexFormat {
// position(3)
// paddingFloat(1)
// }, particleCount)

// With BufferAlignment.STD430 padding is taken care of
val vb = vertexBuffer(vertexFormat(BufferAlignment.STD430) {
position(3)
}, particleCount)

// Create Compute Shaders
val initCS = computeStyle {
computeTransform = """
uint id = gl_GlobalInvocationID.x;
b_particles.particle[id].pos = vec2(320.0, 240.0);
b_particles.particle[id].velocity = vec2(cos(id), sin(id));
""".trimIndent()
workGroupSize = IntVector3(32, 1, 1)
}
val updateCS = computeStyle {
// We can create GLSL functions in the computePreamble.
// Thanks to an `inout` variable, we can shorten the code.
// (Compare this with compute03.kt)
computePreamble = """
void updateParticle(inout Particle p) {
// Add velocity to position
p.pos += p.velocity;

// Deal with the particle trying to escape the window
if(p.pos.x < 0.0) {
p.pos.x = 0.0;
p.velocity.x = abs(p.velocity.x);
}
if(p.pos.y < 0.0) {
p.pos.y = 0.0;
p.velocity.y = abs(p.velocity.y);
}
if(p.pos.x > p_windowSize.x) {
p.pos.x = p_windowSize.x;
p.velocity.x = -abs(p.velocity.x);
}
if(p.pos.y > p_windowSize.y) {
p.pos.y = p_windowSize.y;
p.velocity.y = -abs(p.velocity.y);
}
}
""".trimIndent()
computeTransform = """
// The id of the element being currently processed
uint id = gl_GlobalInvocationID.x;
updateParticle(b_particles.particle[id]);

// Update the vertexBuffer with data from the shaderStorageBuffer
b_vb.vertex[id].position.xy = b_particles.particle[id].pos;
""".trimIndent()
workGroupSize = IntVector3(32, 1, 1)
}

// Execute initCS
initCS.buffer("particles", particleSSBO)
initCS.execute(particleCount / initCS.workGroupSize.x)

extend {
updateCS.buffer("vb", vb.shaderStorageBufferView())
updateCS.buffer("particles", particleSSBO)
updateCS.parameter("windowSize", drawer.bounds.dimensions)
updateCS.execute(particleCount /updateCS.workGroupSize.x)

drawer.fill = ColorRGBa.WHITE
drawer.vertexBuffer(vb, DrawPrimitive.POINTS)
}
}
}
Loading