
上一篇我们介绍了三维空间到二维平面的 轴测投影
的原理,今天将进一步探索三维空间中的几何图形。

1. 绘制三角形
在第一篇就介绍了绘制空间中 坐标点
的方式, 正所谓: 点动成先,线动成体。有了一点,理论上就可以绘制万物。现在思考一下,如下所示有三个橙色的点,如何在 XOY 平面上绘制三角形:

dart
List<Point3D> points = [
Point3D(-4, 4, 0),
Point3D(-4, 1, 0),
Point3D(-1, 1, 0),
];
可以通过根据顶点形成 Path , 绘制路径即可形成符合三维空间的图形。如下所示,三角形在旋转时会保持三维空间的特性:

dart
Path path = Path();
List<Offset> point2ds = points.map(project).toList();
path.moveTo(point2ds[0].dx, point2ds[0].dy);
path.lineTo(point2ds[1].dx, point2ds[1].dy);
path.lineTo(point2ds[2].dx, point2ds[2].dy);
path.lineTo(point2ds[0].dx, point2ds[0].dy);
Paint paint = Paint()
..color = Colors.cyanAccent
..strokeWidth = 2
..style = PaintingStyle.stroke;
canvas.drawPath(path, paint);
2. 万物皆为三角形
了解过三维建模或者OpenGL 的朋友可能知道: 所有的体和面都是由三角形拼组而成。既然这里可以画出三维空间中的三角形。那么我有一个大胆的想法,可以基于此绘制复杂的三维形体。

首先要做的一件事是: 封装三角形 的路径绘制。在此之前,先了解一下 OpenGL 标准中三角形的三种绘制方式:
GL_TRIANGLES: 独立三角形
- 每三个顶点被当作一个三角形。

GL_TRIANGLE_STRIP: 三角形带
- 每添加一个新顶点,都会创建一个新的三角形,且每次都是通过前两个顶点和当前的新顶点来定义一个三角形

GL_TRIANGLE_FAN: 扇形
第一个顶点是三角形扇形的中心,剩下的每个相邻顶点与中心一起组成一个三角形

3. 封装绘制三角形的方法
现在我们基于 Canvas 中的路径,封装三角形路径的形成方式。三种方式通过 DrawArrays
枚举区分:
dart
enum DrawArrays {
triangles, // 独立三角形
triangleStrip, // 三角形带
triangleFan, // 扇形
}
封装 buildPathByPoints3d 方法,传入点列表和绘制三角形的方式:
dart
Path buildPathByPoints3d(
List<Point3D> points3d, {
DrawArrays type = DrawArrays.triangles,
}) {
Path path = Path();
if (points3d.length == 1) path;
List<Offset> points = points3d.map((project)).toList();
return switch (type) {
DrawArrays.triangles => _buildTrianglesPath(points),
DrawArrays.triangleStrip => _buildStripPath(points),
DrawArrays.triangleFan => _buildFanPath(points),
};
}
然后根据三种不同形式的定义,进行具体实现。OpenGL 中之所要分三种绘制三角形的方式,是因为要适应不同场景:
triangles : 可以灵活绘制任何三角形,但是定点数永远是
三角形数*3
,无法复用顶点。
dart
Path _buildTrianglesPath(List<Offset> points) {
Path path = Path();
for (int i = 0; i + 2 < points.length; i += 3) {
path
..moveTo(points[i].dx, points[i].dy)
..lineTo(points[i + 1].dx, points[i + 1].dy)
..lineTo(points[i + 2].dx, points[i + 2].dy)
..close();
}
return path;
}
triangleStrip : 带状物体,如地形、纸带、绳索等连续结构。相邻三角形顶点可以复用。
dart
Path _buildStripPath(List<Offset> points) {
Path path = Path();
for (int i = 0; i + 2 < points.length; i++) {
path.moveTo(points[i].dx, points[i].dy);
path.lineTo(points[i + 1].dx, points[i + 1].dy);
path.lineTo(points[i + 2].dx, points[i + 2].dy);
path.close();
}
return path;
}
triangleFan : 圆形、扇形、雷达图等放射状结构。三角形共享中心顶点。
ini
Path _buildFanPath(List<Offset> points) {
Path path = Path();
Offset center = points[0];
for (int i = 1; i + 1 < points.length; i++) {
path.moveTo(center.dx, center.dy);
path.lineTo(points[i].dx, points[i].dy);
path.lineTo(points[i + 1].dx, points[i + 1].dy);
path.close();
}
return path;
}
绘制时只需要指定三维空间中的坐标列表即可,比如下面是八个点通过 triangleFan 模式绘制的三角形列表:

dart
List<Point3D> points3d = [
Point3D(4.00, 0.00, 0),
Point3D(2.83, 2.83, 0),
Point3D(0.00, 4.00, 0),
Point3D(-2.83, 2.83, 0),
Point3D(-4.00, 0.00, 0),
Point3D(-2.83, -2.83, 0),
Point3D(0.00, -4.00, 0),
Point3D(2.83, -2.83, 0),
];
Path path = buildPathByPoints3d(points3d, type: DrawArrays.triangleFan);
canvas.drawPath(path, paint);
4. 项目与源码
后续 《Flutter 伪 3D 绘制系列》
的项目将归属到 toly_game 中,大家可以在 github 开源仓库中得到可以运行的源码。如下所示,TolyGameBox 中已经集成 3D 世界
:

打开后就是可以看到该系列的效果,以后也会在这里继续玩耍 3D 世界。

伪 3d 绘制的代码也是单独分库,在项目中的 toly3d
模块中:

5. 尾声
通过这系列,仅希望探索 2d 绘制 3d 效果的可行性,并基于此介绍一些三维空间的特点。这和真正的 OpenGL 三维绘制是不可同日而语的。目前来看,不依赖与任何三维引擎,可以做到目前的效果,我已经非常满意了。下一篇,将会基于三角形,绘制空间中的其他图形,比如圆、环、多边形、甚至是曲面、三维形体。敬请期待 ~
更多文章和视频知识资讯,大家可以关注我的公众号、掘金和 B 站 。