目录
前言
如何实现雾化
线性雾化公式
雾化因子关系图
根据雾化因子计算片元颜色公式
示例程序(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种那样,雾化效果基本不变。