Friday, July 29, 2016

OpenTK Tutorial 9 Part 3: Specular Maps and Attenuation

In the previous part of this tutorial, we added spot lights and directional lights. This time, we're adding specular maps and light attenuation.




If you look at a picture of Earth taken from the ISS, you'll notice something about how light reflects off of it. The parts with water are much shinier than the land:


With the lighting we have now, it doesn't have this effect:



The way to fix this is by adding a specular map. This is a texture that stores information about which parts of the object are shiny, and which aren't. For Earth, this is very easy. We can just use a map of Earth's oceans:



Download this image and save it as earthspec.png. Add it to your project and set it to copy to the output directory.

The other topic for today is light attenuation. Right now, our lights will shine infinitely far and be just as bright a mile away as they are an inch away. In reality, it follows an "inverse-square law".

Attenuation adds a factor reducing the intensity of the light due to distance. However, we will not be using just an inverse square law:



Instead, we'll add a linear term to give us more control (and a 1 to prevent division by zero). The linear attenuation is commonly used to make the lights look better without gamma correction (which would be a different tutorial), but sometimes people feel it looks better, and sometimes a mix of both has the desired effect.

Here's a slightly exaggerated example of what effect this has:

Point light with no attenuation

Point light with linear attenuation

Point light with quadratic attenuation

The "room" these lights are in is 10 units across and the light is at (-4,4,-4). The attenuated lights needed their brightness increased many times over the constant light just to be really visible. If the constant light was as bright as the quadratic light, I would've just uploaded a screenshot that's 99% white!

Now, let's get to adding both of these features to the project. In Tutorial 8, we already set up the program to handle loading a specular map, if the material has one, so we can reuse that code.

Create earth.mtl with the following (and set it to copy to the output directory):

newmtl earth
 Ns 10.0000
 Ni 1.5000
 d 1.0000
 Tr 0.0000
 Tf 1.0000 1.0000 1.0000 
 illum 2
 Ka 0.5880 0.5880 0.5880
 Kd 0.5880 0.5880 0.5880
 Ks 0.5800 0.5800 0.5800
 map_Ka earth.png
 map_Kd earth.png
 map_Ks earthspec.png


In the Game class' loadResources method, replace the line that loads the earth.png texture with the following:

            loadMaterials("earth.mtl");

Next, in setupScene, replace the existing line that sets earth.Material with:

            earth.Material = materials["earth"];

Now the earth textures are being loaded in a more flexible way, but more importantly the specular map is loaded and assigned to the model.

The next step is to send this information to the shader. In onRenderFrame, in the loop sending information about each volume (ideally this should go after the part sending material_specExponent), add the following:

                if (shaders[activeShader].GetUniform("map_specular") != -1)
                {
                    // Object has a specular map
                    if (v.Material.SpecularMap != "")
                    {
                        GL.ActiveTexture(TextureUnit.Texture1);
                        GL.BindTexture(TextureTarget.Texture2D, textures[v.Material.SpecularMap]);
                        GL.Uniform1(shaders[activeShader].GetUniform("map_specular"), 1);
                        GL.Uniform1(shaders[activeShader].GetUniform("hasSpecularMap"), 1);
                        GL.ActiveTexture(TextureUnit.Texture0);
                    }
                    else // Object has no specular map
                    {
                        GL.Uniform1(shaders[activeShader].GetUniform("hasSpecularMap"), 0);
                    }
                }

Here we (if the shader accepts it), send the specular map as a second texture and set a flag telling the shader that we gave it a specular map. By changing which texture unit we're using (with GL.ActiveTexture), we can send multiple textures at once.

While we're modifying onRenderFrame, add the following to the light loop:

                    if (shaders[activeShader].GetUniform("lights[" + i + "].linearAttenuation") != -1)
                    {
                        GL.Uniform1(shaders[activeShader].GetUniform("lights[" + i + "].linearAttenuation"), lights[i].LinearAttenuation);
                    }

                    if (shaders[activeShader].GetUniform("lights[" + i + "].quadraticAttenuation") != -1)
                    {
                        GL.Uniform1(shaders[activeShader].GetUniform("lights[" + i + "].quadraticAttenuation"), lights[i].QuadraticAttenuation);
                    }

This will send any attenuation information to the shader as well.

Before we move on, we'll need to add the LinearAttenuation and QuadraticAttenuation variables to the Light class. In the Light class, add:

        public float LinearAttenuation;
        public float QuadraticAttenuation;

Now that the data is all set up, let's update the shader to use it. Replace the contents of fs_lit_advanced.glsl with the following:

#version 330

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

 int type;
 vec3 direction;
 float coneAngle;

 float linearAttenuation;
 float quadraticAttenuation;
 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];

void
main()
{
 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))
  {
   continue;
  }
  
  vec3 lightvec = normalize(lights[i].position - v_pos);
  vec4 lightcolor = vec4(0,0,0,1);

  // 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
  lightcolor = lightcolor + 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){
   lightcolor = lightcolor + (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);

  // Specular map
  if(hasSpecularMap)
  {
   material_specularreflection = material_specularreflection * texture2D(map_specular, flipped_texcoord.xy).r;
  }

  // Spotlight, specular reflections are also limited by angle
  if(lights[i].type != 1 || inCone){
   lightcolor = lightcolor + vec4(material_specular * lights[i].color, 0.0) * material_specularreflection;
  }

  // Attenuation
  float distancefactor = distance(lights[i].position, v_pos);
  float attenuation = 1.0 / (1.0 + (distancefactor * lights[i].linearAttenuation) + (distancefactor * distancefactor * lights[i].quadraticAttenuation));
  outputColor = outputColor + lightcolor * attenuation;
 }

}

This adds a check for the specular map to multiple the specular reflection by what value the map has at the same texture coordinates.

If you run the project now, you'll be able to see the specular map in action:



Next, we should modify the lights to add attenuation. Replace the existing code for the spot lights in setupScene with the following:

            Light pointLight = new Light(new Vector3(2, 7, 0), new Vector3(1.5f, 0.2f, 0.2f));
            pointLight.QuadraticAttenuation = 0.05f;
            lights.Add(pointLight);

            Light pointLight2 = new Light(new Vector3(2, 0, 3), new Vector3(0.2f, 1f, 0.25f));
            pointLight2.QuadraticAttenuation = 0.05f;
            lights.Add(pointLight2);

            Light pointLight3 = new Light(new Vector3(6, 4, 0), new Vector3(0.2f, 0.25f, 1.5f));
            pointLight3.QuadraticAttenuation = 0.05f;
            lights.Add(pointLight3);

Now, if you run the code, you should be able to see how the lights have a nice looking falloff effect to them.




7 comments:

  1. I think activeLights is just supposed to be lights

    ReplyDelete
  2. I think activeLights is just supposed to be lights

    ReplyDelete
  3. Replies
    1. If you use GL.Enable to turn on blending (EnableCap.Blend), you can set up transparency with GL.BlendFunc, and then assign alpha values to colors in the fragment shader (e.g. load them from a texture, a map, etc). You can play around with the arguments to GL.BlendFunc to get different effects.

      Just keep in mind that the transparency effect relies on the order objects are drawn, so you may need to sort them by distance to see it.

      Delete
  4. Hi,

    Thank you very much for this great tutorial.
    I didn't fine the source code for this example on GitHub.
    https://github.com/neokabuto/OpenTKTutorialContent

    Can you please upload it to GitHub or send by mail?

    Thanks again,

    David

    ReplyDelete
  5. Please replace
    float.TryParse(vertparts[0], out vec.X);
    to
    float.TryParse(vertparts[0], numberStyle, CultureInfo.InvariantCulture, out vec.X);
    For international use.

    ReplyDelete