Flutter三维渲染插件three_js的使用

发布于 1周前 作者 sinazl 来自 Flutter

Flutter三维渲染插件three_js的使用

three_js简介

three_js 是一个基于 three.jsthree_dart 的 Dart 版本 3D 渲染引擎,允许用户在 Flutter 应用中查看、编辑和操作 3D 对象。当前版本使用 ANGLE 来支持桌面和移动平台,并使用 WebGL2 支持 Web 应用。

Gif of dash playing a game.

特性

  • 支持多种平台(MacOS, iOS, Android, Windows, Web)
  • 提供丰富的 3D 模型加载和操作功能
  • 支持动画、光照、材质等高级特性

要求

MacOS

  • 最低部署目标:10.14
  • Xcode 13 或更新版本
  • Swift 5
  • Metal 支持

iOS

  • 最低部署目标:12.0
  • Xcode 13 或更新版本
  • Swift 5
  • Metal 支持

Android

  • compileSdkVersion: 34
  • OpenGL 支持

Windows

  • Intel 支持
  • AMD 支持
  • Direct3D 11 和 OpenGL 支持

Web

  • WebGL2 支持

Linux

  • 不支持

开始使用

要开始使用 three_js,请在 pubspec.yaml 文件中添加依赖:

dependencies:
  three_js: ^latest_version

示例代码

以下是一个完整的示例代码,展示了如何在 Flutter 中使用 three_js 创建一个简单的 3D 场景:

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

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

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

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

class _MyAppState extends State<ExampleApp> {
  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const FlutterGame(),
    );
  }
}

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

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

class _FlutterGameState extends State<FlutterGame> {
  late three.ThreeJS threeJs;

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

  @override
  void dispose() {
    threeJs.dispose();
    three.loading.clear();
    joystick?.dispose();
    super.dispose();
  }

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

  Map<LogicalKeyboardKey, bool> keyStates = {
    LogicalKeyboardKey.space: false,
    LogicalKeyboardKey.arrowUp: false,
    LogicalKeyboardKey.arrowLeft: false,
    LogicalKeyboardKey.arrowDown: false,
    LogicalKeyboardKey.arrowRight: false,
  };

  double gravity = 30;
  int stepsPerFrame = 5;
  three.Joystick? joystick;

  Future<void> setup() async {
    joystick = threeJs.width < 850 ? three.Joystick(
      size: 150,
      margin: const EdgeInsets.only(left: 35, bottom: 35),
      screenSize: Size(threeJs.width, threeJs.height), 
      listenableKey: threeJs.globalKey
    ) : null;

    threeJs.camera = three.PerspectiveCamera(45, threeJs.width / threeJs.height, 1, 2200);
    threeJs.camera.position.setValues(3, 6, 10);

    threeJs.scene = three.Scene();

    final ambientLight = three.AmbientLight(0xffffff, 0.3);
    threeJs.scene.add(ambientLight);

    final pointLight = three.PointLight(0xffffff, 0.1);
    pointLight.position.setValues(0, 0, 0);
    threeJs.camera.add(pointLight);
    threeJs.scene.add(threeJs.camera);

    threeJs.camera.lookAt(threeJs.scene.position);

    three.GLTFLoader loader = three.GLTFLoader(flipY: true).setPath('assets/');

    final sky = await loader.fromAsset('sky_sphere.glb');
    threeJs.scene.add(sky!.scene);

    final groundGLB = await loader.fromAsset('ground.glb');
    final ground = groundGLB!.scene;
    ground.rotation.y = 90 * (math.pi / 180);
    threeJs.scene.add(ground);

    final List<three.Vector3> coinPositions = [
      three.Vector3(-1.4 - 0.8 * 0, 1.5, -6 - 2 * 0),
      three.Vector3(-1.4 - 0.8 * 1, 1.5, -6 - 2 * 1),
      three.Vector3(-1.4 - 0.8 * 2, 1.5, -6 - 2 * 2),
      three.Vector3(-1.4 - 0.8 * 3, 1.5, -6 - 2 * 3),
      three.Vector3(-15 + 2 * 0, 1.5, 0.5 - 1.2 * 0),
      three.Vector3(-15 + 2 * 1, 1.5, 0.5 - 1.2 * 1),
      three.Vector3(-15 + 2 * 2, 1.5, 0.5 - 1.2 * 2),
      three.Vector3(-15 + 2 * 3, 1.5, 0.5 - 1.2 * 3),
      three.Vector3(7 + 2 * 0, 1.5, -16 + 1.3 * 0),
      three.Vector3(7 + 2 * 1, 1.5, -16.5 + 1.3 * 1),
      three.Vector3(7 + 2 * 2, 1.5, -16.5 + 1.3 * 2),
      three.Vector3(7 + 2 * 3, 1.5, -16 + 1.3 * 3),
    ];

    final coin = await loader.fromAsset('coin.glb');
    List<Coin> coins = [];
    for (final pos in coinPositions) {
      final object = coin!.scene.clone();
      coins.add(Coin(pos, object));
      threeJs.scene.add(object);
    }

    final dash = await loader.fromAsset('dash.glb');
    Player player = Player(
      dash!.scene,
      dash.animations!,
      threeJs.camera,
      threeJs.globalKey,
      joystick
    );
    threeJs.scene.add(dash.scene);

    threeJs.addAnimationEvent((dt) {
      joystick?.update();
      player.update(dt);
      for (final coin in coins) {
        coin.update(player.position, dt);
      }
    });

    threeJs.renderer?.autoClear = false; // To allow render overlay on top of sprited sphere
    if (joystick != null) {
      threeJs.postProcessor = ([double? dt]) {
        threeJs.renderer!.setViewport(0, 0, threeJs.width, threeJs.height);
        threeJs.renderer!.clear();
        threeJs.renderer!.render(threeJs.scene, threeJs.camera);
        threeJs.renderer!.clearDepth();
        threeJs.renderer!.render(joystick!.scene, joystick!.camera);
      };
    }
  }
}

class Coin {
  three.Vector3 position;
  bool collected = false;
  three.Vector3 startAnimPosition = three.Vector3.zero();
  double collectAnimation = 0;
  late three.Object3D object;

  Coin(this.position, this.object) {
    object.position = position;
  }

  void update(three.Vector3 playerPosition, double dt) {
    if (collected && collectAnimation == 1) {
      object.visible = false;
      return;
    }

    if (!collected) {
      double distance = playerPosition.clone().sub(position).length;
      if (distance < 2.2) {
        collected = true;
        startAnimPosition = position;
      }
    }

    if (collected) {
      collectAnimation = math.min(1, collectAnimation + dt * 2);
      object.position.y = startAnimPosition.y + math.sin(collectAnimation * 5) * 0.4;
      object.rotation.y *= 5;
    }
    object.rotation.y += 0.07;
  }
}

enum PlayerAction { blink, idle, walk, run }

class Player {
  three.Joystick? joystick;
  three.Vector3 get position => object.position;
  late three.Object3D object;
  late three.AnimationMixer mixer;
  late List animations;
  late final three.Vector3 playerVelocity;
  bool playerOnFloor = false;
  PlayerAction currentAction = PlayerAction.idle;
  late three.ThirdPersonControls tpsControl;
  Map<PlayerAction, three.AnimationAction> actions = {};

  Player(
    this.object,
    this.animations,
    three.Camera camera,
    GlobalKey<three.PeripheralsState> globalKey,
    [this.joystick]
  ) {
    mixer = three.AnimationMixer(object);
    actions = {
      PlayerAction.blink: mixer.clipAction(animations[0])!,
      PlayerAction.idle: mixer.clipAction(animations[2])!,
      PlayerAction.walk: mixer.clipAction(animations[4])!,
      PlayerAction.run: mixer.clipAction(animations[3])!
    };

    for (final act in actions.keys) {
      actions[act]!.enabled = true;
      actions[act]!.setEffectiveTimeScale(1);
      double weight = 0;
      if (act == PlayerAction.idle) {
        weight = 1;
      }
      actions[act]!.setEffectiveWeight(weight);
      actions[act]!.play();
    }

    camera.rotation.x = math.sin(-60 * (math.pi / 180));

    tpsControl = three.ThirdPersonControls(
      camera: camera,
      listenableKey: globalKey,
      object: object,
      offset: three.Vector3(5, 15, 10),
      movementSpeed: 5
    );

    playerVelocity = tpsControl.velocity;
  }

  void deactivateActions() {
    for (final act in actions.keys) {
      actions[act]!.setEffectiveWeight(0);
    }
  }

  void updateAction(PlayerAction action) {
    if (currentAction == action) return;
    currentAction = action;
    deactivateActions();
    actions[action]!.setEffectiveWeight(1);
  }

  void _updateDirection() {
    tpsControl.moveBackward = false;
    tpsControl.moveLeft = false;
    tpsControl.moveForward = false;
    tpsControl.moveRight = false;

    object.rotation.y = -joystick!.radians - math.pi / 2;
    if (joystick!.isMoving) {
      tpsControl.moveForward = true;
      tpsControl.movementSpeed = joystick!.intensity * 5;
    } else {
      tpsControl.movementSpeed = 5;
    }
  }

  void update(double dt) {
    tpsControl.update(dt);
    if (joystick != null) {
      _updateDirection();
    }

    if (tpsControl.isMoving) {
      if (tpsControl.movementSpeed > 4) {
        updateAction(PlayerAction.run);
      } else {
        updateAction(PlayerAction.walk);
      }
    } else {
      updateAction(PlayerAction.idle);
    }

    mixer.update(dt);
  }

  void dispose() {
    tpsControl.clearListeners();
  }
}

已知问题

  • MacOS: GroupMaterials 不工作
  • iOS: Buffer Validation 问题,GroupMaterials 不工作,Protoplanets 功能不正确
  • Android: GroupMaterials 不工作
  • Windows: GroupMaterials 不工作
  • Web: Lens Flare 功能不正确

贡献

欢迎贡献!如果有任何问题,请先查看 现有问题,如果没有相关问题可以创建一个新的 issue。对于非 trivial 的修复,请先创建 issue 再提交 pull request;对于 trivial 的修复可以直接提交 pull request。

更多详细信息和示例可以在 GitHub 仓库 中找到。


更多关于Flutter三维渲染插件three_js的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter三维渲染插件three_js的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


在Flutter中,虽然Three.js本身是一个基于WebGL的JavaScript库,用于在网页上进行三维渲染,但你可以通过一些插件或方法来在Flutter应用中集成Three.js。一个常见的方法是使用webview_flutter插件来嵌入一个包含Three.js渲染内容的网页。

以下是一个简单的例子,展示如何在Flutter应用中使用webview_flutter来加载一个包含Three.js渲染的网页。

第一步:添加依赖

首先,在你的pubspec.yaml文件中添加webview_flutter依赖:

dependencies:
  flutter:
    sdk: flutter
  webview_flutter: ^3.0.4  # 请检查最新版本号

第二步:创建Three.js内容的HTML文件

在你的项目目录中创建一个assets文件夹(如果还没有的话),并在其中创建一个threejs_content.html文件。这个文件将包含Three.js的渲染代码。例如:

<!-- assets/threejs_content.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Three.js in Flutter</title>
    <style>
        body { margin: 0; }
        canvas { display: block; }
    </style>
</head>
<body>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <script>
        // Basic Three.js scene setup
        var scene = new THREE.Scene();
        var camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
        var renderer = new THREE.WebGLRenderer();
        renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(renderer.domElement);

        var geometry = new THREE.BoxGeometry();
        var material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
        var cube = new THREE.Mesh(geometry, material);
        scene.add(cube);

        camera.position.z = 5;

        var animate = function () {
            requestAnimationFrame(animate);

            cube.rotation.x += 0.01;
            cube.rotation.y += 0.01;

            renderer.render(scene, camera);
        };

        animate();
    </script>
</body>
</html>

第三步:在Flutter中加载HTML文件

在你的Flutter应用中,使用WebView小部件来加载这个HTML文件。首先,确保在pubspec.yaml中声明了assets:

flutter:
  assets:
    - assets/threejs_content.html

然后,在你的Dart代码中,像这样使用WebView

import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';

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

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

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

class _WebViewExampleState extends State<WebViewExample> {
  late WebViewController _controller;

  @override
  Widget build(BuildContext context) {
    return WebView(
      initialUrl: Uri.dataFromString(
        '''
        <html>
          <head>
            <title>Three.js Placeholder</title>
          </head>
          <body>
            Loading Three.js content...
            <script>
              window.flutter_webview_platform_ready = function() {
                // Load the local HTML file after the platform is ready.
                var xhr = new XMLHttpRequest();
                xhr.open('GET', 'assets/threejs_content.html', true);
                xhr.onreadystatechange = function () {
                  if (xhr.readyState == 4 && xhr.status == 200) {
                    document.open();
                    document.write(xhr.responseText);
                    document.close();
                  }
                };
                xhr.send();
              };
            </script>
          </body>
        </html>
        ''',
        mimeType: 'text/html',
        encoding: Encoding.getByName('utf-8')
      ).toString(),
      javascriptMode: JavascriptMode.unrestricted,
      onWebViewCreated: (WebViewController webViewController) {
        _controller = webViewController;
        // Optionally, you can call a JavaScript function here to notify the web content that the platform is ready.
        // _controller.evaluateJavascript('window.flutter_webview_platform_ready();');
        // However, in this case, we use an event listener in the HTML to handle this.
      },
    );
  }
}

注意:在上面的代码中,我们使用了Uri.dataFromString来加载一个占位HTML文件,该文件在加载完成后通过JavaScript请求本地的threejs_content.html文件。这种方法避免了直接从本地文件系统加载文件的一些限制。

这种方法虽然不是直接在Flutter中使用Three.js,但通过webview_flutter插件,你可以在Flutter应用中嵌入一个完整的Three.js渲染环境。这对于需要复杂三维渲染的应用来说是一个可行的解决方案。

回到顶部