Flutter物理引擎插件oimo_physics的使用
Flutter物理引擎插件oimo_physics的使用
一个为three_dart和three_dart_jsm设计的Flutter插件,允许用户在其3D项目中添加一个简单的物理引擎。
此插件是由Saharan创建的oimo.js的dart版本。
使用
入门
要开始使用,请将oimo_physics
、three_dart
和three_dart_jsm
添加到您的pubspec.yaml
文件中。
Oimo World
是包含所有将被操作的对象的主要场景。首先添加oimo world
,然后添加其中的所有对象。
如果在ObjectConfigure
类中没有形状或类型,则它将无法工作。如果您需要刚体,请使用形状。如果您需要关节,请使用类型。
world = OIMO.World(OIMO.WorldConfigure(isStat: true, scale: 100.0));
OIMO.ShapeConfig config = OIMO.ShapeConfig(
friction: 0.4,
belongsTo: 1,
);
world!.add(OIMO.ObjectConfigure(
shapes: [OIMO.Shapes.box],
size: [400, 40, 400],
position: [0, -20, 0],
shapeConfig: config
));
bodys.add(world!.add(OIMO.ObjectConfigure(
shapes: [OIMO.Shapes.sphere],
size: [w * 0.5, w * 0.5, w * 0.5],
position: [x, y, z],
move: true,
shapeConfig: config,
name: 'sphere'
)) as OIMO.RigidBody);
示例
找到示例应用 这里。
贡献
欢迎贡献。 如果有任何问题,请查看 现有问题,如果找不到与您的问题相关的任何内容,请打开一个问题。 对于非重大修复,在打开 拉取请求 之前请先创建一个issue。 对于简单修复,可以直接打开 拉取请求。
额外信息
该插件仅用于执行基本物理。虽然可以将其作为独立项目使用,但它不渲染场景。
以下是一个完整的示例演示如何使用oimo_physics
插件:
import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter_gl/flutter_gl.dart';
import 'package:oimo_physics/core/rigid_body.dart';
import 'package:oimo_physics/oimo_physics.dart' as oimo;
import 'package:three_dart/three_dart.dart' as THREE;
import 'package:three_dart/three_dart.dart' hide Texture, Color;
import 'package:three_dart_jsm/three_dart_jsm.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
[@override](/user/override)
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const TestBasic(),
);
}
}
extension on oimo.Quat {
Quaternion toQuaternion() {
return Quaternion(x, y, z, w);
}
}
extension on oimo.Vec3 {
Vector3 toVector3() {
return Vector3(x, y, z);
}
}
class TestBasic extends StatefulWidget {
const TestBasic({
Key? key,
this.offset = const Offset(0, 0),
}) : super(key: key);
final Offset offset;
[@override](/user/override)
_TestBasicPageState createState() => _TestBasicPageState();
}
class _TestBasicPageState extends State<TestBasic> {
FocusNode node = FocusNode();
bool animationReady = false;
late FlutterGlPlugin three3dRender;
WebGLRenderTarget? renderTarget;
WebGLRenderer? renderer;
late OrbitControls controls;
late double width;
late double height;
Size? screenSize;
late Scene scene;
late Camera camera;
double dpr = 1.0;
bool verbose = false;
bool disposed = false;
final GlobalKey<DomLikeListenableState> _globalKey = GlobalKey<DomLikeListenableState>();
dynamic sourceTexture;
List<Mesh> meshs = [];
List<Mesh> grounds = [];
Map<String, BufferGeometry> geos = {};
Map<String, THREE.Material> mats = {};
oimo.World? world;
List<oimo.Core?> bodys = [];
List<int> fps = [0, 0, 0, 0];
double ToRad = 0.0174532925199432957;
int type = 4;
[@override](/user/override)
void initState() {
super.initState();
}
[@override](/user/override)
void dispose() {
disposed = true;
three3dRender.dispose();
super.dispose();
}
void initSize(BuildContext context) {
if (screenSize != null) {
return;
}
final mqd = MediaQuery.of(context);
screenSize = mqd.size;
dpr = mqd.devicePixelRatio;
initPlatformState();
}
void animate() {
if (!mounted || disposed) {
return;
}
render();
Future.delayed(const Duration(milliseconds: 1000 ~/ 60), () {
updateOimoPhysics();
animate();
});
}
Future<void> initPage() async {
scene = Scene();
camera = PerspectiveCamera(60, width / height, 1, 10000);
camera.position.set(0, 160, 400);
camera.rotation.order = 'YXZ';
controls = OrbitControls(camera, _globalKey);
scene.add(AmbientLight(0x3D4143));
DirectionalLight light = DirectionalLight(0xffffff, 1.4);
light.position.set(300, 1000, 500);
light.target!.position.set(0, 0, 0);
light.castShadow = true;
int d = 300;
light.shadow!.camera = OrthographicCamera(-d, d, d, -d, 500, 1600);
light.shadow!.bias = 0.0001;
light.shadow!.mapSize.width = light.shadow!.mapSize.height = 1024;
scene.add(light);
// 背景
BufferGeometry buffgeoBack = THREE.IcosahedronGeometry(3000, 2);
Mesh back = THREE.Mesh(buffgeoBack, THREE.MeshLambertMaterial());
scene.add(back);
// 几何体
geos['sphere'] = THREE.SphereGeometry(1, 16, 10);
geos['box'] = THREE.BoxGeometry(1, 1, 1);
geos['cylinder'] = THREE.CylinderGeometry(1, 1, 1);
// 材质
mats['sph'] = MeshPhongMaterial({'shininess': 10, 'name': 'sph'});
mats['box'] = MeshPhongMaterial({'shininess': 10, 'name': 'box'});
mats['cyl'] = MeshPhongMaterial({'shininess': 10, 'name': 'cyl'});
mats['ssph'] = MeshPhongMaterial({'shininess': 10, 'name': 'ssph'});
mats['sbox'] = MeshPhongMaterial({'shininess': 10, 'name': 'sbox'});
mats['scyl'] = MeshPhongMaterial({'shininess': 10, 'name': 'scyl'});
mats['ground'] = MeshPhongMaterial({'shininess': 10, 'color': 0x3D4143, 'transparent': true, 'opacity': 0.5});
animationReady = true;
}
void addStaticBox(List<double> size, List<double> position, List<double> rotation) {
Mesh mesh = THREE.Mesh(geos['box'], mats['ground']);
mesh.scale.set(size[0], size[1], size[2]);
mesh.position.set(position[0], position[1], position[2]);
mesh.rotation.set(rotation[0] * ToRad, rotation[1] * ToRad, rotation[2] * ToRad);
scene.add(mesh);
grounds.add(mesh);
mesh.castShadow = true;
mesh.receiveShadow = true;
}
void clearMesh() {
for (int i = 0; i < meshs.length; i++) {
scene.remove(meshs[i]);
}
for (int i = 0; i < grounds.length; i++) {
scene.remove(grounds[i]);
}
grounds = [];
meshs = [];
}
void initOimoPhysics() {
world = oimo.World(
oimo.WorldConfigure(
isStat: true,
scale: 1.0,
gravity: oimo.Vec3(0, -981, 0)
)
);
populate(type);
}
void populate(n) {
int max = 200;
if (n == 1) {
type = 1;
} else if (n == 2) {
type = 2;
} else if (n == 3) {
type = 3;
} else if (n == 4) {
type = 4;
}
// 重置旧对象
clearMesh();
world!.clear();
bodys = [];
// 添加地面
world!.add(
oimo.ObjectConfigure(
shapes: [oimo.Box(oimo.ShapeConfig(geometry: oimo.Shapes.box), 40.0, 40.0, 390.0)],
position: oimo.Vec3(-180.0, 20.0, 0.0),
)
) as oimo.RigidBody;
world!.add(
oimo.ObjectConfigure(
shapes: [oimo.Box(oimo.ShapeConfig(geometry: oimo.Shapes.box), 40.0, 40.0, 390.0)],
position: oimo.Vec3(180.0, 20.0, 0.0),
)
) as oimo.RigidBody;
world!.add(
oimo.ObjectConfigure(
shapes: [oimo.Box(oimo.ShapeConfig(geometry: oimo.Shapes.box), 400.0, 80.0, 400.0)],
position: oimo.Vec3(0.0, -40.0, 0.0),
)
) as oimo.RigidBody;
addStaticBox([40, 40, 390], [-180, 20, 0], [0, 0, 0]);
addStaticBox([40, 40, 390], [180, 20, 0], [0, 0, 0]);
addStaticBox([400, 80, 400], [0, -40, 0], [0, 0, 0]);
// 添加物体
double x, y, z, w, h, d;
int t;
for (int i = 0; i < max; i++) {
if (type == 4) {
t = Math.floor(Math.random() * 3) + 1;
} else {
t = type;
}
x = -100 + Math.random() * 200;
z = -100 + Math.random() * 200;
y = 100 + Math.random() * 1000;
w = 10 + Math.random() * 10;
h = 10 + Math.random() * 10;
d = 10 + Math.random() * 10;
THREE.Color randColor = THREE.Color().setHex((Math.random() * 0xFFFFFF).toInt());
if (t == 1) {
THREE.Material mat = mats['sph']!;
mat.color = randColor;
bodys.add(world!.add(oimo.ObjectConfigure(
shapes: [oimo.Sphere(oimo.ShapeConfig(geometry: oimo.Shapes.sphere), w * 0.5)],
position: oimo.Vec3(x, y, z),
move: true,
)));
meshs.add(THREE.Mesh(geos['sphere'], mat));
meshs[i].scale.set(w * 0.5, w * 0.5, w * 0.5);
} else if (t == 2) {
THREE.Material mat = mats['box']!;
mat.color = randColor;
bodys.add(world!.add(oimo.ObjectConfigure(
shapes: [oimo.Box(oimo.ShapeConfig(geometry: oimo.Shapes.box), w, h, d)],
position: oimo.Vec3(x, y, z),
move: true,
)) as oimo.RigidBody);
meshs.add(THREE.Mesh(geos['box'], mat));
meshs[i].scale.set(w, h, d);
} else if (t == 3) {
THREE.Material mat = mats['cyl']!;
mat.color = randColor;
bodys.add(world!.add(oimo.ObjectConfigure(
shapes: [oimo.Cylinder(oimo.ShapeConfig(geometry: oimo.Shapes.cylinder), w * 0.5, h)],
position: oimo.Vec3(x, y, z),
move: true,
)));
meshs.add(THREE.Mesh(geos['cylinder'], mat));
meshs[i].scale.set(w * 0.5, h, w * 0.5);
}
meshs[i].castShadow = true;
meshs[i].receiveShadow = true;
scene.add(meshs[i]);
}
}
void updateOimoPhysics() {
if (world == null) return;
world!.step();
var x, y, z;
Mesh mesh;
oimo.RigidBody body;
for (int i = 0; i < bodys.length; i++) {
body = bodys[i] as RigidBody;
mesh = meshs[i];
if (!body.sleeping) {
mesh.position.copy(body.position.toVector3());
mesh.quaternion.copy(body.orientation.toQuaternion());
// 更改材质
if (mesh.material.name == 'sbox') mesh.material = mats['box'];
if (mesh.material.name == 'ssph') mesh.material = mats['sph'];
if (mesh.material.name == 'scyl') mesh.material = mats['cyl'];
// 重置位置
if (mesh.position.y < -100) {
x = -100 + Math.random() * 200;
z = -100 + Math.random() * 200;
y = 100 + Math.random() * 1000;
body.resetPosition(x, y, z);
}
} else {
if (mesh.material.name == 'box') mesh.material = mats['sbox'];
if (mesh.material.name == 'sph') mesh.material = mats['ssph'];
if (mesh.material.name == 'cyl') mesh.material = mats['scyl'];
}
}
}
void render() {
final _gl = three3dRender.gl;
renderer!.render(scene, camera);
_gl.flush();
controls.update();
if (!kIsWeb) {
three3dRender.updateTexture(sourceTexture);
}
}
void initRenderer() {
Map<String, dynamic> _options = {
"width": width,
"height": height,
"gl": three3dRender.gl,
"antialias": true,
"canvas": three3dRender.element,
};
if (!kIsWeb && Platform.isAndroid) {
_options['logarithmicDepthBuffer'] = true;
}
renderer = WebGLRenderer(_options);
renderer!.setPixelRatio(dpr);
renderer!.setSize(width, height, false);
renderer!.shadowMap.enabled = true;
renderer!.shadowMap.type = THREE.PCFShadowMap;
if (!kIsWeb) {
WebGLRenderTargetOptions pars = WebGLRenderTargetOptions({"format": RGBAFormat, "samples": 8});
renderTarget = WebGLRenderTarget((width * dpr).toInt(), (height * dpr).toInt(), pars);
renderer!.setRenderTarget(renderTarget);
sourceTexture = renderer!.getRenderTargetGLTexture(renderTarget!);
} else {
renderTarget = null;
}
}
void initScene() async {
await initPage();
initRenderer();
initOimoPhysics();
animate();
}
Future<void> initPlatformState() async {
width = screenSize!.width;
height = screenSize!.height;
three3dRender = FlutterGlPlugin();
Map<String, dynamic> _options = {
"antialias": true,
"alpha": true,
"width": width.toInt(),
"height": height.toInt(),
"dpr": dpr,
'precision': 'highp'
};
await three3dRender.initialize(options: _options);
setState(() {});
// 等待web dom就绪
Future.delayed(const Duration(milliseconds: 100), () async {
await three3dRender.prepareContext();
initScene();
});
}
Widget threeDart() {
return Builder(builder: (BuildContext context) {
initSize(context);
return Container(
width: screenSize!.width,
height: screenSize!.height,
color: Theme.of(context).canvasColor,
child: DomLikeListenable(
key: _globalKey,
builder: (BuildContext context) {
FocusScope.of(context).requestFocus(node);
return Container(
width: width,
height: height,
color: Theme.of(context).canvasColor,
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();
}
}),
);
},
),
);
});
}
[@override](/user/override)
Widget build(BuildContext context) {
return SizedBox(
height: double.infinity,
width: double.infinity,
child: Stack(
children: [
threeDart(),
],
),
);
}
}
更多关于Flutter物理引擎插件oimo_physics的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter物理引擎插件oimo_physics的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
当然,以下是一个关于如何在Flutter项目中使用oimo_physics
插件的简单示例。这个插件提供了基于Oimo.js的3D物理引擎功能,适用于Flutter应用。
首先,确保你已经在pubspec.yaml
文件中添加了oimo_physics
依赖:
dependencies:
flutter:
sdk: flutter
oimo_physics: ^0.10.0 # 请检查最新版本号
然后运行flutter pub get
来安装依赖。
接下来,我们可以创建一个简单的Flutter应用来展示如何使用oimo_physics
。这个示例将展示如何创建一个物理世界,添加一个立方体并应用重力。
主文件 main.dart
import 'package:flutter/material.dart';
import 'package:oimo_physics/oimo_physics.dart';
import 'package:vector_math/vector_math_64.dart' as vm;
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Oimo Physics Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: OimoPhysicsDemo(),
);
}
}
class OimoPhysicsDemo extends StatefulWidget {
@override
_OimoPhysicsDemoState createState() => _OimoPhysicsDemoState();
}
class _OimoPhysicsDemoState extends State<OimoPhysicsDemo> with SingleTickerProviderStateMixin {
late OimoWorld world;
late OimoBody cube;
late AnimationController controller;
@override
void initState() {
super.initState();
// 创建物理世界
world = OimoWorld();
world.gravity.setValues(0.0, -9.8, 0.0);
// 创建立方体形状
OimoBoxShape boxShape = OimoBoxShape(halfExtents: vm.Vector3(0.5, 0.5, 0.5));
// 创建立方体材质
OimoMaterial material = OimoMaterial();
material.density = 1.0;
material.friction = 0.5;
material.restitution = 0.3;
// 创建立方体刚体
cube = OimoBody(world: world);
cube.addShape(boxShape, vm.Transform());
cube.setMaterial(material);
cube.position.setValues(0.0, 10.0, 0.0);
// 动画控制器用于更新物理世界
controller = AnimationController(
duration: const Duration(seconds: 10),
vsync: this,
)..repeat(reverse: true);
}
@override
void dispose() {
controller.dispose();
world.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Oimo Physics Demo'),
),
body: CustomPaint(
size: Size(double.infinity, double.infinity),
painter: OimoPainter(world: world, bodies: [cube]),
),
);
}
@override
void didUpdateWidget(covariant OimoPhysicsDemo oldDelegate) {
super.didUpdateWidget(oldDelegate);
// 在每一帧更新物理世界
controller.addListener(() {
world.step();
setState(() {}); // 触发UI重建
});
}
}
class OimoPainter extends CustomPainter {
final OimoWorld world;
final List<OimoBody> bodies;
OimoPainter({required this.world, required this.bodies});
@override
void paint(Canvas canvas, Size size) {
final Paint paint = Paint()
..color = Colors.blue
..style = PaintingStyle.fill;
for (var body in bodies) {
final vm.Vector3 position = body.position;
final double scale = 100.0; // 缩放比例,用于在屏幕上显示
canvas.drawRect(
Rect.fromLTWH(
position.x * scale - 0.5 * scale,
position.y * scale - 0.5 * scale,
scale,
scale,
),
paint,
);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true; // 每一帧都重绘
}
}
解释
- 创建物理世界:在
initState
方法中,我们创建了一个OimoWorld
实例并设置了重力。 - 创建立方体:我们创建了一个立方体形状(
OimoBoxShape
),一个材质(OimoMaterial
),并将它们添加到一个刚体(OimoBody
)中。 - 动画控制器:我们使用
AnimationController
来定时更新物理世界。在每一帧中,我们调用world.step()
来模拟物理过程,并通过setState()
触发UI重建。 - 自定义绘制:
OimoPainter
类用于在Canvas上绘制立方体。我们遍历所有的刚体,并将它们的位置转换为屏幕坐标,然后绘制一个矩形来表示立方体。
请注意,这个示例非常基础,仅用于展示如何使用oimo_physics
插件。在实际应用中,你可能需要更复杂的绘制逻辑和物理模拟。