OpenGL in Java: Pipelines & Shaders
By: Mario Emmanuel
Tags: Programming, Java, Data Science, Data Visualization, Graphics

OpenGL in Java: Pipelines & Shaders

In another article, I introduced OpenGL and briefly discussed why I chose Java, OpenGL and JOGL. I also presented a working example in Java which would be the equivalent to the first “Hello World” program. The intended example was using old OpenGL syntax, which is easy to understand but not optimised. It is called the “immediate” mode in OpenGL where the drawing commands are submitted directly to the GPU enclosed in glBegin and glEnd blocks.

Since OpenGL 3.3, it is recommended to program OpenGL using what is known as the core language. In this mode, OpenGL is used through compiled GLSL programs which are called shaders. Those shaders are pipelined so we have a chained software flow to get our graphics rendered. The GLSL programs need to be compiled and are often stored in separate files in our source code, as they are a separate programming language on their own.

When programming OpenGL, we are writing pipelined code for the GPU which will run in parallel. This implies that the way we code is different from a regular program. Routines need to be concise and stateless, as execution order is not known. Each pipeline stage operation can not rely on other operation from the same stage. Code must be also compiled and downloaded to the GPU, this is done in runtime.

In this article, we will briefly explain how this pipeline works, how shaders are coded and how vertex coordinates are used. We will use those concepts to draw a square following the complete process. While the example is quite basic, the process reflects how OpenGL is programmed.

Remember that this is not an exhaustive tutorial explaining every OpenGL concept but a material that I have gathered explaining the steps that I have found more complex while learning OpenGL. The focus is doing basic but complete examples which illustrate how to use OpenGL for 2D data visualisation applications. Do not worry if you are unsure on the scope of certain OpenGL keywords, focusing on getting the overall idea on how shaders and the pipeline work will make much easier to learn complementary material. The examples are prepared also to fill in gaps I have found in the most recommended learning materials (which I will revisit at the end of the tutorial series) and to stress on the areas which took me more time to solve.

The Pipeline and the shaders

OpenGL is all about rendering basic shapes defined by their vertexes. If you want to model a 3D dinosaur, it will be likely shaped using really small triangles, so small that will appear to the human edge as a continuous texture.

OpenGL will, therefore, transform sets of vertexes into actual rendered images, and it will do it using what is called shaders. For 2D, it is the same, we are just always drawing on the z=0 plane.

In plain English, this is how it works:

The application (for example Java or C++) will define the points to be used for each shape/primitive to be drawn. It will be just a list of vertexes, so if you want to draw just a triangle three vertexes will be needed for each triangle primitive. The vertex shader program will be executed once for each vertex and it will define the vertexes locations and the basic shape/primitive/object to be drawn. The vertex shader is intended to do operations such as scaling and translation on each vertex. Optionally a geometry shader will translate vertex into shapes. By doing that it would be possible to pass the program the centre of a square and let the geometry shader infer the four vertices of the square. The fragment shader program will be executed once for each pixel to draw and it defines the colour of the pixel.

All shaders are defined and compiled (at runtime) for the graphics card GPU using GLSL language.

The tessellation shader is an optional shader introduced in OpenGL 4 that allows to define and manipulate grids made up of a large number of triangles. It has been incorporated in OpenGL version 4 and it is intended to build things like 3D landscapes.

In this example, we will use a vertex shader, a geometry shader and a fragment shader. The idea is that given a set of coordinates (vertexes) by the application, our GPU will draw squares centred on those coordinates and will draw them coloured as per their location in the window. This is a perfectly valid example which illustrates perfectly how to program OpenGL using “core” mode.

OpenGL pipeline includes several chained steps to generate the rasterized image. Image Source: Khronos Group consortium.

One thing that was novel for me was the concept of shaders being stateless. The idea behind the shaders is that they can be executed concurrently and in any order. This effectively enables to execute by the GPU a large number of shaders (each one processing one vertex/pixel) in parallel. This is the reason why fast response and renderization can be achieved even on high definition displays without affecting the CPU. As a trade-off every shader we code must be stateless and can not depend on other shaders. This implies that the way in which we code the shaders is slightly different from what we are used to do.

Drawing a pixel using shaders and GLSL

The vertex shader

Let’s prepare a program to draw a pixel using a vertex shader and a fragment shader. We will first define the source code which will be allocated in a separate file, which is what you would likely want to do.

Our vertex shader does nothing, it just passes the vertex point to the next shader, but it is still required.

Our first Vertex shader just defines the vertices of a square.

Let’s analyse in detail the source code.

The first line states that we are using OpenGL 3.3 core. We will code for 3.3 even if — at the time of writing this article — OpenGL has reached version 4.6. The reason for doing this is that 3.3 is the first one enforcing core programming and that by doing that we can use old Graphics Cards.

The second line states that we are going to use an input layout variable which we will name it squareCenterPosition, the reason for this name is that in the next example we will use it as the centre of a square. For the moment, just bear in mind that it is just the position for an individual pixel.

The layout clause allows qualifiers to be specified, which in this context defines which position we are using in the vertex matrix. The full specification about what layout qualifiers do can be read on the OpenGL specification page. Be prepared to always check the specification to clarify doubts about code examples.

The variable is of type vec2 defines a two float vector. As we are dealing with 2D, we just need two points. More data could be provided if needed.

The second block is the main function. OpenGL resembles C syntax in many aspects, so shaders will always have a “main” function. In this function, we define the variable gl_Position which has a special meaning in OpenGL. It defines the final position of the vertex. In this case, we define a 4 float vector which includes the 3D coordinates plus a value used for projection. In our case Z coordinate is 0 and the projection value is hardcoded to 1.

This is all we have to do in our vertex shader, which is turning an x/y location into a proper 4 float vector.

The geometry shader

The next shader is the geometry shader. This shader can manipulate vertexes and compute primitives. It is important to understand that GPUs manipulate basic primitives: points, lines and triangles. And that is all.

As an example we will first provide a geometry shader which just generates 4 points around the vertex:

Our first geometry shader receives an input vertex and generates 4 vertexes out of it. Think it as a square generator from its central point.

For the geometry shader, we define layout clauses for both in and out data from the shader. In our shader, we are defining that for each point received we are delivering 4 points. It could be a different case (we will see an example later) and we could be delivering a triangle for each point.

gl_Position is an OpenGL variable used to define the position of a vertex. In this case, we are defining it four times, one per each generated vertex. Notice how we get the input value (gl_in[0]) and translate it with an offset of 0.2 to build each point of our imaginary square. For each point, we invoke EmitVertex() and EndPrimitive() as we are generating 4 primitives of 1 vertex each (a point is still a primitive). Later we will generate four triangles in each location to further illustrate how geometry shaders work.

The fragment shader

Now that we have our pixel defined by the vertex shader, we will code our fragment shader, which again, it is a dumb routine which just draws the pixel in red colour (RGB 1,0,0). The code is as follows:

Fragment shader, it just draws a red pixel.

VBAs and VBOs

Before moving forward into the actual JOGL implementation code let’s revisit what are VAOs and VBOs in OpenGL. A VBO (Vertex Buffer Object) is a buffer stored in the Graphics Card memory which contains data to be operated by the shaders. Being allocated and stored inside the Graphics Card it is much faster than moving data back and forth from the CPU. The idea is that we load those buffers with the rendering data and the buffers are processed by the GPU. Each VBO contains usually data related to an entity. A VBO could contain all the information related to a 3D model of a dinosaur, in 2D, each VBO could, for example, contain the vertexes and necessary info to draw a logic gate or a transistor in an EDA (Electronic Design Automation) CAD program.

A VAO is a superset of VBOs, which means that we need to have at least one VAO. One potential example of VAO usage would be to group all render VBOs of a certain game level in a VAO. By doing that it is easier for the main program to manage which buffers need to be loaded. It is a way to group buffers that are likely to be required at the same time. Basic 2D applications can work with just one VAO. For complex applications, its usage might provide some advantages, especially if you can not put all information in memory at the same time.

The OpenGL Java class

We will create a new class named Window C which contains the three shaders. We will define one VAO and two VBOs, each one will contain one 2D x,y coordinates. We will process both VBOs and we will end up getting two imaginary squares drawn on the screen.

OpenGL class extending JFrame and implementing GLEventListener. It defines one VAO, two VBOs containing just one (x,y) vec2 each. Those vec2 are passed through a vertex, geometry and fragment shader to get imaginary squares drawn around those coordinates.

We will also need our main class:

Main class, it just invokes WindowsC JFrame object.

The result we get is, as expected, the vertexes of two imaginary squares, located each around (0.5,0.5) and (-0.5,-0.5).

Your first program output using Shaders. Notice that all work has been done at the GPU. The application level just defined the centres of our imaginary squares.

Drawing triangles instead of points

Before finishing this example, let’s imagine that we want to draw one triangle at each point instead of the vertexes of an imaginary square. To do that, we just modify our Geometry shader using triangle_strip instead of points as output. Three vertexes need to be defined before calling EndPrimitive().

Modified geometry shader code to draw triangles at each point.

Output of the program with the modified geometry shader.

Summary

While the example is pretty basic, it illustrates the basic workflow for programs using hardware acceleration using OpenGL. It uses the three main steps used in rendering programs: the vertex shader, the geometry shader and the fragment shader. It illustrates how OpenGL programs are concise and not that easy to follow, but at the same time provide great computational power to software, as all graphic drawing efforts are delivered by the GPU. This frees our application and CPU which effectively simplifies the code at that level and boosts performance.

I have intentionally left out any other aspect which would complicate this basic workflow used in OpenGL. Remember that we have defined some coordinates, each coordinate has been stored in a VBO, the two VBOs has been included in a single VAO, the GPU has been coded to use a Vertex, Geometry and Fragment shader and we got our shapes rendered.

In the next articles we will continue covering things like scaling, panning, and debugging (error check at least).