1. 介绍
直接看效果
简单的说就是实现一个周围环境有雾气的效果。
2.原理
这里实现最简单的一种线性雾化
。
- 雾化说白了和光照一样,只是按照某种计算改变绘制的颜色。
- 结合现实,一个物体被笼罩在雾气中,当我们(观察点)靠近物体的时候,能看见的部分越来越多,反之越少。
- 转化成绘图,就是观察点距离与物体颜色呈线性关系。线性系数称为
迷雾因子
,值在[0, 1]
当迷雾因子等于0
时,输出颜色=0 + 雾的颜色
,也就是表示观察点离物体很远(或者说物体上的点离观察点很远),完全看不见物体,只能看见雾了。
当迷雾因子等于1
时,输出颜色=物体表面颜色+0
,也就是完全能看清物体,迷雾消失。
2.1. 雾化因子
上图就是雾化因子和顶点与视点距离之间的关系。可以看到顶点离视点越远,雾化因子越接近0。
同时,雾化因子的下降梯度(趋势)和起点
和终点
有关系,当起点和终点距离越长,观察者就需要站在越远的地方(物体离观察者越远),才会完全消失(雾化因子为零)。
因此我们可以归纳出雾化因子的公式:
3. 代码实现
先从片元着色器开始,主要就是根据上面的公式计算雾化因子和雾化颜色
// 片元着色器
precision mediump float;
// 光源颜色
uniform vec3 lightColor;
// 光源方向
uniform vec3 lightDirection;
// 环境光颜色
uniform vec3 ambientColor;
// 法向量变换矩阵 也就是逆转置矩阵
uniform mat4 normalMat;
varying vec4 _color;
varying vec4 _originalNormal;
varying vec4 _vertexPosition;
// 雾颜色
uniform vec3 fogColor;
// 雾距离 [开始, 结束]
uniform vec2 fogDist;
// 顶点离视点的距离(通过顶点着色器计算)
varying float dist;
void main(){
// 计算该顶点的光线方向
vec3 vertexLightDirection = normalize(lightDirection - vec3(_vertexPosition));
// 计算变换后的法向量
vec3 normal = normalize(vec3(normalMat * _originalNormal));
// 计算反射颜色
float cos = max(dot(vertexLightDirection, normal), 0.0);
vec3 diffuse = lightColor * _color.rgb * cos;
// 计算环境反射光
vec3 ambient = ambientColor * _color.rgb;
vec4 objectFaceColor = vec4(diffuse + ambient, _color.a);
/** 计算迷雾 */
// 迷雾因子
float fogFactor = (fogDist[1] - dist) / (fogDist[1] - fogDist[0]);
// 限制因子在0,1之间
fogFactor = min(max(fogFactor, 0.0), 1.0);
// 计算迷雾笼罩后的颜色
vec3 inFragColor = objectFaceColor.rgb * fogFactor + fogColor * (1.0 - fogFactor);
gl_FragColor = vec4(inFragColor, _color.a);
}
顶点着色器唯一的操作就是计算物体顶点和视点的距离。
// 顶点着色器
// mvp矩阵
uniform mat4 mvpMat;
// model矩阵
uniform mat4 modelMat;
// 顶点位置
attribute vec4 pos;
// 顶点颜色
attribute vec4 color;
// 初始位置法线向量
attribute vec4 originalNormal;
varying vec4 _color;
varying vec4 _originalNormal;
varying vec4 _vertexPosition;
// 顶点到观察点的距离
varying float dist;
// 记录视点位置
uniform vec4 eye;
void main(){
// 绘制立方体顶点
gl_Position = mvpMat * pos;
gl_PointSize = 10.0;
// 计算顶点世界坐标
_vertexPosition = modelMat * pos;
// 法向量是存储在顶点缓冲区的,所以只能间接传递给片元着色器
_originalNormal = originalNormal;
// 计算距离
dist = distance(_vertexPosition, eye);
_color = color;
}
主程序
// ...省略初始化,顶点绘制
// 视点位置
const eye = gl.getUniformLocation(program, 'eye')
const defaultEye = [0, 10, 30, 1]
// 设置雾颜色
const fogColor = gl.getUniformLocation(program,'fogColor')
gl.uniform3fv(fogColor, [0.137, 0.231, 0.423])
// 设置雾开始和迷雾完全笼罩物体的位置距离
const fogDist = gl.getUniformLocation(program, 'fogDist')
const dist = [30, 40]
gl.uniform2fv(fogDist, dist)
// 绘制立方体(绘制过程就不用细看了)
let fov = glMatrix.toRadian(10)
let aspect = canvas.width / canvas.height
let near = 1
let far = 1000
gl.enable(gl.DEPTH_TEST)
const normalMatrix = gl.getUniformLocation(program, 'normalMat')
const modelMatrix = gl.getUniformLocation(program, 'modelMat')
let modelMat: mat4 = getModelMat([0, 0, 0], [0, 0, 0])
const drawBox = (modelMat: mat4) => {
const perspectiveMat = mat4.perspective(mat4.create(), fov, aspect, near, far)
const _viewMat = mat4.lookAt(mat4.create(), vec3.fromValues.apply({}, defaultEye.slice(0, 3)), [0, 0, 0], [0, 1, 0])
gl.uniform4fv(eye, vec4.fromValues.apply({}, defaultEye))
let mvpMat = mat4.multiply(mat4.create(), perspectiveMat, _viewMat)
mat4.multiply(mvpMat, mvpMat, modelMat)
// modelMat的逆转置
gl.uniformMatrix4fv(normalMatrix, false, mat4.transpose(mat4.create(), mat4.invert(mat4.create(), modelMat)))
gl.uniformMatrix4fv(modelMatrix, false, modelMat)
gl.uniformMatrix4fv(mvpMatrix, false, mvpMat)
gl.drawElements(gl.TRIANGLES, indices.length, gl.UNSIGNED_BYTE, 0);
}
const draw = () => {
gl.clear(gl.DEPTH_BUFFER_BIT | gl.COLOR_BUFFER_BIT)
gl.clearColor(0.4, 0.4, 0.4, 1)
drawBox(modelMat)
}
draw()
上面通过修改雾气的结束位置,实现了不同雾气的效果。改变视点位置也一样。
4. 值得关注的地方
4.1. 看着不是很像雾
将背景色换成和雾一样的颜色就像了
4.2. 简便计算
刚才的迷雾因子和迷雾颜色的计算直接使用的基本运算,其实glsl
自带了两个函数可以帮助我们
// 迷雾因子
- float fogFactor = (fogDist[1] - dist) / (fogDist[1] - fogDist[0]);
- fogFactor = min(max(fogFactor, 0.0), 1.0);
+ float fogFactor = (fogDist[1] - dist) / (fogDist[1] - fogDist[0]);
+ fogFactor = clamp(fogFactor, 0.0, 1.0)
clamp
函数接收三个参数,将第一个参数的值限定在第二和第三参数之间。
// 计算迷雾笼罩后的颜色
- vec3 inFragColor = objectFaceColor.rgb * fogFactor + fogColor * (1.0 - fogFactor)
+ vec3 inFragColor = mix(fogColor, objectFaceColor.rgb, fogFactor);
mix(x, y, z)
函数就是上面这个公式x*(1-z) + y*z
,直接传入对应的参数就行了。
4.3. 迷雾距离设置
代码中我将视点位置设置到了[10, 10, 30, 1]
,也就是z=30
大概就是距物体有30
的距离。所以如果下面这样设置就会有不同效果:
- 雾气完全笼罩的距离设置为
30
,这样设置会导致大于30
的顶点处的雾气因子是0
,物体将会是看不见。
- 雾气开始距离小于
30
,越小说明雾气开始的位置越靠近视点。本来
30
距离位置的顶点开始受雾化因子的影响开始变色。
5.改进
上面有一个十分耗时的操作,dist = distance(_vertexPosition, eye);
每个顶点都在计算距离。还记得坐标的第四个分量w
吗,通常是1,但是在设置了可是空间后,w
的值就不是固定的1
了,他能近似表示顶点和视点的距离。
关于w
分量与坐标系、可视空间的关系,后面一章来讲
修改很简单:
// 顶点着色器
-dist = distance(_vertexPosition, eye);
+dist = gl_Position.w;
效果几乎不变