(UPDATED 2-10-16: Missed a line in the Volume class. Thanks, NNYC, for pointing it out!)
We can see the normals now, but that's not really what our end goal was. We want lighting. In this tutorial, we'll be adding a light to the scene.
The lighting method we'll be using has three steps. The first is ambient lighting, which represents the general light in the environment that isn't coming from a specific source. The second is diffuse shading, which is the lighting based on the angle the light hits the surface, making sides facing the light brighter than sides facing away. The final step is specular highlighting. Specular highlights make an object look shiny, giving it an implied texture.
The end result here is essentially the Blinn-Phong shading model.
Steps in the lighting process (from Wikimedia) |
First, add a "Light" class. This will store information about a light in the scene. Fill in the class with the following code:
class Light { public Light(Vector3 position, Vector3 color, float diffuseintensity = 1.0f, float ambientintensity = 1.0f) { Position = position; Color = color; DiffuseIntensity = diffuseintensity; AmbientIntensity = ambientintensity; } public Vector3 Position; public Vector3 Color = new Vector3(); public float DiffuseIntensity = 1.0f; public float AmbientIntensity = 0.1f; }
Our lights right now are "point lights", which only have a position and a color. The two intensities will allow us to tweak the lighting easier when the scene gets more interactive.
Next, add an instance of this class to the Game class:
Light activeLight = new Light(new Vector3(), new Vector3(0.9f, 0.80f, 0.8f));
We'll have a lot more information to send to the shader now. We need to send all the light's properties and the material properties from the objects. Replace the existing OnRenderFrame method in the Game class with the following:
protected override void OnRenderFrame(FrameEventArgs e) { base.OnRenderFrame(e); GL.Viewport(0, 0, Width, Height); GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); GL.Enable(EnableCap.DepthTest); GL.UseProgram(shaders[activeShader].ProgramID); shaders[activeShader].EnableVertexAttribArrays(); int indiceat = 0; foreach (Volume v in objects) { GL.BindTexture(TextureTarget.Texture2D, v.TextureID); GL.UniformMatrix4(shaders[activeShader].GetUniform("modelview"), false, ref v.ModelViewProjectionMatrix); if (shaders[activeShader].GetAttribute("maintexture") != -1) { GL.Uniform1(shaders[activeShader].GetAttribute("maintexture"), v.TextureID); } if (shaders[activeShader].GetUniform("view") != -1) { GL.UniformMatrix4(shaders[activeShader].GetUniform("view"), false, ref view); } if (shaders[activeShader].GetUniform("model") != -1) { GL.UniformMatrix4(shaders[activeShader].GetUniform("model"), false, ref v.ModelMatrix); } if (shaders[activeShader].GetUniform("material_ambient") != -1) { GL.Uniform3(shaders[activeShader].GetUniform("material_ambient"), ref v.Material.AmbientColor); } if (shaders[activeShader].GetUniform("material_diffuse") != -1) { GL.Uniform3(shaders[activeShader].GetUniform("material_diffuse"), ref v.Material.DiffuseColor); } if (shaders[activeShader].GetUniform("material_specular") != -1) { GL.Uniform3(shaders[activeShader].GetUniform("material_specular"), ref v.Material.SpecularColor); } if (shaders[activeShader].GetUniform("material_specExponent") != -1) { GL.Uniform1(shaders[activeShader].GetUniform("material_specExponent"), v.Material.SpecularExponent); } if (shaders[activeShader].GetUniform("light_position") != -1) { GL.Uniform3(shaders[activeShader].GetUniform("light_position"), ref activeLight.Position); } if (shaders[activeShader].GetUniform("light_color") != -1) { GL.Uniform3(shaders[activeShader].GetUniform("light_color"), ref activeLight.Color); } if (shaders[activeShader].GetUniform("light_diffuseIntensity") != -1) { GL.Uniform1(shaders[activeShader].GetUniform("light_diffuseIntensity"), activeLight.DiffuseIntensity); } if (shaders[activeShader].GetUniform("light_ambientIntensity") != -1) { GL.Uniform1(shaders[activeShader].GetUniform("light_ambientIntensity"), activeLight.AmbientIntensity); } GL.DrawElements(BeginMode.Triangles, v.IndiceCount, DrawElementsType.UnsignedInt, indiceat * sizeof(uint)); indiceat += v.IndiceCount; } shaders[activeShader].DisableVertexAttribArrays(); GL.Flush(); SwapBuffers(); }
If you look over the code, there's two variables there that isn't defined yet: view, and the Material of each Volume.
To add the Material property for each volume, add it to the Volume class:
public Material Material = new Material();
"view" will be the view matrix from the camera, which will be required for part of the shader code. First, add it as a variable to the Game class:
Matrix4 view = Matrix4.Identity;
Now we need to assign it a real value. At the end of OnUpdateFrame, add:
view = cam.GetViewMatrix();
Next we'll add the shaders. Add a "vs_lit.glsl" file to your project, and make sure Copy to Output Directory is set to "Copy if newer" or "Copy always". In it, put the following:
#version 330 in vec3 vPosition; in vec3 vNormal; in vec2 texcoord; out vec3 v_norm; out vec3 v_pos; out vec2 f_texcoord; uniform mat4 modelview; uniform mat4 model; uniform mat4 view; void main() { gl_Position = modelview * vec4(vPosition, 1.0); f_texcoord = texcoord; mat3 normMatrix = transpose(inverse(mat3(model))); v_norm = normMatrix * vNormal; v_pos = (model * vec4(vPosition, 1.0)).xyz; }
This shader is pretty simple. The first two lines of the main method are from the textured shader we've used before. The remainder is similar to the normal visualization shader, but with a change to move normals with the model. Now we change the normals to transform with the object.
Next comes the fragment shader. This time, add a new file as "fs_lit.glsl" and put in it:
#version 330 in vec3 v_norm; in vec3 v_pos; in vec2 f_texcoord; out vec4 outputColor; uniform sampler2D maintexture; uniform mat4 view; uniform vec3 material_ambient; uniform vec3 material_diffuse; uniform vec3 material_specular; uniform float material_specExponent; uniform vec3 light_position; uniform vec3 light_color; uniform float light_ambientIntensity; uniform float light_diffuseIntensity; void main() { vec2 flipped_texcoord = vec2(f_texcoord.x, 1.0 - f_texcoord.y); vec3 n = normalize(v_norm); // Colors vec4 texcolor = texture2D(maintexture, flipped_texcoord.xy); vec4 light_ambient = light_ambientIntensity * vec4(light_color, 0.0); vec4 light_diffuse = light_diffuseIntensity * vec4(light_color, 0.0); // Ambient lighting outputColor = texcolor * light_ambient * vec4(material_ambient, 0.0); // Diffuse lighting vec3 lightvec = normalize(light_position - v_pos); float lambertmaterial_diffuse = max(dot(n, lightvec), 0.0); outputColor = outputColor + (light_diffuse * texcolor * vec4(material_diffuse, 0.0)) * lambertmaterial_diffuse; // Specular lighting vec3 reflectionvec = normalize(reflect(-lightvec, v_norm)); vec3 viewvec = normalize(vec3(inverse(view) * vec4(0,0,0,1)) - v_pos); float material_specularreflection = max(dot(v_norm, lightvec), 0.0) * pow(max(dot(reflectionvec, viewvec), 0.0), material_specExponent); outputColor = outputColor + vec4(material_specular * light_color, 0.0) * material_specularreflection; }
This is a much longer shader, but it's the three steps we talked about earlier. First it creates ambient light, then it calculates diffuse light, then it add specular highlights.
Vectors involved in our lighting shader (from Wikimedia) |
We have our shader in now, but we need to load it if we want to render anything with it. In the Game class' initProgram method, after the other shaders are loaded, add:
shaders.Add("lit", new ShaderProgram("vs_lit.glsl", "fs_lit.glsl", true));
Next, change the line in initProgram where activeShader is set to the normal shader to use the new one:
activeShader = "lit";
If we run this...
Everything is black. But don't worry, everything is (probably) okay! This is because both of the cubes were set to use a texture, but they're using a material with everything set to pitch black. We've loaded materials in a previous tutorial, but we never assigned them properly to our objects!
In initProgram, where the first TexturedCube is created, add:
tc.Material = materials["opentk1"];
Where the second TexturedCube is created, add:
tc2.Material = materials["opentk2"];
If we run it now:
This is looking better. If you move around the scene, you should see that the lighting is consistent in direction.
To be able to see the light better, let's add a textured sphere to the scene.
Download the sphere model and texture from this link. Add the two files from that archive to the project, make sure to set "Copy to Output Directory", and then we can add it to the scene.
In initProgram:
textures.Add("earth.png", loadImage("earth.png")); ObjVolume earth = ObjVolume.LoadFromFile("earth.obj"); earth.TextureID = textures["earth.png"]; earth.Position += new Vector3(1f, 1f, -2f); earth.Material = new Material(new Vector3(0.15f), new Vector3(1), new Vector3(0.2f), 5); objects.Add(earth);
Now, if you run the program one more time, you'll see:
Now we have a texture mapped, smooth sphere in our scene, which really helps to demonstrate the lighting.
These are great man, was worried you'd stopped working on them! Would love to see multiple light sources next. Invaluable resource you're making here, keep it up! Have a great next cuppa weeks an all, peace.
ReplyDelete*cuppla weeks and all
Deleteaka couple of weeks and all
This comment has been removed by the author.
ReplyDeleteI had given up on finding a good set of tutorials for opengl in any language, but you sir have created just that. Not only will this help people programming in C#, it can also be easily ported to other languages!
ReplyDeleteDidn't tell when to initial colors in the ObjVolume class and the vertices also.
ReplyDeleteSystem.NullReferenceException was unhandled
StackTrace:
OpenTKTutorial7.ObjVolume.get_ColorDataCount()
OpenTKTutorial7.ObjVolume.GetColorData()
OpenTKTutorial7.Game.OnUpdateFrame(FrameEventArgs e)
Hi NeoKabuto,
ReplyDeleteThese tutorials are really useful and have helped me get a working engine up and running.
What is necessary to use multiple shaders? You make a dictionary which stores all shaders, yet you never show how we could use more than one for an OnRenderFrame.
I assume you need to call
GL.UseProgram(shaders[activeShader].ProgramID);
shaders[activeShader].EnableVertexAttribArrays();
//Some draw code here
shaders[activeShader].DisableVertexAttribArrays();
//and then..
GL.UseProgram(shaders["otherShader"].ProgramID);
shaders["otherShader"].EnableVertexAttribArrays();
//Some other draw code here
shaders["otherShader"].DisableVertexAttribArrays();
however this does not seem to work, am I missing some extra steps?
Thanks,
-j
I had to stop on this tutorial, because I got BadImageFormatException while trying to run this. (ShaderProgram, line GL.GetActiveUniform(ProgramID, i, 256, out length, out info.size, out info.type, name); )
ReplyDeleteErm. Help, please? :|
I started experimenting with all the code, and what I changed:
Delete- Light to struct from class
- Same for Material
- After throwing entire shader code to trash and replacing it with simple setting white as an output color - it started working. -> thus I presume there was something wrong within the fragment shader itself.
I am trying to run the program using the lighting shaders. Half the time it crashes without throwing an error while reading the fragment shader. I have not a clue why any advice would be appreciated.
ReplyDeleteFurthermore occasionally i get a "AccessViolationException: Attempted to read or write protected memory." error message on GL.CompileShader(address). I also have no clue why this happens either.
Thanks for any information give.
To anyone who had similar problems of it crashing using lighting shaders, the solution I achieved was to set the target framework to .NET 4, and use an older version of openTK (v1.1.2349.61993) instaed of v2.
Deleteeven i have the same problem program is crushing and stop working
DeleteYou never initialized the Vector3[] vertices and Vector3[] colors in the ObjVolume class, which causes problems here.
ReplyDeleteIf anyone gets crashes while loading the lit shader and loading models, replace
ReplyDeleteStringBuilder name = new StringBuilder();
GL.GetActiveUniform(ProgramID, i, 256, out length, out info.size, out info.type, name);
with
StringBuilder name = new StringBuilder(256);
GL.GetActiveUniform(ProgramID, i, name.Capacity, out length, out info.size, out info.type, name);
took me a while to find the problem and solve it :D
in ShaderProgram -> Link()
Delete