Flutter 伪 3D 绘制#1 | 三维空间

最近我在想,使用二维的 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 站 。

相关推荐
每次的天空6 小时前
Android学习总结之算法篇四(字符串)
android·学习·算法
x-cmd7 小时前
[250331] Paozhu 发布 1.9.0:C++ Web 框架,比肩脚本语言 | DeaDBeeF 播放器发布 1.10.0
android·linux·开发语言·c++·web·音乐播放器·脚本语言
bst@微胖子7 小时前
Flutter项目之登录注册功能实现
开发语言·javascript·flutter
小墙程序员9 小时前
Flutter 教程(十一)多语言支持
flutter
tangweiguo0305198710 小时前
Android BottomNavigationView 完全自定义指南:图标、文字颜色与选中状态
android
遥不可及zzz11 小时前
Android 应用程序包的 adb 命令
android·adb
无知的前端11 小时前
Flutter 一文精通Isolate,使用场景以及示例
android·flutter·性能优化
_一条咸鱼_11 小时前
Android Compose 入门之字符串与本地化深入剖析(五十三)
android
yidahis11 小时前
Flutter 运行新建项目也报错?
flutter·trae
木马不在转11 小时前
Flutter-权限permission_handler插件配置
flutter