GAMES202实时渲染(2)-Precomputed Radiance Transfer

内容参考闫令琪课程《games202-高质量实时渲染及作业2》、花桑博客、learnopengl

实现效果

GAMES202实时渲染(2)-Precomputed Radiance Transfer_第1张图片

GAMES202实时渲染(2)-Precomputed Radiance Transfer_第2张图片

GAMES202实时渲染(2)-Precomputed Radiance Transfer_第3张图片

原理

IBL(Image based light)

IBL是一类光照技术的合集,将周围环境整体视为一个大光源,对一个点着色时要计算所有环境造成的影响。
GAMES202实时渲染(2)-Precomputed Radiance Transfer_第4张图片

最直接的方法是,对环境贴图颜色进行采样作为光源,采样足够密集也能逼近真实场景,但是性能比较差,实际中有很多方法,本篇讲述的PRT是其中一种。

PBR回顾

回顾下反射方程,PBR渲染模型中,分漫反射、镜面反射两部分,这两部分可以分开积分。
GAMES202实时渲染(2)-Precomputed Radiance Transfer_第5张图片

这篇文章讲环境光的漫反射实现

GAMES202实时渲染(2)-Precomputed Radiance Transfer_第6张图片

漫反射实现又分两种

  • Diffuse Unshadowed(不考虑阴影)

GAMES202实时渲染(2)-Precomputed Radiance Transfer_第7张图片

  • Diffuse Shadowed(带阴影)

GAMES202实时渲染(2)-Precomputed Radiance Transfer_第8张图片
增加了V项,表示有遮挡形成的阴影

球谐函数

如果你了解傅里叶变换的原理,理解球谐函数就比较容易了。

可以参考我前面写的文章《傅里叶变换及一应用》

傅里叶变换是将周期函数分解成正余弦函数,是二维平面的分解,正余弦函数作为基向量。

三维空间的函数分解可以将球谐函数做为基函数。也就是说任意三维空间的形状都可以由球谐函数组合

看看球谐函数的可视化效果

GAMES202实时渲染(2)-Precomputed Radiance Transfer_第9张图片
看起来就是空间中相互正交的函数

球谐函数是理解PRT的核心知识,网上有很多相关的科普文章,此处不做展开

球谐函数是怎么和环境光漫反射关联起来的呢?

GAMES202实时渲染(2)-Precomputed Radiance Transfer_第10张图片

看上面这页图,计算一个点的着色,把光强和光的传播拆开看,

环境光L(i)项可以分解成球谐函数的组合,基函数B(i)和V(i)max(0, n.i)看成后一项在基函数上的投影系数

最终,漫反射方程简化成

GAMES202实时渲染(2)-Precomputed Radiance Transfer_第11张图片

i的个数是有限的,一般去3阶,一共9个参数,9维度的向量点乘计算量可接受

  • ρ是反射率,一种特定的材质反射率是固定的
  • l i l_i li代表环境光,每个环境光对应一组 l i l_i li
  • T i T_i Ti是场景的固有属性,和光照无关,即给定一个场景,它的传播系数、遮挡关系是固定的

PRT就是要对 l i l_i li T i T_i Ti预计算。

核心代码说明

PRT实现

环境光照和传播的系数计算是在C++工程中实现的,作业框架中已提供,就是求积分。需要注意的是,光传播系数是对每个点都要计算一个,比如模型有3个点,每个点有9个系数,则一共有3 * 9个值,存成txt格式。

环境光的球谐系数是9 * 3(光有RGB三通道)

GAMES202实时渲染(2)-Precomputed Radiance Transfer_第12张图片

光传播系数,这个模型有3万多个点

GAMES202实时渲染(2)-Precomputed Radiance Transfer_第13张图片

注意:环境光是6个张图片,分别代表空间中的6个面,需要将每个像素换算到立体角,实现细节较多,不展开了。

代码在prt.cpp中

virtual void preprocess(const Scene *scene) override
{
  // 1. 计算环境光的球谐系数,手动计算
  // 2. 光传播的球谐系数,需要采样、光追,有已经提供的函数
}

prt文件的使用

生成好的.txt文件copy到前端工程中,注意GraceCathedral、Indoor、Skybox三个场景都需要预计算好。

新建PRTMaterial.js,新增加了预备计算的球谐系数

class PRTMaterial extends Material {
    constructor(vertexShader, fragmentShader) {
        super( {
            'uPrecomputeL[0]' : {type: 'precomputeL', value:null},
            'uPrecomputeL[1]' : {type: 'precomputeL', value:null},
            'uPrecomputeL[2]' : {type: 'precomputeL', value:null},
        },
        ['aPrecomputeLT'],
        vertexShader, fragmentShader, null);
    }
}

async function buildPRTMaterial(vertexPath, fragmentPath) {
    // xiatian05
    let vertexShader = await getShaderString(vertexPath);
    let fragmentShader = await getShaderString(fragmentPath);

    return new PRTMaterial(vertexShader, fragmentShader);
}

光的球谐系数在 WebGLRenderer.js中设置,是统一环境变量

let Mat3Value = getMat3ValueFromRGB(precomputeL[guiParams.envmapId]);
for (let j = 0; j < 3; j++) {
    if (k == 'uPrecomputeL[' + j + ']') {
        gl.uniformMatrix3fv(
            this.meshes[i].shader.program.uniforms[k],
            false,
            Mat3Value[j]);
    }
}

光传播的球谐系数在MeshRender.js中设置,属于每个顶点的属性.需要注意的是,用3x3的矩阵来放9个参数,所以顶点属性设置时整了个for循环,分三次设置到aPrecomputeLT矩阵中。

这里我还没太理解,为啥不用长度为9的向量来存放呢?

// Bind attribute mat3 - LT
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(precomputeLT[guiParams.envmapId]), gl.STATIC_DRAW);

for (var ii = 0; ii < 3; ++ii) {
  gl.enableVertexAttribArray(this.shader.program.attribs['aPrecomputeLT'] + ii);
  gl.vertexAttribPointer(this.shader.program.attribs['aPrecomputeLT'] + ii, 3, gl.FLOAT, false, 36, ii * 12);
}

对应到shader,顶点着色器 prtVertex.glsl

attribute vec3 aVertexPosition;
attribute vec3 aNormalPosition;
attribute mat3 aPrecomputeLT;

uniform mat4 uModelMatrix;
uniform mat4 uViewMatrix;
uniform mat4 uProjectionMatrix;

uniform mat3 uPrecomputeL[3];

varying highp vec3 vNormal;
varying highp mat3 vPrecomputeLT;
varying highp vec3 vColor;

float L_dot_LT(mat3 PrecomputeL, mat3 PrecomputeLT) {
    vec3 L_0 = PrecomputeL[0];
    vec3 L_1 = PrecomputeL[1];
    vec3 L_2 = PrecomputeL[2];
    vec3 LT_0 = PrecomputeLT[0];
    vec3 LT_1 = PrecomputeLT[1];
    vec3 LT_2 = PrecomputeLT[2];
    return dot(L_0, LT_0) + dot(L_1, LT_1) + dot(L_2, LT_2);
}

void main(void) {
    vNormal = (uModelMatrix * vec4(aNormalPosition, 0.0)).xyz;
    for(int i = 0; i < 3; i++)
    {
        vColor[i] = L_dot_LT(aPrecomputeLT, uPrecomputeL[i]);
    }
    gl_Position = uProjectionMatrix * uViewMatrix * uModelMatrix * vec4(aVertexPosition, 1.0);
}

片元着色器比较简单

#ifdef GL_ES
precision mediump float;
#endif

varying highp vec3 vColor;

vec3 toneMapping(vec3 color){
    vec3 result;

    for(int i = 0; i < 3; i++){
        if (color[i] <= 0.0031308){
            result[i] = 12.92 * color[i];
        } else {
            result[i] = (1.0 + 0.055) * pow(color[i], 1.0/2.4) - 0.055;
        }
    }

    return result;
}

void main() {
    vec3 color = toneMapping(vColor);
    gl_FragColor = vec4(color, 1.0);
}

需要注意的是,片元着色器中增加了个调色+伽马校正,否则颜色会偏暗,如下图:

无伽马校正
GAMES202实时渲染(2)-Precomputed Radiance Transfer_第14张图片

加了伽马校正
GAMES202实时渲染(2)-Precomputed Radiance Transfer_第15张图片

你可能感兴趣的:(图形学,图形渲染,前端)