Flutter物理引擎插件oimo_physics的使用

Flutter物理引擎插件oimo_physics的使用

Pub Version analysis License: BSD

一个为three_dart和three_dart_jsm设计的Flutter插件,允许用户在其3D项目中添加一个简单的物理引擎。

此插件是由Saharan创建的oimo.js的dart版本。

使用

入门

要开始使用,请将oimo_physicsthree_dartthree_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

1 回复

更多关于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; // 每一帧都重绘
  }
}

解释

  1. 创建物理世界:在initState方法中,我们创建了一个OimoWorld实例并设置了重力。
  2. 创建立方体:我们创建了一个立方体形状(OimoBoxShape),一个材质(OimoMaterial),并将它们添加到一个刚体(OimoBody)中。
  3. 动画控制器:我们使用AnimationController来定时更新物理世界。在每一帧中,我们调用world.step()来模拟物理过程,并通过setState()触发UI重建。
  4. 自定义绘制OimoPainter类用于在Canvas上绘制立方体。我们遍历所有的刚体,并将它们的位置转换为屏幕坐标,然后绘制一个矩形来表示立方体。

请注意,这个示例非常基础,仅用于展示如何使用oimo_physics插件。在实际应用中,你可能需要更复杂的绘制逻辑和物理模拟。

回到顶部