WebGL雾化

目录

前言 

如何实现雾化

线性雾化公式

雾化因子关系图

根据雾化因子计算片元颜色公式

示例程序(Fog.js) 

代码详解​编辑

详解如何计算雾化因子(clamp())

详解如何计算最终片元颜色(根据雾化因子计算片元颜色公式  mix())

示例效果

示例程序(使用w分量代替顶点与视点的距离 Fog_w.js)


前言 

在三维图形学中,术语雾化(fog)用来描述远处的物体看上去较为模糊的现象。在现实中,任何介质中的物体都可能表现出雾化现象,比如水下的物体。本文的示例程序Fog将实现一个雾化的场景,场景中有一个立方体。程序的效果如下图所示,用户可以使用上下方向键调节雾的浓度。运行示例程序,试试上下方向键,看看雾的浓度改变的效果。

 

如何实现雾化

实现雾化的方式有很多种,这里使用最简单的一种: 线性雾化(linear fog)。在线性雾化中,某一点的雾化程度取决于它与视点之间的距离,距离越远雾化程度越高。线性雾化有起点和终点,起点表示开始雾化之处,终点表示完全雾化之处,两点之间某一点的雾化程度与该点与视点的距离呈线性关系。注意,比终点更远的点完全雾化了,即完全看不见了。某一点雾化的程度可以被定义为雾化因子(fog factor),并在线性雾化公式中被计算出来,如下式所示。

线性雾化公式

<雾化因子>=(<终点>-<当前点与视点间的距离>)/ (<终点>-<起点>)

这里 <起点>≤<当前点与视点间的距离>≤<终点> 

 如果雾化因子为1.0,表示该点完全没有被雾化,可以很清晰地看到此处的物体。如果其为0.0,就表示该点完全雾化了,此处的物体完全看不见,如下图所示。在视线上,起点之前的点的雾化因子为1.0,终点之后的点的雾化因子为0.0。

雾化因子关系图

在片元着色器中根据雾化因子计算片元的颜色,如下等式。 

根据雾化因子计算片元颜色公式

<片元颜色>=<物体表面颜色>×<雾化因子>+<雾的颜色>×(1-<雾化因子>)

来看一下示例程序

示例程序(Fog.js) 

如下显示了示例程序的代码。这里:(1)顶点着色器计算出当前顶点与视点的距离,并传入片元着色器;(2)片元着色器根据片元与视点的距离,计算雾化因子,最终计算出片元的颜色。注意,程序向着色器传入了视点在世界坐标系下的坐标(见附录G“世界坐标系和局部坐标系”),所以雾化因子是在世界坐标系下计算的。 

var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute vec4 a_Color;\n' +
  'uniform mat4 u_MvpMatrix;\n' +
  'uniform mat4 u_ModelMatrix;\n' +
  'uniform vec4 u_Eye;\n' +     // 视点位置(世界坐标)
  'varying vec4 v_Color;\n' + 
  'varying float v_Dist;\n' +
  'void main() {\n' +
  '  gl_Position = u_MvpMatrix * a_Position;\n' +
  '  v_Color = a_Color;\n' +
     // 计算从视点到每个顶点的距离
  '  v_Dist = distance(u_ModelMatrix * a_Position, u_Eye);\n' +
  '}\n';
var FSHADER_SOURCE =
  '#ifdef GL_ES\n' +
  'precision mediump float;\n' +
  '#endif\n' +
  'uniform vec3 u_FogColor;\n' + // 雾的颜色
  'uniform vec2 u_FogDist;\n' +  // 雾的距离(起点、终点)
  'varying vec4 v_Color;\n' +
  'varying float v_Dist;\n' +
  'void main() {\n' +
     /* 
      计算雾化因子(雾化因子 = (终点 - 当前点与视点见的距离) / (终点 - 起点))
      clamp函数:将第一个参数的值限制在第2个和第3个参数区间内,如果值在区间内,函数就直接返回第一个值,如果值小于区间的最小值或大于区间的最大值,函数就直接返回第二个参数或第三个参数
     */
  '  float fogFactor = clamp((u_FogDist.y - v_Dist) / (u_FogDist.y - u_FogDist.x), 0.0, 1.0);\n' +
    /* 计算片元颜色 mix函数:uFogColor*(1-雾因子)+v_Color*雾因子 */ 
  '  vec3 color = mix(u_FogColor, vec3(v_Color), fogFactor);\n' +
  '  gl_FragColor = vec4(color, v_Color.a);\n' +
  '}\n';

function main() {
  var canvas = document.getElementById('webgl');
  var gl = getWebGLContext(canvas);
  if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE))  return
  var n = initVertexBuffers(gl);
  var fogColor = new Float32Array([0.137, 0.231, 0.423]);  // 雾色
  var fogDist = new Float32Array([55, 80]); // 雾的距离[雾开始的地方,雾完全覆盖物体的地方]
  var eye = new Float32Array([25, 65, 35, 1.0]); // 视点位置

  var u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix');
  var u_ModelMatrix = gl.getUniformLocation(gl.program, 'u_ModelMatrix');
  var u_Eye = gl.getUniformLocation(gl.program, 'u_Eye');
  var u_FogColor = gl.getUniformLocation(gl.program, 'u_FogColor');
  var u_FogDist = gl.getUniformLocation(gl.program, 'u_FogDist'); // 获取用于存储雾起始点的uniform变量
	
  // 将雾的颜色、视点和雾距离传递给统一变量
  gl.uniform3fv(u_FogColor, fogColor); 
  gl.uniform2fv(u_FogDist, fogDist); 
  gl.uniform4fv(u_Eye, eye);  
  gl.clearColor(fogColor[0], fogColor[1], fogColor[2], 1.0); // 用雾的颜色清除
  gl.enable(gl.DEPTH_TEST);

  var modelMatrix = new Matrix4();
  modelMatrix.setScale(10, 10, 10);
  gl.uniformMatrix4fv(u_ModelMatrix, false, modelMatrix.elements);
  var mvpMatrix = new Matrix4();
  mvpMatrix.setPerspective(30, canvas.width/canvas.height, 1, 1000);
  mvpMatrix.lookAt(eye[0], eye[1], eye[2], 0, 2, 0, 0, 1, 0);
  mvpMatrix.multiply(modelMatrix);
  gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);
  document.onkeydown = function(ev){ keydown(ev, gl, n, u_FogDist, fogDist); };

  // Clear color and depth buffer
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  // Draw
  gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0);
}

function keydown(ev, gl, n, u_FogDist, fogDist) {
  switch (ev.keyCode) {
    case 38: // Up arrow key -> Increase the maximum distance of fog
      fogDist[1]  += 1;
      break;
    case 40: // Down arrow key -> Decrease the maximum distance of fog
      if (fogDist[1] > fogDist[0]) fogDist[1] -= 1;
      break;
    default: return;
  }
  gl.uniform2fv(u_FogDist, fogDist);   // Pass the distance of fog
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0);
}

function initVertexBuffers(gl) {
  //    v6----- v5
  //   /|      /|
  //  v1------v0|
  //  | |     | |
  //  | |v7---|-|v4
  //  |/      |/
  //  v2------v3

  var vertices = new Float32Array([   // Vertex coordinates
     1, 1, 1,  -1, 1, 1,  -1,-1, 1,   1,-1, 1,    // v0-v1-v2-v3 front
     1, 1, 1,   1,-1, 1,   1,-1,-1,   1, 1,-1,    // v0-v3-v4-v5 right
     1, 1, 1,   1, 1,-1,  -1, 1,-1,  -1, 1, 1,    // v0-v5-v6-v1 up
    -1, 1, 1,  -1, 1,-1,  -1,-1,-1,  -1,-1, 1,    // v1-v6-v7-v2 left
    -1,-1,-1,   1,-1,-1,   1,-1, 1,  -1,-1, 1,    // v7-v4-v3-v2 down
     1,-1,-1,  -1,-1,-1,  -1, 1,-1,   1, 1,-1     // v4-v7-v6-v5 back
  ]);
  var colors = new Float32Array([     // Colors
    0.4, 0.4, 1.0,  0.4, 0.4, 1.0,  0.4, 0.4, 1.0,  0.4, 0.4, 1.0,  // v0-v1-v2-v3 front
    0.4, 1.0, 0.4,  0.4, 1.0, 0.4,  0.4, 1.0, 0.4,  0.4, 1.0, 0.4,  // v0-v3-v4-v5 right
    1.0, 0.4, 0.4,  1.0, 0.4, 0.4,  1.0, 0.4, 0.4,  1.0, 0.4, 0.4,  // v0-v5-v6-v1 up
    1.0, 1.0, 0.4,  1.0, 1.0, 0.4,  1.0, 1.0, 0.4,  1.0, 1.0, 0.4,  // v1-v6-v7-v2 left
    1.0, 1.0, 1.0,  1.0, 1.0, 1.0,  1.0, 1.0, 1.0,  1.0, 1.0, 1.0,  // v7-v4-v3-v2 down
    0.4, 1.0, 1.0,  0.4, 1.0, 1.0,  0.4, 1.0, 1.0,  0.4, 1.0, 1.0   // v4-v7-v6-v5 back
  ]);
  var indices = new Uint8Array([       // Indices of the vertices
     0, 1, 2,   0, 2, 3,    // front
     4, 5, 6,   4, 6, 7,    // right
     8, 9,10,   8,10,11,    // up
    12,13,14,  12,14,15,    // left
    16,17,18,  16,18,19,    // down
    20,21,22,  20,22,23     // back
  ]);
  var indexBuffer = gl.createBuffer();
  if (!initArrayBuffer(gl, vertices, 3, gl.FLOAT, 'a_Position')) return -1;
  if (!initArrayBuffer(gl, colors, 3, gl.FLOAT, 'a_Color')) return -1;
  gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
  gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
  return indices.length;
}

function initArrayBuffer (gl, data, num, type, attribute) {
  var buffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
  var a_attribute = gl.getAttribLocation(gl.program, attribute);
  gl.vertexAttribPointer(a_attribute, num, type, false, 0, 0);
  gl.enableVertexAttribArray(a_attribute);
  gl.bindBuffer(gl.ARRAY_BUFFER, null);
  return true;
}

代码详解

顶点着色器计算了顶点与视点间的距离:首先将顶点坐标转换到世界坐标系下,然后调用内置函数distance()并将视点坐标(也是在世界坐标系下)和顶点坐标作为参数传入,distance()函数算出二者间的距离,并赋值给v_Dist变量以传入片元着色器(第13行)。 

片元着色器根据上式 线性雾化公式和式 雾化因子计算片元公式计算出雾化后的片元颜色。我们分别通过u_FogColor变量和u_FogDist变量来传入雾的颜色(第19行)和范围(第20行),其中u_FogDist.x和u_FogDist.y分别是起点和终点与视点间的距离。

详解如何计算雾化因子(clamp()

在根据式 线性雾化公式计算雾化因子时(第28行),我们用到了内置函数clamp(),这个函数的作用是将第1个参数的值限制在第2个和第3个参数的构成区间内。如果值在区间中,函数就直接返回这个值,如果值小于区间的最小值或大于区间的最大值,函数就返回区间的最小值或最大值。比如,本例将雾化因子限制在了0到1之间,因为视线上起点前的点和终点后的点直接根据式10.1计算出的雾化因子会是负数或大于1的数,需要将其修正成0和1。

详解如何计算最终片元颜色(根据雾化因子计算片元颜色公式  mix())

然后,片元着色器根据式 雾化因子计算片元公式,利用雾化因子和雾的颜色计算雾化后的片元颜色(第30行)。这里用到了内置函数mix(),该函数会计算x*(1-z)+y*z,其中x、y和z分别是第1、2和3个参数。 
JavaScript中的main()函数将创建计算雾化效果需要的那些值,并通过相应的uniform变量传入着色器。

你应当知道,除了线性雾化,还有多种其他雾化算法,如OpenGL中常用的指数雾化(见OpenGL Programming Guide)。使用其他的雾化算法也很简单,只需在着色器中修改雾化指数的计算方法即可。

示例效果

示例程序(使用w分量代替顶点与视点的距离 Fog_w.js)

在顶点着色器中计算顶点与视点的距离,会造成较大的开销,也许会影响性能。我们可以使用另外一种方法来近似估算出这个距离,那就是使用顶点经过模型视图投影矩阵变换后的坐标的w分量。在在本例中,顶点变换后的坐标就是gl_Position。之前,我们并未显式使用过gl_Position的w分量,实际上,这个w分量的值就是顶点的视图坐标的z分量乘以-1。在视图坐标系中,视点在原点,视线沿着Z轴负方向,观察者看到的物体其视图坐标系值z分量都是负的,而gl_Position的w分量值正好是z分量值乘以-1,所以可以直接使用该值来近似顶点与视点的距离。 

在顶点着色器中,将计算顶点与视点距离的部分替换成例10.7种那样,雾化效果基本不变。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

山楂树の

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值