基于Flutter Transformations实现伪3D纸飞机的技术问题咨询
Flutter 伪3D纸飞机实现问题解决方案
效果说明
- 预期效果:纸飞机呈现完整立体结构,旋转时前景面自动遮挡后方部件,机尾机翼向上翘起,与目标纸飞机外观一致
- 当前问题:旋转时前景面无法遮挡后方部件,机尾机翼角度不符合预期
问题1:实现3D遮挡效果
Flutter的Stack默认按组件列表顺序绘制(后添加的组件覆盖前面的),无法自动处理3D空间的遮挡关系。解决思路是根据每个部件的实时Z轴深度动态调整绘制顺序:Z值越小(越靠近观察者)的部件越晚绘制,从而实现正确遮挡。
实现步骤
- 定义每个部件的初始3D位置和变换参数
- 应用全局旋转矩阵后,计算每个部件的实际Z坐标
- 根据Z坐标从小到大排序,重新排列
Stack的子组件顺序
问题2:调整机尾机翼角度
目标图中的机尾机翼是向上翘起的,通过以下调整实现:
- 给机尾的两个部件分别添加
rotateX角度(一个正角度,一个负角度,模拟对称翘起) - 微调机尾部件的
translate位置,让其更贴合机身 - 适当缩小机尾部件的尺寸,让比例更自然
修改后的完整代码
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
相关产品推荐
相关产品推荐

