Tucano  0.1
A library for rapid prototyping with modern OpenGL and GLSL
Using the Effect and Shader Classes

The Effect and Shader Classes hold the core of the OpenGL/GLSL applications. A typical application has a Class (inheriting from Effect) that contains Shader objects. Many Effects are multipass and contain many shaders.

It is possible to build an application without using the Effect Class at all, however there a few advantages in using the Effect:

  • keeps all shaders belonging to the same effect encapsulated
  • keeps all variables for the effect encapsulated
  • keeps your application API simple
  • enables reloading all the effect's shaders with a single command
  • enhances portability, to use the same effect in another application, only need to include one header

A simple example effect

Let's look at a simple Effect from Tucano's Collection, the NormalMap. This effect renders a mesh using normals directions as color (normal map). We only need one Shader to create this effect.

First we create our class:

1 #include "effect.hpp"
2 class NormalMap : public Tucano::Effect {
3 ...

This class contains a single Shader:

1 Tucano::Shader normalmap_shader;

We need to overload the initialization method to load the shader. The loadShader method will look in the set shader directory for files beginning with "normalmap" and shader extensions, in this case it will find two (.vert and .frag). The method also initializes the shaders and keeps references to them for reloading and deleting.

1 virtual void initialize ()
2 {
3  loadShader(normalmap_shader, "normalmap");
4 }

Note that the above method assumes that the shader files (.vert, .frag, etc...) are in the "/shaders" directory relative to your executing directory. You may need to change this directory before calling the initialization using the setShadersDir method.

If you are using pointers for the Shaders, you can call normalmap_shader = loadShader("normalmap");

Now, all we need is the core of the NormalMap application. We can implement it as a method that receives a Mesh and a camera Trackball, and renders the Mesh using the normal directions as colors:

1 virtual void render (Tucano::Mesh& mesh, const Tucano::Trackball& cameraTrackball)
2 {
3  Eigen::Vector4f viewport = cameraTrackball.getViewport();
4  glViewport(viewport[0], viewport[1], viewport[2], viewport[3]);
5 
6  // enables the shader
7  normalmap_shader.bind();
8 
9  // passes the transformation matrices to the shader as uniforms
10  normalmap_shader.setUniform("projectionMatrix", cameraTrackball.getProjectionMatrix());
11  normalmap_shader.setUniform("modelMatrix", mesh.getModelMatrix());
12  normalmap_shader.setUniform("viewMatrix", cameraTrackball.getViewMatrix());
13 
14  // links the Mesh attributes with the Shader vertex attributes (in this case gl_Position and gl_Normal)
15  mesh.setAttributeLocation(phong_shader);
16 
17  mesh.render();
18 
19  normalmap_shader.unbind();
20 }

And that's it. In the application's draw routine, we clear the screen and call the render method above. To see a visual representation of the trackball just render it! Just make sure that depth test is enabled. Here is a sample code for a typical draw method (you can find a similar method in the GLWidget Class of the phongViewer sample):

1 glClearColor(1.0, 1.0, 1.0, 0.0);
2 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
3 
4 normalmap_shader.render(mesh, camera_trackball);
5 camera_trackball.render();

Here are the vertex and fragment shader codes for the files "normalmap.vert" and "normalmap.frag" (they are all included in the effects/shaders directory):

normalmap.vert

1 #version 430
2 
3 in vec4 in_Position;
4 in vec3 in_Normal;
5 out vec3 normal;
6 
7 uniform mat4 modelMatrix;
8 uniform mat4 viewMatrix;
9 uniform mat4 projectionMatrix;
10 
11 void main(void)
12 {
13  normal = in_Normal;
14  gl_Position = projectionMatrix * viewMatrix * modelMatrix * in_Position;
15 }

normalmap.frag

1 #version 430
2 
3 in vec3 normal;
4 out vec4 out_Color;
5 
6 void main(void)
7 {
8  out_Color = vec4(abs(normal), 1.0);
9 }

A more complex example

Let's say we want to create an effect called BlurredNormals. Its only an illustrative example. This effect uses the normalmap above and then blurs the final resulting image. It's a two pass method: first pass stores the normalmap result in an FBO; second pass blurs the resulting image from the first pass. If you are not familiar with FBOs, check out the Framebuffer Tutorial.

We now need two Shaders plus a Quad Mesh and an FBO for the screen space blur:

1 Tucano::Shader normalmap_shader;
2 Tucano::Shader blur_shader;
3 Tucano::Mesh quad;
4 Tucano::Framebuffer fbo;

The initialization method is completed as follows:

1 virtual void initialize ()
2 {
3  loadShader(normalmap_shader, "normalmap");
4  loadShader(blur_shader, "meanfilter");
5  quad.createQuad();
6  fbo.create(w, h, 1);
7 }

Note that you need to pass the size of your viewport to initialize the FBO, we assume we know this and its (w,h). The last parameter of the FBO says that we only need one texture attachment. It's usually a good idea to initialize and re-initialize the FBO every time your window is resized (checkout the QtTrackballWidget for an example).

We show another way to set the FBO size below, after this example.

The render method now needs two passes, one to store the result in a FBO, and the second to blur the image:

1 virtual void render (Tucano::Mesh& mesh, const Tucano::Trackball& cameraTrackball)
2 {
3  // clears the FBO and sets the FBO first (and only) attachment as output
4  fbo.clearAttachments();
5  fbo.bindRenderBuffer(0);
6 
7  // *** this part of the code is identical to the first example ***
8  Eigen::Vector4f viewport = cameraTrackball.getViewport();
9  glViewport(viewport[0], viewport[1], viewport[2], viewport[3]);
10 
11  // enables the shader
12  normalmap_shader.bind();
13 
14  // passes the transformation matrices to the shader as uniforms
15  normalmap_shader.setUniform("projectionMatrix", cameraTrackball.getProjectionMatrix());
16  normalmap_shader.setUniform("modelMatrix", mesh.getModelMatrix());
17  normalmap_shader.setUniform("viewMatrix", cameraTrackball.getViewMatrix());
18 
19  // links the Mesh attributes with the Shader vertex attributes (in this case gl_Position and gl_Normal)
20  mesh.setAttributeLocation(phong_shader);
21  mesh.render();
22  phong_shader.unbind();
23 
24  // *** unbind the buffer as output buffer, and applies the blur filter ****
25  fbo.unbind(); // automatically returns the draw buffer to GL_BACK
26 
27  blur_shader.bind();
28  // makes the FBO attachment read available from the shader
29  blur_shader.setUniform("imageTexture", fbo.bindAttachment(0));
30  // sets the blur kernel (higher == more)
31  blur_shader.setUniform("kernelsize", 5);
32 
33  quad.render();
34  blur_shader.unbind();
35  fbo.unbindAttachments();
36 }

And that is all!! The blur shaders files (meanfilter.vert and meanfilter.frag) are in the effects/shaders directory of the Tucano Lib.

Alternative to setting the FBO size during the initialization.

During the render method check if the FBO size is the same as the Viewport size, otherwise re-create the FBO (it will be initialized with zero)

1 Eigen::Vector4f viewport = cameraTrackball.getViewport();
2 if (fbo.getWidth() != (viewport[2]-viewport[1]) || fbo.getHeight() != (viewport[3]-viewport[1]))
3 {
4  fbo.create(viewport[2]-viewport[1], viewport[3]-viewport[1], 1);
5 }

Note that usually the first two elements of the viewport vector are zero (ex. [0, 0, w, h]). However, we have left it with as four elements for completeness.