Sunday, June 12, 2016

OpenTK Tutorial 9 Part 1: Multiple Lights

This will be the first part of our more advanced lighting arc. In this part, we'll allow the scene to have many lights instead of just one.

This next part will add two new types of light: spot lights that only shine in a limited cone, and directional lights that shine at a constant angle on the whole scene. A third part will add specular maps and attenuation, to make the lighting look a little more realistic.

The first thing we need to do is get rid of the activeLight variable in the Game class. From now on, we're committing to storing information about multiple lights. We'll clean up the references to it in other parts of the code soon. Replace the declaration for activeLight in the Game class with the following:

        List<Light> lights = new List<Light>();
        const int MAX_LIGHTS = 5;

This list will store all of the lights in our scene. MAX_LIGHTS holds the maximum number of lights we can draw. This is possible to change, but for our examples we'll only be using five.

Before we start adding lights to the list, we should clean up the initProgram method. Right now it covers a bunch of semi-unrelated things, and it's in need of some refactoring to extract these into their own methods. Replace the current initProgram method with the following:

        void initProgram()
            lastMousePos = new Vector2(Mouse.X, Mouse.Y);
            CursorVisible = false;

            GL.GenBuffers(1, out ibo_elements);


            activeShader = "lit_multiple";

Now let's make these new methods in the Game class. loadResources will include the code to load the shaders (including a new shader for multiple lights) from files, and to load the materials/textures from files:

        private void loadResources()
            // Load shaders from file
            shaders.Add("default", new ShaderProgram("vs.glsl", "fs.glsl", true));
            shaders.Add("textured", new ShaderProgram("vs_tex.glsl", "fs_tex.glsl", true));
            shaders.Add("normal", new ShaderProgram("vs_norm.glsl", "fs_norm.glsl", true));
            shaders.Add("lit", new ShaderProgram("vs_lit.glsl", "fs_lit.glsl", true));
            shaders.Add("lit_multiple", new ShaderProgram("vs_lit.glsl", "fs_lit_multiple.glsl", true));
// Load materials and textures loadMaterials("opentk.mtl"); textures.Add("earth.png", loadImage("earth.png")); }

The setupScene method will add objects and lights to our scene:

        private void setupScene()
            // Create our objects
            TexturedCube tc = new TexturedCube();
            tc.TextureID = textures[materials["opentk1"].DiffuseMap];
            tc.Material = materials["opentk1"];

            TexturedCube tc2 = new TexturedCube();
            tc2.Position += new Vector3(1f, 1f, 1f);
            tc2.TextureID = textures[materials["opentk2"].DiffuseMap];
            tc2.Material = materials["opentk2"];

            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);

            // Create lights
            lights.Add(new Light(new Vector3(), new Vector3(0.4f, 0.4f, 0.4f)));
            // Move camera away from origin
            cam.Position += new Vector3(0f, 0f, 3f);

Now that we've cleaned things up a little bit, we can work on adding the new lighting code. Create a new text file in your project, with the name fs_lit_mutiple.glsl . This will be our new fragment shader:

#version 330

// Holds information about a light
struct Light {
 vec3 position;
 vec3 color;
 float ambientIntensity;
 float diffuseIntensity;

 int type;
 vec3 direction;
 float coneAngle;

 float attenuationConstant;
 float attenuationLinear;
 float attenuationQuadratic;
 float radius;

in vec3 v_norm;
in vec3 v_pos;
in vec2 f_texcoord;
out vec4 outputColor;

// Texture information
uniform sampler2D maintexture;
uniform bool hasSpecularMap;
uniform sampler2D map_specular;

uniform mat4 view;

// Material information
uniform vec3 material_ambient;
uniform vec3 material_diffuse;
uniform vec3 material_specular;
uniform float material_specExponent;

// Array of lights used in the shader
uniform Light lights[5];

 outputColor = vec4(0,0,0,1);
 // Texture information
 vec2 flipped_texcoord = vec2(f_texcoord.x, 1.0 - f_texcoord.y);
 vec4 texcolor = texture2D(maintexture, flipped_texcoord.xy);

 vec3 n = normalize(v_norm);
 // Loop through lights, adding the lighting from each one
 for(int i = 0; i < 5; i++){

  // Skip lights with no effect
  if(lights[i].color == vec3(0,0,0))
  vec3 lightvec = normalize(lights[i].position - v_pos);

  // Colors
  vec4 light_ambient = lights[i].ambientIntensity * vec4(lights[i].color, 0.0);
  vec4 light_diffuse = lights[i].diffuseIntensity * vec4(lights[i].color, 0.0);

  // Ambient lighting
  outputColor = outputColor + texcolor * light_ambient * vec4(material_ambient, 0.0);

  // Diffuse lighting
  float lambertmaterial_diffuse = max(dot(n, lightvec), 0.0);

  // Spotlight, limit light to specific angle
  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);
  // Spotlight, specular reflections are also limited by angle
  outputColor = outputColor + vec4(material_specular * lights[i].color, 0.0) * material_specularreflection;


The only difference between  this and the old fs_lit shader is that all the code is run in a loop, and an array of structs is used to store the information about the lights. Interacting with this array is very simple. Each element in the array creates a new uniform we can access, with a name referring to which element of the array it is and which variable in the struct it is. For example, the position of the first element will be in "lights[0].position". This means it's fairly simple to make a loop in our C# code to iterate through these and set them to the values for the lights in our scene.

Although we could add a special vertex shader for this, I chose to reuse the vs_lit shader to save time.

To set the light values, we'll need to change the OnRenderFrame method. Replace it with the following:

        protected override void OnRenderFrame(FrameEventArgs e)
            GL.Viewport(0, 0, Width, Height);
            GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);


            int indiceat = 0;

            // Draw all objects
            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 lights[0].Position);

                if (shaders[activeShader].GetUniform("light_color") != -1)
                    GL.Uniform3(shaders[activeShader].GetUniform("light_color"), ref lights[0].Color);

                if (shaders[activeShader].GetUniform("light_diffuseIntensity") != -1)
                    GL.Uniform1(shaders[activeShader].GetUniform("light_diffuseIntensity"), lights[0].DiffuseIntensity);

                if (shaders[activeShader].GetUniform("light_ambientIntensity") != -1)
                    GL.Uniform1(shaders[activeShader].GetUniform("light_ambientIntensity"), lights[0].AmbientIntensity);

                for (int i = 0; i < Math.Min(lights.Count, MAX_LIGHTS); i++)
                    if (shaders[activeShader].GetUniform("lights[" + i + "].position") != -1)
                        GL.Uniform3(shaders[activeShader].GetUniform("lights[" + i + "].position"), ref lights[i].Position);

                    if (shaders[activeShader].GetUniform("lights[" + i + "].color") != -1)
                        GL.Uniform3(shaders[activeShader].GetUniform("lights[" + i + "].color"), ref lights[i].Color);

                    if (shaders[activeShader].GetUniform("lights[" + i + "].diffuseIntensity") != -1)
                        GL.Uniform1(shaders[activeShader].GetUniform("lights[" + i + "].diffuseIntensity"), lights[i].DiffuseIntensity);

                    if (shaders[activeShader].GetUniform("lights[" + i + "].ambientIntensity") != -1)
                        GL.Uniform1(shaders[activeShader].GetUniform("lights[" + i + "].ambientIntensity"), lights[i].AmbientIntensity);

                GL.DrawElements(BeginMode.Triangles, v.IndiceCount, DrawElementsType.UnsignedInt, indiceat * sizeof(uint));
                indiceat += v.IndiceCount;



This adds a loop to detect and set the variables for our new shader. Now, run the program. You should see the same scene as before, with one light:

This doesn't really show much. To know that we made a change, we'll add more lights.

In setupScene, after the first light, add the following:

            lights.Add(new Light(new Vector3(5, 0, 0), new Vector3(0.9f, 0.1f, 0.1f)));
            lights.Add(new Light(new Vector3(-5, 0, 0), new Vector3(0.1f, 0.9f, 0.1f)));
            lights.Add(new Light(new Vector3(0, 5, 0), new Vector3(0.1f, 0.1f, 0.9f)));

Now you should be able to run this again, and see multiple lights shining on the scene at once:

In the next tutorial, we'll go further with lights, making spot lights and directional lights.