Empowering Africa's Next Generation: The AI Leadership Blueprint
Shaders project
1. Working with Shaders (GLSL)
Timothy Kim
My project involved using different shaders to produce different effects in a bunny mesh.
I used the assignment 8 infrastructure as a base for my project.
1. The Lava Bunny
This effect involves animating lava on the bunny mesh.
I learned about animating textures in this tutorial by Jacobo Rodriguez Villar from Typhoon Labs:
http://www.opengl.org/sdk/docs/tutorials/TyphoonLabs/Chapter_3.pdf
First we apply a seamless lava texture to the bunny.
For the lava texture, we use a seamless 512x512 image called "lava2.ppm" in the project folder.
Credit for the image goes to Patrick Hoesly. His website is zooboing.com.
We enable the repeating of texture coordinates in the texture.
For the shaders, we use basic-gl3.vshader and bunny-gl3.fshader as bases.
Now to apply the texture to the bunny, we need some way of defining the texture coordinates a given
pixel of the bunny will use. We could started off by defining an in-attribute variable called aTexCoord.
However, since the texture is seamless and the repeating of texture coordinates is enabled, we don't
need to care about the texture coordinates as long as they are consistent. So we can use the vertex
positions as the texture coordinates.
Looking at basic-gl3.vshader, if we use vPosition as the texture coordinate in the fragment shader, we
will run into trouble because vPosition is multiplied by the ModelView matrix and the Projection
matrix. This results in the texture not being able to "stick on" to the bunny, and when we transform the
bunny, the texture acts like a fixed projection. To fix this problem, we use the in-variable aPosition,
which is vPosition without the matrices multiplied to it. We pass in the value of aPosition to the
fragment shader using a new out-variable called vPosition2.
To use the texture in the fragment shader, we need to define a new sampler2D uniform variable called
"uTexture." We can get the appropriate texel from the texture by writing:
vec4 texColor = texture(uTexture, vPosition2.xy * .25);
The multiplication of .25 helps enlarge the texture on the bunny.
Now to animate the lava on the bunny, we need to define a new uniform variable called "uTime" that
will hold a value that increases with time. We use a timer callback function called "lavaAnimate()"
(located in "final_project.h") that increases the value of uTime at a set interval. lavaAnimate can be
turned on by pressing the 'l' (lowercase "L") key.
We now add uTime as an offset to the texture coordinates:
vec4 texColor = texture(uTexture, vec2(vPosition2.x - uTime, vPosition2.y - uTime) * .25);
This has the effect of moving the lava texture diagonally up on the bunny.
2. Here are some pictures of the lava bunny:
The shaders for the lava bunny are called "lava-texture.vshader" and "lava-texture.fshader."
2. The Toon Bunny
This effect draws the bunny in a cartoony style.
Note that we will do not need to use a new vertex shader. It will remain as basic-gl3.vshader.
We use bunny-gl3.fshader as the base for the new fragment shader.
We can draw the bunny in a cartoony style by limiting the colors of the bunny to only 3 shades of its
original color. We can measure the intensity with which the lights in the environment hit a point on the
bunny. If the intensity is strong, we use the lightest of the 3 shades. If the intensity is moderate, we use
the 2nd lightest of the 3 shades. If the intensity is weak, we use the darkest of the 3 shades.
Credits to lighthouse3d.com for this first toon shading method.
We measure the intensity the lights hit a point by using the dot product of the normalized normal of the
point and the normalized vector from the point to a light. This dot products computes the cosine of the
angle between the normal of the point and the ray of light hitting the point. A cosine function is used
because it decreases as the angle increases from 0 to 90 degrees. A light ray parallel to the normal
should have a stronger effect than a light ray off from the normal. Luckily, this intensity is already
computed for us in the "diffuse" variable in bunny-gl3.fshader.
Here are the ways we separated the intensities:
if (diffuse > .91)
diffuse = 1.0;
else if (diffuse > .6)
diffuse = .6;
else
diffuse = .4;
As you can see, only 3 shades of the bunny will be drawn.
This shader is called "toon1.fshader" in the project.
Now, we can stop here and we would have a decent toon shader, but one thing is missing: outlines.
We could compute the outlines by making a 4th shade in the toon shader, where when the light
intensity of a point is really low, we just color it black.
However, we run into a problem if we use a light intensity as the measure of when to color a point
black. Since the light intensity of a point is measured with respect to the ray of light hitting the point, if
3. we change our view of the bunny, the light ray will not change. This means the outline on the bunny
will not move with respect to the viewer. So if we change our view to see an area of the bunny that is
not hit strongly by the light, we will see areas of black on the bunny and not an appropriate outline.
What we really need to do is create the outline with respect to the viewer (or the camera).
We can compute the "intensity" with which the camera views a point on the bunny using the dot
product of the normal with the vector from the point to the camera (everything should be normalized of
course). If this intensity is low, we color the point black.
Since in our shaders we are working in eyespace, the position of the camera will always be (0,0,0).
This means that the vector from a point (with eyespace coordinate vector vPosition) to the camera will
be -vPosition.
Here's what this looks like in code:
vec3 toCamera = normalize(-vPosition);
float cameraHit = max(0.0, dot(normal, toCamera));
if (cameraHit < .4)
intensity = vec3(0, 0, 0);
"intensity" here does not refer to the camera view intensity. It is a variable from bunny-gl3.fshader that
represents the final color output of a point. "cameraHit" represents the camera view intensity.
*This method for computing an outline was original and not taken from another source.
For our other 3 shades, we would still want them to be colored with respect to the light intensity.
Thus we only need to add a few lines of code to toon1.fshader.
The new toon shader is called "toon2.fshader" in the project folder.
Here are some pictures of the toon bunny:
The 1st picture shows the effect from our 1st toon shader. The 2nd picture shows us the problem we
can get if we use light intensity to compute the outlines. The 3rd and 4th picture are examples using the
corrected, 2nd toon shader.
3. The Eroding Bunny
This effect makes the bunny gradually crumble and disappear.
Credits to 3Dlabs (3dshaders.com) for the idea for this effect. I looked at the source code for the
shaders they used to make the erode effect. I only really used one of their coding ideas, but the
implementation of the effect into my project was original. Also, the texture used was original.
4. The vertex shader for this effect is "lava-texture.vshader."
The fragment shader for this effect used "lava-texture.fshader" as its base.
To make the bunny gradually disappear, we can use a texture. A texture is not only useful for applying
an image onto another object. It can be used as data as well.
We create a texture that only consists of black, white, and shades of gray in between. We can use these
texture values to represent the time at which a certain point on the bunny will disappear. To make the
disappearing smooth, we need to make sure that there are appropriate transitions of gray between black
and white points in the texture. So basically all we need to do is draw an image that has black dots on a
white background, and use a smudge tool to create the transitions between the black and white.
The texture we use is called "erosion_map.ppm" in the project folder.
With that texture in place, we can have a uniform variable "uTime" in the fragment shader that
increases with time through a timer callback function (this callback function is called
"erosionAnimate()" in "final_project.h").
After getting the color of a point on the texture in the fragment shader, we measure the darkness of that
point and compare it to a value. If the measured darkness is less than the value, we discard the
associated fragment (lighter values are higher than darker values). This comparison value should
increase with time to let lighter fragments get discarded.
Here's what this looks like in code:
vec4 texColor = texture(uTexture, vPosition2.xy);
if (texColor.r < 0 + uTime)
discard;
As you can see, the uTime variable increases the comparison value.
We can turn on the eroding animation by pressing the 'e' key.
We also disable backface culling so that the eroding parts of the back of the bunny can be seen.
Here are some pictures of the eroding bunny:
You can see the bunny gradually disappearing. The last picture is a picture of the texture we used.
Resources:
1. The GLSL tutorials from lighthouse3d.com, Clockworkcoders
(http://www.opengl.org/sdk/docs/tutorials/ClockworkCoders/ ), and Typhoon Labs
(http://www.opengl.org/sdk/docs/tutorials/TyphoonLabs/Chapter_3.pdf)
2. The shaders demo and source code from 3dshaders.com.