In this section, we will delve into advanced shading techniques in OpenGL. Shading is a crucial aspect of rendering that determines how surfaces interact with light. Advanced shading techniques can significantly enhance the visual quality of your graphics.

Key Concepts

  1. Phong Shading
  2. Blinn-Phong Shading
  3. Normal Mapping
  4. Parallax Mapping
  5. Physically Based Rendering (PBR)

  1. Phong Shading

Phong Shading is a technique that interpolates surface normals across rasterized polygons and computes pixel colors based on the interpolated normals and a lighting model.

Phong Shading Steps

  1. Vertex Shader: Calculate the vertex normals.
  2. Fragment Shader: Interpolate the normals and compute the lighting per pixel.

Example Code

Vertex Shader (Phong Shading)

#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNormal;

out vec3 FragPos;
out vec3 Normal;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
    FragPos = vec3(model * vec4(aPos, 1.0));
    Normal = mat3(transpose(inverse(model))) * aNormal;  
    gl_Position = projection * view * vec4(FragPos, 1.0);
}

Fragment Shader (Phong Shading)

#version 330 core
out vec4 FragColor;

in vec3 FragPos;
in vec3 Normal;

uniform vec3 lightPos;
uniform vec3 viewPos;
uniform vec3 lightColor;
uniform vec3 objectColor;

void main()
{
    // Ambient
    float ambientStrength = 0.1;
    vec3 ambient = ambientStrength * lightColor;
    
    // Diffuse 
    vec3 norm = normalize(Normal);
    vec3 lightDir = normalize(lightPos - FragPos);
    float diff = max(dot(norm, lightDir), 0.0);
    vec3 diffuse = diff * lightColor;
    
    // Specular
    float specularStrength = 0.5;
    vec3 viewDir = normalize(viewPos - FragPos);
    vec3 reflectDir = reflect(-lightDir, norm);  
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
    vec3 specular = specularStrength * spec * lightColor;  
    
    vec3 result = (ambient + diffuse + specular) * objectColor;
    FragColor = vec4(result, 1.0);
}

Explanation

  • Ambient: A constant light that affects all objects equally.
  • Diffuse: Light that is scattered in many directions when it hits a rough surface.
  • Specular: The bright spot of light that appears on shiny objects.

  1. Blinn-Phong Shading

Blinn-Phong Shading is a modification of Phong Shading that uses the halfway vector between the light direction and the view direction to calculate the specular reflection.

Example Code

Fragment Shader (Blinn-Phong Shading)

#version 330 core
out vec4 FragColor;

in vec3 FragPos;
in vec3 Normal;

uniform vec3 lightPos;
uniform vec3 viewPos;
uniform vec3 lightColor;
uniform vec3 objectColor;

void main()
{
    // Ambient
    float ambientStrength = 0.1;
    vec3 ambient = ambientStrength * lightColor;
    
    // Diffuse 
    vec3 norm = normalize(Normal);
    vec3 lightDir = normalize(lightPos - FragPos);
    float diff = max(dot(norm, lightDir), 0.0);
    vec3 diffuse = diff * lightColor;
    
    // Specular
    float specularStrength = 0.5;
    vec3 viewDir = normalize(viewPos - FragPos);
    vec3 halfwayDir = normalize(lightDir + viewDir);  
    float spec = pow(max(dot(norm, halfwayDir), 0.0), 32);
    vec3 specular = specularStrength * spec * lightColor;  
    
    vec3 result = (ambient + diffuse + specular) * objectColor;
    FragColor = vec4(result, 1.0);
}

Explanation

  • Halfway Vector: A vector that is halfway between the light direction and the view direction, used to calculate the specular reflection more efficiently.

  1. Normal Mapping

Normal Mapping is a technique used to add detail to the surface of a 3D model without increasing the number of polygons. It uses a texture (normal map) to perturb the normals of the surface.

Example Code

Fragment Shader (Normal Mapping)

#version 330 core
out vec4 FragColor;

in vec3 FragPos;
in vec2 TexCoords;
in vec3 TangentLightPos;
in vec3 TangentViewPos;
in vec3 TangentFragPos;

uniform sampler2D normalMap;
uniform sampler2D diffuseMap;

void main()
{
    // Obtain normal from normal map in range [0,1]
    vec3 normal = texture(normalMap, TexCoords).rgb;
    // Transform normal vector to range [-1,1]
    normal = normalize(normal * 2.0 - 1.0);   
    
    // Ambient
    vec3 ambient = 0.1 * texture(diffuseMap, TexCoords).rgb;
    
    // Diffuse
    vec3 lightDir = normalize(TangentLightPos - TangentFragPos);
    float diff = max(dot(lightDir, normal), 0.0);
    vec3 diffuse = diff * texture(diffuseMap, TexCoords).rgb;
    
    // Specular
    vec3 viewDir = normalize(TangentViewPos - TangentFragPos);
    vec3 reflectDir = reflect(-lightDir, normal);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);
    vec3 specular = vec3(0.2) * spec;
    
    vec3 result = ambient + diffuse + specular;
    FragColor = vec4(result, 1.0);
}

Explanation

  • Normal Map: A texture that stores normal vectors in its RGB channels.
  • Tangent Space: A coordinate system that is aligned with the surface of the model, used to correctly apply the normal map.

  1. Parallax Mapping

Parallax Mapping is an enhancement of normal mapping that simulates depth by offsetting texture coordinates based on the view angle.

Example Code

Fragment Shader (Parallax Mapping)

#version 330 core
out vec4 FragColor;

in vec3 FragPos;
in vec2 TexCoords;
in vec3 TangentLightPos;
in vec3 TangentViewPos;
in vec3 TangentFragPos;

uniform sampler2D normalMap;
uniform sampler2D diffuseMap;
uniform sampler2D heightMap;

float heightScale = 0.1;

vec2 ParallaxMapping(vec2 texCoords, vec3 viewDir)
{
    float height = texture(heightMap, texCoords).r;     
    vec2 p = viewDir.xy * (height * heightScale); 
    return texCoords - p;   
}

void main()
{
    vec3 viewDir = normalize(TangentViewPos - TangentFragPos);
    vec2 texCoords = ParallaxMapping(TexCoords, viewDir);
    
    // Obtain normal from normal map in range [0,1]
    vec3 normal = texture(normalMap, texCoords).rgb;
    // Transform normal vector to range [-1,1]
    normal = normalize(normal * 2.0 - 1.0);   
    
    // Ambient
    vec3 ambient = 0.1 * texture(diffuseMap, texCoords).rgb;
    
    // Diffuse
    vec3 lightDir = normalize(TangentLightPos - TangentFragPos);
    float diff = max(dot(lightDir, normal), 0.0);
    vec3 diffuse = diff * texture(diffuseMap, texCoords).rgb;
    
    // Specular
    vec3 reflectDir = reflect(-lightDir, normal);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0);
    vec3 specular = vec3(0.2) * spec;
    
    vec3 result = ambient + diffuse + specular;
    FragColor = vec4(result, 1.0);
}

Explanation

  • Height Map: A texture that stores height information in its channels.
  • Parallax Mapping Function: Adjusts the texture coordinates based on the view direction and height map.

  1. Physically Based Rendering (PBR)

PBR is a shading model that aims to simulate the physical properties of materials more accurately. It uses parameters like albedo, metallic, and roughness to achieve realistic lighting.

Example Code

Fragment Shader (PBR)

#version 330 core
out vec4 FragColor;

in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoords;

uniform vec3 lightPos;
uniform vec3 viewPos;
uniform vec3 lightColor;

uniform sampler2D albedoMap;
uniform sampler2D metallicMap;
uniform sampler2D roughnessMap;
uniform sampler2D aoMap;

const float PI = 3.14159265359;

vec3 getNormalFromMap()
{
    vec3 tangentNormal = texture(normalMap, TexCoords).xyz * 2.0 - 1.0;

    vec3 Q1  = dFdx(FragPos);
    vec3 Q2  = dFdy(FragPos);
    vec2 st1 = dFdx(TexCoords);
    vec2 st2 = dFdy(TexCoords);

    vec3 N   = normalize(Normal);
    vec3 T  = normalize(Q1 * st2.t - Q2 * st1.t);
    vec3 B  = -normalize(cross(N, T));
    mat3 TBN = mat3(T, B, N);

    return normalize(TBN * tangentNormal);
}

void main()
{
    vec3 albedo = pow(texture(albedoMap, TexCoords).rgb, vec3(2.2));
    float metallic = texture(metallicMap, TexCoords).r;
    float roughness = texture(roughnessMap, TexCoords).r;
    float ao = texture(aoMap, TexCoords).r;

    vec3 N = getNormalFromMap();
    vec3 V = normalize(viewPos - FragPos);
    vec3 L = normalize(lightPos - FragPos);
    vec3 H = normalize(V + L);

    float distance = length(lightPos - FragPos);
    float attenuation = 1.0 / (distance * distance);
    vec3 radiance = lightColor * attenuation;

    // Cook-Torrance BRDF
    float NDF = DistributionGGX(N, H, roughness);
    float G   = GeometrySmith(N, V, L, roughness);
    vec3 F    = fresnelSchlick(max(dot(H, V), 0.0), F0);

    vec3 nominator    = NDF * G * F;
    float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 0.001;
    vec3 specular = nominator / denominator;

    vec3 kS = F;
    vec3 kD = vec3(1.0) - kS;
    kD *= 1.0 - metallic;

    float NdotL = max(dot(N, L), 0.0);
    vec3 Lo = (kD * albedo / PI + specular) * radiance * NdotL;

    vec3 ambient = vec3(0.03) * albedo * ao;
    vec3 color = ambient + Lo;

    color = color / (color + vec3(1.0));
    color = pow(color, vec3(1.0/2.2));

    FragColor = vec4(color, 1.0);
}

Explanation

  • Albedo: The base color of the material.
  • Metallic: Determines if the material is metallic.
  • Roughness: Determines the roughness of the surface.
  • Ambient Occlusion (AO): Simulates the occlusion of ambient light.

Practical Exercises

Exercise 1: Implement Phong Shading

  1. Create a simple 3D model (e.g., a cube).
  2. Implement Phong Shading in the vertex and fragment shaders.
  3. Adjust the light position and observe the changes in shading.

Solution

Refer to the Phong Shading example code provided above.

Exercise 2: Apply Normal Mapping

  1. Create a 3D model with a normal map texture.
  2. Implement normal mapping in the fragment shader.
  3. Adjust the light position and observe the changes in surface detail.

Solution

Refer to the Normal Mapping example code provided above.

Exercise 3: Experiment with PBR

  1. Create a 3D model with albedo, metallic, roughness, and AO textures.
  2. Implement PBR in the fragment shader.
  3. Adjust the light position and observe the realistic lighting effects.

Solution

Refer to the PBR example code provided above.

Common Mistakes and Tips

  • Incorrect Normal Calculation: Ensure normals are correctly calculated and normalized.
  • Texture Coordinates: Make sure texture coordinates are correctly mapped to the model.
  • Lighting Calculations: Verify that lighting calculations are performed in the correct space (e.g., world space, view space).

Conclusion

In this section, we explored advanced shading techniques in OpenGL, including Phong Shading, Blinn-Phong Shading, Normal Mapping, Parallax Mapping, and Physically Based Rendering (PBR). These techniques can significantly enhance the visual quality of your graphics by simulating realistic lighting and surface details. Practice implementing these techniques to gain a deeper understanding and improve your OpenGL rendering skills.

© Copyright 2024. All rights reserved