JRuby, LWJGL & OpenGL – Getting Started with Shaders

In Part 2 we drew a triangle using a Vertex Buffer, and some basic shaders. While on the surface this can seem overtly complicated, it actually becomes the basis of a powerful OpenGL achitecture to enable you to leverage the GPU is a variety of very interesting ways without having to rely on the CPU.

We’ll be look at the example show_triangle_vert_frag_offset.rb, which can be run from the command bin/triangle_vert_frag_offset.

This also comes from the “OpenGL’s Moving Triangle” section of the Learning Modern 3D Graphics Programming online book.

With this code, we are going to take our original triangle, and we will make it move around a bit, and also change colour at the same time.

The clever thing about this is, we won’t be changing the vertex data stored in the buffer, but will instead be manipulating it with Fragment and Vertex shaders. This almost feels like Uber-CSS over the top of HTML.

This is pretty powerful stuff, as we can let the GPU do a lot of the processing by using shader programs, and passing them attributes to control the overall affect that we want.

Let’s look at our vertex shader offset_vertex.glsl

#version 330

layout(location = 0) in vec4 position;
uniform vec2 offset;

void main()
{
vec4 totalOffset = vec4(offset.x, offset.y, 0.0, 0.0);
gl_Position = position + totalOffset;
}

You can see we now have a uniform vec2 offset;. This defines a value that is going to get passed in from outside the Shader. It has the keyword uniform as the value stays the same on the same rendering frame.

The offset attribute will be expecting a vector with an x and y coordinate to be passed through (vec2) for the uniform value.

We convert the offset vec2 to a vec4, as you can’t add a vec2 to a vec4. Then we can add these two together (GLSL will do vector arithmatic for you out of the box) to get our final gl_Position vector.

This means we can change the position of our triangle with relative ease, just by changing the offset of each of the vertices.

To do this from our JRuby code, is quite straight forward. The first thing we need to do is find out the location / position of the offset attribute. This is done through:

@offset_location = GL20.gl_get_uniform_location(@program_id, "offset")

Now we have the capability to change this value as we need to. In our display function, we have:

x_offset, y_offset = compute_position_offsets(time)

compute_position_offsets calculates x and y offsets based on the current time. (Check out the code for more details, it’s just some fun trig). To set the uniform value, we then do:

GL20.gl_uniform2f(@offset_location, x_offset, y_offset)

That’s it. The shader then does the rest of the work. Our triangle will now go round and round in a circle.

We do similar things to change the colour of the triangle as it goes around in a circle. Here is our fragment shader:

#version 330
out vec4 outputColor;
uniform float fragLoopDuration;
uniform float time;

const vec4 firstColor = vec4(1.0f, 1.0f, 1.0f, 1.0f);
const vec4 secondColor = vec4(0.0f, 1.0f, 0.0f, 1.0f);

void main()
{
float currTime = mod(time, fragLoopDuration);
float currLerp = currTime / fragLoopDuration;
outputColor = mix(firstColor, secondColor, currLerp);
}

You can see we can define constants with the const keyword. This sets up two colours to mix between, in this case, white and green.

We have two uniform attributes to in this time, fragLoopDuration the loop duration of the colour change, and time, how many seconds have passed since the application began.

mix is a GLSL function that blends two colours together based on the third float that is passed through, and gives us the slow fade between the two as the time value changes.

Setting this up is almost exactly the same as before. We will set the fragLoopDuration in our init_program method, as it never changes across the execution of our code.

frag_loop_location = GL20.gl_get_uniform_location(@program_id, "fragLoopDuration")
@frag_loop = 50.0
GL20.gl_uniform1f(frag_loop_location, @frag_loop)

For the time uniform attribute, we have some code that tracks how much time passes between frames and we just add it all up, and then set the uniform attribute with gl_uniform like usual:

current_time = Sys.get_time
@elapsed_time += (current_time - @last_time)
@last_time = current_time
time = @elapsed_time / 1000.0

 

GL20.gl_uniform1f(@time_location, time)

That’s the basics of using shaders with vertex data. We now have a triangle that goes around in a circle!

Leave a Comment