Thursday, 11 November 2010

Imitating OpenGL Fixed functionality pipeline in OpenGL 4.x (Part 2)

So as explained in the previous post I've decided to write a shader to imitate the OpenGL fixed functionality shading pipeline in GLSL, I started simply with getting the OpenGL 4.x transforms working and this is the result of an unshaded scene with translations and rotations working

Now we have to add some code to calculate the fragmentNormal and some values for the eye co-ordinates for the shader, our final Vertex shader is
/// @brief projection matrix passed in from camera class in main app
uniform mat4 projectionMatrix;
/// @brief View transform matrix passed in from camera class in main app
uniform mat4 ViewMatrix;
/// @brief Model transform matrix passed in from Transform Class in main app
uniform mat4 ModelMatrix;
/// @brief flag to indicate if model has unit normals if not normalize
uniform bool Normalize;
/// @brief flag to indicate if we are using texturing
uniform bool TextureEnabled;
varying vec3 fragmentNormal;
/// @brief the vertex in eye co-ordinates non homogeneous
varying vec3 eyeCord3;
/// @brief the number of lights enabled
uniform int numLightsEnabled;
/// @brief the eye position passed in from main app
uniform vec3 eye;

void main(void)
{
// pre-calculate for speed we will use this a lot
mat4 ModelView=ViewMatrix*ModelMatrix;
// calculate the fragments surface normal
fragmentNormal = (ModelView*vec4(gl_Normal, 0.0)).xyz;

if (Normalize == true)
{
fragmentNormal = normalize(fragmentNormal);
}

// calculate the vertex position
gl_Position = projectionMatrix*ModelView*gl_Vertex;
// Transform the vertex to eye co-ordinates for frag shader
/// @brief the vertex in eye co-ordinates  homogeneous
vec4 eyeCord;
eyeCord=ModelView*gl_Vertex;
// divide by w for non homogenous
eyeCord3=(vec3(eyeCord))/eyeCord.w;
if (TextureEnabled == true)
{
gl_TexCoord[0] = gl_TextureMatrix[0]*gl_MultiTexCoord0;
}

}



So next to write the Fragment shader to set the colour / shading properties of the elements being processed.

Most of this posting is based on the Orange book (OpenGL Shading Language 1st Edition Randi J. Rost)

The output of the fragment shader is to set the fragment colour, and with most lighting models this is based on a simple lighting model where we have the following material properties
1. Ambient contribution an RGB colour value for ambient light
2. Diffuse contribution an RGB colour value for the diffuse light (basic colour of the model)
3. Specular contribution an RGB colour value for the specular highlights of the material
In the shader we use the following code
vec4 ambient=vec4(0.0);
vec4 diffuse=vec4(0.0);
vec4 specular=vec4(0.0);

//calculate values for each light and surface material

gl_FragColor = ambient+diffuse+specular;


We now need to loop for every light in the scene and accumulate the total contribution from each and set the final fragment colour.

Directional Lights
Directional Lights are the simplest lighting model to compute as we only pass a vector indicating the lighting direction to the shader. OpenGL only has two basic lights one called a light the other a spotlight, to differentiate between Directional lights and Point lights OpenGL uses a homogenous vector to indicate which light to use.

Usually in CG we specify a Point and a Vector using a 4 tuple V=[x,y,z,w] where the w component is to indicate if we have a point or a vector. We set w=0 to indicate a vector and w=1 to indicate a point, and thus setting w=0 we have a Directional light. We can add this to the shader code as follows
if(gl_LightSource[i].position.w ==0.0)
{
directionalLight(i,fragmentNormal,ambient,diffuse,specular);
}


This depends upon the gl_LightSource[] built in structure which is defined as follows

struct gl_LightSourceParameters
{
vec4 ambient;
vec4 diffuse;
vec4 specular;
vec4 position;
vec4 halfVector;
vec3 spotDirection;
float spotExponent;
float spotCutoff;
float spotCosCutoff;
float constantAttenuation;
float linearAttenuation;
};

uniform gl_LightSourceParameters gl_LightSource[gl_MaxLights];


*Note after re-reading the spec this has also been marked for deprecation so this will have to be replaced in the next iteration !

This structure is passed data from the OpenGL glLight mechanism and the existing ngl::Light class will set these value for us.

So using these values we can write the code for the Directional light function as follows
/// @brief a function to compute point light values
/// @param[in] _light the number of the current light
/// @param[in] _normal the current fragmentNormal
/// @param[in,out] _ambient the ambient colour to be contributed to
/// @param[in,out] _diffuse the diffuse colour to be contributed to
/// @param[in,out] _specular the specular colour to be contributed to

void directionalLight(
in int _light,
in vec3 _normal,
inout vec4 _ambient,
inout vec4 _diffuse,
inout vec4 _specular
)
{
/// @brief normal . light direction
float nDotVP;
/// @brief normal . half vector
float nDotHV;
/// @brief the power factor
float powerFactor;
// calculate the lambert term for the position vector
nDotVP= max(0.0, dot(_normal, normalize(vec3 (gl_LightSource[_light].position))));

// now see if we have any specular contribution
if (nDotVP == 0.0)
{
powerFactor = 0.0; // no contribution
}
else
{
// and for the half vector for specular
nDotHV= max(0.0, dot(_normal, vec3 (gl_LightSource[_light].halfVector)));
// here we raise the shininess value to the power of the half vector
// Phong / Blinn shading method
powerFactor = pow(nDotHV, gl_FrontMaterial.shininess);
}
// finally add the lighting contributions using the material properties
_ambient+=gl_FrontMaterial.ambient*gl_LightSource[_light].ambient;
// diffuse is calculated by n.v * colour
_diffuse+=gl_FrontMaterial.diffuse*gl_LightSource[_light].diffuse*nDotVP;
// compute the specular value
_specular+=gl_FrontMaterial.specular*gl_LightSource[_light].specular*powerFactor;
}



This function can be broken down into the following steps

1. Calculate the diffuse contribution using Lambert law
2. Calculate the specular contribution using the half way vector (Phong / Blinn)
3. Calculate the ambient contribution (just add the ambient light values to the ambient material properties
To calculate the diffuse we take the dot product of the fragmentNormal with the normalized version of the light position vector the result of this will be multiplied by the material diffuse property to calculate the diffuse colour.

Next we determine if we have any specular contribution, if the diffuse is 0 then we have not contribution so we set the  powerFactor to 0 and specular will not be added.

If we do we calculate the dot product of the fragmentNormal and the pre-calculated by OpenGL halfway Vector, this is then raised to the power of the specularExponent which is passed as the shininess parameter of the material.

Finally we calculate the colours and return them to the main light loop

The following image shows two directional lights shading the scene and you can see the direction of the two highlights for the two different sources.

Point Light
The point light is an extension of the directional light and adds in attenuation over distance as well as calculating the direction of the maximum highlight for each vertex rather than using the halfway vector.

The following code shows this shader
/// @brief a function to compute point light values
/// @param[in] _light the number of the current light
/// @param[in] _normal the current fragmentNormal
/// @param[in,out] _ambient the ambient colour to be contributed to
/// @param[in,out] _diffuse the diffuse colour to be contributed to
/// @param[in,out] _specular the specular colour to be contributed to

void pointLight(
in int _light,
in vec3 _normal,
inout vec4 _ambient,
inout vec4 _diffuse,
inout vec4 _specular
)
{
/// @brief normal . light direction
float nDotVP;
/// @brief normal . half vector
float nDotHV;
/// @brief the power factor
float powerFactor;
/// @brief the distance to the surface from the light
float distance;
/// @brief the attenuation of light with distance
float attenuation;
/// @brief the direction from the surface to the light position
vec3 VP;
/// @brief halfVector the direction of maximum highlights
vec3 halfVector;

/// compute vector from surface to light position
VP=vec3(gl_LightSource[_light].position)-eyeCord3;
// get the distance from surface to light
distance=length(VP);
VP=normalize(VP);
// calculate attenuation of light through distance
attenuation= 1.0 / (gl_LightSource[_light].constantAttenuation +
gl_LightSource[_light].linearAttenuation * distance +

halfVector=normalize(VP+eye);
// calculate the lambert term for the position vector
nDotVP= max(0.0, dot(_normal,VP));
// and for the half vector for specular
nDotHV= max(0.0, dot(_normal, halfVector));

// now see if we have any specular contribution
if (nDotVP == 0.0)
{
powerFactor = 0.0; // no contribution
}
else
{
// here we raise the shininess value to the power of the half vector
// Phong / Blinn shading method
powerFactor = pow(nDotHV, gl_FrontMaterial.shininess);
}
// finally add the lighting contributions using the material properties
_ambient+=gl_FrontMaterial.ambient*gl_LightSource[_light].ambient*attenuation;
// diffuse is calculated by n.v * colour
_diffuse+=gl_FrontMaterial.diffuse*gl_LightSource[_light].diffuse*nDotVP*attenuation;
// compute the specular value
_specular+=gl_FrontMaterial.specular*gl_LightSource[_light].specular*powerFactor*attenuation;

}

The main difference in this function is the calculation of the vector VP which is the vector from the surface to the light position, we then calculate the length of this to determine the distance of the light from the point being shaded. This will be used in the attenuation calculations to make the light strength weaker as the fragment is further away from the light.

The following image show the basic pointLight in action with two lights placed in the scene one above the sphere and the other over the cube.

self.Light0 = Light(Vector(-3,1,0),Colour(1,1,1),Colour(1,1,1),LIGHTMODES.LIGHTLOCAL)
self.Light1 = Light(Vector(0,1,3),Colour(1,1,1),Colour(1,1,1),LIGHTMODES.LIGHTLOCAL)
self.Light0.setAttenuation(0,0.8,0.0)
self.Light1.setAttenuation(0,0.8,0.0)

self.Light0.enable()
self.Light1.enable()

We set the attenuation of the light using the setAttenuation method which has the following prototype
/// @brief set the light attenuation
/// @param[in] _constant the constant attenuation
/// @param[in] _linear attenuation
void setAttenuation(
const Real _constant=1.0,
const Real _linear=0.0,
);


1. Wow - so with only a few hundred lines of code, and a months work openGL 4 can do almost everything openGL 1.0 can!!!

2. yes Ian, but about 500 times faster, this also how OpenGL ES works all in the shaders.

3. Did you get the teapot using glutSolidTeapot() ?
I want to generate the teapot and try depth peeling on it, but I can't find any help on google which says how to render complex objects in OGL 4.x.

4. No it was actually converted from an OBJ file into C++ code as a series of triangles. (each one is part of a vertex array object where I store UV, Normals and Verts) If you download the ngl:: library from here http://nccastaff.bournemouth.ac.uk/jmacey/GraphicsLib/ you will see a file called src/ngl/Teapot.h which has the raw triangle data you can feed to a Vertex Array Object.

Hope this helps

Jon

5. Hey, have you considered also replacing fixed uniforms such as gl_LightSource (which also implicitly include some extra computations) with custom uniforms?

1. This is quite an old post, so the new version of the lib uses a later version of the GLSL shading language which doesn't have gl_LightSource etc so basically I do this now. Not really updated this post for a while.