以下是 Matt DesLauriers 的客座文章。Matt 结合了一系列技术和开源模块,构建了一个令人难以置信的演示,其中简单的可识别图标爆炸成矢量三角形,然后重新形成一个全新的图标。在这里,他讨论了实现这一目标的一些工具和想法,但除此之外,还有其他方法和其他相关概念。这是一些非常高级的内容,我希望它能为对这种数学、算法网络动画感兴趣的人打开一些思路。Matt,开始吧。
SVG 是一种以分辨率无关的方式传递图标、字体、徽标和各种其他图像的绝佳方法。该格式得到广泛支持,非常灵活且高度紧凑。例如,新的 Google 徽标可以用 SVG 和一些技巧表示为 仅 146 字节。
该格式的核心是<path>
元素,它提供了一种简洁的方法来描述一组复杂的路径操作,例如字体集中的字形。简单来说,它描述了一系列moveTo
、lineTo
、curveTo
和类似的命令。例如,以下是 SVG 中的 Google“G”
<path d="M173 102a51 51 0 1 1-13-30m20 37h-53" stroke="#4a87ee"/>
查看 Matt DesLauriers (@mattdesl) 在 CodePen 上的 Pen QjMrXV。
然而,在 WebGL 中渲染 SVG 路径更具挑战性。WebGL API,以及扩展的 ThreeJS,主要用于渲染许多三角形。复杂的形状渲染和文本布局等任务留给开发人员实现。
在这里,我将探讨在 WebGL 中处理 SVG <path>
元素的一些方法,并简要讨论我为 svg-mesh-3d 模块 的最新演示所涉及的工具。如果您有启用 WebGL 的浏览器,您可以在这里查看演示。

实现
此处的代码示例将使用 ThreeJS 来包装 WebGL API,并使用 browserify 来整合代码的小型 npm 模块。有关此方法的更多详细信息,请查看 前端 JavaScript 模块 指南。
开发工具
在开发 ThreeJS 演示时,我使用 budo 作为开发服务器,并使用 babelify 来转译 ES6。这使得即使在大型捆绑包中也能实现轻松快速的开发周期。 brfs 用于内联着色器文件和我们要渲染的静态 SVG 文件列表。而不是使用 Gulp 或 Grunt,整个开发和生产都由 package.json
文件中的两个 npm run 任务组成。
"scripts": {
"start": "budo demo/:bundle.js --live -- -t babelify -t brfs | garnish",
"build": "browserify demo/index.js -t babelify -t brfs | uglifyjs -cm > bundle.js"
}
然后,gh-pages
部署通过 单个 shell 脚本 自动化。
有关 npm run
的更多详细信息,请参阅 Keith Cirkel 撰写的 “如何将 npm 用作构建工具”。
近似和三角剖分
对于我的 svg-mesh-3d 演示,我只需要渲染 <path>
数据的简单轮廓,并使用纯色。 Entypo 图标 非常适合此目的,并且三角剖分适用于此类 SVG。
我采用的方法是近似 SVG 路径中的曲线,对其轮廓进行三角剖分,并将三角形作为静态几何体发送到 WebGL。这是一个代价高昂的步骤。这不是您每帧都会执行的操作,但在三角形位于 GPU 上之后,您可以使用顶点着色器自由地对其进行动画处理。
大部分工作已经通过 npm 上的各种模块完成。“粘合剂”将它们组合在一起,代码不到 200 行。
最终的 ThreeJS 演示在其捆绑包中使用了 70 多个模块,但在幕后主要依赖以下一些模块:
- parse-svg-path 将 SVG
<path>
字符串解析为一系列操作 - normalize-svg-path 将所有路径段转换为三次贝塞尔曲线
- adaptive-bezier-curve 使用 Anti-Grain Geometry 中的算法,自适应地将贝塞尔曲线细分为特定比例
- simplify-path 简化轮廓并减少三角剖分时间
- clean-pslg 在三角剖分之前清理路径数据
- cdt2d 计算约束 Delaunay 2D 三角剖分
- three-simplicial-complex 将通用路径数据转换为 ThreeJS 几何体
最终,用户体验变得像需要 svg-mesh-3d
模块并操作其返回的数据一样简单。该模块不特定于 ThreeJS,可用于任何渲染引擎,例如 Babylon.js、stackgl、Pixi.js,甚至普通的 Canvas2D。以下是它在 ThreeJS 中的使用方法
// our utility functions
var createGeometry = require('three-simplicial-complex')(THREE);
var svgMesh3d = require('svg-mesh-3d');
// our SVG <path> data
var svgPath = 'M305.214,374.779c2.463,0,3.45,0.493...';
// triangulate to generic mesh data
var meshData = svgMesh3d(svgPath);
// convert the mesh data to THREE.Geometry
var geometry = createGeometry(meshData);
// wrap it in a mesh and material
var material = new THREE.MeshBasicMaterial({
side: THREE.DoubleSide,
wireframe: true
});
var mesh = new THREE.Mesh(geometry, material);
// add to scene
scene.add(mesh);
顶点动画
在使用 WebGL 时,最好避免过于频繁地将数据上传到 GPU。这意味着我们应该尝试一次构建静态几何体,然后通过顶点和片段着色器在多帧上对几何体进行动画处理。
为了使我们的网格“爆炸”成小块,我们可以在着色器中更改一个uniform,这有点像 GLSL 的变量。顶点着色器将在我们的 3D 几何体的每个顶点上运行,使我们能够从世界原点[ x=0, y=0, z=0 ]
向外爆炸。
为此,我们需要在着色器中使用自定义顶点属性。对于每个三角形,其三个顶点将使用相同的Vector3
来描述一个direction
。此direction
是 球体上的随机点。
下面的基本顶点着色器只是通过其direction.xyz
(按animation
因子缩放)变换每个顶点position.xyz
。position.xyz
位于模型空间中,范围为-1.0
到1.0
。
attribute vec3 direction;
uniform float animation;
void main() {
// transform model-space position by explosion amount
vec3 tPos = position.xyz + direction.xyz * animation;
// final position
gl_Position = projectionMatrix * modelViewMatrix * vec4(tPos, 1.0);
}
当 uniform 为0.0
时,三角形将位于其初始位置,徽标将为实心填充。

当 uniform 设置为1.0
时,三角形将沿着方向向量向外推离世界中心,形成某种“爆炸”。

这看起来不错,但还有两个功能可以进行一些润色。第一个是将三角形缩放到其 质心。这用于独立于爆炸对三角形进行动画处理。为此,我们需要提交另一个自定义顶点属性,就像我们对direction
所做的那样。
第二个是通过旋转矩阵变换direction.xyz
向量,为爆炸添加一些旋转和混乱。旋转矩阵的角度由animation
因子以及三角形质心的x 位置的sign()
决定。这意味着某些三角形将朝相反方向旋转。
使用 最终的顶点着色器,我们的应用程序现在可以以流畅的 60 FPS 动画处理数千个三角形。下面是使用线框渲染的屏幕截图,以及将 randomization
参数 设置为1500
。

其他方法
我选择 cdt2d 是因为它在数值上稳健,可以处理带有孔的任意输入,并且与引擎无关。其他三角剖分器,例如 earcut、tess2 和 ThreeJS 自身的三角剖分器也是不错的选择。
除了三角剖分之外,还值得一提的是在 WebGL 中渲染 SVG <path>
的一些其他方法。曲线近似和三角剖分有其缺点:构建几何体速度慢,难以正确执行,会增加最终代码捆绑包的大小,并且在放大时会显示“阶梯”或锯齿状边缘。
光栅化
一个简单有效的方法是将 SVG 数据栅格化到一个 HTMLImageElement
中,然后将其上传到 WebGL 纹理。这不仅支持 <path>
元素,还支持浏览器涵盖的 SVG 功能的全部范围,例如滤镜、图案、文本,甚至 Firefox 和 Chrome 中的 HTML/CSS。
例如,请参阅 svg-to-image,了解如何使用 Blob
和 URL.createObjectURL
实现此功能。在 ThreeJS 和 browserify 中,代码可能如下所示
// our rasterizing function
var svgToImage = require('svg-to-image');
// create a box with a dummy texture
var texture = new THREE.Texture();
var geo = new THREE.BoxGeometry(1, 1, 1);
var mat = new THREE.MeshBasicMaterial({
map: texture
});
// add it to the scene
scene.add(new THREE.Mesh(geo, mat));
// convert SVG data into an Image
svgToImage(getSvgData(), {
crossOrigin: 'Anonymous'
}, function (err, image) {
if (err) {
// there was a problem rendering the SVG data
throw new Error('could rasterize SVG: ' + err.message);
}
// no error; update the WebGL texture
texture.image = image;
texture.needsUpdate = true;
});
function getSvgData () {
// make sure that "width" and "height" are specified!
return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="1024px" height="1024px">.....</svg>';
}
结果

但是,由于我们将图形栅格化成像素,因此它不再可缩放,并且在放大时会产生锯齿。通常,您需要较大的图像才能生成平滑的边缘,这会给纹理内存带来压力。
这不会生成三角形,因此为了模拟“爆炸”,我们可以使用简单的 Delaunay 三角剖分,它比受约束的 Delaunay 三角剖分快得多且更容易。
模板缓冲区
一个老技巧是使用模板缓冲区来渲染具有孔洞的复杂多边形。这比我的演示中涉及的三角剖分步骤快得多,但有一个主要缺点:在大多数浏览器中它缺乏 MSAA(抗锯齿)。一些引擎可能会将此与其他技术结合使用,例如 FXAA(后期处理中的抗锯齿),或使用预过滤线条为形状添加轮廓。

您可以在此处阅读更多相关信息
Loop-Blinn 曲线渲染
Loop 和 Blinn 在 分辨率无关曲线渲染 中描述了一种更高级的硬件路径渲染方法。有关更易于理解的介绍,请查看 Michael Dominic 的 “弯曲的蓝色”。
此技术生成最佳的曲线和路径渲染。它速度快、无限可缩放,并且可以利用 GPU 进行抗锯齿和特殊效果。但是,它的实现要复杂得多。在任意 SVG 路径中可能会出现许多边缘情况,例如重叠或自相交曲线。此外,它已获得专利,因此对于开源项目来说不是一个好选择。
进一步阅读
这篇文章仅仅触及了 WebGL 中可缩放矢量图形的表面。SVG 格式远远超出了填充路径的范围,还包括渐变、描边、文本和滤镜等功能。这些功能中的每一个都为 WebGL 的范围引入了新的复杂性,并且在 GPU 上完全渲染该格式将是一项巨大的工作量。
有关其中一些主题的更多阅读,请查看以下内容
- GPU 加速路径渲染 (2012):此 NVIDIA 扩展结合了一些用于任意路径渲染的最佳技术(Loop-Blinn、模板缓冲区等)。
- 绘制线条很难 (2015):在这篇文章中,我介绍了一些在 WebGL 中渲染线条和描边的常用方法。
- GPU 上的材质设计 (2015):这篇文章探讨了一些在 GPU 上模拟动态、可缩放且令人信服的排版效果的技术。
不错!我一直对使用 OpenGL 渲染矢量图形很感兴趣。毫不奇怪地看到 JS 和 WebGL 是实现的好选择,特别是通过 npm 可以使用大量工具。
好技巧。感谢分享!
演示在哪里?
这些东西看起来太棒了.. 干得好,Matt!
这是主要的
http://mattdesl.github.io/svg-mesh-3d/
这是一篇非常有趣的文章。肯定要看看
svg-mesh-3d
。查看以下内容,了解基于 GPU 的分辨率无关曲线渲染 http://jogamp.org/doc/gpunurbs2011/p70-santina.pdf
它是免专利权的 :)