Add computeStyle demos (#354)

This commit is contained in:
Abe Pazos
2025-09-01 18:40:06 +00:00
committed by GitHub
parent df2a596ec0
commit af4c2b20d6
11 changed files with 1042 additions and 0 deletions

View File

@@ -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}")
}
}
}

View File

@@ -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}")
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}
}
}

View File

@@ -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)
}
}
}
}