最近我在想,使用二维的 Canvas 能否通过投影的方式,模拟三维的坐标系,这样就可以给渲染三维坐标,从而实现简单的伪 3D 效果。如下所示:
绘制一个三维坐标系,通过三维的点绘制了两个平面, 并通过 Slider 交互可以让坐标系沿 z 轴旋转:
1. 定义数据和画板
首先定义一下承载三维点数据的类 Point3D
,其中有 x,y,z 三个数值表示坐标的三个维度:
dart
class Point3D {
final double x, y, z;
Point3D(this.x, this.y, this.z);
Point3D.zero() : this(0, 0, 0);
}
如下所示,定义 World3D
类继承自 CustomPainter,由于需要让坐标系沿 z 轴旋转,旋转的弧度数据由外界通过构造函数传入。现在关键的两步是:
-
1\]. 将三维点映射为二维点
dart
class World3D extends CustomPainter {
final double rotationZ;
World3D({this.rotationZ = 0});
Offset project(Point3D p) {
// TODO 将三维点映射为二维点
}
@override
void paint(Canvas canvas, Size size) {
// TODO 绘制逻辑
}
@override
bool shouldRepaint(World3D oldDelegate) => rotation != oldDelegate.rotation;
}
2. 如何将三维点映射为二维点
Canvas 绘制绘制的点是 Offset
对象,只有 x,y 两个维度,如下所示的白色坐标系: 想要绘制如下的三维空间,就是将三维点,通过运算转换成二维点,绘制在白色的坐标系上。
下面虽然产生了空间感,但本质上还是二维的线条,只不过按照三维的视觉规律进行投影。所以是一种伪 3d 的绘制方式:
二维 x 轴和三维 x 的夹角是 angle
,通过如下的投影变换,就可以将 Point3D
转换为 Offset
:
dart
Offset project(Point3D p) {
double scale = 40.0;
double angle = 30 / 180 * pi;
final x = (p.x - p.y) * cos(angle);
final y = (p.x + p.y) * sin(angle) - p.z;
return Offset(x * scale, y * scale);
}
3. 绘制坐标系
有了三维点的映射关系,就可以很轻松地计算出,在当前三维空间中,某个点坐落在二维空间中的坐标。比如 Point3D(5, 0, 0)
表示 x 轴正方向 5 个点位点的位置,得到位置后,使用 canvas.drawLine
就可以画出原点到 (5, 0, 0)
的直线,也就是下面的红色 x 轴线,其他两个轴类似:
dart
@override
void paint(Canvas canvas, Size size) {
Offset center = Offset(size.width / 2, size.height / 2);
canvas.translate(center.dx, center.dy);
// 绘制坐标系轴
_drawAxis(canvas, Colors.red, Point3D(5, 0, 0), 'X'); // X轴
_drawAxis(canvas, Colors.green, Point3D(0, 5, 0), 'Y'); // Y轴
_drawAxis(canvas, Colors.blue, Point3D(0, 0, 5), 'Z'); // Z轴
}
void _drawAxis(Canvas canvas, Color color, Point3D endPoint, String label) {
Paint paint = Paint()..color = color..strokeWidth = 2;
Offset start = project(Point3D.zero());
Offset end = project(endPoint);
canvas.drawLine(start, end, paint);
// 绘制轴标签
TextStyle style = TextStyle(color: color, fontSize: 16);
TextPainter(
text: TextSpan(text: label, style: style),
textDirection: TextDirection.ltr)
..layout()
..paint(canvas, end + const Offset(5, -10));
}
4. 绘制三维坐标点
同理,只要给出三维坐标,就可以通过 project
映射出二维坐标,通过 Canvas 绘制在平面上。于是我们就完成了任意三维空间点的展示,你也可以自己尝试画一下点,
dart
void _drawLines(Canvas canvas) {
List<(Point3D, Point3D)> bottomLines = [
(Point3D(0, 0, 0), Point3D(0, 1, 0)),
(Point3D(0, 0, 0), Point3D(1, 0, 0)),
(Point3D(0, 0, 0), Point3D(1, 1, 0)),
(Point3D(0, 1, 0), Point3D(1, 1, 0)),
(Point3D(1, 0, 0), Point3D(1, 1, 0)),
(Point3D(0, 1, 0), Point3D(1, 0, 0)),
];
Paint paint = Paint() ..color = Colors.white ..strokeWidth = 1;
for (var line in bottomLines) {
Offset p0 = project(line.$1);
Offset p1 = project(line.$2);
canvas.drawLine(p0, p1, paint..color = Colors.white);
}
}
5. 旋转 z 轴
想要让坐标系根据 z 轴旋转,只要根据旋转角度值,运算 Point3D
在该角度时的在平面上的投影即可,代码如下所示,就可以非常简单地实现下面的旋转效果:
dart
Offset project(Point3D p) {
double scale = 40.0; // 缩放系数
double angle = 30 / 180 * pi; // 30度弧度值(π/6)
// 绕Z轴旋转
final rx = p.x * cos(rotationZ) - p.y * sin(rotationZ);
final ry = p.x * sin(rotationZ) + p.y * cos(rotationZ);
// 等轴测投影
final xProj = (rx - ry) * cos(angle);
final yProj = (rx + ry) * sin(angle) - p.z;
return Offset(xProj * scale, yProj * scale);
}
尾声
我没想到,绘制一个伪 3 D 的空间这么简单,其中最重要的 project 方法是 AI 帮忙处理的。下一篇我将会详细分析一下这个投影的映射逻辑,敬请期待 ~
更多文章和视频知识资讯,大家可以关注我的公众号、掘金和 B 站 。