更新于 

雾化/圆点

什么是雾化?

答: 在三维图形学中,雾化(fog) 指的是远处的物体看上去较为模糊的现象。

如何实现雾化?

线性雾化(linear fog)

线性雾化表示 雾化程度与该点与视点的距离呈线性关系
雾化程度取决于 它与视点之间的距离

  • 雾化起点 开始雾化之处
  • 雾化终点 完全雾化之处
雾化因子(fog factor)

某一点雾化的程度可以被定义为 雾化因子(fog factor)

雾化因子计算公式

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

雾化因子与视点距离线性关系图

物体离视点越远,雾化因子越小,物体越模糊。

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

片元颜色 = 【物体表面颜色 * 雾化因子】 + 【雾的颜色 * (1 - 雾化因子)】

雾化程序

实现思路

注意此程序的雾化因子是在世界坐标系下计算的。

  1. 顶点着色器计算出当前顶点与视点距离,传入片元着色器
  2. 片元着色器根据 片元与视点的距离,计算出 雾化因子
Fog Code
Fog运行效果
Fog运行效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
let VSHADER_SOURCE = `
attribute vec4 a_Position;
attribute vec4 a_Color;

uniform mat4 u_MvpMatrix;
uniform mat4 u_ModelMatrix;
uniform vec4 u_Eye; // 视点

varying vec4 v_Color;
varying float v_Dist;
void main(){
gl_Position = u_MvpMatrix * a_Position;
v_Color = a_Color;
// Calculate the distance to each vertex from eye point
// 计算世界坐标系下,视点和物体之间的距离
v_Dist = distance(u_ModelMatrix * a_Position, u_Eye);
}
`;

let FSHADER_SOURCE = `
#ifdef GL_ES
precision mediump float;
#endif

uniform vec3 u_FogColor;
uniform vec2 u_FogDist;

varying vec4 v_Color;
varying float v_Dist;

void main(){
float fogFactor = clamp((u_FogDist.y - v_Dist)/(u_FogDist.y-u_FogDist.x),0.0,1.0);
vec3 color = mix(u_FogColor,vec3(v_Color),fogFactor);
gl_FragColor = vec4(color,v_Color.a);
}
`;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
function main() {
let canvas = document.getElementById('webgl');
let gl = getWebGLContext(canvas);
if (!initShaders(gl,VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('failed to init shader');
return;
}

let n = initVertexBuffers(gl);

let fogColor = new Float32Array([0.137, 0.231, 0.323]);
let fogDist = new Float32Array([55, 80]);
let eye = new Float32Array([25, 65, 35, 1.0]);

let u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix');
let u_ModelMatrix = gl.getUniformLocation(gl.program, 'u_ModelMatrix');
let u_Eye = gl.getUniformLocation(gl.program, 'u_Eye');
let u_FogColor = gl.getUniformLocation(gl.program, 'u_FogColor');
let u_FogDist = gl.getUniformLocation(gl.program, 'u_FogDist');

gl.uniform3fv(u_FogColor, fogColor);
gl.uniform2fv(u_FogDist, fogDist);
gl.uniform4fv(u_Eye, eye);

gl.clearColor(...fogColor,1.0);
gl.enable(gl.DEPTH_TEST);

let modelMatrix = new Matrix4();
modelMatrix.setScale(10, 10, 10); //世界坐标转换矩阵
gl.uniformMatrix4fv(u_ModelMatrix, false, modelMatrix.elements);

let mvpMatrix = new Matrix4();
mvpMatrix.setPerspective(
30, 1, 1, 1000
).lookAt(
...eye, 0, 2, 0, 0, 1, 0
).multiply(modelMatrix);
gl.uniformMatrix4fv(u_MvpMatrix, false, mvpMatrix.elements);

document.onkeydown = function (ev) {
keydown(ev, gl, n, u_FogDist, fogDist);
}

gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0);
}
1
2
3
4
5
6
7
8
9
10
11
12
function keydown(ev, gl, n, u_FogDist, fogDist) {
switch (ev.keyCode) {
case 38: fogDist[1] += 1; break; //↑
case 40:
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);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
function initVertexBuffers(gl) {
// Create a cube
// 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
]);

// Create a buffer object
var indexBuffer = gl.createBuffer();
if (!indexBuffer)
return -1;

// Write the vertex property to buffers (coordinates and normals)
if (!initArrayBuffer(gl, vertices, 3, gl.FLOAT, 'a_Position')) return -1;
if (!initArrayBuffer(gl, colors, 3, gl.FLOAT, 'a_Color')) return -1;

// Write the indices to the buffer object
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) {
// Create a buffer object
var buffer = gl.createBuffer();
if (!buffer) {
console.log('Failed to create the buffer object');
return false;
}
// Write date into the buffer object
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
// Assign the buffer object to the attribute variable
var a_attribute = gl.getAttribLocation(gl.program, attribute);
if (a_attribute < 0) {
console.log('Failed to get the storage location of ' + attribute);
return false;
}
gl.vertexAttribPointer(a_attribute, num, type, false, 0, 0);
// Enable the assignment of the buffer object to the attribute variable
gl.enableVertexAttribArray(a_attribute);
// Unbind the buffer object
gl.bindBuffer(gl.ARRAY_BUFFER, null);

return true;
}

程序解析
如何使用distance()计算顶点与视点间的距离?
1
2
// 顶点与视点间距 = distance( 顶点坐标, 视点坐标)
v_Dist = distance( u_ModelMatrix * a_Position, u_Eye);
  • u_ModelMatrix: 将顶点转换到 世界坐标系 下的模型矩阵
  • u_ModelMatrix*a_Position: 世界坐标系下的 顶点坐标
  • u_Eye: 世界坐标系下的 视点坐标
如何使用clamp()计算雾化因子大小?
1
2
3
4
5
float fogFactor = clamp(
(u_FogDist.y - v_Dist) / (u_FogDist.y - u_FogDist.x),
0.0,
1.0
);
  • u_FogDist 雾的范围
    • u_FogDist.x 雾的起点
    • u_FogDist.y 雾的终点
    • u_FogDist.y - u_FogDist.x 起点与终点之间的距离
  • v_Dist 顶点与视点间的距离
clamp函数
  • 功能 对计算结果进行区间限制
  • 参数
    • 参数1 计算出的雾化因子
    • 参数2 区间的最小值
    • 参数3 区间的最大值
如何使用mix()计算片元颜色?
1
vec3 color = mix(u_FogColor, vec3(v_Color), fogFactor);

在这里我们可以这样设置:

  • x = u_FogColor 表示雾的颜色
  • y = vec3(v_Color) 表示物体表面颜色
  • z = fogFactor 表示雾化因子
1
mix(x,y,z) = x*(1-z) + y*z;
  • x与z成 反比
  • y与z成 正比
除了线性雾化,还有其他哪些雾化算法?

答: 还有指数雾化算法等等。
想要使用其他的雾化算法,只需 修改雾化指数的计算方法

使用w分量

近似估算法
Fog.js中shader程序的缺陷

即在 顶点着色器 中计算 顶点与视点的距离 ,
会造成较大的开销,影响性能。

近似估算法需要为视点和视线设置 特殊条件

  • 视点原点
  • 视线 沿 Z轴负方向

由此可以推出几个结论:

  • 在观察者坐标系下,物体及视图的 z分量都是负值
  • w分量可以直接看做 顶点与视点的距离
    • w分量是vec4类型的顶点坐标的最后一个分量
    • w分量的值刚好是 z分量值乘以-1
比较两种方法的vertex shader程序

通过distance计算距离

1
2
3
4
5
void main(){
gl_Position = u_MvpModel * a_Position;
v_Color = a_Color;
v_Dist = distance( u_ModelMatrix * a_Position, u_Eye);
}

通过w分量估算距离

1
2
3
4
5
void main(){
gl_Position = u_MvpMatrix * a_Position;
v_Color = a_Color;
v_Dist = gl_Position.w;
}

绘制圆形的点

如何绘制圆点?

答: 像做饼干一样,需要将原先的方点 削减 成圆形。

削减的过程就和做饼干一样
削减的过程就和做饼干一样

在做饼干的时候,饼干模具会帮助我们切割甜饼。
但是在计算机中没有模具,要借助什么工具判断切割边界呢?
答案就是坐标系。

需要切割的片元
需要切割的片元
以坐标轴作为切割工具
以坐标轴作为切割工具
gl_PointCoord
  • 步骤1 检查当前片元是否距离点中心 (0.5,0.5)距离超过0.5
  • 步骤2 如果是,使用 discard 语句放弃此片元
RoundedPoint Code
RoundedPoint运行效果
RoundedPoint运行效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let VSHADER_SOURCE = `
attribute vec4 a_Position;
void main(){
gl_Position = a_Position;
gl_PointSize = 10.0;
}
`;

let FSHADER_SOURCE = `
#ifdef GL_ES
precision mediump float;
#endif
void main(){
float d = distance(gl_PointCoord, vec2(0.5,0.5));
if(d<0.5){
gl_FragColor = vec4(0.0,1.0,0.0,1.0);
}else{
discard;
}
}
`;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function main() {
let canvas = document.getElementById('webgl');
let gl = getWebGLContext(canvas);
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('failed to init shaders');
return;
}

let n = initVertexBuffers(gl);
gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0, n);

}

1
2
3
4
5
6
7
8
9
10
11
12
13
function initVertexBuffers(gl) {
let vertices = new Float32Array([
0, 0.5, -0.5, -0.5, 0.5, -0.5
]);
let n = 3;
let a_Position = gl.getAttribLocation(gl.program, 'a_Position');
let vertexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_Position);
return n;
}
绘制逻辑简述

实际上所有的绘制逻辑在 片元着色器 中实现:

  1. 计算片元距离所属点中心的距离
    1
    float d = distance(gl_PointCoord,vec2(0.5,0.5));
  2. 如果距离小于0.5就绘制,否则舍弃
    1
    2
    if(d<0.5) gl_FragColor = vec4(0.0,1.0,0.0,1.0);
    else discard;