最近各种太空人很流行,于是我在想,用flutter该怎么实现?
经过一天多时间的努力,终于成功出了一个残次版,之所以是残次版,是因为没有太空人的模型数据,只得用贫瘠的绘画能力造了一个机器人版本的。
期间被初中时代的三角函数虐了一遍又一遍。
此文让我受益良多:www.jianshu.com/p/e3ebe08dd…
先看图:
思路
- 解决在二维平面上怎么绘制3d图形的问题
- 怎么画一个太空人儿
- 太空人旋转
- 画背景,也就是划过的线,给人以前进的错觉
技术点
- 3d投影到二维平面,参看上面提到的文章,详细讲解了空间点投影计算过程。不再赘述。掌握了这些,其实已经掌握了核心。
- 在一个技术点,就是旋转,绕xyz单独的轴旋转时比较好计算的,难得是如何计算任意一个点绕任意一条直线旋转后的坐标。之前参看了一篇文章,是python版的,惭愧已经找不到是哪篇了,但是作者明确表示,虽然算法正确,但是他也没理解。参考后,我翻译成了dart版本的,运行正常,但也没理解,惭愧!!!如果这道题放到当年初中,怕是很快就能计算出来。十多年不用,已经生疏了很多,主要是绘制三位几何图形的能力降为了0,导致无法绘制出计算路径。后续再看吧。
- 另一个点,肯定就是动画的使用,不再多言。
- 关于canvas 3d的绘制和旋转,已经封装成了一个工具类。便于后续重复使用。
- 不再将具体的实现过程,毕竟成品跟过程有很大不同。想要深入研究的应该都会遇到这些问题。不想入深入研究的,有过程也没用。
- 最终要的关键点,要自己去画,闭卷的那种。
代码
Point3d 顶一个三维空间点
class Point3d {
double x;
double y;
double z;
Point3d({this.x = 0, this.y = 0, this.z = 0});
}
Canvas3DUtil 封装的工具类
import 'dart:ui';
import 'dart:math';
import 'Point3d.dart';
class Canvas3DUtil{
// Point3d 绕x轴旋转角度后,返回新的Point3d 顺时针
Point3d getRotateXPoint(double rotateAngle, Point3d src) {
double ry = src.y * cos(rotateAngle) - src.z * sin(rotateAngle);
double rz = src.z * cos(rotateAngle) + src.y * sin(rotateAngle);
return Point3d(x: src.x, y: ry, z: rz);
}
// Point3d 绕y轴旋转角度后,返回新的Point3d 顺时针
Point3d getRotateYPoint(double rotateAngle, Point3d src) {
double rz = src.z * cos(rotateAngle) - src.x * sin(rotateAngle);
double rx = src.x * cos(rotateAngle) + src.z * sin(rotateAngle);
return Point3d(x: rx, y: src.y, z: rz);
}
// Point3d 绕z轴旋转角度后,返回新的Point3d 顺时针
Point3d getRotateZPoint(double rotateAngle, Point3d src) {
double rx = src.x * cos(rotateAngle) - src.y * sin(rotateAngle);
double ry = src.y * cos(rotateAngle) + src.x * sin(rotateAngle);
return Point3d(x: rx, y: ry, z: src.z);
}
// Point3d 绕空间直线轴旋转角度后,返回新的Point3d 顺时针 直线由p1和p2两个空间点确定
Point3d getRotateLinePoint(double angle, Point3d p, Point3d p1, Point3d p2) {
//计算两点之间距离
double distance = getDistanceBetweenTwoPoints(p1, p2);
// 计算p1 -> p2 的矢量
double u = (p1.x - p2.x) / distance;
double v = (p1.y - p2.y) / distance;
double w = (p1.z - p2.z) / distance;
double SinA = sin(angle);
double CosA = cos(angle);
double uu = u * u;
double vv = v * v;
double ww = w * w;
double uv = u * v;
double uw = u * w;
double vw = v * w;
double t00 = uu + (vv + ww) * CosA;
double t10 = uv * (1 - CosA) + w * SinA;
double t20 = uw * (1 - CosA) - v * SinA;
double t01 = uv * (1 - CosA) - w * SinA;
double t11 = vv + (uu + ww) * CosA;
double t21 = vw * (1 - CosA) + u * SinA;
double t02 = uw * (1 - CosA) + v * SinA;
double t12 = vw * (1 - CosA) - u * SinA;
double t22 = ww + (uu + vv) * CosA;
double a0 = p2.x;
double b0 = p2.y;
double c0 = p2.z;
double t03 = (a0 * (vv + ww) - u * (b0 * v + c0 * w)) * (1 - CosA) +
(b0 * w - c0 * v) * SinA;
double t13 = (b0 * (uu + ww) - v * (a0 * u + c0 * w)) * (1 - CosA) +
(c0 * u - a0 * w) * SinA;
double t23 = (c0 * (uu + vv) - w * (a0 * u + b0 * v)) * (1 - CosA) +
(a0 * v - b0 * u) * SinA;
return Point3d(
x: t00 * p.x + t01 * p.y + t02 * p.z + t03,
y: t10 * p.x + t11 * p.y + t12 * p.z + t13,
z: t20 * p.x + t21 * p.y + t22 * p.z + t23);
}
// 从给定点将3d坐标投影到xy平面后的坐标,从eye看eyedPoint,映射在xy平面的坐标
Offset transform3DPointToXY(Point3d eye, Point3d eyedPoint,
{double offsetX = 0, double offsetY = 0}) {
return Offset(
(eyedPoint.x - eye.x) * eye.z / (eye.z - eyedPoint.z) + offsetX,
(eyedPoint.y - eye.y) * eye.z / (eye.z - eyedPoint.z) + offsetY);
}
// 求空间两点之间距离
double getDistanceBetweenTwoPoints(Point3d a, Point3d b) {
return sqrt(pow(a.x - b.x, 2) + pow(a.y - b.y, 2) + pow(a.z - b.z, 2));
}
// 点p1和p2组成一条直线,求空间点point到该直线的距离,就是点到直线的距离
double getLengthOfSrcToStartEnd(Point3d point, Point3d p1, Point3d p2) {
// 三个点,组成一个三角形,先求三条边长
double a = getDistanceBetweenTwoPoints(point, p1);
double b = getDistanceBetweenTwoPoints(point, p2);
double c = getDistanceBetweenTwoPoints(p1, p2);
double s = (a + b + c) / 2;
double area = sqrt(s * (s - a) * (s - b) * (s - c));
return area * 2 / c;
}
}
Cuboid 定义的一个长方体
太空人的 头、身体、腿、眼睛、胳膊都是用这个实现的。
import 'dart:math';
import 'dart:ui';
import 'package:flutter_can/c14_3d/util/Canvas3DUtil.dart';
import 'package:flutter_can/c14_3d/util/Point3d.dart';
class Cuboid with Canvas3DUtil {
double x, y, z, long, width, height;
Point3d p3A, p3B, p3C, p3D, p3E, p3F, p3G, p3H;
Point3d eye;
Offset p2A, p2B, p2C, p2D, p2E, p2F, p2G, p2H;
Cuboid(
{this.x = 0,
this.y = 0,
this.z = 0,
this.long = 50,
this.width = 50,
this.height = 10}) {
p3A = Point3d(x: x - width / 2, y: y - long / 2, z: z + height / 2);
p3E = Point3d(x: x - width / 2, y: y - long / 2, z: z - height / 2);
p3B = Point3d(x: x + width / 2, y: y - long / 2, z: z + height / 2);
p3F = Point3d(x: x + width / 2, y: y - long / 2, z: z - height / 2);
p3C = Point3d(x: x + width / 2, y: y + long / 2, z: z + height / 2);
p3G = Point3d(x: x + width / 2, y: y + long / 2, z: z - height / 2);
p3D = Point3d(x: x - width / 2, y: y + long / 2, z: z + height / 2);
p3H = Point3d(x: x - width / 2, y: y + long / 2, z: z - height / 2);
eye = Point3d(x: 0, y: 0, z: 300);
// rotateX();
// rotateY();
// rotateZ();
}
rotateX() {
double rotateAngle = pi / 4;
p3A = getRotateXPoint(rotateAngle, p3A);
p3B = getRotateXPoint(rotateAngle, p3B);
p3C = getRotateXPoint(rotateAngle, p3C);
p3D = getRotateXPoint(rotateAngle, p3D);
p3E = getRotateXPoint(rotateAngle, p3E);
p3F = getRotateXPoint(rotateAngle, p3F);
p3G = getRotateXPoint(rotateAngle, p3G);
p3H = getRotateXPoint(rotateAngle, p3H);
}
rotateY({double rotateAngle = pi * 5 / 8}) {
p3A = getRotateYPoint(rotateAngle, p3A);
p3B = getRotateYPoint(rotateAngle, p3B);
p3C = getRotateYPoint(rotateAngle, p3C);
p3D = getRotateYPoint(rotateAngle, p3D);
p3E = getRotateYPoint(rotateAngle, p3E);
p3F = getRotateYPoint(rotateAngle, p3F);
p3G = getRotateYPoint(rotateAngle, p3G);
p3H = getRotateYPoint(rotateAngle, p3H);
}
rotateZ() {
double rotateAngle = pi * 7 / 8;
p3A = getRotateZPoint(rotateAngle, p3A);
p3B = getRotateZPoint(rotateAngle, p3B);
p3C = getRotateZPoint(rotateAngle, p3C);
p3D = getRotateZPoint(rotateAngle, p3D);
p3E = getRotateZPoint(rotateAngle, p3E);
p3F = getRotateZPoint(rotateAngle, p3F);
p3G = getRotateZPoint(rotateAngle, p3G);
p3H = getRotateZPoint(rotateAngle, p3H);
}
void rotateLine(double rotateAngle) {
// 绕x轴
// Point3d p1 = Point3d(x:0,y:0,z:0);
// Point3d p2 = Point3d(x:-100,y:0,z:0);
// 绕y轴
// Point3d p1 = Point3d(x:0,y:0,z:0);
// Point3d p2 = Point3d(x:0,y:100,z:0);
// 绕z轴
// Point3d p1 = Point3d(x:0,y:0,z:0);
// Point3d p2 = Point3d(x:0,y:0,z:100);
// 绕西北东南轴
Point3d p1 = Point3d(x: 100, y: 100, z: 100);
Point3d p2 = Point3d(x: -100, y: -100, z: -100);
p3A = getRotateLinePoint(rotateAngle, p3A, p1, p2);
p3B = getRotateLinePoint(rotateAngle, p3B, p1, p2);
p3C = getRotateLinePoint(rotateAngle, p3C, p1, p2);
p3D = getRotateLinePoint(rotateAngle, p3D, p1, p2);
p3E = getRotateLinePoint(rotateAngle, p3E, p1, p2);
p3F = getRotateLinePoint(rotateAngle, p3F, p1, p2);
p3G = getRotateLinePoint(rotateAngle, p3G, p1, p2);
p3H = getRotateLinePoint(rotateAngle, p3H, p1, p2);
}
// 投影到xy平面的点
void projectionXY() {
p2A = transform3DPointToXY(eye, p3A);
p2B = transform3DPointToXY(eye, p3B);
p2C = transform3DPointToXY(eye, p3C);
p2D = transform3DPointToXY(eye, p3D);
p2E = transform3DPointToXY(eye, p3E);
p2F = transform3DPointToXY(eye, p3F);
p2G = transform3DPointToXY(eye, p3G);
p2H = transform3DPointToXY(eye, p3H);
}
Path getPath(double rotateAngle) {
rotateY(rotateAngle: rotateAngle);
projectionXY();
Path path1 = Path()
// ABCD
..moveTo(p2A.dx, p2A.dy)
..lineTo(p2B.dx, p2B.dy)
..lineTo(p2C.dx, p2C.dy)
..lineTo(p2D.dx, p2D.dy)
..lineTo(p2A.dx, p2A.dy);
//AEDH
Path path2 = Path()
..moveTo(p2A.dx, p2A.dy)
..lineTo(p2D.dx, p2D.dy)
..lineTo(p2H.dx, p2H.dy)
..lineTo(p2E.dx, p2E.dy)
..lineTo(p2A.dx, p2A.dy);
// ABEF
Path path3 = Path()
..moveTo(p2A.dx, p2A.dy)
..lineTo(p2B.dx, p2B.dy)
..lineTo(p2F.dx, p2F.dy)
..lineTo(p2E.dx, p2E.dy)
..lineTo(p2A.dx, p2A.dy);
// GCDH
Path path4 = Path()
..moveTo(p2G.dx, p2G.dy)
..lineTo(p2C.dx, p2C.dy)
..lineTo(p2D.dx, p2D.dy)
..lineTo(p2H.dx, p2H.dy)
..lineTo(p2G.dx, p2G.dy);
// GCBF
Path path5 = Path()
..moveTo(p2G.dx, p2G.dy)
..lineTo(p2C.dx, p2C.dy)
..lineTo(p2B.dx, p2B.dy)
..lineTo(p2F.dx, p2F.dy)
..lineTo(p2G.dx, p2G.dy);
// GFEH
Path path6 = Path()
..moveTo(p2G.dx, p2G.dy)
..lineTo(p2F.dx, p2F.dy)
..lineTo(p2E.dx, p2E.dy)
..lineTo(p2H.dx, p2H.dy)
..lineTo(p2G.dx, p2G.dy);
Path path = Path();
path.addPath(path1, Offset.zero);
path.addPath(path2, Offset.zero);
path.addPath(path3, Offset.zero);
path.addPath(path4, Offset.zero);
path.addPath(path5, Offset.zero);
path.addPath(path6, Offset.zero);
return path;
}
}
C14SpaceMan 模拟的时间流逝
import 'package:flutter/material.dart';
import 'package:flutter_can/c14_3d/spaceman/SpaceMan.dart';
class C14SpaceMan extends StatefulWidget {
@override
_Test3dState createState() => _Test3dState();
}
class _Test3dState extends State<C14SpaceMan> with SingleTickerProviderStateMixin{
AnimationController _controller;
@override
void initState() {
super.initState();
_controller = new AnimationController(vsync: this,duration: const Duration(seconds: 1));
_controller.repeat();
}
@override
Widget build(BuildContext context) {
return Container(
child: CustomPaint(
size: MediaQuery.of(context).size,
painter: SpaceMan(_controller),
),
);
}
}
SpaceMan 具体的绘制过程
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_can/c14_3d/util/Canvas3DUtil.dart';
import 'package:flutter_can/c14_3d/util/Point3d.dart';
import 'package:flutter_can/c14_3d/spaceman/Cuboid.dart';
// 太空人
class SpaceMan extends CustomPainter with Canvas3DUtil {
Animation<double> animation;
double rotateAngle = 0.06;
Cuboid _body,_head,_leftArm,_rightArm,_leftLeg,_rightLeg,_leftEye,_rightEye,_leftFoot,_rightFoot;
SpaceMan(this.animation) : super(repaint: animation) {
_body = Cuboid(long:100,width: 100,height: 40);
_head = Cuboid(x:0,y:-80,long:60,width: 60,height: 40);
_leftArm = Cuboid(x:-60,y:-80,z:0,long:120,width: 20,height: 20);
_rightArm = Cuboid(x:60,y:-80,z:0,long:120,width: 20,height: 20);
_leftLeg = Cuboid(x:-25,y:110,z:0,long:120,width: 30,height: 20);
_rightLeg = Cuboid(x:25,y:110,z:0,long:120,width: 30,height: 20);
_leftEye = Cuboid(x:-10,y:-90,z:15,long:10,width: 10,height: 10);
_rightEye = Cuboid(x:10,y:-90,z:15,long:10,width: 10,height: 10);
_leftFoot = Cuboid(x:-25,y:180,z:10,long:20,width: 30,height: 40);
_rightFoot = Cuboid(x:25,y:180,z:10,long:20,width: 30,height: 40);
}
@override
void paint(Canvas canvas, Size size) {
translateToCenter(canvas, size);
// drawXY(canvas, size);
canvas.save();
canvas.rotate(-pi/4);
drawRect3d(canvas, size);
drawWind(canvas);
canvas.restore();
}
void drawWind(Canvas canvas){
Paint paint = Paint();
paint.color = Colors.grey;
paint.strokeWidth = 1;
paint.style = PaintingStyle.stroke;
canvas.drawLine(Offset(100,-150+800*animation.value), Offset(100,-100+800*animation.value), paint);
canvas.drawLine(Offset(120,-300+1700*animation.value), Offset(120,-250+1700*animation.value), paint);
canvas.drawLine(Offset(-110,-450+900*animation.value), Offset(-110,-400+900*animation.value), paint);
}
// 画长方体
void drawRect3d(Canvas canvas, Size size) {
Paint paint = Paint();
paint.color = Colors.grey;
paint.strokeWidth = 1;
paint.style = PaintingStyle.stroke;
Path path = Path();
path.addPath(_body.getPath(rotateAngle), Offset.zero);
path.addPath(_head.getPath(rotateAngle), Offset.zero);
path.addPath(_leftArm.getPath(rotateAngle), Offset.zero);
path.addPath(_rightArm.getPath(rotateAngle), Offset.zero);
path.addPath(_leftLeg.getPath(rotateAngle), Offset.zero);
path.addPath(_rightLeg.getPath(rotateAngle), Offset.zero);
path.addPath(_leftEye.getPath(rotateAngle), Offset.zero);
path.addPath(_rightEye.getPath(rotateAngle), Offset.zero);
path.addPath(_leftFoot.getPath(rotateAngle), Offset.zero);
path.addPath(_rightFoot.getPath(rotateAngle), Offset.zero);
canvas.drawPath(path, paint);
}
// 画xy坐标系
void drawXY(Canvas canvas, Size size) {
Paint paint = Paint();
paint.color = Colors.grey;
paint.strokeWidth = 1;
paint.style = PaintingStyle.stroke;
canvas.drawLine(Offset.zero, Offset(200, 0), paint);
canvas.drawLine(Offset.zero, Offset(0, 200), paint);
}
// 原点移到中心点
void translateToCenter(Canvas canvas, Size size) {
canvas.translate(size.width / 2, size.height / 2);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}