OpenGL_JS-lighting PBR 直接光照

1,064 阅读7分钟

Ue4 BRDF公式解析 - 知乎 (zhihu.com)

UE4中的基于物理的着色(一) - 知乎 (zhihu.com)

Image Based Lighting: Diffuse irradiance - 闪之剑圣 - 博客园 (cnblogs.com)

看目录

COOK-TORRANCE BRDF

  • 几乎所有实时渲染管线使用的都是一种被称为Cook-Torrance BRDF模型。
  • image.png
    • 能量守恒定律(kd ks)
    • COOK-TORRANCE BRDF的漫反射(f lambert)
    • COOK-TORRANCE BRDF的镜面反射(f cook−torrance)
  • 公式展开:image.png
  • 计算COOK-TORRANCE BRDF现有代码
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

能量守恒定律

image.png

  • 物理正确渲染的关键特征之一是能量守恒。
  • 漫射光和反射光均来自照射材料的光,因此漫射光和反射光的总和不能超过撞击材料的总光。
  • 实践中,这意味着如果表面具有高反射性,则它将显示非常少的漫反射颜色。相反,如果材料具有明亮的漫反射颜色,则它不能反射太多。
  • image.png
  • kd 是早先提到过的入射光线中被折射部分的能量所占的比率,而ks是被反射部分的比率
  • 计算能量守恒的现有代码
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;	  

COOK-TORRANCE BRDF的漫反射

  • Lambertian漫反射模型已经足够应付大多数实时渲染的用途了。
  • image.png
    • c表示表面颜色

COOK-TORRANCE BRDF的镜面反射

  • 正态分布函数:估算在受到表面粗糙度的影响下,取向方向与中间向量一致的微平面的数量。这是用来估算微平面的主要函数。
  • 几何函数:描述了微平面自成阴影的属性。当一个平面相对比较粗糙的时候,平面表面上的微平面有可能挡住其他的微平面从而减少表面所反射的光线。
  • 菲涅尔方程:菲涅尔方程描述的是在不同的表面角下表面所反射的光线所占的比率。
  • image.png
  • 计算镜面反射:COOK-TORRANCE现有代码
// 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); //半程向量
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都需要注意
vec3 specular = nominator / max(denominator, 0.001); // prevent divide by zero for NdotV=0.0 or NdotL=0.0     

正态分布函数 D

  • 正态分布函数D,或者说镜面分布,从统计学上近似的表示了与某些(中间)向量h取向一致的微平面的比率。
  • image.png
    • a表示表面粗糙度
    • h 当成是不同粗糙度参数下,平面法向量和光线方向向量之间的中间向量的话
      • image.png
  • 计算正态分布函数网上代码
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;
}
  • 计算正态分布函数现有代码
uniform float roughness;
vec3 N = normalize(Normal);
vec3 V = normalize(camPos - WorldPos);
vec3 L = normalize(lightPositions[i] - WorldPos);
vec3 H = normalize(V + L); //半程向量
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
}

几何函数 G

  • 几何函数从统计学上近似的求得了微平面间相互遮蔽的比率,这种相互遮蔽会损耗光线的能量。
  • 几何函数是一个值域为[0.0, 1.0]的乘数,其中白色或者说1.0表示没有微平面阴影,而黑色或者说0.0则表示微平面彻底被遮蔽。 image.png
  • image.png
    • G函数 image.png
    • K变量
      • 直接光照image.png
      • IBL环境光image.png
  • 计算几何函数网上代码
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;
}
  • 计算几何函数现有代码
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;
}

菲涅尔方程 F

  • 菲涅尔(发音为Freh-nel)方程描述的是被反射的光线对比光线被折射的部分所占的比率,这个比率会随着我们观察的角度不同而不同
  • 一开始只适用非金属,现在可以适用金属了
  • 用通俗语言讲就是当法线和视觉角度越大镜面反射强度会越大,当趋近90度基本就是完全反射
  • image.png
  • image.png
    • F0,表示平面的基础反射率,它是利用所谓折射指数(Indices of Refraction)或者说IOR计算得出的
      • 非金属 大部分不会高于0.17
      • 非金属 0.04作为基础反射率已经足够好了
      • 金属 大部分0.5和1.0之间变化
      • 金属表面而言基础反射率一般是带有色彩的
      • image.png
  • 计算F0网上代码
vec3 F0 = vec3(0.04);
F0      = mix(F0, surfaceColor.rgb, metalness);
  • 计算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);
  • 计算菲涅尔方程 网上代码
vec3 fresnelSchlick(float cosTheta, vec3 F0)
{
    return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}
//其中cosTheta是表面法向量n与观察方向v的点乘的结果。
  • 计算菲涅尔方程 现有代码
vec3 V = normalize(camPos - WorldPos);
vec3 L = normalize(lightPositions[i] - WorldPos);
vec3 H = normalize(V + L); //半程向量
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);
}

色彩映射与Gamma矫正

  • PBR所有计算都在线性的颜色空间中进行的
  • Lo作为结果可能会变大得很快(超过1),但是因为默认的LDR输入而取值被截断。
  • 在伽马矫正之前我们采用色调映射使Lo从LDR的值映射为HDR的值。
// HDR tonemapping
//color = color / (color + vec3(1.0));
// gamma correct
color = pow(color, vec3(1.0/2.2)); 

代码

image.png

  • 顶点着色器
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;
    WorldPos = vec3(model * vec4(aPos, 1.0));
    Normal = mat3(model) * aNormal;   
    gl_Position =  projection * view * vec4(WorldPos, 1.0);
}`
  • 片元着色器
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);
    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 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;
}

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

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 attenuation = 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 = 4.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 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;	  

        // 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);
}`
  • 一些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);
  • 金属度 递减,粗糙度递减
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)) * spacing, (row - (nrRows / 2)) * spacing, 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);
}