
在上一篇,我们了解了如何绘制三维空间中的 三角形。本文将进一步基于三角形绘制更为复杂的形体。

1. 绘制正多边形
下面是一个正八边形,它可以看成是由 8 个共顶点的三角形构成的,橙色点时绘制三角形所需的顶点。想绘制出正多边形, 本质问题就在于:
如何计算这些橙色点在坐标系中的坐标。

记,八边形中分割的等边三角形顶角为 θ ,腰长为 r ,其实很容易计算出:
第 2 个点坐标:
(r * cos(θ), r * sin(θ))
第 3 个点坐标:
(r * cos(2θ), r * sin(2θ))
第 4 个点坐标:
(r * cos(3θ), r * sin(3θ))
... 第 n 个点坐标:
(r * cos((n-1)*θ), r * sin((n-1)*θ))
于是可以根据分析的公式,通过代码来创建符合正多边形的顶点列表:
dart
class Shape3d {
List<Point3D> circle(double r, int splitCount) {
Point3D center = Point3D(0, 0, 0);
List<Point3D> vertexes = [center];
double thta = 2 * pi / splitCount;
int count = splitCount + 2;
for (int n = 1; n < count; n++) {
double rad = (n - 1) * thta;
double x = r * cos(rad);
double y = r * sin(rad);
double z = 0;
vertexes.add(Point3D(x, y, z));
}
return vertexes;
}
}
根据上一篇中使用顶点列表绘制三角形的方式,就可以在坐标轴上得到如下的正八边形图案:

dart
List<Point3D> points3d = Shape3d().circle(4, 8);
Path path = buildPathByPoints3d(points3d, type: DrawArrays.triangleFan);
canvas.drawPath(path, paint);
2. 绘制圆形与圆锥
当正多边形的边数越来越大,将会近似于一个圆形。如下所示,通过输入框控制边数,就可以动态地查看边数逐渐增加的效果:

代码中为画板提供变量即可,数值由输入框控制,输入框内容改变时通知画板重绘:
dart
int sideCount = present.count;
List<Point3D> points3d = Shape3d().circle(4, sideCount);
Path path = buildPathByPoints3d(points3d, type: DrawArrays.triangleFan);
canvas.drawPath(path, paint);
我们还可以做一些更有趣的事,比如将圆周上的点向上移动,顶点不变,就可以得到一个圆锥。如下所示:

可以在 Shape3d
中封装一个 cone
方法绘制圆锥,可以传入 z 轴坐标。通过旋转空间,可以
dart
List<Point3D> cone(double r, int splitCount, {double z = 0}) {
Point3D center = Point3D(0, 0, 0);
List<Point3D> vertexes = [center];
double thta = 2 * pi / splitCount;
int count = splitCount + 2;
for (int n = 1; n < count; n++) {
double rad = (n - 1) * thta;
double x = r * cos(rad);
double y = r * sin(rad);
vertexes.add(Point3D(x, y, z));
}
return vertexes;
}

3. 三维点线绘制模式
在 OpenGL 标准中,绘制三维点集除了三角形之外,还有点和线。这里通过 螺旋线
介绍一下另外几种点集的绘制方式:

dart
enum DrawArrays {
points, // 点
lines, // 独立线段
lineStrip, // 连续折线
lineLoop, // 闭合折线
triangles, // 独立三角形
triangleStrip, // 三角形带
triangleFan, // 扇形
}
上面螺旋线折线在绘制时用的 lineStrip
模式,点集将以此连接成为折线。绘制是时只需将 path 移到第一点,然后遍历剩余点,通过 lineTo
连接即可:
dart
Path _buildLineStripPath(List<Offset> points) {
Path path = Path();
if (points.isEmpty) return path;
path.moveTo(points[0].dx, points[0].dy);
for (int i = 1; i < points.length; i++) {
path.lineTo(points[i].dx, points[i].dy);
}
return path;
}
如下所示 lines
模式的每两个顶点组成一条独立的线段。绘制时遍历点集列表,两次处理两个点,形成两点之间的线段路径:

dart
Path _buildLinesPath(List<Offset> points) {
Path path = Path();
for (int i = 0; i + 1 < points.length; i += 2) {
path
..moveTo(points[i].dx, points[i].dy)
..lineTo(points[i + 1].dx, points[i + 1].dy);
}
return path;
}
lineLoop
模式绘制闭合的曲线,只需要在 lineStrip
基础上增加连接起点的操作即可:

dart
Path _buildLineLoopPath(List<Offset> points) {
Path path = _buildLineStripPath(points);
if (points.length > 2) {
path.close(); // 回到起点
}
return path;
}
points
模式绘制点,这里遍历点集,通过 addOval 添加小圆点路径:

dart
Path _buildPointsPath(List<Offset> points) {
Path path = Path();
for (Offset p in points) {
path.addOval(Rect.fromCircle(center: p, radius: 1)); // 小圆点
}
return path;
}
4. 螺旋线的绘制
另外四种绘制方式介绍完毕,现在看一下螺旋线的绘制。在数学中,可以通过参数方程给出螺旋线的表达式:
c
x(t) = r⋅cos(2πnt)
y(t) = r⋅sin(2πnt)
z(t) = h⋅t
其中:
- t∈[0,1],表示螺旋线在起点到终点之间的插值;
- r 是半径,h 是高度,n 是圈数。
于是在代码层面,可以基于此来收集符合螺旋线规律的点集合:
参数 | 说明 |
---|---|
radius |
螺旋的半径(即绕 z 轴旋转的圆的半径) |
height |
总高度(z 方向的位移) |
turns |
旋转的圈数(每圈 2π 弧度) |
splitCount |
将螺旋细分为多少段(决定曲线的精细程度) |
dart
List<Point3D> helicoid(double radius, double height, int turns, int splitCount) {
List<Point3D> vertexes = [];
double totalAngle = 2 * pi * turns;
for (int i = 0; i <= splitCount; i++) {
double t = i / splitCount;
double angle = t * totalAngle;
double x = radius * cos(angle);
double y = radius * sin(angle);
double z = height * t;
vertexes.add(Point3D(x, y, z));
}
return vertexes;
}
5. 尾声
其实,任何有规则的图形,我们只要直到构成它的规律。进行采样,就可以通过收集点的方式,从而将它在三维空间中表达出来。比如:
空间中的贝塞尔曲线:

球体

更多文章和视频知识资讯,大家可以关注我的公众号、掘金和 B 站 。