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:
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 |
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.
cool
ReplyDeleteI think activeLights is just supposed to be lights
ReplyDeleteI think activeLights is just supposed to be lights
ReplyDeletehow to make transparency? cube or earth
ReplyDeleteIf 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.
DeleteJust 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.
Hi,
ReplyDeleteThank 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
Please replace
ReplyDeletefloat.TryParse(vertparts[0], out vec.X);
to
float.TryParse(vertparts[0], numberStyle, CultureInfo.InvariantCulture, out vec.X);
For international use.