You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于Flutter Transformations实现伪3D纸飞机的技术问题咨询

Flutter 伪3D纸飞机实现问题解决方案

效果说明

  • 预期效果:纸飞机呈现完整立体结构,旋转时前景面自动遮挡后方部件,机尾机翼向上翘起,与目标纸飞机外观一致
  • 当前问题:旋转时前景面无法遮挡后方部件,机尾机翼角度不符合预期

问题1:实现3D遮挡效果

Flutter的Stack默认按组件列表顺序绘制(后添加的组件覆盖前面的),无法自动处理3D空间的遮挡关系。解决思路是根据每个部件的实时Z轴深度动态调整绘制顺序:Z值越小(越靠近观察者)的部件越晚绘制,从而实现正确遮挡。

实现步骤

  1. 定义每个部件的初始3D位置和变换参数
  2. 应用全局旋转矩阵后,计算每个部件的实际Z坐标
  3. 根据Z坐标从小到大排序,重新排列Stack的子组件顺序

问题2:调整机尾机翼角度

目标图中的机尾机翼是向上翘起的,通过以下调整实现:

  1. 给机尾的两个部件分别添加rotateX角度(一个正角度,一个负角度,模拟对称翘起)
  2. 微调机尾部件的translate位置,让其更贴合机身
  3. 适当缩小机尾部件的尺寸,让比例更自然

修改后的完整代码

import 'package:flutter/material.dart';
import 'dart:math';
import 'package:vector_math/vector_math_64.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Paper plane',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const PaperPlane(title: '3D Paper plane'),
    );
  }
}

class PaperPlane extends StatefulWidget {
  const PaperPlane({super.key, required this.title});

  final String title;

  @override
  State<PaperPlane> createState() => _PaperPlaneState();
}

class _PaperPlaneState extends State<PaperPlane> {
  Offset offset = Offset.zero;

  Matrix4 _applyPerspective(Matrix4 matrix) {
    const double perspective = 0.003;
    matrix.setEntry(3, 2, perspective);
    return matrix;
  }

  @override
  Widget build(BuildContext context) {
    // 定义所有飞机部件的配置
    final planeParts = [
      {'transform': Matrix4.identity()..translate(-50.0, 0, 0.0), 'color': Colors.blue, 'rotate': true, 'size': const Size(100, 200)},
      {'transform': Matrix4.identity()..translate(50.0, 0.0, 0.0), 'color': Colors.blue, 'rotate': false, 'size': const Size(100, 200)},
      // 调整后的机尾部件:添加rotateX实现上翘,缩小尺寸
      {'transform': Matrix4.identity()..translate(0.0, 0.0, -50.0)..rotateY(90 * pi / 180)..rotateX(15 * pi / 180), 'color': Colors.red, 'rotate': false, 'size': const Size(80, 160)},
      {'transform': Matrix4.identity()..translate(0.0, 0.0, -50.0)..rotateY(90 * pi / 180)..rotateX(-15 * pi / 180), 'color': Colors.orange, 'rotate': false, 'size': const Size(80, 160)},
    ];

    // 全局旋转矩阵
    final globalTransform = _applyPerspective(Matrix4.identity()
      ..rotateX(-offset.dy * pi / 180)
      ..rotateY(-offset.dx * pi / 180));

    // 计算每个部件的实时Z坐标并排序
    final sortedParts = planeParts.map((part) {
      final combinedTransform = globalTransform.clone()..multiply(part['transform'] as Matrix4);
      final z = combinedTransform.transform3(Vector3(0,0,0)).z;
      return {'z': z, ...part};
    }).toList()
      ..sort((a, b) => (a['z'] as double).compareTo(b['z'] as double));

    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
        backgroundColor: Colors.blue,
      ),
      body: Stack(
        children: [
          Center(
            child: Transform(
              key: const ValueKey('plane'),
              transform: globalTransform,
              alignment: Alignment.center,
              child: Stack(
                children: sortedParts.map((part) {
                  return Transform(
                    transform: part['transform'] as Matrix4,
                    alignment: Alignment.center,
                    child: CustomPaint(
                      size: part['size'] as Size,
                      painter: TrianglePainter(
                        color: part['color'] as Color,
                        rotate: part['rotate'] as bool,
                      ),
                    ),
                  );
                }).toList(),
              ),
            ),
          ),
          Positioned.fill(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Expanded(
                  child: GestureDetector(
                    onPanUpdate: (details) {
                      setState(() {
                        offset = Offset((offset.dx + details.delta.dx) % 360,
                            (offset.dy + details.delta.dy) % 360);
                      });
                    },
                    child: Container(color: Colors.transparent),
                  ),
                ),
                Text('x: ${offset.dx.toStringAsFixed(1)} y: ${offset.dy.toStringAsFixed(1)}')
              ],
            ),
          ),
        ],
      ),
    );
  }
}

class TrianglePainter extends CustomPainter {
  final Color color;
  final bool rotate;
  TrianglePainter({required this.color, required this.rotate});

  @override
  void paint(Canvas canvas, Size size) {
    final Paint fillPaint = Paint()
      ..color = color
      ..style = PaintingStyle.fill;

    final Paint borderPaint = Paint()
      ..color = Colors.black
      ..style = PaintingStyle.stroke
      ..strokeWidth = 3.0;
    final Path path = Path();

    if (!rotate) {
      path
        ..moveTo(0, size.height)
        ..lineTo(size.width, size.height)
        ..lineTo(0, 0)
        ..close();
    } else {
      path
        ..moveTo(size.width, 0)
        ..lineTo(0, size.height)
        ..lineTo(size.width, size.height)
        ..close();
    }

    canvas.drawPath(path, fillPaint);
    canvas.drawPath(path, borderPaint);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return false;
  }
}

注意事项

  • 需要在pubspec.yaml中添加vector_math依赖:
    dependencies:
      flutter:
        sdk: flutter
      vector_math: ^2.1.4
    
  • 可根据需求调整perspective值(控制透视强度)、机尾的rotateX角度(控制翘起程度)和部件尺寸

内容的提问来源于stack exchange,提问作者Marc

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 03:35:57