WebGL学习(十)迷雾效果(线性雾化)

1,078 阅读5分钟

1. 介绍

直接看效果

Code_f0CCyM5qzD.gif

简单的说就是实现一个周围环境有雾气的效果。

2.原理

这里实现最简单的一种线性雾化

  1. 雾化说白了和光照一样,只是按照某种计算改变绘制的颜色。
  2. 结合现实,一个物体被笼罩在雾气中,当我们(观察点)靠近物体的时候,能看见的部分越来越多,反之越少。
  3. 转化成绘图,就是观察点距离与物体颜色呈线性关系。线性系数称为迷雾因子,值在[0, 1]
<输出颜色>=<物体表面颜色>×<迷雾因子>+<雾的颜色>×(1迷雾因子)<输出颜色>\quad= \\<物体表面颜色> \times<迷雾因子>+<雾的颜色>\times(1-迷雾因子)

当迷雾因子等于0时,输出颜色=0 + 雾的颜色,也就是表示观察点离物体很远(或者说物体上的点离观察点很远),完全看不见物体,只能看见雾了。

当迷雾因子等于1时,输出颜色=物体表面颜色+0,也就是完全能看清物体,迷雾消失。

2.1. 雾化因子

image.png

上图就是雾化因子和顶点与视点距离之间的关系。可以看到顶点离视点越远,雾化因子越接近0。

同时,雾化因子的下降梯度(趋势)和起点终点有关系,当起点和终点距离越长,观察者就需要站在越远的地方(物体离观察者越远),才会完全消失(雾化因子为零)。

因此我们可以归纳出雾化因子的公式:

设起点坐标(x1,0),终点坐标(x2,0)已知两点(x1,1)(x2,0),可求得直线公式<雾化因子>=x2xx2x1这里的x就是顶点到视点的距离变量换个表示方法<雾化因子>=终点顶点与视点的距离终点起点设起点坐标(x_1, 0),终点坐标(x_2, 0)\\\quad\\ 已知两点(x_1, 1)和(x_2,0),可求得直线公式\\\quad\\ <雾化因子>=\dfrac{x_2-x}{x_2-x_1}\qquad这里的x就是顶点到视点的距离\\\quad\\ 变量换个表示方法\\\quad\\ <雾化因子>=\dfrac{终点-顶点与视点的距离}{终点-起点}

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()

msedge_F0I0bW22Ov.gif 上面通过修改雾气的结束位置,实现了不同雾气的效果。改变视点位置也一样。

4. 值得关注的地方

4.1. 看着不是很像雾

将背景色换成和雾一样的颜色就像了

msedge_RwL1rK3EkF.gif

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,物体将会是看不见。

msedge_72D2VSfls3.gif

  • 雾气开始距离小于30,越小说明雾气开始的位置越靠近视点。 PDFExpert_fAWhPBCiDE.png 本来30距离位置的顶点开始受雾化因子的影响开始变色。

msedge_6Ludc2QpUg.gif

5.改进

上面有一个十分耗时的操作,dist = distance(_vertexPosition, eye);每个顶点都在计算距离。还记得坐标的第四个分量w吗,通常是1,但是在设置了可是空间后,w的值就不是固定的1了,他能近似表示顶点和视点的距离。

关于w分量与坐标系、可视空间的关系,后面一章来讲

修改很简单:

// 顶点着色器
-dist = distance(_vertexPosition, eye);
+dist = gl_Position.w;

效果几乎不变