Flutter高级加载器插件three_js_advanced_loaders的使用

Flutter高级加载器插件three_js_advanced_loaders的使用

Pub Version analysis License: MIT

这是一个允许用户将gltf、glb或fbx文件添加到项目的three_js模型加载器。

Gif多个gltf模型。

此项目是由@mrdoob创建的three.js和three_dart的dart版本,并由@wasabia进行了dart转换。

开始使用

pubspec.yaml文件中添加此依赖项以及three_js_math、three_js_core、three_js_animations和three_js_core_loaders。

late Scene scene;

void init() {
  scene = Scene();
  scene.background = Color.fromHex32(0xf0f0f0);
      
  final loader = GLTFLoader();
  final gltf = await loader.fromAsset('assets/${fileName}.glb');

  mesh = gltf!.scene.children[0];
  mesh.scale.setValues(1.5, 1.5, 1.5);
  scene.add(mesh);
}

使用方法

此项目是一个更高级的three_js模型加载器。

示例代码

以下是完整的示例代码:

import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart';

import 'package:three_js_core/three_js_core.dart' as three;
import 'package:three_js_animations/three_js_animations.dart';
import 'package:three_js_helpers/three_js_helpers.dart';
import 'package:three_js_advanced_loaders/three_js_advanced_loaders.dart';
import 'package:three_js_controls/three_js_controls.dart';
import 'package:three_js_math/three_js_math.dart' as tmath;

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

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const WebglAnimationMultiple(),
    );
  }
}

class WebglAnimationMultiple extends StatefulWidget {
  const WebglAnimationMultiple({super.key});

  @override
  createState() => _State();
}

class _State extends State<WebglAnimationMultiple> {
  late three.ThreeJS threeJs;

  @override
  void initState() {
    threeJs = three.ThreeJS(
      settings: three.Settings(
        useSourceTexture: true
      ),
      onSetupComplete: (){setState(() {});},
      setup: setup
    );
    super.initState();
  }

  @override
  void dispose() {
    threeJs.dispose();
    controls.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return threeJs.build();
  }

  late OrbitControls controls;

  late List<Map<String, dynamic>> models;
  late List<Map<String, dynamic>> units;
  final List<AnimationMixer> mixers = []; // 所有场景中动画的three.AnimationMixer对象列表
  int numLoadedModels = 0;

  Future<void> setup() async {
    models = [
      {"name": "Soldier"},
      {"name": "Parrot"},
    ];

    // 定义我们想放置在场景中的模型实例,包括位置、缩放和要播放的动画名称
    units = [
      {
        "modelName": "Soldier", // 将使用models/gltf/Soldier.glb中的3D模型
        "meshName": "vanguard_Mesh", // 要动画的主要网格名称
        "position": {"x": 0, "y": 0, "z": 0}, // 在场景中的放置位置
        "scale": 1, // 单位的缩放比例。1.0表示原始大小,0.1表示“缩小10倍”
        "animationName": "Idle" // 要运行的动画名称
      },
      {
        "modelName": "Soldier",
        "meshName": "vanguard_Mesh",
        "position": {"x": 3, "y": 0, "z": 0},
        "scale": 2,
        "animationName": "Walk"
      },
      {
        "modelName": "Soldier",
        "meshName": "vanguard_Mesh",
        "position": {"x": 1, "y": 0, "z": 0},
        "scale": 1,
        "animationName": "Run"
      },
      {
        "modelName": "Parrot",
        "meshName": "mesh_0",
        "position": {"x": -4, "y": 0, "z": 0},
        "rotation": {"x": 0, "y": math.pi, "z": 0},
        "scale": 0.01,
        "animationName": "parrot_A_"
      },
      {
        "modelName": "Parrot",
        "meshName": "mesh_0",
        "position": {"x": -2, "y": 0, "z": 0},
        "rotation": {"x": 0, "y": math.pi / 2, "z": 0},
        "scale": 0.02,
        "animationName": null
      },
    ];

    setup2();
    loadModels();
  }

  void setup2() {
    threeJs.camera = three.PerspectiveCamera(45, threeJs.width / threeJs.height, 1, 10000);
    threeJs.camera.position.setValues(3, 6, -10);
    threeJs.camera.lookAt(tmath.Vector3(0, 1, 0));

    threeJs.scene = three.Scene();
    threeJs.scene.background = tmath.Color.fromHex32(0xa0a0a0);
    threeJs.scene.fog = three.Fog(0xa0a0a0, 10, 22);

    final hemiLight = three.HemisphereLight(0xffffff, 0x444444);
    hemiLight.position.setValues(0, 20, 0);
    threeJs.scene.add(hemiLight);

    final dirLight = three.DirectionalLight(0xffffff);
    dirLight.position.setValues(-3, 10, -10);
    dirLight.castShadow = true;
    dirLight.shadow!.camera!.top = 10;
    dirLight.shadow!.camera!.bottom = -10;
    dirLight.shadow!.camera!.left = -10;
    dirLight.shadow!.camera!.right = 10;
    dirLight.shadow!.camera!.near = 0.1;
    dirLight.shadow!.camera!.far = 40;
    threeJs.scene.add(dirLight);

    controls = OrbitControls(threeJs.camera, threeJs.globalKey);

    // 地面
    final groundMesh = three.Mesh(three.PlaneGeometry(40, 40), three.MeshPhongMaterial.fromMap({"color": 0x999999, "depthWrite": false}));

    groundMesh.rotation.x = -math.pi / 2;
    groundMesh.receiveShadow = true;
    threeJs.scene.add(groundMesh);
  }

  void loadModels() {
    for (int i = 0; i < models.length; ++i) {
      final m = models[i];

      loadGltfModel(m, () {
        ++numLoadedModels;

        if (numLoadedModels == models.length) {
          three.console.info("所有模型已加载,现在可以实例化单位...");
          instantiateUnits();
        }
      });
    }
  }

  void instantiateUnits() {
    int numSuccess = 0;

    for (int i = 0; i < units.length; ++i) {
      final u = units[i];
      final model = getModelByName(u["modelName"]);

      if (model != null) {
        final clonedScene = SkeletonUtils.clone(model["scene"]);

        if (clonedScene != null) {
          // three.Scene克隆成功,找到一个网格并启动动画
          final clonedMesh = clonedScene.getObjectByName(u["meshName"]);

          if (clonedMesh != null) {
            final mixer = startAnimation(
                clonedMesh,
                List<AnimationClip>.from(model["animations"]),
                u["animationName"]);

            // 保存动画混合器,将在动画循环中使用
            mixers.add(mixer);
            numSuccess++;
          }
          
          threeJs.scene.add(clonedScene);

          if (u["position"] != null) {
            clonedScene.position.setValues(
                u["position"]["x"].toDouble(), u["position"]["y"].toDouble(), u["position"]["z"].toDouble());
          }

          if (u["scale"] != null) {
            clonedScene.scale.setValues(u["scale"].toDouble(), u["scale"].toDouble(), u["scale"].toDouble());
          }

          if (u["rotation"] != null) {
            clonedScene.rotation.x = u["rotation"]["x"].toDouble();
            clonedScene.rotation.y = u["rotation"]["y"].toDouble();
            clonedScene.rotation.z = u["rotation"]["z"].toDouble();
          }
        }
      } else {
        three.console.info("无法找到模型 ${u["modelName"]}");
      }
    }

    three.console.info(" 成功实例化了 $numSuccess 个单位 ");

    threeJs.addAnimationEvent((dt){
      for (int i = 0; i < mixers.length; ++i) {
        mixers[i].update(dt);
      }

      controls.update();
    });
  }

  AnimationMixer startAnimation(
    three.Object3D skinnedMesh, 
    List<AnimationClip> animations, 
    String animationName
  ) {
    final mixer = AnimationMixer(skinnedMesh);
    final clip = AnimationClip.findByName(animations, animationName);

    if (clip != null) {
      final action = mixer.clipAction(clip);
      action!.play();
    }

    return mixer;
  }

  Map<String, dynamic>? getModelByName(String name) {
    for (int i = 0; i < models.length; ++i) {
      if (models[i]["name"] == name) {
        return models[i];
      }
    }

    return null;
  }

  void loadGltfModel(model, onLoaded) {
    final loader = GLTFLoader();
    final modelName = "assets/${model["name"]}.gltf";

    loader.fromAsset(modelName).then((gltf) {
      final scene = gltf!.scene;

      model["animations"] = gltf.animations;
      model["scene"] = scene;

      gltf.scene.traverse((object) {
        if (object is three.Mesh) {
          object.castShadow = true;
        }
      });

      three.console.info("完成加载模型 ${model["name"]} ");
      onLoaded();
    });
  }
}

更多关于Flutter高级加载器插件three_js_advanced_loaders的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter高级加载器插件three_js_advanced_loaders的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,下面是一个关于如何在Flutter项目中使用three_js_advanced_loaders插件的示例代码。这个插件允许你在Flutter应用中使用Three.js及其高级加载器来加载和处理3D模型。

首先,你需要确保你的Flutter项目中已经包含了three_js_advanced_loaders插件。如果还没有添加,可以在pubspec.yaml文件中添加依赖:

dependencies:
  flutter:
    sdk: flutter
  three_js_advanced_loaders: ^最新版本号  # 替换为实际的最新版本号

然后运行flutter pub get来安装依赖。

以下是一个基本的示例代码,展示如何在Flutter中使用three_js_advanced_loaders来加载并显示一个GLTF格式的3D模型。

import 'package:flutter/material.dart';
import 'package:three_js_advanced_loaders/three_js_advanced_loaders.dart';
import 'dart:html' as html;

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Three.js Advanced Loaders Example'),
        ),
        body: WebViewExample(),
      ),
    );
  }
}

class WebViewExample extends StatefulWidget {
  @override
  _WebViewExampleState createState() => _WebViewExampleState();
}

class _WebViewExampleState extends State<WebViewExample> {
  late html.Element _webViewContainer;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Expanded(
          child: LayoutBuilder(
            builder: (BuildContext context, BoxConstraints constraints) {
              _webViewContainer = html.div();
              _webViewContainer.style.width = '${constraints.maxWidth}px';
              _webViewContainer.style.height = '${constraints.maxHeight}px';
              html.document.body?.append(_webViewContainer);

              // Initialize Three.js and load the model
              _initThreeJS();

              return Container(); // Placeholder to take up space
            },
          ),
        ),
      ],
    );
  }

  void _initThreeJS() {
    // Create the Three.js scene, camera, and renderer
    final scene = THREE.Scene();
    final camera = THREE.PerspectiveCamera(75, _webViewContainer.clientWidth / _webViewContainer.clientHeight, 0.1, 1000);
    final renderer = THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(_webViewContainer.clientWidth, _webViewContainer.clientHeight);
    _webViewContainer.append(renderer.domElement);

    // Set the camera position
    camera.position.z = 5;

    // Create a directional light
    final light = THREE.DirectionalLight(0xffffff, 1);
    light.position.set(5, 10, 7.5);
    scene.add(light);

    // Create a helper for the camera (optional, for debugging)
    final cameraHelper = THREE.CameraHelper(camera, 0.1);
    scene.add(cameraHelper);

    // Load the GLTF model using the GLTFLoader
    final loader = THREE.GLTFLoader();
    loader.load('path/to/your/model.glb', (gltf) {
      scene.add(gltf.scene);
      animate();
    }, undefined, (error) {
      console.error(error);
    });

    // Animation loop
    void animate() {
      requestAnimationFrame(animate);
      renderer.render(scene, camera);
    }

    // Start the animation loop
    animate();
  }
}

注意

  1. 由于Flutter的Web视图限制,上述代码需要在Flutter Web环境中运行。对于移动或桌面平台,你需要使用其他方法(如platform_view或原生插件)来集成Three.js。

  2. path/to/your/model.glb需要替换为你实际的GLTF模型文件路径。

  3. 上述代码使用了LayoutBuilder来动态调整Web视图的尺寸,确保Three.js渲染器可以正确调整大小。

  4. 这是一个非常基础的示例,实际项目中可能需要更多的错误处理和优化。

希望这个示例能帮助你在Flutter项目中集成和使用three_js_advanced_loaders插件!

回到顶部