diff --git a/openrndr-demos/src/demo/kotlin/DemoComputeStyle01.kt b/openrndr-demos/src/demo/kotlin/DemoComputeStyle01.kt new file mode 100644 index 00000000..263346a1 --- /dev/null +++ b/openrndr-demos/src/demo/kotlin/DemoComputeStyle01.kt @@ -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}") + } + } +} \ No newline at end of file diff --git a/openrndr-demos/src/demo/kotlin/DemoComputeStyle02.kt b/openrndr-demos/src/demo/kotlin/DemoComputeStyle02.kt new file mode 100644 index 00000000..b57cd04e --- /dev/null +++ b/openrndr-demos/src/demo/kotlin/DemoComputeStyle02.kt @@ -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}") + } + } +} \ No newline at end of file diff --git a/openrndr-demos/src/demo/kotlin/DemoComputeStyle03.kt b/openrndr-demos/src/demo/kotlin/DemoComputeStyle03.kt new file mode 100644 index 00000000..b9f8f2ac --- /dev/null +++ b/openrndr-demos/src/demo/kotlin/DemoComputeStyle03.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/openrndr-demos/src/demo/kotlin/DemoComputeStyle04.kt b/openrndr-demos/src/demo/kotlin/DemoComputeStyle04.kt new file mode 100644 index 00000000..e4579554 --- /dev/null +++ b/openrndr-demos/src/demo/kotlin/DemoComputeStyle04.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/openrndr-demos/src/demo/kotlin/DemoComputeStyle05.kt b/openrndr-demos/src/demo/kotlin/DemoComputeStyle05.kt new file mode 100644 index 00000000..985ce2a3 --- /dev/null +++ b/openrndr-demos/src/demo/kotlin/DemoComputeStyle05.kt @@ -0,0 +1,97 @@ +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.draw.* +import org.openrndr.math.IntVector3 + +/** + * This program demonstrates how to change the rendering type from POINTS to LINE_LOOP. + * In everything else, it's identical to compute04.kt + * + * Output: vertexBuffer -> LINE_LOOP + */ + +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 { + 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]); + 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.LINE_LOOP) + } + } +} \ No newline at end of file diff --git a/openrndr-demos/src/demo/kotlin/DemoComputeStyle06.kt b/openrndr-demos/src/demo/kotlin/DemoComputeStyle06.kt new file mode 100644 index 00000000..d9421352 --- /dev/null +++ b/openrndr-demos/src/demo/kotlin/DemoComputeStyle06.kt @@ -0,0 +1,93 @@ +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.draw.* +import org.openrndr.math.IntVector3 + +/** + * This program is almost identical to compute04.kt, but instead of writing into + * a vertex buffer, it draws the particles into a 2D image (a ColorBuffer). + * + * Output: 2D Image + */ + +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 color buffer to write into. + val cb = colorBuffer(width, height, type = ColorType.FLOAT32) + + // 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 { + 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]); + + // write into the image + imageStore(p_img, ivec2(b_particles.particle[id].pos.xy), vec4(1.0)); + """.trimIndent() + workGroupSize = IntVector3(32, 1, 1) + } + // Execute initCS + initCS.buffer("particles", particleSSBO) + initCS.execute(particleCount, initCS.workGroupSize.x) + + extend { + // Clear the image, otherwise all pixels become eventually white + cb.fill(ColorRGBa.TRANSPARENT) + + // Pass image to the compute shader. + // We can choose between READ, READ_WRITE or WRITE. + updateCS.image("img", cb.imageBinding(0, ImageAccess.WRITE)) + updateCS.buffer("particles", particleSSBO) + updateCS.parameter("windowSize", drawer.bounds.dimensions) + updateCS.execute(particleCount / updateCS.workGroupSize.x) + + drawer.image(cb) + } + } +} \ No newline at end of file diff --git a/openrndr-demos/src/demo/kotlin/DemoComputeStyle07.kt b/openrndr-demos/src/demo/kotlin/DemoComputeStyle07.kt new file mode 100644 index 00000000..edd92235 --- /dev/null +++ b/openrndr-demos/src/demo/kotlin/DemoComputeStyle07.kt @@ -0,0 +1,120 @@ +package compute + +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.draw.* +import org.openrndr.math.IntVector3 + +/** + * This program demonstrates + * - how to modify the particles, adding color, age and ageVelocity. + * - make the particles wrap around the edges instead of bouncing on them. + * + * Output: 2D Image + */ + +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) + primitive("color", BufferPrimitiveType.VECTOR3_FLOAT32) + primitive("age", BufferPrimitiveType.FLOAT32) + primitive("ageVelocity", BufferPrimitiveType.FLOAT32) + } + } + println("Study the padding in the format:\n$fmt\n") + + // Create SSBO + val particleSSBO = shaderStorageBuffer(fmt) + + // Create a color buffer to write into. + val cb = colorBuffer(width, height, type = ColorType.FLOAT32) + + // Create Compute Shaders + val initCS = computeStyle { + computePreamble = """ + // From lygia.xyz + vec3 hue2rgb(const in float hue) { + float R = abs(hue * 6.0 - 3.0) - 1.0; + float G = 2.0 - abs(hue * 6.0 - 2.0); + float B = 2.0 - abs(hue * 6.0 - 4.0); + return clamp(vec3(R,G,B), 0.0, 1.0); + } + void initParticle(uint id, inout Particle p) { + float k = 100.0 / $particleCount; + p.velocity = vec2(cos(id * k), sin(id * k)); + p.pos = vec2(320.0, 240.0) + p.velocity * id * 0.0003; + p.color = hue2rgb(fract(id * k / 4.0)); + p.age = id * k / 5.0; + p.ageVelocity = sin(id * k * 1.0) * 0.1 + 0.2; + } + """.trimIndent() + computeTransform = """ + uint id = gl_GlobalInvocationID.x; + initParticle(id, b_particles.particle[id]); + """.trimIndent() + workGroupSize = IntVector3(32, 1, 1) + } + val updateCS = computeStyle { + computePreamble = """ + void updateParticle(inout Particle p) { + // Add velocity to position + p.pos += p.velocity; + + // Update age + p.age += p.ageVelocity; + + // Deal with the particle trying to escape the window + if(p.pos.x < 0.0) { + p.pos.x += p_windowSize.x; + } + if(p.pos.y < 0.0) { + p.pos.y += p_windowSize.y; + } + if(p.pos.x > p_windowSize.x) { + p.pos.x = 0.0; + } + if(p.pos.y > p_windowSize.y) { + p.pos.y = 0.0; + } + } + """.trimIndent() + computeTransform = """ + // The id of the element being currently processed + uint id = gl_GlobalInvocationID.x; + updateParticle(b_particles.particle[id]); + + float alpha = sin(b_particles.particle[id].age); + alpha = sin(alpha + sin(alpha)) * 0.5 + 0.5; + + // draw particle in the image + imageStore(p_img, + ivec2(b_particles.particle[id].pos), + vec4((b_particles.particle[id].color * alpha), alpha) + ); + """.trimIndent() + } + + // Execute initCS + initCS.buffer("particles", particleSSBO) + initCS.execute(particleCount / initCS.workGroupSize.x) + + extend { + // Clear the image, otherwise all pixels become eventually white + cb.fill(ColorRGBa.TRANSPARENT) + + // Pass image to the compute shader. + // We can choose between READ, READ_WRITE or WRITE. + updateCS.image("img", cb.imageBinding(0, ImageAccess.WRITE)) + updateCS.buffer("particles", particleSSBO) + updateCS.parameter("windowSize", drawer.bounds.dimensions) + updateCS.execute(particleCount / updateCS.workGroupSize.x) + + drawer.image(cb) + } + } +} \ No newline at end of file diff --git a/openrndr-demos/src/demo/kotlin/DemoComputeStyle08.kt b/openrndr-demos/src/demo/kotlin/DemoComputeStyle08.kt new file mode 100644 index 00000000..251e14a4 --- /dev/null +++ b/openrndr-demos/src/demo/kotlin/DemoComputeStyle08.kt @@ -0,0 +1,128 @@ +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.draw.* + +/** + * This program demonstrates + * - How to animate 2D particles but render them as 2D triangles. + * - How to deal with two buffers of different size: the particles buffer + * and the vertex buffer, which has three times more elements. + * The update compute shader calculates 3 vertices for each particle. + * - How to make the init compute shader initialize both the particles (position and velocity) + * and the colors of the vertices. + * - How to create a minimal ShadeStyle to set per-vertex colors, + * and render the colors interpolated across each triangle. + * + * Output: vertexBuffer -> TRIANGLES + */ + +fun main() = application { + program { + val particleCount = 3200 + + // Define SSBO format + val ssboFmt = shaderStorageFormat { + struct("Particle", "particle", particleCount) { + primitive("pos", BufferPrimitiveType.VECTOR2_FLOAT32) + primitive("velocity", BufferPrimitiveType.VECTOR2_FLOAT32) + } + } + println("Study the padding in the format:\n$ssboFmt\n") + + // Create SSBO + val particleSSBO = shaderStorageBuffer(ssboFmt) + + // The padding is required to match the expected layout. + // Even if we are in a 2D world we need to use a 3D position + // because that's what OPENRNDR expects in its shaders. + val vertFormat = vertexFormat(BufferAlignment.STD430) { + position(3) + color(4) + } + println("Study the padding in the vertex buffer format:\n$vertFormat\n") + + // Create vertex buffer. + // Note how me multiply the particleCount by 3 (three vertices per particle). + val vb = vertexBuffer(vertFormat, particleCount * 3) + + // 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)); + + // Generate colors based on id + vec4 col = vec4( + sin(id + 0.000) * 0.5 + 0.5, + sin(id + 2.094) * 0.5 + 0.5, + sin(id + 4.188) * 0.5 + 0.5, 1.0); + + // Swap R, G and B and darken the two rear vertices of each triangle. + // This creates a gradient in each triangle. + b_vb.vertex[id * 3 + 0].color = col.rgba; + b_vb.vertex[id * 3 + 1].color = col.grba * 0.5; + b_vb.vertex[id * 3 + 2].color = col.rbga * 0.25; + """.trimIndent() + } + val updateCS = computeStyle { + computePreamble = """ + const float margin = 16.0; + 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 < -margin) { + p.pos.x += p_windowSize.x + 2.0 * margin; + } + if(p.pos.y < -margin) { + p.pos.y += p_windowSize.y + 2.0 * margin; + } + if(p.pos.x > p_windowSize.x + margin) { + p.pos.x = -margin; + } + if(p.pos.y > p_windowSize.y + margin) { + p.pos.y = -margin; + } + } + """.trimIndent() + computeTransform = """ + // The id of the element being currently processed + uint id = gl_GlobalInvocationID.x; + updateParticle(b_particles.particle[id]); + + // Calculate the vertices of a directed triangle + // pointing towards `velocity`. Hint: + // vel (x,y) has two normals (-y, x) and (y, -x). + vec2 pos = b_particles.particle[id].pos; + vec2 vel = b_particles.particle[id].velocity * margin; + vec2 n0 = vec2(-vel.y, vel.x) * 0.5; + b_vb.vertex[id * 3 + 0].position = vec3(pos + vel, 0.0); + b_vb.vertex[id * 3 + 1].position = vec3(pos + n0, 0.0); + b_vb.vertex[id * 3 + 2].position = vec3(pos - n0, 0.0); + """.trimIndent() + } + + // Execute initCS + initCS.buffer("vb", vb.shaderStorageBufferView()) + 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.shadeStyle = shadeStyle { + // The color of every triangle's pixel is interpolated using + // its three vertex colors + fragmentTransform = "x_fill = va_color;" + } + drawer.vertexBuffer(vb, DrawPrimitive.TRIANGLES) + } + } +} \ No newline at end of file diff --git a/openrndr-demos/src/demo/kotlin/DemoComputeStyle09.kt b/openrndr-demos/src/demo/kotlin/DemoComputeStyle09.kt new file mode 100644 index 00000000..680b39b3 --- /dev/null +++ b/openrndr-demos/src/demo/kotlin/DemoComputeStyle09.kt @@ -0,0 +1,80 @@ +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.draw.DrawPrimitive +import org.openrndr.draw.computeStyle +import org.openrndr.draw.execute +import org.openrndr.draw.shadeStyle +import org.openrndr.extra.camera.Orbital +import org.openrndr.extra.meshgenerators.dodecahedronMesh +import org.openrndr.math.IntVector3 +import org.openrndr.math.Vector3 + +/** + * This program demonstrates + * - How to use a compute shader to deform a 3D shape (a vertex buffer). + * We create a dodecahedron vertex buffer and displace its vertices + * along their normals, exploding the object into 12 pieces with + * 3 triangles each (3 triangles to form a pentagon). + * + * Use the mouse for panning, rotating and zooming. + * + * Output: vertexBuffer -> TRIANGLES + */ + +fun main() = application { + program { + val vb = dodecahedronMesh(2.0) + + //vb.saveOBJ("/tmp/dodecahedron.obj") // study with Blender3D + + println(vb.vertexFormat.toString().replace("), ", "),\n ").replace("[", "[\n ").replace("]", "\n]")) + println("Vertex count: ${vb.vertexCount}") + + // Create Compute Shaders + val updateCS = computeStyle { + computeTransform = """ + // The id of the element being currently processed + uint id = gl_GlobalInvocationID.x; + + b_vb.vertex[id].position += b_vb.vertex[id].normal * 0.01; + """.trimIndent() + workGroupSize = IntVector3(64, 1, 1) + } + + // Debugging: print the mesh data + val shadow = vb.shadow + shadow.download() + val reader = shadow.reader() + reader.rewind() + repeat(vb.vertexCount) { + println(it) + // Notice how we read Vector4's instead of Vector3 or Vector2 + // because the data has been padded to align 16-byte boundaries. + val pos = reader.readVector4() + val nrm = reader.readVector4() + val uv = reader.readVector4() + println(" pos: ${pos.xyz}") + println(" nrm: ${nrm.xyz}") + println(" uv: ${uv.xy}") + } + + val cam = Orbital() + cam.eye = Vector3.UNIT_Z * 5.0 + + extend(cam) + extend { + updateCS.buffer("vb", vb.shaderStorageBufferView()) + // We use a width of 2 because we have 108 vertices and the + // workgroup size is 64. 2 x 64 = 128, which is greater than 108 and + // therefore processes all the vertices. + updateCS.execute(2) + + drawer.clear(ColorRGBa.GRAY) + drawer.fill = ColorRGBa.WHITE + drawer.shadeStyle = shadeStyle { + fragmentTransform = "x_fill.rgb = va_normal.xyz * 0.5 + 0.5;" + } + drawer.vertexBuffer(vb, DrawPrimitive.TRIANGLES) + } + } +} diff --git a/openrndr-demos/src/demo/kotlin/DemoComputeStyle10.kt b/openrndr-demos/src/demo/kotlin/DemoComputeStyle10.kt new file mode 100644 index 00000000..1ed52971 --- /dev/null +++ b/openrndr-demos/src/demo/kotlin/DemoComputeStyle10.kt @@ -0,0 +1,88 @@ +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.draw.DrawPrimitive +import org.openrndr.draw.computeStyle +import org.openrndr.draw.execute +import org.openrndr.draw.shadeStyle +import org.openrndr.extra.camera.Orbital +import org.openrndr.extra.color.presets.WHEAT +import org.openrndr.extra.meshgenerators.dodecahedronMesh +import org.openrndr.math.IntVector3 +import org.openrndr.math.Vector3 + +/** + * This program is a variation of compute09.kt. It draws `vb` + * multiple times, each with a unique translation and rotation. + * + * For each item drawn, the instance number is sent to the shade style + * as a float uniform (named `p_i`) to shade them with unique hues. + * The interpolated normal varying is used to set the color, and + * this color rotated using `p_i` as the rotation angle. + * + * Use the mouse for panning, rotating and zooming. + * + * Output: vertexBuffer -> TRIANGLES + */ + +fun main() = application { + program { + val vb = dodecahedronMesh(2.0) + + // Create Compute Shaders + val updateCS = computeStyle { + computeTransform = """ + // The id of the element being currently processed + uint id = gl_GlobalInvocationID.x; + + b_vb.vertex[id].position += b_vb.vertex[id].normal * 0.01; + """.trimIndent() + workGroupSize = IntVector3(64, 1, 1) + } + + val cam = Orbital() + cam.eye = Vector3.UNIT_Z * 5.0 + + val style = shadeStyle { + // From https://github.com/dmnsgn/glsl-rotate + fragmentPreamble = """ + mat4 rotation3d(vec3 axis, float angle) { + axis = normalize(axis); + float s = sin(angle); + float c = cos(angle); + float oc = 1.0 - c; + + return mat4( + oc * axis.x * axis.x + c, oc * axis.x * axis.y - axis.z * s, oc * axis.z * axis.x + axis.y * s, 0.0, + oc * axis.x * axis.y + axis.z * s, oc * axis.y * axis.y + c, oc * axis.y * axis.z - axis.x * s, 0.0, + oc * axis.z * axis.x - axis.y * s, oc * axis.y * axis.z + axis.x * s, oc * axis.z * axis.z + c, 0.0, + 0.0, 0.0, 0.0, 1.0 + ); + } + + vec3 rotate(vec3 v, vec3 axis, float angle) { + return (rotation3d(axis, angle) * vec4(v, 1.0)).xyz; + } + """.trimIndent() + fragmentTransform = """ + x_fill.rgb = rotate(va_normal.xyz * 0.5 + 0.5, + normalize(vec3(1.0)), p_i); + """.trimIndent() + } + + extend(cam) + extend { + updateCS.buffer("vb", vb.shaderStorageBufferView()) + updateCS.execute(2) + + drawer.clear(ColorRGBa.WHEAT.shade(0.2)) + drawer.fill = ColorRGBa.WHITE + drawer.shadeStyle = style + repeat(10) { + style.parameter("i", it * 0.3) + drawer.translate(1.0, 0.0, 0.0) + drawer.rotate(Vector3.UNIT_Z, 5.0) + drawer.vertexBuffer(vb, DrawPrimitive.TRIANGLES) + } + } + } +} diff --git a/openrndr-demos/src/demo/kotlin/DemoComputeStyle20.kt b/openrndr-demos/src/demo/kotlin/DemoComputeStyle20.kt new file mode 100644 index 00000000..9839bf6c --- /dev/null +++ b/openrndr-demos/src/demo/kotlin/DemoComputeStyle20.kt @@ -0,0 +1,61 @@ +import org.openrndr.application +import org.openrndr.color.ColorRGBa +import org.openrndr.draw.* +import org.openrndr.math.IntVector3 +import kotlin.math.sin + +/** + * Use a compute shader to read from a colorBuffer + * and write into a different colorBuffer. + * + * The input colorBuffer is updated on every animation frame + * with a scaling circle. + * + * Then the compute shader is executed to update every pixel + * in the output colorBuffer by reading a displaced pixel + * from the input colorBuffer. + * + * Output: 2D Image + */ +fun main() { + application { + program { + val input = renderTarget(width, height) { + colorBuffer(type = ColorType.FLOAT32) + } + val output = input.colorBuffer(0).createEquivalent() + + val cs = computeStyle { + computeTransform = """ + ivec2 id = ivec2(gl_GlobalInvocationID.xy); + ivec2 src = ivec2(id + sin(id) * p_m); + vec4 c = imageLoad(p_inputImage, src); + imageStore(p_outputImage, id, c); + """.trimIndent() + workGroupSize = IntVector3(16, 16, 1) + } + + cs.image("inputImage", input.colorBuffer(0).imageBinding(0, ImageAccess.READ)) + cs.image("outputImage", output.imageBinding(0, ImageAccess.WRITE)) + + extend { + // Update input + drawer.isolatedWithTarget(input) { + clear(ColorRGBa.TRANSPARENT) + circle(bounds.center, 100.0 + 80 * sin(seconds)) + } + + // Apply the compute shader to update output + cs.parameter("m", sin(seconds * 0.8) * 13.0 + 15.0) + cs.execute( + output.width / cs.workGroupSize.x, + output.height / cs.workGroupSize.y, + 1 + ) + + // Draw result + drawer.image(output) + } + } + } +}