Analysis of Ue4 BRDF Formula – Zhihu (Zhihu.com)

Physics-based Coloring in UE4 (1) – Zhihu (zhihu.com)

Image Based Lighting: Diffuse Irradiance – Cnblogs.com

Look at the directory

COOK-TORRANCE BRDF

  • Almost all real-time rendering pipelines use a model called cook-Torrance BRDF.
    • Law of Conservation of Energy (KD KS)
    • Diffuse reflection of Cook-Torrance BRDF (F Lambert)
    • Specular reflection of cook-Torrance BRDF (F COOK − TORRANCE)
  • Formula expansion:
  • Computes the cook-Torrance BRDF existing code
float distance = length(lightPositions[i] - WorldPos);
float attenuation = 1.0 / (distance * distance);
vec3 radiance = lightColors[i] * attenuation;
float NdotL = max(dot(N, L), 0.0);  
// add to outgoing radiance Lo
Lo += (kD * albedo / PI + specular) * radiance * NdotL;  // note that we already multiplied the BRDF by the Fresnel (kS) so we won't multiply by kS again
Copy the code

Law of conservation of energy

  • One of the key features of physically correct rendering is the conservation of energy.
  • Both diffuse and reflected light come from the light shining on the material, so the sum of diffuse and reflected light cannot exceed the total light striking the material.
  • In practice, this means that if the surface is highly reflective, it will show very little diffuse color. Conversely, if the material has a bright diffuse color, it cannot reflect much.
  • Kd is the proportion of the energy that is refracted in the incident light mentioned earlier, while KS is the proportion of the energy that is reflected
  • Existing code for computing conservation of energy
uniform float metallic;
// kS is equal to Fresnel
vec3 kS = F;
// for energy conservation, the diffuse and specular light can't
// be above 1.0 (unless the surface emits light); to preserve this
// Relationship the diffuse Component (kD) should equal 1.0-ks.
vec3 kD = vec3(1.0) - kS;
// multiply kD by the inverse metalness such that only non-metals 
// have diffuse lighting, or a linear blend if partly metal (pure metals
// have no diffuse light).
kD *= 1.0 - metallic;	  
Copy the code

Diffuse reflection of Cook-Torrance BRDF

  • The Lambertian diffuse model is sufficient for most real-time rendering purposes.
    • C represents the surface color

Specular reflection of cook-Torrance BRDF

  • Normal distribution function: Estimates the number of microplanes oriented in the same direction as the intermediate vector under the influence of surface roughness. This is the main function used to estimate the microplane.
  • Geometric function: describes the properties of the microplane as a shadow. When one surface is relatively rough, the microplanes on the surface may block out other microplanes and thus reduce the light reflected from the surface.
  • Fresnel equation: The Fresnel equation describes the fraction of light reflected from a surface at different surface angles.
  • Calculate specular reflection: Cook-Torrance existing code
// Cook-Torrance BRDF
uniform float roughness;
vec3 N = normalize(Normal);
vec3 V = normalize(camPos - WorldPos);
vec3 L = normalize(lightPositions[i] - WorldPos);
vec3 H = normalize(V + L); // The half-range vector
float NDF = DistributionGGX(N, H, roughness);   
float G   = GeometrySmith(N, V, L, roughness);      
vec3 F    = fresnelSchlick(clamp(dot(H, V), 0.0.1.0), F0);
vec3 nominator    =  NDF * G * F; 
float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0); //dot all need attention
vec3 specular = nominator / max(denominator, 0.001); // prevent divide by zero for NdotV=0.0 or NdotL=0.0     
Copy the code

The normal distribution function D

  • The normal distribution function D, or specular distribution, is a statistical approximation of the ratio of microplanes aligned with some (middle) vector H.
    • A represents surface roughness
    • H is taken as the intermediate vector between the plane normal vector and the ray direction vector for different roughness parameters
  • Compute the code on the net for the normal distribution function
float D_GGX_TR(vec3 N, vec3 H, float a)
{
    float a2     = a*a;
    float NdotH  = max(dot(N, H), 0.0);
    float NdotH2 = NdotH*NdotH;

    float nom    = a2;
    float denom  = (NdotH2 * (a2 - 1.0) + 1.0);
    denom        = PI * denom * denom;

    return nom / denom;
}
Copy the code
  • Calculate normal distribution function existing code
uniform float roughness;
vec3 N = normalize(Normal);
vec3 V = normalize(camPos - WorldPos);
vec3 L = normalize(lightPositions[i] - WorldPos);
vec3 H = normalize(V + L); // The half-range vector
const float PI = 3.14159265359;
float NDF = DistributionGGX(N, H, roughness);   
float DistributionGGX(vec3 N, vec3 H, float roughness)
{
    float a = roughness*roughness;
    float a2 = a*a;
    float NdotH = max(dot(N, H), 0.0);
    float NdotH2 = NdotH*NdotH;
    float nom   = a2;
    float denom = (NdotH2 * (a2 - 1.0) + 1.0);
    denom = PI * denom * denom;
    return nom / max(denom, 0.001); // prevent divide by zero for roughness=0.0 and NdotH=1.0
}

Copy the code

Geometric function G

  • The geometric function approximates statistically the rate at which the microplanes are shadowing each other, which consumes the energy of the light.
  • The geometric function is a multiplier with a range of [0.0, 1.0], where white or 1.0 indicates no microplane shadow and black or 0.0 indicates that the microplane is completely obscured.

    • G function
    • K variables
      • Direct sunlight
      • IBL ambient light
  • Computational geometry function code on the web
float GeometrySchlickGGX(float NdotV, float k)
{
    float nom   = NdotV;
    float denom = NdotV * (1.0 - k) + k;

    return nom / denom;
}

float GeometrySmith(vec3 N, vec3 V, vec3 L, float k)
{
    float NdotV = max(dot(N, V), 0.0);
    float NdotL = max(dot(N, L), 0.0);
    float ggx1 = GeometrySchlickGGX(NdotV, k);
    float ggx2 = GeometrySchlickGGX(NdotL, k);

    return ggx1 * ggx2;
}
Copy the code
  • Computes existing code for geometric functions
uniform float roughness;
vec3 N = normalize(Normal);
vec3 V = normalize(camPos - WorldPos);
vec3 L = normalize(lightPositions[i] - WorldPos);
float G   = GeometrySmith(N, V, L, roughness);
float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness)
{
    float NdotV = max(dot(N, V), 0.0);
    float NdotL = max(dot(N, L), 0.0);
    float ggx2 = GeometrySchlickGGX(NdotV, roughness);
    float ggx1 = GeometrySchlickGGX(NdotL, roughness);
    return ggx1 * ggx2;
}
float GeometrySchlickGGX(float NdotV, float roughness)
{
    float r = (roughness + 1.0);
    float k = (r*r) / 8.0;
    float nom   = NdotV;
    float denom = NdotV * (1.0 - k) + k;
    return nom / denom;
}
Copy the code

Fresnel equation F

  • The Fresnel (pronounced Freh-nel) equation describes the ratio of reflected light to the part of light that is refracted, and this ratio varies depending on the Angle we look at it from
  • First it was nonmetals, now it’s metals
  • In popular language, the larger the normal and the greater the visual Angle, the greater the intensity of mirror reflection, and the closer it approaches 90 degrees, it is almost complete reflection
    • F0 represents the basic reflectivity of a plane, calculated using the Indices of Refraction or IOR
      • Most non-metals will not be higher than 0.17
      • Nonmetal 0.04 is good enough for base reflectivity
      • Metals mostly vary between 0.5 and 1.0
      • The base reflectance of metal surfaces is usually colored
  • Calculate the F0 code on the net
vec3 F0 = vec3(0.04);
F0      = mix(F0, surfaceColor.rgb, metalness);
Copy the code
  • Evaluate existing F0 code
// of 0.04 and if it's a metal, use the albedo color as F0 (metallic workflow)    
vec3 F0 = vec3(0.04); 
F0 = mix(F0, albedo, metallic);
Copy the code
  • Calculate Fresnel equation online code
vec3 fresnelSchlick(float cosTheta, vec3 F0)
{
    return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}
// where cosTheta is the dot product of the surface normal vector n and the observation direction v.
Copy the code
  • Calculate the existing code of Fresnel equation
vec3 V = normalize(camPos - WorldPos);
vec3 L = normalize(lightPositions[i] - WorldPos);
vec3 H = normalize(V + L); // The half-range vector
vec3 F    = fresnelSchlick(clamp(dot(H, V), 0.0.1.0), F0);
vec3 fresnelSchlick(float cosTheta, vec3 F0)
{
    return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}
Copy the code

Color mapping and Gamma correction

  • PBR Everything is done in a linear color space
  • Lo as a result can get very large (over 1), but the value is truncated because of the default LDR input.
  • Before gamma correction we used tonal mapping to map Lo from the value of LDR to the value of HDR.
// HDR tonemapping
//color = color/(color + vec3(1.0));
// gamma correct
color = pow(color, vec3(1.0/2.2)); 
Copy the code

code

  • Vertex shader
export var vs_pbr =
`#version 300 es precision mediump float; layout (location = 0) in vec3 aPos; layout (location = 1) in vec2 aTexCoords; layout (location = 2) in vec3 aNormal; out vec2 TexCoords; out vec3 WorldPos; out vec3 Normal; uniform mat4 projection; uniform mat4 view; uniform mat4 model; void main() { TexCoords = aTexCoords; Vec3 (Model * VEC4 (aPos, 1.0)); Normal = mat3(model) * aNormal; Gl_Position = projection * view * VEC4 (WorldPos, 1.0); } `
Copy the code
  • Chip shader
export var fs_pbr =
`#version 300 es precision mediump float; out vec4 FragColor; in vec2 TexCoords; in vec3 WorldPos; in vec3 Normal; // // material parameters uniform vec3 albedo; uniform float metallic; uniform float roughness; uniform float ao; // // lights uniform vec3 lightPositions[4]; uniform vec3 lightColors[4]; uniform vec3 camPos; Const float PI = 3.14159265359; float DistributionGGX(vec3 N, vec3 H, float roughness) { float a = roughness*roughness; float a2 = a*a; Float NdotH = Max (dot(N, H), 0.0); float NdotH2 = NdotH*NdotH; float nom = a2; Float denom = (NdotH2 * (a2-1.0) + 1.0); float denom = (NdotH2 * (a2-1.0) + 1.0); denom = PI * denom * denom; Return nom/Max (Denom, 0.001); // Prevent divide by zero for roughness=0.0 and NdotH=1.0} float GeometrySchlickGGX Float roughness) {float r = (roughness + 1.0); Float k = (r*r) / 8.0; float k = (r*r) / 8.0; float nom = NdotV; Float denom = NdotV * (1.0-k) + k; float denom = NdotV * (1.0-k) + k; return nom / denom; } Float GeometrySmith(VEC3 N, VEC3 V, VEC3 L, float roughness) {float NdotV = Max (dot(N, V), 0.0); Float NdotL = Max (dot(N, L), 0.0); float NdotL = Max (dot(N, L), 0.0); float ggx2 = GeometrySchlickGGX(NdotV, roughness); float ggx1 = GeometrySchlickGGX(NdotL, roughness); return ggx1 * ggx2; } vec3 fresnelSchlick(float cosTheta, vec3 F0) {return F0 + (1.0-f0) * pow(1.0-costheta, 5.0); } // ---------------------------------------------------------------------------- void main() { vec3 N = normalize(Normal); vec3 V = normalize(camPos - WorldPos); // calculate reflectance at normal incidence; If dia-electric (like plastic) use F0 // of 0.04 and if it's a metal, Use the Albedo color as F0 (metallic Workflow) VEC3 F0 = VEC3 (0.04); F0 = mix(F0, albedo, metallic); // Reflectance equation vec3 Lo = vec3(0.0); for(int i = 0; i < 4; i++) { // calculate per-light radiance vec3 L = normalize(lightPositions[i] - WorldPos); vec3 H = normalize(V + L); float distance = length(lightPositions[i] - WorldPos); Float elevation = 1.0 / (distance * distance); float elevation = 1.0 / (distance * distance); vec3 radiance = lightColors[i] * attenuation; // Cook-Torrance BRDF float NDF = DistributionGGX(N, H, roughness); float G = GeometrySmith(N, V, L, roughness); Vec3 F = fresnelSchlick(clamp(dot(H, V), 0.0, 1.0), F0); vec3 nominator = NDF * G * F; Float denominator = 0 * Max (dot(N, V), 0.0) * Max (dot(N, L), 0.0); float denominator = 0 * Max (dot(N, V), 0.0) * Max (dot(N, L), 0.0); Vec3 specular = nominator/Max (denominator, 0.001); // prevent divide by zero for NdotV=0.0 or NdotL=0.0 // kS is equal to Fresnel vec3; For energy conservation, the diffuse and specular light can't // be above 1.0 (unless the surface emits light); To preserve this // Relationship the diffuse Component (kD) should equal 1.0-ks. Vec3 kD = vec3(1.0) -ks; // multiply kD by the inverse metalness such that only non-metals // have diffuse lighting, Or a linear blend if partly metal (pure metal // have no diffuse light). KD *= 1.0-metallic; // Scale light by NdotL float NdotL = Max (dot(N, L), 0.0); // add to outgoing radiance Lo Lo += (kD * albedo / PI + specular) * radiance * NdotL; // note that we already multiplied the BRDF by the Fresnel (kS) so we won't multiply by kS again } // ambient lighting (note that the next IBL tutorial will replace // this ambient lighting with environment lighting). vec3 ambient = Vec3 (0.03) * albedo * AO; vec3 color = ambient+ Lo; //ambient + Lo; // HDR tonemapping //color = color (color + vec3(1.0)); // gamma correct color = pow(color, vec3(1.0/2.2)); FragColor = vec4 (color, 1.0); //FragColor = vec4(1.0, 1.0, 1.0, 1.0); } `
Copy the code
  • Some of the uniform
const sizeFloat = 4;
let indexCount = 0;
let projection = mat4.create(), view = mat4.create();
let model = mat4.create();
const nrRows = 7;
const nrColumns = 7;
const spacing = 2.5;
let camera = new Camera(vec3.fromValues(0.0.0.0.10.0), vec3.fromValues(0.0.1.0.0.0));
let deltaTime = 0.0;
let lastFrame = 0.0;
let mouse = new Mouse();
gl.enable(gl.DEPTH_TEST);
shader = new Shader(gl, vs_pbr, fs_pbr);
shader.use(gl);
gl.uniform3f(gl.getUniformLocation(shader.programId, "albedo"), 0.5.0.0.0.0);
shader.setFloat(gl, "ao".1.0);
lightPositions = new Float32Array([-10.0.10.0.10.0.10.0.10.0.10.0,
    -10.0, -10.0.10.0.10.0, -10.0.10.0
]);
lightColors = new Float32Array([
    300.0.300.0.300.0.300.0.300.0.300.0.300.0.300.0.300.0.300.0.300.0.300.0
]);
gl.uniform3fv(gl.getUniformLocation(shader.programId, "lightPositions"), lightPositions);
gl.uniform3fv(gl.getUniformLocation(shader.programId, "lightColors"), lightColors);
Copy the code
  • The metallicity decreases and the roughness decreases
function render() { let currentFrame = performance.now(); deltaTime = currentFrame - lastFrame; lastFrame = currentFrame; processInput(); Gl. ClearColor (0.1, 0.1, 0.1, 1.0); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); shader.use(gl); view = camera.GetViewMatrix(); gl.uniformMatrix4fv(gl.getUniformLocation(shader.programId, "view"), false, view); gl.uniform3fv(gl.getUniformLocation(shader.programId, "camPos"), new Float32Array(camera.Position)); Mat4. Perspective (projection, (camera.Zoom) * math.pi / 180, Canvas.width/canvas.height, 0.1, 100.0); gl.uniformMatrix4fv(gl.getUniformLocation(shader.programId, "projection"), false, projection); mat4.identity(model); for (let row = 0; row < nrRows; row++) { shader.setFloat(gl, "metallic", row / (nrRows - 1)); for (let col = 0; col < nrColumns; Col ++) {let r = Math. Max (col/(nrColumns - 1), 0.025); shader.setFloat(gl, "roughness", r); mat4.identity(model); Mat4. Translate (model, model, vec3) fromValues((col - (nrColumns / 2)) * 0, (row - (nrRows / 2)) * 0, 0.0)); gl.uniformMatrix4fv(gl.getUniformLocation(shader.programId, "model"), false, model); renderSphere(); } } for (let i = 0; i < lightPositions.length / 3; ++i) { let newPos = vec3.fromValues(lightPositions[3 * i], lightPositions[3 * i + 1], lightPositions[3 * i + 2]); Shader. SetFloat (gl, "metallic", 1.0); Shader. SetFloat (gl, "roughness", 1.0); mat4.identity(model); mat4.translate(model, model, newPos); Mat4. scale(model, model, vec3. FromValues (0.5, 0.5, 0.5)); gl.uniformMatrix4fv(gl.getUniformLocation(shader.programId, "model"), false, model); renderSphere(); } } function renderSphere() { if (sphereVAO == null) { sphereVAO = gl.createVertexArray(); let vbo, ebo; vbo = gl.createBuffer(); ebo = gl.createBuffer(); let positions = []; let uv = []; let normals = []; let indices = []; ; const X_SEGMENTS = 64; const Y_SEGMENTS = 64; Const PI = 3.14159265359; for (let y = 0; y <= Y_SEGMENTS; ++y) { for (let x = 0; x <= X_SEGMENTS; ++x) { let xSegment = x / X_SEGMENTS; let ySegment = y / Y_SEGMENTS; Let xPos = math. cos(xSegment * 2.0 * PI) * math. sin(ySegment * PI); let yPos = Math.cos(ySegment * PI); Let zPos = math.sin (xSegment * 2.0 * PI) * math.sin (ySegment * PI); positions.push(vec3.fromValues(xPos, yPos, zPos)); uv.push(vec2.fromValues(xSegment, ySegment)); normals.push(vec3.fromValues(xPos, yPos, zPos)); } } let oddRow = false; for (let y = 0; y < Y_SEGMENTS; ++y) { if (! oddRow) { for (let x = 0; x <= X_SEGMENTS; ++x) { indices.push(y * (X_SEGMENTS + 1) + x); indices.push((y + 1) * (X_SEGMENTS + 1) + x); } } else { for (let x = X_SEGMENTS; x >= 0; --x) { indices.push((y + 1) * (X_SEGMENTS + 1) + x); indices.push(y * (X_SEGMENTS + 1) + x); } } oddRow = ! oddRow; } indexCount = indices.length; let data = []; for (let i = 0; i < positions.length; ++i) { data.push(positions[i][0]); data.push(positions[i][1]); data.push(positions[i][2]); if (uv.length > 0) { data.push(uv[i][0]); data.push(uv[i][1]); } if (normals.length > 0) { data.push(normals[i][0]); data.push(normals[i][1]); data.push(normals[i][2]); } } gl.bindVertexArray(sphereVAO); gl.bindBuffer(gl.ARRAY_BUFFER, vbo); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW); gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ebo); gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW); let stride = (3 + 2 + 3) * 4; gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 3, gl.FLOAT, false, stride, 0); gl.enableVertexAttribArray(1); gl.vertexAttribPointer(1, 2, gl.FLOAT, false, stride, (3 * 4)); gl.enableVertexAttribArray(2); gl.vertexAttribPointer(2, 3, gl.FLOAT, false, stride, (5 * 4)); } gl.bindVertexArray(sphereVAO); gl.drawElements(gl.TRIANGLE_STRIP, indexCount, gl.UNSIGNED_SHORT, 0); }Copy the code