WebGL Programing Guide 学习笔记

之前我们已经了解了:

现在可以进行一些更加具体,或者说贴近实际的效果模拟。以下例子来自「WebGL Programing Guide」一书的 Chapter 10 Advanced Techniques 章节。

选中物体

选中物体并不是一件简单的事。在「Interactive.Computer.Graphics.Top.Down.Approach」一书的 3.9 Picking 一节中介绍了一种通用的也很直观的思路:从鼠标点击处沿着投影方向发射一根射线,接触到的第一个对象就是当前被选中的。

具体到判定方法,可以使用盒模型判定,也可以使用下面的这种,存储额外标志信息的方式。

以一个立方体为例,当鼠标点击到物体区域时,希望找到选中的表面。 第一步就是给每个 vertex 标注上表面序号,例如 1-6,通过 a_Face 传入 vertex shader 中。

// 给每个顶点附上所在面的序号
var faces = new Uint8Array([
    1, 1, 1, 1,
    2, 2, 2, 2,
    ...
    6, 6, 6, 6
]);

这里有一个 GLSL 的限制,attribute 不能设置 int 类型,所以需要用内置函数转换:

attribute float a_Face;
int face = int(a_Face);

剩下的问题就是获取当前点击位置的表面编号,跟当前顶点所属的面比较一下,就能交给后续 fragment shader 进行着色了。

// 当前点击事件发生的面序号
uniform int u_PickedFace;
// 是否发生在当前顶点所在的面上,如果是则着色
vec3 color = (face == u_PickedFace) ? vec3(1.0) : a_Color.rgb;

那么问题来了,给 vertex 附上所在面的信息很容易,如何通知 fragment 呢? 毕竟点击是发生在 fragment 像素点,并不是在 vertex 上。

利用 A 通道存储信息

这里有一个很巧妙的办法,利用 RGBA 的 A 存储额外的信息。 反正到了 fragment shader 中,会忽略掉透明度。 这里需要处理两种情况,即初始化信息和之后正常的着色,通过 u_PickedFace = 0 进行切换。

if(u_PickedFace == 0) {
    v_Color = vec4(color, a_Face/255.0);
} else {
    v_Color = vec4(color, a_Color.a);
}

至于获取鼠标点击位置坐标,就需要使用前端熟悉的 DOM API 了:

// 点击事件信息对象
var x = ev.clientX, y = ev.clientY;
// canvas 尺寸
var rect = ev.target.getBoundingClientRect();
// 点击事件在 canvas 内的坐标
var x_in_canvas = x - rect.left, y_in_canvas = rect.bottom - y;
// 获取当前坐标所在的面
var face = checkFace();
// 传入 vertex shader
gl.uniform1i(u_PickedFace, face);

首先下达初始化指令,这时所有像素点都写入了面序号。 利用工具方法读取出所在像素点的 RGBA 值,从 A 通道中取出我们附带的面序号即可。

var pixels = new Uint8Array(4);
// 初始化每个点面信息
gl.uniform1i(u_PickedFace, 0);
// 读取当前点击点的 RGBA
gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
// 从 A 通道中取出面序号
return pixels[3];

HUD (Head Up Display)

很炫酷的缩写,借用“反派影评”的一个词,即所谓“高概念”。 但是实现起来无非增加一个 <canvas> 盖在上面就行了。

HUD

HUD

如果使用 WebGL API 来绘制文本呢? 最近看的另一本「Interactive Computer Graphics Top Down Approach」中介绍了图形系统中常见的两种绘制文本的方法。 分别是 Stroke text 和 Raster text。前者需要存储文字的顶点信息,如果是闭合的就可以填充颜色,和其他图形没啥区别,也很容易进行缩放旋转操作。

而后者有点像涂色卡,标记文本占据的小方块,优点是只要通过 bit-block-transfer (bitblt)操作就能迅速放入 framebuffer 中。但是存在一个问题,缩放还好,旋转可就不行了。

Raster text

Raster text

创建迷雾

这个例子使用了一种较为简单的计算方法:线性迷雾。

迷雾效果

迷雾效果

之所以叫线性,是假设迷雾均匀的分布在一块区域,人眼距离这块区域的距离决定了清晰度。 这个清晰度也叫迷雾因子,介于 0-1 之间。

迷雾因子

迷雾因子

通过这个迷雾因子可以计算出迷雾中物体的表面颜色。

fragment color = surface color × fog factor + fog color × (1 − fog factor )

还是那句话,有了公式,用 GLSL 实现就很方便了,可以使用内置函数。 clamp() 类似 JS 中的 Math.min(Math.max(MIN, value), MAX),把输入值框定在一个范围内:

float fogFactor = clamp((u_FogDist.y - v_Dist) /
    (u_FogDist.y - u_FogDist.x), 0.0, 1.0);

mix() 如同字面意思,根据迷雾因子(第三个参数)混合两个颜色。

vec3 color = mix(u_FogColor, vec3(v_Color), fogFactor)

优化计算性能

上面的计算方法中,关于人眼到的每个 vertex 距离是在 vertex shader 中计算的:

v_Dist = distance(u_ModelMatrix * a_Position, u_Eye);

有一种近似的估算方法,可以代替以提升性能。

gl_Position = u_MvpMatrix * a_Position;
v_Dist = gl_Position.w;

除了 xyz,w 很少被直接使用,所以起初看到这种做法,是会有一些困惑的。

这个 w 是以视点为原点的视角坐标系中每个 vertex 在 z 轴的坐标值,再乘以 -1,由于视角的方向刚好是 z 轴的反向,乘以 -1 后就刚好是在 z 轴上的距离了。

画一个圆点

之前画的点在 fragment shader 之后,都是由点阵内的 fragments 组成的一个方形。 为了画出近似圆形的点,需要丢弃掉一些 fragment,例如图中黄色的:

gl_PointCoord 坐标系

gl_PointCoord 坐标系

因此我们需要在 fragment shader 中获取到每个 fragment 的坐标。 之前使用过 gl_FragCoord,这是相对于窗口坐标系的。在这种情况下,使用 gl_PointCoord 在点阵坐标系中计算更加方便。

这里使用 discard 语句丢弃掉圆形区域之外的 fragment:

float dist = distance(gl_PointCoord, vec2(0.5, 0.5));
if(dist < 0.5) {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
} else { discard; }