diff --git a/example/lib/camera_matrix_overlay.dart b/example/lib/camera_matrix_overlay.dart new file mode 100644 index 00000000..ab86c354 --- /dev/null +++ b/example/lib/camera_matrix_overlay.dart @@ -0,0 +1,51 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_filament/filament_controller.dart'; + +class CameraMatrixOverlay extends StatefulWidget { + final FilamentController controller; + + CameraMatrixOverlay({super.key, required this.controller}); + + @override + State createState() => _CameraMatrixOverlayState(); +} + +class _CameraMatrixOverlayState extends State { + Timer? _cameraTimer; + String? _cameraPosition; + String? _cameraRotation; + + @override + void initState() { + super.initState(); + + _cameraTimer = + Timer.periodic(const Duration(milliseconds: 50), (timer) async { + var cameraPosition = await widget.controller.getCameraPosition(); + var cameraRotation = await widget.controller.getCameraRotation(); + _cameraPosition = cameraPosition.toString(); + _cameraRotation = cameraRotation.toString(); + setState(() {}); + }); + } + + @override + void dispose() { + super.dispose(); + _cameraTimer?.cancel(); + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), + borderRadius: BorderRadius.circular(29)), + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + child: Text("Camera position : $_cameraPosition $_cameraRotation", + style: const TextStyle(color: Colors.white, fontSize: 12))); + } +} diff --git a/example/lib/camera_menu.dart b/example/lib/camera_menu.dart new file mode 100644 index 00000000..29327f43 --- /dev/null +++ b/example/lib/camera_menu.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_filament/filament_controller.dart'; + +class CameraMenu extends StatefulWidget { + final FilamentController? controller; + + CameraMenu({super.key, required this.controller}); + + @override + State createState() { + return _CameraMenuState(); + } +} + +class _CameraMenuState extends State { + bool _frustumCulling = true; + + final FocusNode _buttonFocusNode = FocusNode(debugLabel: 'Camera Menu'); + + @override + void didUpdateWidget(CameraMenu oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.controller != oldWidget.controller) { + setState(() {}); + } + } + + @override + Widget build(BuildContext context) { + return MenuAnchor( + childFocusNode: _buttonFocusNode, + menuChildren: [ + MenuItemButton( + child: Text("Camera"), + onPressed: () {}, + ), + ], + builder: + (BuildContext context, MenuController controller, Widget? child) { + return Align( + alignment: Alignment.bottomLeft, + child: TextButton( + onPressed: widget.controller?.hasViewer != true + ? null + : () { + if (controller.isOpen) { + controller.close(); + } else { + controller.open(); + } + }, + child: const Text("Camera"), + )); + }, + ); + } +} diff --git a/example/lib/controller_menu.dart b/example/lib/controller_menu.dart new file mode 100644 index 00000000..414819c2 --- /dev/null +++ b/example/lib/controller_menu.dart @@ -0,0 +1,95 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_filament/filament_controller.dart'; +import 'package:flutter_filament/filament_controller_ffi.dart'; + +class ControllerMenu extends StatefulWidget { + final void Function(FilamentController controller) onControllerCreated; + final void Function() onControllerDestroyed; + + ControllerMenu( + {required this.onControllerCreated, required this.onControllerDestroyed}); + + @override + State createState() => _ControllerMenuState(); +} + +class _ControllerMenuState extends State { + FilamentController? _filamentController; + final FocusNode _buttonFocusNode = FocusNode(debugLabel: 'Camera Menu'); + + void _createController({String? uberArchivePath}) { + _filamentController = + FilamentControllerFFI(uberArchivePath: uberArchivePath); + widget.onControllerCreated(_filamentController!); + } + + @override + Widget build(BuildContext context) { + var items = []; + if (_filamentController?.hasViewer != true) { + items.addAll([ + MenuItemButton( + child: const Text("create viewer"), + onPressed: _filamentController == null + ? null + : () { + _filamentController!.createViewer(); + }, + ), + MenuItemButton( + child: const Text("create FilamentController (default ubershader)"), + onPressed: () { + _createController(); + }, + ), + MenuItemButton( + child: const Text( + "create FilamentController (custom ubershader - lit opaque only)"), + onPressed: () { + _createController( + uberArchivePath: Platform.isWindows + ? "assets/lit_opaque_32.uberz" + : Platform.isMacOS + ? "assets/lit_opaque_43.uberz" + : Platform.isIOS + ? "assets/lit_opaque_43.uberz" + : "assets/lit_opaque_43_gles.uberz"); + }, + ) + ]); + } else { + items.addAll([ + MenuItemButton( + child: const Text("destroy viewer"), + onPressed: () { + _filamentController!.destroy(); + _filamentController = null; + widget.onControllerDestroyed(); + setState(() {}); + }, + ) + ]); + } + return MenuAnchor( + childFocusNode: _buttonFocusNode, + menuChildren: items, + builder: + (BuildContext context, MenuController controller, Widget? child) { + return Align( + alignment: Alignment.bottomLeft, + child: TextButton( + onPressed: () { + if (controller.isOpen) { + controller.close(); + } else { + controller.open(); + } + }, + child: const Text("Controller / Viewer"), + )); + }); + } +} diff --git a/example/lib/example_viewport.dart b/example/lib/example_viewport.dart new file mode 100644 index 00000000..4cfe2ee5 --- /dev/null +++ b/example/lib/example_viewport.dart @@ -0,0 +1,26 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_filament/filament_controller.dart'; +import 'package:flutter_filament/widgets/filament_gesture_detector.dart'; +import 'package:flutter_filament/widgets/filament_widget.dart'; + +class ExampleViewport extends StatelessWidget { + final FilamentController? controller; + final EdgeInsets padding; + + const ExampleViewport( + {super.key, required this.controller, required this.padding}); + + @override + Widget build(BuildContext context) { + return controller != null + ? Padding( + padding: padding, + child: FilamentGestureDetector( + showControlOverlay: true, + controller: controller!, + child: FilamentWidget( + controller: controller!, + ))) + : Container(); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index b87071ae..82f41134 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -2,6 +2,10 @@ import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; import 'package:flutter/material.dart'; +import 'package:flutter_filament_example/controller_menu.dart'; +import 'package:flutter_filament_example/example_viewport.dart'; +import 'package:flutter_filament_example/picker_result_widget.dart'; +import 'package:flutter_filament_example/scene_menu.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:flutter_filament/animations/animation_data.dart'; @@ -14,6 +18,8 @@ import 'package:flutter_filament/animations/animation_builder.dart'; import 'package:flutter_filament/widgets/filament_gesture_detector.dart'; import 'package:flutter_filament/widgets/filament_widget.dart'; +import 'camera_menu.dart'; + void main() async { runApp(const MyApp()); } @@ -29,6 +35,7 @@ class _MyAppState extends State with SingleTickerProviderStateMixin { @override Widget build(BuildContext context) { return MaterialApp( + theme: ThemeData(useMaterial3: true), // showPerformanceOverlay: true, home: Scaffold(body: ExampleWidget())); } @@ -41,13 +48,11 @@ class ExampleWidget extends StatefulWidget { } } +enum MenuType { controller, assets, camera, misc } + class _ExampleWidgetState extends State { FilamentController? _filamentController; - Timer? _cameraTimer; - String? _cameraPosition; - String? _cameraRotation; - FilamentEntity? _shapes; FilamentEntity? _flightHelmet; FilamentEntity? _buster; @@ -55,9 +60,6 @@ class _ExampleWidgetState extends State { List? _animations; - StreamSubscription? _pickResultListener; - String? picked; - final weights = List.filled(255, 0.0); bool _loop = false; @@ -70,13 +72,6 @@ class _ExampleWidgetState extends State { bool _postProcessing = true; bool _coneHidden = false; - bool _frustumCulling = true; - - @override - void dispose() { - super.dispose(); - _pickResultListener?.cancel(); - } Widget _item(void Function() onTap, String text) { return GestureDetector( @@ -87,364 +82,271 @@ class _ExampleWidgetState extends State { }, child: Container( color: Colors.transparent, - padding: EdgeInsets.symmetric(vertical: 10, horizontal: 10), + padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 10), child: Text(text))); } - void _createController({String? uberArchivePath}) { - _cameraTimer?.cancel(); - _filamentController = - FilamentControllerFFI(uberArchivePath: uberArchivePath); - _filamentController!.pickResult.listen((entityId) { - setState(() { - picked = _filamentController!.getNameForEntity(entityId!); - }); - }); - } - - void _createViewer() { - _filamentController!.createViewer(); - setState(() { - _hasViewer = true; - }); - - _cameraTimer = - Timer.periodic(const Duration(milliseconds: 50), (timer) async { - var cameraPosition = await _filamentController!.getCameraPosition(); - var cameraRotation = await _filamentController!.getCameraRotation(); - _cameraPosition = cameraPosition.toString(); - _cameraRotation = cameraRotation.toString(); - setState(() {}); - }); - } + final FocusNode _buttonFocusNode = FocusNode(debugLabel: 'Menu Button'); @override Widget build(BuildContext context) { - var children = []; - - if (_filamentController == null) { - children.addAll([ - _item(() { - _createController(); - }, "create FilamentController (default ubershader)"), - _item(() { - _createController( - uberArchivePath: Platform.isWindows - ? "assets/lit_opaque_32.uberz" - : Platform.isMacOS - ? "assets/lit_opaque_43.uberz" - : Platform.isIOS - ? "assets/lit_opaque_43.uberz" - : "assets/lit_opaque_43_gles.uberz"); - }, "create FilamentController (custom ubershader - lit opaque only)"), - ]); - } else { - if (!_hasViewer) { - children.addAll([_item(_createViewer, "create FilamentViewer")]); - } else { - children.addAll([ - _item(() { - _cameraTimer?.cancel(); - _filamentController!.destroy(); - _filamentController = null; - setState(() { - _hasViewer = true; - }); - }, "destroy viewer/texture"), - _item(() { - _filamentController!.render(); - }, "render"), - _item(() { - setState(() { - _rendering = !_rendering; - _filamentController!.setRendering(_rendering); - }); - }, "Rendering: $_rendering"), - _item(() { - setState(() { - _framerate = _framerate == 60 ? 30 : 60; - _filamentController!.setFrameRate(_framerate); - }); - }, "$_framerate fps"), - _item(() { - _filamentController!.setBackgroundColor(Color(0xFF73C9FA)); - }, "set background color"), - _item(() { - _filamentController!.setBackgroundImage('assets/background.ktx'); - }, "load background image"), - _item(() { - _filamentController! - .setBackgroundImage('assets/background.ktx', fillHeight: true); - }, "load background image (fill height)"), - _item(() { - _filamentController! - .loadSkybox('assets/default_env/default_env_skybox.ktx'); - }, 'load skybox'), - _item(() { - _filamentController! - .loadIbl('assets/default_env/default_env_ibl.ktx'); - }, 'load IBL'), - _item(() async { - _light = await _filamentController! - .addLight(1, 6500, 150000, 0, 1, 0, 0, -1, 0, true); - }, "add directional light"), - _item(() async { - await _filamentController!.clearLights(); - }, "clear lights"), - _item(() { - setState(() { - _postProcessing = !_postProcessing; - }); - _filamentController!.setPostProcessing(_postProcessing); - }, "${_postProcessing ? "Disable" : "Enable"} postprocessing"), - _item( - () { - _filamentController!.removeSkybox(); - }, - 'remove skybox', - ), - _item(() async { - _shapes = - await _filamentController!.loadGlb('assets/shapes/shapes.glb'); - _animations = - await _filamentController!.getAnimationNames(_shapes!); - setState(() {}); - }, 'load shapes GLB'), - _item(() async { - _animations = await _filamentController!.setCamera(_shapes!, null); - setState(() {}); - }, 'set camera to first camera in shapes GLB'), - _item(() async { - if (_coneHidden) { - _filamentController!.reveal(_shapes!, "Cone"); - } else { - _filamentController!.hide(_shapes!, "Cone"); - } - setState(() { - _coneHidden = !_coneHidden; - }); - }, _coneHidden ? 'show cone' : 'hide cone'), - _item(() async { - if (_shapes != null) { - _filamentController!.removeAsset(_shapes!); - } - _shapes = await _filamentController! - .loadGltf('assets/shapes/shapes.gltf', 'assets/shapes'); - }, 'load shapes GLTF'), - _item(() async { - _filamentController!.transformToUnitCube(_shapes!); - }, 'transform to unit cube'), - _item(() async { - _filamentController!.setPosition(_shapes!, 1.0, 1.0, -1.0); - }, 'set shapes position to 1, 1, -1'), - _item(() async { - _filamentController!.setCameraPosition(1.0, 1.0, -1.0); - }, 'move camera to 1, 1, -1'), - _item(() async { - var frameData = Float32List.fromList( - List.generate(120, (i) => i / 120).expand((x) { - var vals = List.filled(7, x); - vals[3] = 1.0; - // vals[4] = 0; - vals[5] = 0; - vals[6] = 0; - return vals; - }).toList()); - - _filamentController!.setBoneAnimation( - _shapes!, - BoneAnimationData( - "Bone.001", ["Cube.001"], frameData, 1000.0 / 60.0)); - // , - // "Bone.001", - // "Cube.001", - // BoneTransform([Vec3(x: 0, y: 0.0, z: 0.0)], - // [Quaternion(x: 1, y: 1, z: 1, w: 1)])); - }, 'construct bone animation'), - _item(() async { - _filamentController!.removeAsset(_shapes!); - _shapes = null; - }, 'remove shapes'), - _item(() async { - _filamentController!.clearAssets(); - _shapes = null; - }, 'clear all assets'), - _item(() async { - var names = await _filamentController! - .getMorphTargetNames(_shapes!, "Cylinder"); - await showDialog( - context: context, - builder: (ctx) { - return Container( - height: 100, - width: 100, - color: Colors.white, - child: Text(names.join(","))); - }); - }, "show morph target names for Cylinder"), - _item(() { - _filamentController!.setMorphTargetWeights( - _shapes!, "Cylinder", List.filled(4, 1.0)); - }, "set Cylinder morph weights to 1"), - _item(() { - _filamentController!.setMorphTargetWeights( - _shapes!, "Cylinder", List.filled(4, 0.0)); - }, "set Cylinder morph weights to 0.0"), - _item(() async { - var morphs = await _filamentController! - .getMorphTargetNames(_shapes!, "Cylinder"); - final animation = AnimationBuilder( - availableMorphs: morphs, - framerate: 30, - meshName: "Cylinder") - .setDuration(4) - .setMorphTargets(["Key 1", "Key 2"]) - .interpolateMorphWeights(0, 4, 0, 1) - .build(); - _filamentController!.setMorphAnimationData(_shapes!, animation); - }, "animate cylinder morph weights #1 and #2"), - _item(() async { - var morphs = await _filamentController! - .getMorphTargetNames(_shapes!, "Cylinder"); - final animation = AnimationBuilder( - availableMorphs: morphs, - framerate: 30, - meshName: "Cylinder") - .setDuration(4) - .setMorphTargets(["Key 3", "Key 4"]) - .interpolateMorphWeights(0, 4, 0, 1) - .build(); - _filamentController!.setMorphAnimationData(_shapes!, animation); - }, "animate cylinder morph weights #3 and #4"), - _item(() async { - var morphs = await _filamentController! - .getMorphTargetNames(_shapes!, "Cube"); - final animation = AnimationBuilder( - availableMorphs: morphs, framerate: 30, meshName: "Cube") - .setDuration(4) - .setMorphTargets(["Key 1", "Key 2"]) - .interpolateMorphWeights(0, 4, 0, 1) - .build(); - _filamentController!.setMorphAnimationData(_shapes!, animation); - }, "animate shapes morph weights #1 and #2"), - _item(() { - _filamentController! - .setMaterialColor(_shapes!, "Cone", 0, Colors.purple); - }, "set cone material color to purple"), - _item(() { - _loop = !_loop; - setState(() {}); - }, "toggle animation looping ${_loop ? "OFF" : "ON"}"), - _item(() { - setState(() { - _viewportMargin = _viewportMargin == EdgeInsets.zero - ? EdgeInsets.all(50) - : EdgeInsets.zero; - }); - }, "resize"), - _item(() async { - await Permission.microphone.request(); - }, "request permissions (tests inactive->resume)") - ]); - if (_animations != null) { - children.addAll(_animations!.map((a) => _item(() { - _filamentController!.playAnimation( - _shapes!, _animations!.indexOf(a), - replaceActive: true, crossfade: 0.5, loop: _loop); - }, "play animation ${_animations!.indexOf(a)} (replace/fade)"))); - children.addAll(_animations!.map((a) => _item(() { - _filamentController!.playAnimation( - _shapes!, _animations!.indexOf(a), - replaceActive: false, loop: _loop); - }, "play animation ${_animations!.indexOf(a)} (noreplace)"))); - } - - children.add(_item(() { - _filamentController!.setToneMapping(ToneMapper.LINEAR); - }, "Set tone mapping to linear")); - - children.add(_item(() { - _filamentController!.moveCameraToAsset(_shapes!); - }, "Move camera to asset")); - - children.add(_item(() { - setState(() { - _frustumCulling = !_frustumCulling; - }); - _filamentController!.setViewFrustumCulling(_frustumCulling); - }, "${_frustumCulling ? "Disable" : "Enable"} frustum culling")); - - children.addAll([ - _item(() async { - await Permission.microphone.request(); - }, "request permissions (tests inactive->resume)"), - _item(() async { - if (_buster != null) { - await _filamentController!.removeAsset(_buster!); - } - _buster = await (_filamentController as FilamentControllerFFI) - .loadGltf("assets/BusterDrone/scene.gltf", "assets/BusterDrone", - force: true); - await _filamentController!.playAnimation(_buster!, 0, loop: true); - }, "load buster") - ]); - } - - if (_animations != null) { - children.addAll(_animations!.map((a) => _item(() { - _filamentController!.playAnimation( - _shapes!, _animations!.indexOf(a), - replaceActive: true, crossfade: 0.5, loop: _loop); - }, "play animation ${_animations!.indexOf(a)} (replace/fade)"))); - children.addAll(_animations!.map((a) => _item(() { - _filamentController!.playAnimation( - _shapes!, _animations!.indexOf(a), - replaceActive: false, loop: _loop); - }, "play animation ${_animations!.indexOf(a)} (noreplace)"))); - } - } return Stack(children: [ - _filamentController != null - ? Positioned.fill( - child: Padding( - padding: _viewportMargin, - child: FilamentGestureDetector( - showControlOverlay: true, - controller: _filamentController!, - child: FilamentWidget( - controller: _filamentController!, - )))) - : Container(), - Positioned( - right: 50, - top: 50, - child: Text(picked ?? "", - style: const TextStyle(color: Colors.green, fontSize: 24))), - _cameraTimer == null - ? Container() - : Positioned( - top: 10, - left: 10, - child: Container( - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.5), - borderRadius: BorderRadius.circular(29)), - padding: - const EdgeInsets.symmetric(horizontal: 10, vertical: 5), - child: Text( - "Camera position : $_cameraPosition $_cameraRotation", - style: - const TextStyle(color: Colors.white, fontSize: 12)))), + Positioned.fill( + child: ExampleViewport( + controller: _filamentController, + padding: _viewportMargin, + ), + ), Align( alignment: Alignment.bottomCenter, - child: OrientationBuilder(builder: (ctx, orientation) { - return Container( - alignment: Alignment.bottomCenter, - height: orientation == Orientation.landscape ? 100 : 200, - color: Colors.white.withOpacity(0.75), - child: SingleChildScrollView(child: Wrap(children: children))); - })) + child: Container( + height: 30, + color: Colors.white, + child: Row(children: [ + ControllerMenu( + onControllerDestroyed: () {}, + onControllerCreated: (controller) { + setState(() { + _filamentController = controller; + }); + }), + SceneMenu( + controller: _filamentController, + ) + ]))), ]); + +// _item(() { + +// _item(() async { +// _light = await _filamentController! +// .addLight(1, 6500, 150000, 0, 1, 0, 0, -1, 0, true); +// }, "add directional light"), +// _item(() async { +// await _filamentController!.clearLights(); +// }, "clear lights"), +// _item(() { +// setState(() { +// _postProcessing = !_postProcessing; +// }); +// _filamentController!.setPostProcessing(_postProcessing); +// }, "${_postProcessing ? "Disable" : "Enable"} postprocessing"), + +// _item(() async { +// _animations = await _filamentController!.setCamera(_shapes!, null); +// setState(() {}); +// }, 'set camera to first camera in shapes GLB'), +// _item(() async { +// if (_coneHidden) { +// _filamentController!.reveal(_shapes!, "Cone"); +// } else { +// _filamentController!.hide(_shapes!, "Cone"); +// } +// setState(() { +// _coneHidden = !_coneHidden; +// }); +// }, _coneHidden ? 'show cone' : 'hide cone'), +// _item(() async { +// if (_shapes != null) { +// _filamentController!.removeAsset(_shapes!); +// } +// _shapes = await _filamentController! +// .loadGltf('assets/shapes/shapes.gltf', 'assets/shapes'); +// }, 'load shapes GLTF'), +// _item(() async { +// _filamentController!.transformToUnitCube(_shapes!); +// }, 'transform to unit cube'), +// _item(() async { +// _filamentController!.setPosition(_shapes!, 1.0, 1.0, -1.0); +// }, 'set shapes position to 1, 1, -1'), +// _item(() async { +// _filamentController!.setCameraPosition(1.0, 1.0, -1.0); +// }, 'move camera to 1, 1, -1'), +// _item(() async { +// var frameData = Float32List.fromList( +// List.generate(120, (i) => i / 120).expand((x) { +// var vals = List.filled(7, x); +// vals[3] = 1.0; +// // vals[4] = 0; +// vals[5] = 0; +// vals[6] = 0; +// return vals; +// }).toList()); + +// _filamentController!.setBoneAnimation( +// _shapes!, +// BoneAnimationData( +// "Bone.001", ["Cube.001"], frameData, 1000.0 / 60.0)); +// // , +// // "Bone.001", +// // "Cube.001", +// // BoneTransform([Vec3(x: 0, y: 0.0, z: 0.0)], +// // [Quaternion(x: 1, y: 1, z: 1, w: 1)])); +// }, 'construct bone animation'), +// _item(() async { +// _filamentController!.removeAsset(_shapes!); +// _shapes = null; +// }, 'remove shapes'), +// _item(() async { +// _filamentController!.clearAssets(); +// _shapes = null; +// }, 'clear all assets'), +// _item(() async { +// var names = await _filamentController! +// .getMorphTargetNames(_shapes!, "Cylinder"); +// await showDialog( +// context: context, +// builder: (ctx) { +// return Container( +// height: 100, +// width: 100, +// color: Colors.white, +// child: Text(names.join(","))); +// }); +// }, "show morph target names for Cylinder"), +// _item(() { +// _filamentController!.setMorphTargetWeights( +// _shapes!, "Cylinder", List.filled(4, 1.0)); +// }, "set Cylinder morph weights to 1"), +// _item(() { +// _filamentController!.setMorphTargetWeights( +// _shapes!, "Cylinder", List.filled(4, 0.0)); +// }, "set Cylinder morph weights to 0.0"), +// _item(() async { +// var morphs = await _filamentController! +// .getMorphTargetNames(_shapes!, "Cylinder"); +// final animation = AnimationBuilder( +// availableMorphs: morphs, +// framerate: 30, +// meshName: "Cylinder") +// .setDuration(4) +// .setMorphTargets(["Key 1", "Key 2"]) +// .interpolateMorphWeights(0, 4, 0, 1) +// .build(); +// _filamentController!.setMorphAnimationData(_shapes!, animation); +// }, "animate cylinder morph weights #1 and #2"), +// _item(() async { +// var morphs = await _filamentController! +// .getMorphTargetNames(_shapes!, "Cylinder"); +// final animation = AnimationBuilder( +// availableMorphs: morphs, +// framerate: 30, +// meshName: "Cylinder") +// .setDuration(4) +// .setMorphTargets(["Key 3", "Key 4"]) +// .interpolateMorphWeights(0, 4, 0, 1) +// .build(); +// _filamentController!.setMorphAnimationData(_shapes!, animation); +// }, "animate cylinder morph weights #3 and #4"), +// _item(() async { +// var morphs = await _filamentController! +// .getMorphTargetNames(_shapes!, "Cube"); +// final animation = AnimationBuilder( +// availableMorphs: morphs, framerate: 30, meshName: "Cube") +// .setDuration(4) +// .setMorphTargets(["Key 1", "Key 2"]) +// .interpolateMorphWeights(0, 4, 0, 1) +// .build(); +// _filamentController!.setMorphAnimationData(_shapes!, animation); +// }, "animate shapes morph weights #1 and #2"), +// _item(() { +// _filamentController! +// .setMaterialColor(_shapes!, "Cone", 0, Colors.purple); +// }, "set cone material color to purple"), +// _item(() { +// _loop = !_loop; +// setState(() {}); +// }, "toggle animation looping ${_loop ? "OFF" : "ON"}"), +// _item(() { +// setState(() { +// _viewportMargin = _viewportMargin == EdgeInsets.zero +// ? EdgeInsets.all(50) +// : EdgeInsets.zero; +// }); +// }, "resize"), +// _item(() async { +// await Permission.microphone.request(); +// }, "request permissions (tests inactive->resume)") +// ]); +// if (_animations != null) { +// children.addAll(_animations!.map((a) => _item(() { +// _filamentController!.playAnimation( +// _shapes!, _animations!.indexOf(a), +// replaceActive: true, crossfade: 0.5, loop: _loop); +// }, "play animation ${_animations!.indexOf(a)} (replace/fade)"))); +// children.addAll(_animations!.map((a) => _item(() { +// _filamentController!.playAnimation( +// _shapes!, _animations!.indexOf(a), +// replaceActive: false, loop: _loop); +// }, "play animation ${_animations!.indexOf(a)} (noreplace)"))); +// } + +// children.add(_item(() { +// _filamentController!.setToneMapping(ToneMapper.LINEAR); +// }, "Set tone mapping to linear")); + +// children.add(_item(() { +// _filamentController!.moveCameraToAsset(_shapes!); +// }, "Move camera to shapes asset")); + +// children.add(_item(() { +// setState(() { +// _frustumCulling = !_frustumCulling; +// }); +// _filamentController!.setViewFrustumCulling(_frustumCulling); +// }, "${_frustumCulling ? "Disable" : "Enable"} frustum culling")); + +// children.addAll([ +// _item(() async { +// await Permission.microphone.request(); +// }, "request permissions (tests inactive->resume)"), +// _item(() async { +// if (_buster != null) { +// await _filamentController!.removeAsset(_buster!); +// } +// _buster = await (_filamentController as FilamentControllerFFI) +// .loadGltf("assets/BusterDrone/scene.gltf", "assets/BusterDrone", +// force: true); +// await _filamentController!.playAnimation(_buster!, 0, loop: true); +// }, "load buster") +// ]); +// } + +// if (_animations != null) { +// children.addAll(_animations!.map((a) => _item(() { +// _filamentController!.playAnimation( +// _shapes!, _animations!.indexOf(a), +// replaceActive: true, crossfade: 0.5, loop: _loop); +// }, "play animation ${_animations!.indexOf(a)} (replace/fade)"))); +// children.addAll(_animations!.map((a) => _item(() { +// _filamentController!.playAnimation( +// _shapes!, _animations!.indexOf(a), +// replaceActive: false, loop: _loop); +// }, "play animation ${_animations!.indexOf(a)} (noreplace)"))); +// } +// } + // return Stack(children: [ + // Viewport(_filamentController, _viewportPadding), + // Positioned( + // right: 50, + // top: 50, + // child: PickerResultWidget(controller: _filamentController!)), + // _cameraTimer == null + // ? Container() + // : Positioned( + // top: 10, + // left: 10, + // child: , + // Align( + // alignment: Alignment.bottomCenter, + // child: OrientationBuilder(builder: (ctx, orientation) { + // return Container( + // alignment: Alignment.bottomCenter, + // height: orientation == Orientation.landscape ? 100 : 200, + // color: Colors.white.withOpacity(0.75), + // child: SingleChildScrollView(child: Wrap(children: children))); + // })) + // ]); } } diff --git a/example/lib/picker_result_widget.dart b/example/lib/picker_result_widget.dart new file mode 100644 index 00000000..f509ce77 --- /dev/null +++ b/example/lib/picker_result_widget.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_filament/filament_controller.dart'; +import 'package:flutter_filament/generated_bindings.dart'; + +class PickerResultWidget extends StatelessWidget { + final FilamentController controller; + + const PickerResultWidget({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: controller.pickResult.map((FilamentEntity? entityId) { + if (entityId == null) { + return null; + } + return controller.getNameForEntity(entityId); + }), + builder: (ctx, snapshot) => snapshot.data == null + ? Container() + : Text(snapshot.data!, + style: const TextStyle(color: Colors.green, fontSize: 24))); + } +} diff --git a/example/lib/scene_menu.dart b/example/lib/scene_menu.dart new file mode 100644 index 00000000..7691013a --- /dev/null +++ b/example/lib/scene_menu.dart @@ -0,0 +1,157 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_filament/filament_controller.dart'; + +class SceneMenu extends StatefulWidget { + final FilamentController? controller; + + SceneMenu({super.key, required this.controller}); + + @override + State createState() { + return _SceneMenuState(); + } +} + +class _SceneMenuState extends State { + FilamentEntity? _shapes; + List? _animations; + bool _hasSkybox = false; + final FocusNode _buttonFocusNode = FocusNode(debugLabel: 'Camera Menu'); + + @override + void didUpdateWidget(SceneMenu oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.controller != oldWidget.controller || + widget.controller!.hasViewer != oldWidget.controller!.hasViewer) { + setState(() {}); + } + } + + List _assetMenu() { + return [ + MenuItemButton( + onPressed: () async { + if (_shapes == null) { + _shapes = + await widget.controller!.loadGlb('assets/shapes/shapes.glb'); + _animations = + await widget.controller!.getAnimationNames(_shapes!); + } else { + await widget.controller!.removeAsset(_shapes!); + _shapes = null; + _animations = null; + } + setState(() {}); + }, + child: + Text(_shapes == null ? 'load shapes GLB' : 'remove shapes GLB')), + MenuItemButton( + onPressed: () { + widget.controller!.setBackgroundColor(const Color(0xFF73C9FA)); + }, + child: const Text("set background color")), + MenuItemButton( + onPressed: () { + widget.controller!.setBackgroundImage('assets/background.ktx'); + }, + child: const Text("load background image")), + MenuItemButton( + onPressed: () { + widget.controller! + .setBackgroundImage('assets/background.ktx', fillHeight: true); + }, + child: const Text("load background image (fill height)")), + MenuItemButton( + onPressed: () { + if (_hasSkybox) { + widget.controller!.removeSkybox(); + } else { + widget.controller! + .loadSkybox('assets/default_env/default_env_skybox.ktx'); + } + _hasSkybox = !_hasSkybox; + setState(() {}); + }, + child: Text(_hasSkybox ? 'remove skybox' : 'load skybox')), + MenuItemButton( + onPressed: () { + widget.controller! + .loadIbl('assets/default_env/default_env_ibl.ktx'); + }, + child: const Text('load IBL')) + ]; + } + + bool _rendering = false; + int _framerate = 60; + + List _renderingMenu() { + return [ + MenuItemButton( + onPressed: () { + widget.controller!.render(); + }, + child: const Text("Render single frame"), + ), + MenuItemButton( + onPressed: () { + _rendering = !_rendering; + widget.controller!.setRendering(_rendering); + }, + child: Text("Set continuous rendering to ${!_rendering}"), + ), + MenuItemButton( + onPressed: () { + _framerate = _framerate == 60 ? 30 : 60; + widget.controller!.setFrameRate(_framerate); + }, + child: const Text("Toggle framerate (currently ) "), + ), + ]; + } + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: + widget.controller?.hasViewer ?? ValueNotifier(false), + builder: (BuildContext ctx, bool hasViewer, Widget? child) { + return MenuAnchor( + childFocusNode: _buttonFocusNode, + menuChildren: [ + SubmenuButton( + menuChildren: _renderingMenu(), + child: const Text("Rendering"), + ), + SubmenuButton( + menuChildren: _assetMenu(), + child: const Text("Assets"), + ), + const SubmenuButton( + menuChildren: [], + child: Text("Camera"), + ), + ], + builder: (BuildContext context, MenuController controller, + Widget? child) { + return Align( + alignment: Alignment.bottomLeft, + child: TextButton( + onPressed: !hasViewer + ? null + : () { + if (controller.isOpen) { + controller.close(); + } else { + controller.open(); + } + }, + child: const Text("Scene"), + )); + }, + ); + }); + } +} diff --git a/ios/include/FlutterFilamentApi.h b/ios/include/FlutterFilamentApi.h index 0a5837d3..bf6988b8 100644 --- a/ios/include/FlutterFilamentApi.h +++ b/ios/include/FlutterFilamentApi.h @@ -47,7 +47,7 @@ #include "ResourceBuffer.hpp" typedef int32_t EntityId; -typedef int32_t ManipulatorMode; +typedef int32_t _ManipulatorMode; #ifdef __cplusplus extern "C" { @@ -154,7 +154,7 @@ FLUTTER_PLUGIN_EXPORT const double* const get_camera_view_matrix(const void* con FLUTTER_PLUGIN_EXPORT const double* const get_camera_projection_matrix(const void* const viewer); FLUTTER_PLUGIN_EXPORT void set_camera_focal_length(const void* const viewer, float focalLength); FLUTTER_PLUGIN_EXPORT void set_camera_focus_distance(const void* const viewer, float focusDistance); -FLUTTER_PLUGIN_EXPORT void set_camera_manipulator_options(const void* const viewer, ManipulatorMode mode, double orbitSpeedX, double orbitSpeedY, double zoomSpeed); +FLUTTER_PLUGIN_EXPORT void set_camera_manipulator_options(const void* const viewer, _ManipulatorMode mode, double orbitSpeedX, double orbitSpeedY, double zoomSpeed); FLUTTER_PLUGIN_EXPORT int hide_mesh(void* assetManager, EntityId asset, const char* meshName); diff --git a/lib/filament_controller.dart b/lib/filament_controller.dart index 55dadd5f..fc691d3b 100644 --- a/lib/filament_controller.dart +++ b/lib/filament_controller.dart @@ -1,3 +1,5 @@ +// ignore_for_file: constant_identifier_names + import 'dart:async'; import 'dart:ui' as ui; import 'package:flutter/widgets.dart'; @@ -5,10 +7,14 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_filament/animations/animation_data.dart'; import 'package:vector_math/vector_math_64.dart'; +// a handle that can be safely passed back to the rendering layer to manipulate an Entity typedef FilamentEntity = int; enum ToneMapper { ACES, FILMIC, LINEAR } +// see filament Manipulator.h for more details +enum ManipulatorMode { ORBIT, MAP, FREE_FLIGHT } + class TextureDetails { final int textureId; @@ -27,6 +33,13 @@ abstract class FilamentController { /// ValueNotifier get rect; + /// + /// A [ValueNotifier] to indicate whether a FilamentViewer is currently available. + /// (FilamentViewer is a C++ type, hence why it is not referenced) here. + /// Call [createViewer]/[destroyViewer] to create/destroy a FilamentViewer. + /// + ValueNotifier get hasViewer; + /// /// Whether a Flutter Texture widget should be inserted into the widget hierarchy. /// This will be false on certain platforms where we use a transparent window underlay. @@ -424,4 +437,13 @@ abstract class FilamentController { /// Retrieves the name assigned to the given FilamentEntity (usually corresponds to the glTF mesh name). /// String? getNameForEntity(FilamentEntity entity); + + /// + /// Sets the options for manipulating the camera via the viewport. + /// + Future setCameraManipulatorOptions( + {ManipulatorMode mode = ManipulatorMode.FREE_FLIGHT, + double orbitSpeedX = 0.01, + double orbitSpeedY = 0.01, + double zoomSpeed = 0.01}); } diff --git a/lib/filament_controller_ffi.dart b/lib/filament_controller_ffi.dart index 31db5cfb..565237c8 100644 --- a/lib/filament_controller_ffi.dart +++ b/lib/filament_controller_ffi.dart @@ -39,6 +39,9 @@ class FilamentControllerFFI extends FilamentController { @override final rect = ValueNotifier(null); + @override + final hasViewer = ValueNotifier(false); + @override Stream get pickResult => _pickResultController.stream; final _pickResultController = StreamController.broadcast(); @@ -135,6 +138,7 @@ class FilamentControllerFFI extends FilamentController { _assetManager = null; _lib.destroy_filament_viewer_ffi(viewer!); + hasViewer.value = false; } @override @@ -217,6 +221,7 @@ class FilamentControllerFFI extends FilamentController { print("texture details ${textureDetails.value}"); _lib.update_viewport_and_camera_projection_ffi( _viewer!, rect.value!.width.toInt(), rect.value!.height.toInt(), 1.0); + hasViewer.value = true; } Future _createRenderingSurface() async { @@ -1024,4 +1029,17 @@ class FilamentControllerFFI extends FilamentController { calloc.free(arrayPtr); return rotationMatrix; } + + @override + Future setCameraManipulatorOptions( + {ManipulatorMode mode = ManipulatorMode.FREE_FLIGHT, + double orbitSpeedX = 0.01, + double orbitSpeedY = 0.01, + double zoomSpeed = 0.01}) async { + if (_viewer == null) { + throw Exception("No viewer available"); + } + _lib.set_camera_manipulator_options( + _viewer!, mode.index, orbitSpeedX, orbitSpeedX, zoomSpeed); + } } diff --git a/lib/generated_bindings.dart b/lib/generated_bindings.dart index ecc8df66..c7f35881 100644 --- a/lib/generated_bindings.dart +++ b/lib/generated_bindings.dart @@ -1322,22 +1322,29 @@ class NativeLibrary { late final _set_camera_focus_distance = _set_camera_focus_distancePtr .asFunction, double)>(); - void set_camera_manipulator_mode( + void set_camera_manipulator_options( ffi.Pointer viewer, int mode, + double orbitSpeedX, + double orbitSpeedY, + double zoomSpeed, ) { - return _set_camera_manipulator_mode( + return _set_camera_manipulator_options( viewer, mode, + orbitSpeedX, + orbitSpeedY, + zoomSpeed, ); } - late final _set_camera_manipulator_modePtr = _lookup< + late final _set_camera_manipulator_optionsPtr = _lookup< ffi.NativeFunction< - ffi.Void Function(ffi.Pointer, - ManipulatorMode)>>('set_camera_manipulator_mode'); - late final _set_camera_manipulator_mode = _set_camera_manipulator_modePtr - .asFunction, int)>(); + ffi.Void Function(ffi.Pointer, _ManipulatorMode, ffi.Double, + ffi.Double, ffi.Double)>>('set_camera_manipulator_options'); + late final _set_camera_manipulator_options = + _set_camera_manipulator_optionsPtr.asFunction< + void Function(ffi.Pointer, int, double, double, double)>(); int hide_mesh( ffi.Pointer assetManager, @@ -2456,7 +2463,7 @@ typedef FreeFilamentResourceFromOwner = ffi.Pointer< /// This header replicates most of the methods in FlutterFilamentApi.h, and is only intended to be used to generate client FFI bindings. /// The intention is that calling one of these methods will call its respective method in FlutterFilamentApi.h, but wrapped in some kind of thread runner to ensure thread safety. typedef EntityId = ffi.Int32; -typedef ManipulatorMode = ffi.Int32; +typedef _ManipulatorMode = ffi.Int32; typedef FilamentRenderCallback = ffi.Pointer< ffi.NativeFunction owner)>>;