Tuesday, July 26, 2016

OpenTK Tutorial 9 Part 2: New Light Types (Spot and Directional)

In the first part of this tutorial, we made it possible to use more than one light in a scene. In this part of the tutorial, we'll be adding two new light types.

The next (and final) part of this tutorial will add specular mapping and attenuation.

Before we implement anything, let's cover how both of these lighting types work. Right now we only have point lights. The light "rays" leave the point light in every direction, pointing away from the light source.

Image created by Tcpp and released under CC BY-SA 3.0

Directional lights are the simplest variation. Instead of the light rays pointing outwards from the light source, they all point the same way and don't have a single source. In our shader, this will just mean that the light direction vector is the same for every point on every object, rather than something recalculated. This can be used to create sunlight in a scene.

Spot lights act like point lights, but they're limited to a specific angle. This requires a little bit of math. To determine if a point is in the angle the spot light shines on, we need to use the dot product:

Public Domain illustration by Limited Atonement
In this example, A and B are two vectors, θ is the angle between them, and |A| |B| cos θ is the dot product of A and B (|A| and |B| are the lengths of A and B, respectively). The dot product can also be calculated by adding the products of the components of two vectors (A's x value times B's x value plus A's y value times B's y value, etc.). If A and B are both normalized vectors (they have a length of 1), their dot product is equal to the cosine of the angle between them.

This means we can find the angle between a point and the direction of the spot light by using the inverse cosine of the dot product of the direction of the light and the vector from the light to the point. If the angle is greater than the angle we've set for the light, we don't apply the light.

Now, let's create these new light types. The first change will be to the Light class, to enable us to choose these light types. Replace the existing Light class with the following:

    class Light
        public Light(Vector3 position, Vector3 color, float diffuseintensity = 1.0f, float ambientintensity = 1.0f)
            Position = position;
            Color = color;

            DiffuseIntensity = diffuseintensity;
            AmbientIntensity = ambientintensity;

            Type = LightType.Point;
            Direction = new Vector3(0, 0, 1);
            ConeAngle = 15.0f;

        public Vector3 Position;
        public Vector3 Color;
        public float DiffuseIntensity;
        public float AmbientIntensity;

        public LightType Type;
        public Vector3 Direction;
        public float ConeAngle;

    enum LightType { Point, Spot, Directional }

This adds an enum called LightType to make it easy to define additional lighting types and compare them in the future. We add a member to the Light class storing what type of light it is, along with a direction for the light to point in, and the angle (for spot lights).

Next, we'll need to make an additional shader. This shader will implement the new types of light.

Create a new text file named fs_lit_advanced.glsl, and add the following to it:

#version 330

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

 int type;
 vec3 direction;
 float coneAngle;

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

// Texture information
uniform sampler2D maintexture;

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

  // Check spotlight angle
  bool inCone = false;
  if(lights[i].type == 1 && degrees(acos(dot(lightvec, lights[i].direction))) < lights[i].coneAngle)
   inCone = true;

  // Directional lighting
  if(lights[i].type == 2){
   lightvec = lights[i].direction;

  // 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
  if(lights[i].type != 1 || inCone){
   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
  if(lights[i].type != 1 || inCone){
   outputColor = outputColor + vec4(material_specular * lights[i].color, 0.0) * material_specularreflection;


This is largely the same code as the previous part of the tutorial, with the change of restricting the angle for spot lights and not calculating a direction for directional lights.

Before this shader will work, it needs to be loaded. In the Game class, in loadResources, add the following:

            shaders.Add("lit_advanced", new ShaderProgram("vs_lit.glsl", "fs_lit_advanced.glsl", true));

Now that it's loaded, go ahead and change the value of activeShader to lit_advanced, so the program knows to use the new shader.

Next, we have to add some code in the lighting loop in OnRenderFrame to send the new data to the shader:

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

                    if (shaders[activeShader].GetUniform("lights[" + i + "].type") != -1)
                        GL.Uniform1(shaders[activeShader].GetUniform("lights[" + i + "].type"), (int)activeLights[i].Type);

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

If we run the project right now, we'll see no change from before. All the current lights are set to be point lights by default, which is the type of light the old shader handled.

To really demonstrate the new lighting, we'll need a new scene. In this scene, we'll add a floor (really a rescaled box) and some objects, with spotlights shining on them and a directional light overall.

Replace the existing setupScene function with the following:

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

            TexturedCube floor = new TexturedCube();
            floor.TextureID = textures[materials["opentk1"].DiffuseMap];
            floor.Scale = new Vector3(20, 0.1f, 20);
            floor.Position += new Vector3(0, -2, 0);
            floor.Material = materials["opentk1"];

            TexturedCube backWall = new TexturedCube();
            backWall.TextureID = textures[materials["opentk1"].DiffuseMap];
            backWall.Scale = new Vector3(20, 20, 0.1f);
            backWall.Position += new Vector3(0, 8, -10);
            backWall.Material = materials["opentk1"];

            // Create lights
            Light sunLight = new Light(new Vector3(), new Vector3(0.7f, 0.7f, 0.7f));
            sunLight.Type = LightType.Directional;
            sunLight.Direction = (sunLight.Position - floor.Position).Normalized();

            Light spotLight = new Light(new Vector3(2, 7, 0), new Vector3(0.7f, 0.2f, 0.2f));
            spotLight.Type = LightType.Spot;
            spotLight.Direction = (spotLight.Position - earth.Position).Normalized();
            spotLight.ConeAngle = 35.0f;

            Light spotLight2 = new Light(new Vector3(2, 0, 3), new Vector3(0.2f, 0.6f, 0.25f));
            spotLight2.Type = LightType.Spot;
            spotLight2.Direction = (spotLight2.Position - earth.Position).Normalized();
            spotLight2.ConeAngle = 15;

            Light spotLight3 = new Light(new Vector3(6, 4, 0), new Vector3(0.2f, 0.25f, 0.6f));
            spotLight3.Type = LightType.Spot;
            spotLight3.Direction = (spotLight3.Position - earth.Position).Normalized();
            spotLight3.ConeAngle = 30;

            // Move camera away from origin
            cam.Position += new Vector3(0f, 1f, 3f);

This will add a floor and a wall to the scene, with three different colored spot lights and one directional light over the whole scene to act like the sun.

Close-up on how the spotlights interact