Flutter绘制-09-华为太空人-残次版

1,263 阅读7分钟

查看目录-->

本文代码地址-->

最近各种太空人很流行,于是我在想,用flutter该怎么实现?

经过一天多时间的努力,终于成功出了一个残次版,之所以是残次版,是因为没有太空人的模型数据,只得用贫瘠的绘画能力造了一个机器人版本的。

期间被初中时代的三角函数虐了一遍又一遍。

此文让我受益良多:www.jianshu.com/p/e3ebe08dd…

先看图:

spaceman.gif

思路

  • 解决在二维平面上怎么绘制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;
  }
}