内容参考闫令琪课程《games202-高质量实时渲染及作业2》、花桑博客、learnopengl
IBL是一类光照技术的合集,将周围环境整体视为一个大光源,对一个点着色时要计算所有环境造成的影响。
最直接的方法是,对环境贴图颜色进行采样作为光源,采样足够密集也能逼近真实场景,但是性能比较差,实际中有很多方法,本篇讲述的PRT是其中一种。
回顾下反射方程,PBR渲染模型中,分漫反射、镜面反射两部分,这两部分可以分开积分。
这篇文章讲环境光的漫反射实现
漫反射实现又分两种
如果你了解傅里叶变换的原理,理解球谐函数就比较容易了。
可以参考我前面写的文章《傅里叶变换及一应用》
傅里叶变换是将周期函数分解成正余弦函数,是二维平面的分解,正余弦函数作为基向量。
三维空间的函数分解可以将球谐函数做为基函数。也就是说任意三维空间的形状都可以由球谐函数组合
看看球谐函数的可视化效果
球谐函数是理解PRT的核心知识,网上有很多相关的科普文章,此处不做展开
球谐函数是怎么和环境光漫反射关联起来的呢?
看上面这页图,计算一个点的着色,把光强和光的传播拆开看,
环境光L(i)项可以分解成球谐函数的组合,基函数B(i)和V(i)max(0, n.i)看成后一项在基函数上的投影系数
最终,漫反射方程简化成
i的个数是有限的,一般去3阶,一共9个参数,9维度的向量点乘计算量可接受
PRT就是要对 l i l_i li 和 T i T_i Ti预计算。
环境光照和传播的系数计算是在C++工程中实现的,作业框架中已提供,就是求积分。需要注意的是,光传播系数是对每个点都要计算一个,比如模型有3个点,每个点有9个系数,则一共有3 * 9个值,存成txt格式。
环境光的球谐系数是9 * 3(光有RGB三通道)
光传播系数,这个模型有3万多个点
注意:环境光是6个张图片,分别代表空间中的6个面,需要将每个像素换算到立体角,实现细节较多,不展开了。
代码在prt.cpp中
virtual void preprocess(const Scene *scene) override
{
// 1. 计算环境光的球谐系数,手动计算
// 2. 光传播的球谐系数,需要采样、光追,有已经提供的函数
}
生成好的.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);
}
需要注意的是,片元着色器中增加了个调色+伽马校正,否则颜色会偏暗,如下图: