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
- Ambient contribution an RGB colour value for ambient light
- Diffuse contribution an RGB colour value for the diffuse light (basic colour of the model)
- Specular contribution an RGB colour value for the specular highlights of the material
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; float quadraticAttenuation; }; 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
- Calculate the diffuse contribution using Lambert law
- Calculate the specular contribution using the half way vector (Phong / Blinn)
- Calculate the ambient contribution (just add the ambient light values to the ambient material properties
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 + gl_LightSource[_light].quadraticAttenuation * distance *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 /// @param[in] _quadratic attenuation void setAttenuation( const Real _constant=1.0, const Real _linear=0.0, const Real _quadratic=0.0 );
Wow - so with only a few hundred lines of code, and a months work openGL 4 can do almost everything openGL 1.0 can!!!
ReplyDeleteyes Ian, but about 500 times faster, this also how OpenGL ES works all in the shaders.
ReplyDeleteDid you get the teapot using glutSolidTeapot() ?
ReplyDeleteI 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.
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.
ReplyDeleteHope this helps
Jon
Hey, have you considered also replacing fixed uniforms such as `gl_LightSource` (which also implicitly include some extra computations) with custom uniforms?
ReplyDeleteThis 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.
Delete