Flutter三维渲染插件three_js的使用
Flutter三维渲染插件three_js的使用
three_js简介
three_js
是一个基于 three.js 和 three_dart 的 Dart 版本 3D 渲染引擎,允许用户在 Flutter 应用中查看、编辑和操作 3D 对象。当前版本使用 ANGLE 来支持桌面和移动平台,并使用 WebGL2 支持 Web 应用。
特性
- 支持多种平台(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
更多关于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渲染环境。这对于需要复杂三维渲染的应用来说是一个可行的解决方案。