Blinn-Phong 光照模型
Blinn-Phong
光照模型,是由 Jim Blinn 提出的冯氏光照模型的改进版。
这个模型只需要对之前我们的文章里的冯氏光照模型进行一些修改就可以实现。其引入了半程向量halfVec
,简化了镜面反射这一步骤的计算。它避免了计算反射向量reflectDir
,在光源与视点都不断变化的情况下,计算半程向量要比反射向量更加的高效
// 镜面反射
// vec3 reflectDir=reflect(-lightDir,vNormal);
vec3 viewDir=normalize(cameraPosition-vWorldPosition);
// float spec=dot(viewDir,reflectDir);
vec3 halfVec = normalize(lightDir + viewDir);
float spec = dot(vNormal, halfVec);
也就是说:
halfVec
是光照方向的向量lightDir
与眼睛的方向向量viewDir
相加并且归一化之后的值- 镜面高光因子
spec
就变成了法向量vNormal
与半程向量halfVec
的点积。
如图所示,还是有很大的差距的,左边的Blinn-Phong
模型的亮斑要比右边的冯氏光照模型的大一点。
也就是说,这个光照模型的过渡要比冯氏光照模型更加的顺滑,我们之后可以优先把它作为基本使用的光照模型了。
IBL 镜面反射
IBL
镜面反射是一种基于环境贴图的反射技术,IBL
的意思就是Image-Based Lighting
,基于图像的光照,这里的图像指的就是环境贴图。
我们使用的是 CubeTextureLoader来加载的环境贴图。这个就是立方体贴图,而立方体贴图需要有六个面,所以我们需要有六个方向的图,每个面都代表着一个方向,分别是前、后、左、右、上、下
,如此一来就可以完全包裹住物体,营造成一种 360 度环绕的效果。
那么这个环境贴图我们在哪儿找呢?
可以参考这个网站:polyhaven。接下来我会演示如何处理成THREEJS
需要的六面图。
比如说我随便选中一个网站展出的图:
因为我们这边只介绍到处理 HDR 图形的方法,所以我们选择 HDR 格式下载。
下载之后我们得到的是一个 HDR 的图片,接下来来到HDRI-to-CubeMap处理它。
第一步:上传 HDR
第二步:按下 SAVE 按钮
第三步:我们需要的是六面图,所以要选择第三个,等到 PROCESS 结束,按钮变为 SAVE 之后,就可以保存一个 ZIP 压缩包了,里面就是我们得到的六面图。
当然了,你也可以偷个懒,毕竟我这边也会提供六面图给你直接用:
const envmap = new THREE.CubeTextureLoader().load([
"https://cdn.jsdelivr.net/gh/ArisaTaki/MyBlogPic@image/img/px.png", // 正X
"https://cdn.jsdelivr.net/gh/ArisaTaki/MyBlogPic@image/img/nx.png", // 负X
"https://cdn.jsdelivr.net/gh/ArisaTaki/MyBlogPic@image/img/py.png", // 正Y
"https://cdn.jsdelivr.net/gh/ArisaTaki/MyBlogPic@image/img/ny.png", // 负Y
"https://cdn.jsdelivr.net/gh/ArisaTaki/MyBlogPic@image/img/pz.png", // 正Z
"https://cdn.jsdelivr.net/gh/ArisaTaki/MyBlogPic@image/img/nz.png", // 负Z
]);
在材质的uniform
变量里面,添加iChannel0
变量,将环境贴图丢进去:
const material = new THREE.ShaderMaterial({
vertexShader: /* glsl */ `...`,
fragmentShader: /* glsl */ `...`,
uniforms: {
iChannel0: {
value: envmap,
},
},
});
在着色器的顶部,声明一下iChannel0
变量,要注意一下它的类型samplerCube
,意思就是立方体贴图
。
uniform samplerCube iChannel0;
最后我们就在主函数里面开始计算IBL
镜面反射。
void main() {
...,
// IBL镜面反射
// 光照强度
float iblIntensity=1.;
// 反射光方向向量
vec3 iblCoord=normalize(reflect(-viewDir,vNormal));
vec3 ibl = textureCube(iChannel0, iblCoord);
vec3 iblLight = ibl * iblIntensity;
col += iblLight * objectColor;
gl_FragColor=vec4(col,1.);
}
来解释一下代码
iblCoord
:反射光的方向向量。这里的入射光方向参数就用眼睛的方向向量viewDir
,法向量依然是vNormal
,之前已经讲过了反射光要怎么计算,就是用reflect
函数,第一个参数是入射光方向向量的反方向,第二个就是法向量;处理之后记得归一化。ibl
:使用textureCube
来对环境贴图iChannel0
进行采样,坐标选择就是反射光的反向向量iblCoord
了,取前面三个分量。这里有一个内容需要指明一下:使用的是
textureCube
而不是texture
;学过前面的纹理的话都知道,glsl 采样纹理的方法都是texture
;这是因为,在Three.js
中,如果你使用的是WebGL 2.0
或者开启了相关扩展,你可以使用texture
函数采样立方体贴图。否则,通常使用textureCube
函数来采样立方体贴图。textureCube
在WebGL 1.0
和WebGL 2.0
中都可以使用,适用于立方体贴图采样。texture
在WebGL 2.0
中可以用来采样多种类型的纹理,包括立方体贴图。如果你在WebGL 1.0
中使用texture
来采样立方体贴图,可能会出现兼容性问题。- 将
ibl
变量与定义好的光照强度iblIntensity
相乘,就得到了 IBL 镜面反射光iblLight
。 - 将 IBL 镜面反射光
iblLight
与物体颜色相乘,最后与输出颜色相加,就得到了光照影响后的物体。
菲涅尔反射
菲涅尔反射指的是光照到物体的表面之后,一部分光进入了物体的内部,一部分发生了反射,产生折射和散射的现象。被反射的光和入射光之间存在一定的比率关系,这个比率关系可以通过菲涅耳等式进行计算
再举一个简单的例子,视线越接近垂直物体表面,反射越弱,折射越强,反之,视线越平行于表面,反射越强。比如说,我站在湖边,近处的视线和湖面更倾向于垂直,反射弱,就可以看到湖面之下的东西,
远处视线更倾向于平行,只能看到湖面反射的内容,看不到湖面下的东西。
在现实里,要计算菲涅尔反射的复杂程度非比寻常,但是我们在计算机图形学里,可以使用近似等式来计算,如下:
float fresenl (float bias, float scale, float power, vec3 I, vec3 N) {
return bias +scale * pow(1, -dot(I,N), power);
}
- bias:偏移量,通常用于设置菲涅尔反射系数的最小值。在许多情况下,bias 设置为 0。
- scale:比例因子,控制菲涅尔反射的强度,根据需求自由调整
- power:指数因子,控制菲涅尔反射的衰减速率,根据需求自由调整
- I:入射向量,通常是视线方向向量 viewDir。
- N:法向量,通常是表面法线向量 vNormal。
把这个方法写在片元着色器的main
主函数之外。
让我们在main
主函数里面计算菲涅尔光照:
// 菲涅尔反射
vec3 frenselColor=vec3(1.);
float frenselIntensity=.6;
float fres = fresnel(0.,1.,5.,viewDir,vNormal);
vec3 frenselLight = frenselColor * fres * frenselIntensity;
col += frenselLight * objectColor;
用fresnel
函数计算出菲涅尔因子,bias
一般固定是 0,scale
和power
分别设置为 1.和 5.,I
传眼睛的方向向量 viewDir
,N
传法向量 vNormal
。
将菲涅尔因子、菲涅尔光照颜色和菲涅尔光照强度三者相乘,就得到了菲涅尔光照。
将菲涅尔光照与输出颜色相加,就得到了光照影响后的物体。
可以看到我们的外围多了一圈边缘光,这就是菲涅尔反射的效果。
可以自行把 scale 拉高一点,这样就可以控制更大的菲涅尔反射强度,边缘光的效果更加明显。