此文上接 WebGL 系列文章,如果没读建议先看这个, 然后再回来。
一个常见的问题就是“如何在 WebGL 中绘制文字”,首先你需要问自己绘制文字的目的是什么, 你在浏览器中,浏览器可以显示文字,所以第一个答案就是使用 HTML 显示文字。
先来做个简单的例子:假设你只需要在 WebGL 上绘制文字,我们就就把它叫做覆盖文字, 这样的文字基本上是固定在某个位置的。
简单的实现方式是创建一个或多个 HTML 元素,然后使用 CSS 让它们覆盖在画布上。
例如:首先创建一个容器然后将画布和一些 HTML 元素放在里面,让这些元素在容器中覆盖在画布上。
<div class="container">
<canvas id="canvas" width="400" height="300"></canvas>
<div id="overlay">
<div>Time: <span id="time"></span></div>
<div>Angle: <span id="angle"></span></div>
</div>
</div>
然后设置 CSS 使 HTML 元素覆盖在画布上
.container {
position: relative;
}
#overlay {
position: absolute;
left: 10px;
top: 10px;
}
在初始化时找到这些元素并设置需要的属性。
// 找到需要用到的元素
var timeElement = document.querySelector("#time");
var angleElement = document.querySelector("#angle");
// 创建文字节点为浏览器节省一些时间
var timeNode = document.createTextNode("");
var angleNode = document.createTextNode("");
// 将文字节点放入需要的地方
timeElement.appendChild(timeNode);
angleElement.appendChild(angleNode);
最后在渲染时更新节点
function drawScene(time) {
var now = time * 0.001; // 转换成秒
...
// 将旋转角从弧度转换为角度
var angle = radToDeg(rotation[1]);
// 范围在 0 - 360 之间
angle = angle % 360;
// 设置节点
angleNode.nodeValue = angle.toFixed(0); // 不保留小数
timeNode.nodeValue = now.toFixed(2); // 保留两个小数
这是结果
注意到我将修改的内容放入 span 中再放到 div 里,我觉的这样比直接放在 div 中要快一些, 像这样
timeNode.nodeValue = "Time " + now.toFixed(2);
我也使用的是 node = document.createTextNode()
并在之后调用的是
node.nodeValue = someMsg
,我也可以使用 someElement.innerHTML = someHTML
,
那样就更灵活,可以插入 HTML 字符串在里面,但是那样可能就会慢一些,
因为每次设置浏览器都要创建和消除节点,哪种方式更好取决于你自己。
对覆盖层技术而言,重要的是要清楚 WebGL 时运行在浏览器中的,可能的话要记得使用浏览器的特性。 许多 OpenGL 程序员想将所有的东西都 100%使用 WebGL 绘制,但是 WebGL 运行在浏览器中, 浏览器已经帮你完成了很多工作,可以提供很多便利。例如你可以使用 CSS 非常简单的实现的修改覆盖层的样式。
这有一个例子加了一些样式,背景是圆角的,字母有光晕,有红色的边框, 你可以使用 HTML 得到你想要的各种结果。
还有一个重要的事情是让文字的位置和渲染的东西关联,我们也可以使用 HTML 实现。
在这个例子中我们还是创建一个容器,放入画布和其他容器,用来放移动的 HTML
<div class="container">
<canvas id="canvas" width="400" height="300"></canvas>
<div id="divcontainer"></div>
</div>
设置 CSS
.container {
position: relative;
overflow: none;
}
#divcontainer {
position: absolute;
left: 0px;
top: 0px;
width: 400px;
height: 300px;
z-index: 10;
overflow: hidden;
}
.floating-div {
position: absolute;
}
position: absolute;
使 #divcontainer
的位置变为绝对值,
意思是和第一个包含 position:relative
或 position: absolute
样式的父容器相关,
在这个例子中就是画布和 #divcontainer
所在容器。
left: 0px; top: 0px
使 #divcontainer
左上对齐一切,
z-index: 10
使它浮在画布上,overflow: hidden
使子元素被边界裁剪。
最后 .floating-div
用来定位创建的 div。
所以现在需要找到 divcontainer,创建 div 然后加在里面。
// 找到 divcontainer
var divContainerElement = document.querySelector("#divcontainer");
// 创建 div
var div = document.createElement("div");
// 设置 CSS 类
div.className = "floating-div";
// 创建一个文字节点作为内容
var textNode = document.createTextNode("");
div.appendChild(textNode);
// 加到 divcontainer 里
divContainerElement.appendChild(div);
现在就通过设置它的样式来改变位置。
div.style.left = Math.floor(x) + "px";
div.style.top = Math.floor(y) + "px";
textNode.nodeValue = now.toFixed(2);
这是例子,将文字限制在框内运动。
下一步使将位置和三维场景中的东西关联,如何实现呢?我们使用之前讲过的 透视投影,就像让 GPU 做的事情一样。
通过那篇文章我们知道了如何使用矩阵,如何将它们乘起来,如何使用一个透视投影矩阵将坐标转换到裁剪空间。 我们也可以在 JavaScript 中做相同的运算,然后将裁剪空间坐标 (-1 到 +1) 转换到像素坐标, 使用这个像素坐标设置 div 的位置。
gl.drawArrays(...);
// 我们刚计算出绘制三维 F 的矩阵
// 选择物体空间中 'F' 的一点
// X Y Z W
var point = [100, 0, 0, 1]; // 这是正面的右上角
// 使用矩阵计算出这一点的裁剪空间坐标
var clipspace = m4.transformVector(matrix, point);
// 将 X 和 Y 除以 W,和 GPU 一样
clipspace[0] /= clipspace[3];
clipspace[1] /= clipspace[3];
// 从裁剪空间转换到像素值
var pixelX = (clipspace[0] * 0.5 + 0.5) * gl.canvas.width;
var pixelY = (clipspace[1] * -0.5 + 0.5) * gl.canvas.height;
// 定位 div
div.style.left = Math.floor(pixelX) + "px";
div.style.top = Math.floor(pixelY) + "px";
textNode.nodeValue = now.toFixed(2);
瞧, div 的左上角已经和 F 的正面右上角关联起来了。
当然我们也可以使用更多的 div。
你可以查看最后一个例子的源码获取更多细节。 一个重要的点是,我推测从 DOM 创建,附加和移除 HTML 元素是比较慢的, 所以上方的例子创建元素后调整它们的位置,隐藏不用的部分而不是从 DOM 移除。 你可能需要做一个对比样例来看它是不是更快,这只是我选择的解决方式。
希望这篇文章让你了解了如何使用 HTML 创建文字,接下来我们将 使用二维画布创建文字。