WebGL Programing Guide 学习笔记
之前我们已经了解了:
- Shader 基础知识
- WebGL 3D 基础知识,包括基本的矩阵变换和观察视角
- 光照基础,物体表面颜色在光照下的计算方法
现在可以进行一些更加具体,或者说贴近实际的效果模拟。以下例子来自「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>
盖在上面就行了。
如果使用 WebGL API 来绘制文本呢? 最近看的另一本「Interactive Computer Graphics Top Down Approach」中介绍了图形系统中常见的两种绘制文本的方法。 分别是 Stroke text 和 Raster text。前者需要存储文字的顶点信息,如果是闭合的就可以填充颜色,和其他图形没啥区别,也很容易进行缩放旋转操作。
而后者有点像涂色卡,标记文本占据的小方块,优点是只要通过 bit-block-transfer (bitblt)操作就能迅速放入 framebuffer 中。但是存在一个问题,缩放还好,旋转可就不行了。
创建迷雾
这个例子使用了一种较为简单的计算方法:线性迷雾。
之所以叫线性,是假设迷雾均匀的分布在一块区域,人眼距离这块区域的距离决定了清晰度。 这个清晰度也叫迷雾因子,介于 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,例如图中黄色的:
因此我们需要在 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; }