flutter 3D动效之欧拉角和四元数

本文介绍了如何在Flutter应用中通过three_dart库实现实时步态采集数据的3D动画效果,包括使用欧拉角和四元组表示物体旋转,以及如何处理镜像反转和模型加载。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、最近项目功能要实现步态采集实时传输数据3D动效,找了一只右脚的obj模型,展示左脚用到了镜像反转,数学知识欧拉角和四元组,首先介绍一下欧拉角和四元组的区别:

允澄 2023-09-09 22.30.22

“欧拉角是指三个旋转角度,分别为绕X轴旋转的角度、绕Y轴旋转的角度和绕Z轴旋转的角度,表示物体在空间中的位置和方向。而四元数是一种复数形式的数字,由实部和虚部组成,可以用来表示旋转。 欧拉角比较直观,易于理解,但是欧拉角存在万向锁问题,即当某一轴旋转角度为90度时,其他两个轴的旋转将变得不可控。而四元数则没有万向锁问题,并且可以实现平滑旋转,因此在计算机图形学和游戏开发中被广泛应用

2、pubspec.yaml

three_dart: ^0.0.16
three_dart_jsm: ^0.0.10

3、 下载three_dart,在MultiViews2里添加,3D模型下载链接

import 'dart:async';
import 'dart:convert';
import 'dart:math';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' hide Matrix4;
import 'package:flutter/services.dart';

import 'package:flutter_gl/flutter_gl.dart';
import 'package:three_dart/three3d/math/index.dart';
import 'package:three_dart/three3d/math/matrix4.dart';
import 'package:three_dart/three_dart.dart' as three;
import 'package:three_dart_jsm/three_dart_jsm.dart' as three_jsm;

class MultiViews extends StatefulWidget {
  final String fileName;

  const MultiViews({Key? key, required this.fileName}) : super(key: key);

  @override
  State<MultiViews> createState() => _MyAppState();
}

class _MyAppState extends State<MultiViews> {
  three.WebGLRenderer? renderer;
  bool show = false;



  @override
  void initState() {
    super.initState();

    if (!kIsWeb) {
      init();
    }
  }

  init() async {
    var three3dRender = FlutterGlPlugin();
    await three3dRender.initialize(options: {"width": 1024, "height": 1024, "dpr": 1.0});
    await three3dRender.prepareContext();

    Map<String, dynamic> options = {
      "width": 1024,
      "height": 1024,
      "gl": three3dRender.gl,
      "antialias": true,
    };
    renderer = three.WebGLRenderer(options);
    renderer!.autoClear = true;

    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.fileName),
      ),
      body: SingleChildScrollView(child: _build(context)),
    );
  }

  Widget _build(BuildContext context) {
    return Column(
      children: [
        MultiViews2(renderer: renderer)
      ],
    );
  }
}

class MultiViews2 extends StatefulWidget {
  final three.WebGLRenderer? renderer;

  const MultiViews2({Key? key, this.renderer}) : super(key: key);

  @override
  State<MultiViews2> createState() => _MultiViews2State();
}

class _MultiViews2State extends State<MultiViews2> {
  three.WebGLRenderer? renderer;
  late FlutterGlPlugin three3dRender;
  int? fboId;
  double width = 300;
  double height = 300;

  List list = [];
  List list_euler = [];
  Size? screenSize;

  late three.Scene scene;
  late three.Camera camera;
  late three.Mesh mesh;

  double dpr = 1.0;

  var amount = 4;

  bool verbose = true;
  bool disposed = false;

  bool loaded = false;
  int count = 0;
  int count1 = 0;
  late three.Object3D object;
  late three.Object3D object1;

  // late three.Texture texture;

  late three.WebGLMultisampleRenderTarget renderTarget;

  three.AnimationMixer? mixer;
  three.Clock clock = three.Clock();

  dynamic sourceTexture;

  var angle = 0.0;

  @override
  void initState() {
    super.initState();

  }

  // Platform messages are asynchronous, so we initialize in an async method.
  Future<void> initPlatformState() async {
    width = screenSize!.width;
    height = width;
    three3dRender = FlutterGlPlugin();

    Map<String, dynamic> options = {
      "antialias": true,
      "alpha": false,
      "width": width.toInt(),
      "height": height.toInt(),
      "dpr": dpr
    };

    await three3dRender.initialize(options: options);

    setState(() {});

    Future.delayed(const Duration(milliseconds: 100), () async {
      await three3dRender.prepareContext();

      initScene();
      // initScene();
    });
  }

  initSize(BuildContext context) {
    if (screenSize != null) {
      return;
    }

    final mqd = MediaQuery.of(context);

    screenSize = mqd.size;
    dpr = mqd.devicePixelRatio;

    initPlatformState();
  }

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: width,
      height: height,
      child: Builder(
        builder: (BuildContext context) {
          initSize(context);
          return SingleChildScrollView(child: _build(context));
        },
      ),
    );
  }

  Widget _build(BuildContext context) {
    return Column(
      children: [
        Stack(
          children: [
            Container(
                width: width,
                height: height,
                color: Colors.black,
                child: Builder(builder: (BuildContext context) {
                  if (kIsWeb) {
                    return three3dRender.isInitialized
                        ? HtmlElementView(viewType: three3dRender.textureId!.toString())
                        : Container();
                  } else {
                    return three3dRender.isInitialized
                        ? Texture(textureId: three3dRender.textureId!)
                        : Container(
                            color: Colors.yellow,
                          );
                  }
                })),

          ],
        ),
      ],
    );
  }

  clickRender() {
    print(" click render... ");
    animate();
  }

  render() {
    int t = DateTime.now().millisecondsSinceEpoch;

    final gl = three3dRender.gl;
    if (!kIsWeb) renderer!.setRenderTarget(renderTarget);
    renderer!.render(scene, camera);

    int t1 = DateTime.now().millisecondsSinceEpoch;

    if (verbose) {
      print("render cost: ${t1 - t} ");
      print(renderer!.info.memory);
      print(renderer!.info.render);
    }

    // 重要 更新纹理之前一定要调用 确保gl程序执行完毕
    gl.flush();

    print("three3dRender 2: ${three3dRender.textureId} render: sourceTexture: $sourceTexture ");

    if (!kIsWeb) {
      three3dRender.updateTexture(sourceTexture);
    }
  }

  initRenderer() {
    renderer = widget.renderer;

    if (renderer == null) {
      Map<String, dynamic> options = {
        "width": width,
        "height": height,
        "gl": three3dRender.gl,
        "antialias": true,
      };
      renderer = three.WebGLRenderer(options);
      renderer!.autoClear = true;
    }

    if (!kIsWeb) {
      var pars = three.WebGLRenderTargetOptions({"format": three.RGBAFormat});
      renderTarget = three.WebGLMultisampleRenderTarget((width * dpr).toInt(), (height * dpr).toInt(), pars);
      renderTarget.samples = 4;
      renderer!.setRenderTarget(renderTarget);
      sourceTexture = renderer!.getRenderTargetGLTexture(renderTarget);
    }
  }

  initScene() {
    initRenderer();
    initPage();
  }

  initPage() async {
    camera = three.PerspectiveCamera(45, width / height, 1, 2200);
    camera.position.set(3, 6, 100);
    list = await loadData();
    list_euler = await load_eulerData();
    scene = three.Scene();

    // scene.background = three.Color(1, 1, 0);

    var ambientLight = three.AmbientLight(0xcccccc, 0.4);
    scene.add(ambientLight);

    var pointLight = three.PointLight(0xffffff, 0.6);

    pointLight.position.set(10, 10, 5);

    camera.add(pointLight);


    scene.add(camera);

    camera.lookAt(scene.position);


    var loader = three_jsm.OBJLoader(null);

    //添加本地obj模型
    object = await loader.loadAsync('assets/11536_foot_V3.obj');
    object.position.set(-10, 10, 5);
    //镜像反转
    object.scale.set(-1, 1);
    scene.add(object);
    

    object1 = await loader.loadAsync('assets/11536_foot_V3.obj');
    object1.position.set(10, 10, 5);
    object1.castShadow = true;
    object1.receiveShadow = true;
    scene.add(object1);

    //这里的quaternion模拟6轴数据中结算后的四元数
    // var quaternion = object.quaternion;
    // quaternion.setFromAxisAngle(three.Vector3(1,0,0), angle);
    // angle += 0.1;
    // object.applyQuaternion(quaternion);


    loaded = true;

    animate();
  }

  animate() async {


    var delta = clock.getDelta();

    //这里quaternion模拟从6轴数据中解算后的四元组

    object.rotation.x = list_euler[count][0];
    object.rotation.y = list_euler[count][1];
    object.rotation.z = list_euler[count][2];

    count += 1;

    object1.rotation.x = list_euler[count1][0];
    object1.rotation.y = list_euler[count1][1];
    object1.rotation.z = list_euler[count1][2];
    count1 += 1;

    mixer?.update(delta);

    render();

    Future.delayed(const Duration(milliseconds: 10), () {
      animate();
    });

  }



  @override
  void dispose() {
    print(" dispose ............. ");
    disposed = true;
    three3dRender.dispose();

    super.dispose();
  }
}

Future<String> loadJsonFromAssets(String assetsPath) async {
  return await rootBundle.loadString(assetsPath);
}

Future<Map<String, dynamic>> loadAndDecodeJson(String assetsPath) async {
  String jsonData = await rootBundle.loadString(assetsPath);
  return jsonDecode(jsonData);
}

Future<List<List>> loadData() async {
  // 1. 从 assets 中读取文件
  String jsonString = await rootBundle.loadString('assets/data.json');

  // 2. 将 JSON 字符串解析为 Dart 对象
  List<dynamic> jsonList = jsonDecode(jsonString);

  // 3. 转换每个对象为所需的数组格式
  List<List<double>> result = jsonList.map((item) {
    return [
      (item['x'] as num).toDouble(),
      (item['y'] as num).toDouble(),
      (item['z'] as num).toDouble(),
      (item['w'] as num).toDouble(),
    ];
  }).toList();

  return result;
}

Future<List<List>> load_eulerData() async {
  // 1. 从 assets 中读取文件
  String jsonString = await rootBundle.loadString('assets/data_euler.json');

  // 2. 将 JSON 字符串解析为 Dart 对象
  List<dynamic> jsonList = jsonDecode(jsonString);

  // 3. 转换每个对象为所需的数组格式
  List<List<double>> result = jsonList.map((item) {
    return [
      (item['roll'] as num).toDouble(),
      (item['pitch'] as num).toDouble(),
      (item['yaw'] as num).toDouble(),
    ];
  }).toList();

  return result;
}
 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

允澄

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值