diff --git a/CHANGELOG.md b/CHANGELOG.md index f78c012c..91f155bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.6.0 + +* `createViewer` is no longer called by `FilamentWidget` and must be called manually at least one frame after a FilamentWidget has been inserted into the widget hierarchy. + + ## 0.5.0 * Replaced `isReadyForScene` Future in `FilamentController` with the `Stream` `hasViewer`. diff --git a/README.md b/README.md index 2d186450..3e654d71 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ class MyApp extends StatelessWidget { This is a relatively lightweight object, however its constructor will load/bind symbols from the native library. This may momentarily block the UI, so you may wish to structure your app so that this is hidden behind a static widget until it is available. -Next, create an instance of `FilamentWidget` in the widget hierarchy where you want the rendering canvas to appear. This can be sized as large or as small as you want. Flutter widgets can be positioned above or below the `FilamentWidget`. +Next, create an instance of `FilamentWidget` in the widget hierarchy where you want the rendering canvas to appear. This can be sized as large or as small as you want. On most platforms, Flutter widgets can be positioned above or below the `FilamentWidget`. ``` class MyApp extends StatelessWidget { @@ -105,20 +105,20 @@ class MyApp extends StatelessWidget { ``` When a `FilamentWidget` is added to the widget hierarchy: -1) on the first frame, by default a Container will be rendered with solid red. If you want to change this, pass a widget as the `initial` paramer to the `FilamentWidget` constructor. -2) on the second frame, `FilamentWidget` will retrieve its actual size and request the `FilamentController` to create: - * the backing textures needed to insert a `Texture` widget into +1) by default a Container will be rendered with solid red. If you want to change this, pass a widget as the `initial` paramer to the `FilamentWidget` constructor. +2) on the second frame, `FilamentWidget` will pass its dimensions/pixel ratio to the `FilamentController` +3) You can then call `createViewer` to create: + * the rendering surface (on most platforms, a backing texture that will be registered with Flutter for use in a `Texture` widget) * a rendering thread * a `FilamentViewer` and an `AssetManager`, which will allow you to load assets/cameras/lighting/etc via the `FilamentController` -3) after an indeterminate number of frames, `FilamentController` will notify `FilamentWidget` when a texture is available the viewport -4) `FilamentWidget` will replace the default `initial` Widget with the viewport (which will initially be solid black or white, depending on your platform). +4) after an indeterminate number of frames, `FilamentController` will notify `FilamentWidget` when a rendering surface is available the viewport +5) `FilamentWidget` will replace the default `initial` Widget with the viewport (which will initially be solid black or white, depending on your platform). -It's important to note that there *will* be a delay between adding a `FilamentWidget` and the actual rendering viewport becoming available. This is why we fill `FilamentWidget` with red - to make it abundantly clear that you need to handle this asynchronous delay appropriately. You can call `await _filamentController.isReadyForScene` if you need to wait until the viewport is actually ready for rendering. +IMPORTANT: there *will* be a delay between adding a `FilamentWidget`, calling `createViewer` and the actual rendering viewport becoming available. This is why we fill `FilamentWidget` with red - to make it abundantly clear that you need to handle this asynchronous delay appropriately. Once `createViewer` has completed, the viewport is available for rendering. > Currently, the `initial` widget will also be displayed whenever the viewport is resized (including changing orientation on mobile and drag-to-resize on desktop). You probably want to change this from the default red. - -Congratulations! You now have a scene. It's completely empty, so you probably want to add. +Congratulations! You now have a scene. It's completely empty, so you probably want to add something visible. ### Load a background diff --git a/lib/filament_controller.dart b/lib/filament_controller.dart index 0ce110c0..06048107 100644 --- a/lib/filament_controller.dart +++ b/lib/filament_controller.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:ui' as ui; -import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_filament/animations/animation_data.dart'; @@ -11,9 +10,9 @@ enum ToneMapper { ACES, FILMIC, LINEAR } class TextureDetails { final int textureId; - + // both width and height are in physical, not logical pixels - final int width; + final int width; final int height; TextureDetails( @@ -21,12 +20,11 @@ class TextureDetails { } abstract class FilamentController { - - /// + /// /// 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. /// Used internally by [FilamentWidget]; you probably don't need to access this property directly. - /// + /// bool get requiresTextureWidget; /// @@ -35,17 +33,6 @@ abstract class FilamentController { /// final textureDetails = ValueNotifier(null); - - /// - /// A stream to indicate whether a FilamentViewer is available. - /// [FilamentWidget] will (asynchronously) create a [FilamentViewer] after being inserted into the widget hierarchy; - /// listen to this stream beforehand to perform any work necessary once the viewer is available. - /// [FilamentWidget] may also destroy/recreate the viewer on certain lifecycle events (e.g. backgrounding a mobile app); - /// listen for any corresponding [false]/[true] events to perform related work. - /// Note this is not a broadcast stream; only one listener can be registered and events will be buffered. - /// - Stream get hasViewer; - /// /// The result(s) of calling [pick] (see below). /// This may be a broadcast stream, so you should ensure you have subscribed to this stream before calling [pick]. @@ -73,13 +60,6 @@ abstract class FilamentController { /// Future setFrameRate(int framerate); - /// - /// Called by FilamentGestureDetector to set the pixel ratio (obtained from [MediaQuery]) before creating the texture/viewport. - /// You may call this yourself if you want to increase/decrease the pixel density of the viewport, but calling this method won't do anything on its own. - /// You will need to manually recreate the texture/viewer afterwards. - /// - void setPixelRatio(double ratio); - /// /// Destroys the viewer and all backing textures. You can leave the FilamentWidget in the hierarchy after this is called, but you will need to manually call [createViewer] to /// @@ -96,24 +76,26 @@ abstract class FilamentController { Future destroyTexture(); /// - /// Called by [FilamentWidget]; you generally will not need to call this yourself. - /// To recap, you can create a viewport is created in the Flutter rendering hierarchy by: - /// 1) Create a FilamentController - /// 2) Insert a FilamentWidget into the rendering tree, passing your FilamentController - /// 3) Initially, the FilamentWidget will only contain an empty Container (by default, with a solid red background). - /// This widget will render a single frame to get its actual size, then will itself call [createViewer]. You do not need to call [createViewer] yourself. - /// This will dispatch a request to the native platform to create a hardware texture (Metal on iOS, OpenGL on Linux, GLES on Android and Windows) and a FilamentViewer (the main interface for manipulating the 3D scene) . - /// 4) The FilamentController will notify FilamentWidget that a texture is available - /// 5) The FilamentWidget will replace the empty Container with a Texture widget - /// If you need to wait until a FilamentViewer has been created, listen to the [viewer] stream. + /// Create a FilamentViewer. Must be called at least one frame after a [FilamentWidget] has been inserted into the rendering hierarchy. /// - Future createViewer(Rect rect); + /// Before a FilamentViewer is created, the FilamentWidget will only contain an empty Container (by default, with a solid red background). + /// FilamentWidget will then call [setDimensions] with dimensions/pixel ratio of the viewport + /// Calling [createViewer] will then dispatch a request to the native platform to create a hardware texture (Metal on iOS, OpenGL on Linux, GLES on Android and Windows) and a FilamentViewer (the main interface for manipulating the 3D scene) . + /// [FilamentWidget] will be notified that a texture is available and will replace the empty Container with a Texture widget + /// + Future createViewer(); /// - /// Resize the viewport & backing texture. + /// Sets the dimensions of the viewport and pixel ratio (obtained from [MediaQuery]) to be used the next time [resize] or [createViewer] is called. /// This is called by FilamentWidget; you shouldn't need to invoke this manually. /// - Future resize(Rect rect); + Future setDimensions(ui.Rect rect, double pixelRatio); + + /// + /// Resize the viewport & backing texture to the current dimensions (as last set by [setDimensions]). + /// This is called by FilamentWidget; you shouldn't need to invoke this manually. + /// + Future resize(); /// /// Set the background image to [path] (which should have a file extension .png, .jpg, or .ktx). diff --git a/lib/filament_controller_ffi.dart b/lib/filament_controller_ffi.dart index c77eaa4d..855ec158 100644 --- a/lib/filament_controller_ffi.dart +++ b/lib/filament_controller_ffi.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'dart:ui' as ui; import 'package:flutter/services.dart'; import 'package:ffi/ffi.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_filament/filament_controller.dart'; @@ -31,9 +32,10 @@ class FilamentControllerFFI extends FilamentController { final String? uberArchivePath; + Pointer _driver = nullptr.cast(); + @override - Stream get hasViewer => _hasViewerController.stream; - final _hasViewerController = StreamController(); + final rect = ValueNotifier(null); @override Stream get pickResult => _pickResultController.stream; @@ -62,8 +64,9 @@ class FilamentControllerFFI extends FilamentController { _resizingWidth = call.arguments[0]; _resizingHeight = call.arguments[1]; _resizeTimer = Timer(const Duration(milliseconds: 500), () async { - await resize(Offset.zero & - ui.Size(_resizingWidth!.toDouble(), _resizingHeight!.toDouble())); + this.rect.value = Offset.zero & + ui.Size(_resizingWidth!.toDouble(), _resizingHeight!.toDouble()); + await resize(); }); }); late DynamicLibrary dl; @@ -73,15 +76,15 @@ class FilamentControllerFFI extends FilamentController { dl = DynamicLibrary.open("libflutter_filament_android.so"); } _lib = NativeLibrary(dl); - if(Platform.isWindows) { + if (Platform.isWindows) { _channel.invokeMethod("usesBackingWindow").then((result) { _usesBackingWindow = result; }); } - } bool _rendering = false; + @override bool get rendering => _rendering; @override @@ -107,9 +110,10 @@ class FilamentControllerFFI extends FilamentController { } @override - void setPixelRatio(double ratio) { + Future setDimensions(Rect rect, double ratio) async { + this.rect.value = Rect.fromLTWH(rect.left, rect.top, + rect.width * _pixelRatio, rect.height * _pixelRatio); _pixelRatio = ratio; - print("Set pixel ratio to $ratio"); } @override @@ -129,7 +133,6 @@ class FilamentControllerFFI extends FilamentController { _assetManager = null; _lib.destroy_filament_viewer_ffi(viewer!); - _hasViewerController.add(false); } @override @@ -141,14 +144,15 @@ class FilamentControllerFFI extends FilamentController { print("Texture destroyed"); } - Pointer _driver = nullptr.cast(); - /// /// Called by `FilamentWidget`. You do not need to call this yourself. /// @override - Future createViewer(Rect rect) async { - + Future createViewer() async { + if (rect.value == null) { + throw Exception( + "Dimensions have not yet been set by FilamentWidget. You need to wait for at least one frame after FilamentWidget has been inserted into the hierarchy"); + } if (_viewer != null) { throw Exception( "Viewer already exists, make sure you call destroyViewer first"); @@ -164,8 +168,6 @@ class FilamentControllerFFI extends FilamentController { throw Exception("Failed to get resource loader"); } - rect = Rect.fromLTWH(rect.left, rect.top, rect.width * _pixelRatio, rect.height * _pixelRatio); - if (Platform.isWindows && requiresTextureWidget) { _driver = Pointer.fromAddress( await _channel.invokeMethod("getDriverPlatform")); @@ -178,7 +180,9 @@ class FilamentControllerFFI extends FilamentController { var renderCallbackOwner = Pointer.fromAddress(renderCallbackResult[1]); - var renderingSurface = await _createRenderingSurface(rect); + var renderingSurface = await _createRenderingSurface(); + + print("Got rendering surface"); _viewer = _lib.create_filament_viewer_ffi( Pointer.fromAddress(renderingSurface.sharedContext ?? 0), @@ -187,7 +191,7 @@ class FilamentControllerFFI extends FilamentController { loader, renderCallback, renderCallbackOwner); - + print("Created viewer"); if (_viewer!.address == 0) { throw Exception("Failed to create viewer. Check logs for details"); } @@ -195,34 +199,36 @@ class FilamentControllerFFI extends FilamentController { _assetManager = _lib.get_asset_manager(_viewer!); _lib.create_swap_chain_ffi(_viewer!, renderingSurface.surface, - rect.width.toInt(), rect.height.toInt()); + rect.value!.width.toInt(), rect.value!.height.toInt()); + print("Created swap chain"); if (renderingSurface.textureHandle != 0) { print( "Creating render target from native texture ${renderingSurface.textureHandle}"); _lib.create_render_target_ffi(_viewer!, renderingSurface.textureHandle, - rect.width.toInt(), rect.height.toInt()); + rect.value!.width.toInt(), rect.value!.height.toInt()); } textureDetails.value = TextureDetails( textureId: renderingSurface.flutterTextureId!, - width: rect.width.toInt(), - height: rect.height.toInt()); - + width: rect.value!.width.toInt(), + height: rect.value!.height.toInt()); + print("texture details ${textureDetails.value}"); _lib.update_viewport_and_camera_projection_ffi( - _viewer!, rect.width.toInt(), rect.height.toInt(), 1.0); - - _hasViewerController.add(true); + _viewer!, rect.value!.width.toInt(), rect.value!.height.toInt(), 1.0); } - Future _createRenderingSurface(Rect rect) async { - return RenderingSurface.from(await _channel.invokeMethod( - "createTexture", - [rect.width, rect.height, rect.left, rect.top])); + Future _createRenderingSurface() async { + return RenderingSurface.from(await _channel.invokeMethod("createTexture", [ + rect.value!.width, + rect.value!.height, + rect.value!.left, + rect.value!.top + ])); } /// /// When a FilamentWidget is resized, it will call the [resize] method below, which will tear down/recreate the swapchain. - /// For "once-off" resizes, this is fine; however, this can be problematic for consecutive resizes + /// For "once-off" resizes, this is fine; however, this can be problematic for consecutive resizes /// (e.g. dragging to expand/contract the parent window on desktop, or animating the size of the FilamentWidget itself). /// It is too expensive to recreate the swapchain multiple times per second. /// We therefore add a timer to FilamentWidget so that the call to [resize] is delayed (e.g. 500ms). @@ -286,8 +292,7 @@ class FilamentControllerFFI extends FilamentController { /// bool _resizing = false; @override - Future resize(Rect rect) async { - + Future resize() async { if (_viewer == null) { throw Exception("Cannot resize without active viewer"); } @@ -296,54 +301,56 @@ class FilamentControllerFFI extends FilamentController { throw Exception("Resize currently underway, ignoring"); } - rect = Rect.fromLTWH(rect.left, rect.top, rect.width * _pixelRatio, rect.height * _pixelRatio); - _resizing = true; _lib.set_rendering_ffi(_viewer!, false); - if(!_usesBackingWindow) { + if (!_usesBackingWindow) { _lib.destroy_swap_chain_ffi(_viewer!); } - + if (requiresTextureWidget) { - if(textureDetails.value != null) { + if (textureDetails.value != null) { await _channel.invokeMethod( "destroyTexture", textureDetails.value!.textureId); } - } else if(Platform.isWindows) { - print("Resizing window with rect $rect"); - await _channel.invokeMethod( - "resizeWindow", [rect.width, rect.height, rect.left, rect.top]); + } else if (Platform.isWindows) { + print("Resizing window with rect $rect"); + await _channel.invokeMethod("resizeWindow", [ + rect.value!.width, + rect.value!.height, + rect.value!.left, + rect.value!.top + ]); } - var renderingSurface = await _createRenderingSurface(rect); + var renderingSurface = await _createRenderingSurface(); if (_viewer!.address == 0) { throw Exception("Failed to create viewer. Check logs for details"); } _assetManager = _lib.get_asset_manager(_viewer!); - - if(!_usesBackingWindow) { + + if (!_usesBackingWindow) { _lib.create_swap_chain_ffi(_viewer!, renderingSurface.surface, - rect.width.toInt(), rect.height.toInt()); + rect.value!.width.toInt(), rect.value!.height.toInt()); } if (renderingSurface.textureHandle != 0) { print( "Creating render target from native texture ${renderingSurface.textureHandle}"); _lib.create_render_target_ffi(_viewer!, renderingSurface.textureHandle, - rect.width.toInt(), rect.height.toInt()); + rect.value!.width.toInt(), rect.value!.height.toInt()); } textureDetails.value = TextureDetails( textureId: renderingSurface.flutterTextureId!, - width: rect.width.toInt(), - height: rect.height.toInt()); + width: rect.value!.width.toInt(), + height: rect.value!.height.toInt()); _lib.update_viewport_and_camera_projection_ffi( - _viewer!, rect.width.toInt(), rect.height.toInt(), 1.0); + _viewer!, rect.value!.width.toInt(), rect.value!.height.toInt(), 1.0); await setRendering(_rendering); @@ -739,6 +746,7 @@ class FilamentControllerFFI extends FilamentController { _assetManager!, asset, index, loop, reverse, replaceActive, crossfade); } + @override Future setAnimationFrame( FilamentEntity asset, int index, int animationFrame) async { if (_viewer == null) { @@ -747,6 +755,7 @@ class FilamentControllerFFI extends FilamentController { _lib.set_animation_frame(_assetManager!, asset, index, animationFrame); } + @override Future stopAnimation(FilamentEntity asset, int animationIndex) async { if (_viewer == null) { throw Exception("No viewer available, ignoring"); @@ -792,6 +801,7 @@ class FilamentControllerFFI extends FilamentController { _lib.set_bloom_ffi(_viewer!, bloom); } + @override Future setCameraFocalLength(double focalLength) async { if (_viewer == null) { throw Exception("No viewer available, ignoring"); @@ -799,6 +809,7 @@ class FilamentControllerFFI extends FilamentController { _lib.set_camera_focal_length(_viewer!, focalLength); } + @override Future setCameraFocusDistance(double focusDistance) async { if (_viewer == null) { throw Exception("No viewer available, ignoring"); @@ -806,6 +817,7 @@ class FilamentControllerFFI extends FilamentController { _lib.set_camera_focus_distance(_viewer!, focusDistance); } + @override Future setCameraPosition(double x, double y, double z) async { if (_viewer == null) { throw Exception("No viewer available, ignoring"); @@ -813,6 +825,7 @@ class FilamentControllerFFI extends FilamentController { _lib.set_camera_position(_viewer!, x, y, z); } + @override Future moveCameraToAsset(FilamentEntity asset) async { if (_viewer == null) { throw Exception("No viewer available, ignoring"); @@ -828,6 +841,7 @@ class FilamentControllerFFI extends FilamentController { _lib.set_view_frustum_culling(_viewer!, enabled); } + @override Future setCameraExposure( double aperture, double shutterSpeed, double sensitivity) async { if (_viewer == null) { @@ -836,6 +850,7 @@ class FilamentControllerFFI extends FilamentController { _lib.set_camera_exposure(_viewer!, aperture, shutterSpeed, sensitivity); } + @override Future setCameraRotation(double rads, double x, double y, double z) async { if (_viewer == null) { throw Exception("No viewer available, ignoring"); @@ -843,6 +858,7 @@ class FilamentControllerFFI extends FilamentController { _lib.set_camera_rotation(_viewer!, rads, x, y, z); } + @override Future setCameraModelMatrix(List matrix) async { if (_viewer == null) { throw Exception("No viewer available, ignoring"); @@ -856,6 +872,7 @@ class FilamentControllerFFI extends FilamentController { calloc.free(ptr); } + @override Future setMaterialColor(FilamentEntity asset, String meshName, int materialIndex, Color color) async { if (_viewer == null) { @@ -875,6 +892,7 @@ class FilamentControllerFFI extends FilamentController { } } + @override Future transformToUnitCube(FilamentEntity asset) async { if (_viewer == null) { throw Exception("No viewer available, ignoring"); @@ -882,6 +900,7 @@ class FilamentControllerFFI extends FilamentController { _lib.transform_to_unit_cube(_assetManager!, asset); } + @override Future setPosition(FilamentEntity asset, double x, double y, double z) async { if (_viewer == null) { throw Exception("No viewer available, ignoring"); @@ -889,6 +908,7 @@ class FilamentControllerFFI extends FilamentController { _lib.set_position(_assetManager!, asset, x, y, z); } + @override Future setScale(FilamentEntity asset, double scale) async { if (_viewer == null) { throw Exception("No viewer available, ignoring"); @@ -896,6 +916,7 @@ class FilamentControllerFFI extends FilamentController { _lib.set_scale(_assetManager!, asset, scale); } + @override Future setRotation( FilamentEntity asset, double rads, double x, double y, double z) async { if (_viewer == null) { @@ -904,6 +925,7 @@ class FilamentControllerFFI extends FilamentController { _lib.set_rotation(_assetManager!, asset, rads, x, y, z); } + @override Future hide(FilamentEntity asset, String meshName) async { if (_viewer == null) { throw Exception("No viewer available, ignoring"); @@ -913,6 +935,7 @@ class FilamentControllerFFI extends FilamentController { 1) {} } + @override Future reveal(FilamentEntity asset, String meshName) async { if (_viewer == null) { throw Exception("No viewer available, ignoring"); @@ -924,6 +947,7 @@ class FilamentControllerFFI extends FilamentController { } } + @override String? getNameForEntity(FilamentEntity entity) { final result = _lib.get_name_for_entity(_assetManager!, entity); if (result == nullptr) { @@ -932,6 +956,7 @@ class FilamentControllerFFI extends FilamentController { return result.cast().toDartString(); } + @override void pick(int x, int y) async { if (_viewer == null) { throw Exception("No viewer available, ignoring"); diff --git a/lib/widgets/filament_widget.dart b/lib/widgets/filament_widget.dart index 4013cfee..e2da3e8f 100644 --- a/lib/widgets/filament_widget.dart +++ b/lib/widgets/filament_widget.dart @@ -121,16 +121,17 @@ class _SizedFilamentWidget extends StatefulWidget { } class _SizedFilamentWidgetState extends State<_SizedFilamentWidget> { - String? _error; late final AppLifecycleListener _appLifecycleListener; - Rect get _rect { - final renderBox =(context.findRenderObject()) as RenderBox; - final size = renderBox.size; - final translation = renderBox.getTransformTo(null).getTranslation(); - return Rect.fromLTWH(translation.x, translation.y, size.width, size.height); + late double _pixelRatio; + + Rect get _rect { + final renderBox = (context.findRenderObject()) as RenderBox; + final size = renderBox.size; + final translation = renderBox.getTransformTo(null).getTranslation(); + return Rect.fromLTWH(translation.x, translation.y, size.width, size.height); } @override @@ -139,13 +140,12 @@ class _SizedFilamentWidgetState extends State<_SizedFilamentWidget> { onStateChange: _handleStateChange, ); - - WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { try { - widget.controller.setPixelRatio(MediaQuery.of(context).devicePixelRatio); - await widget.controller.createViewer(_rect); + _pixelRatio = MediaQuery.of(context).devicePixelRatio; + widget.controller.setDimensions(_rect, _pixelRatio); } catch (err) { + print("Fatal error : $err"); _error = err.toString(); } setState(() {}); @@ -167,17 +167,19 @@ class _SizedFilamentWidgetState extends State<_SizedFilamentWidget> { // debug mode does need a longer timeout. _resizeTimer?.cancel(); - _resizeTimer = - Timer(Duration(milliseconds: (kReleaseMode || Platform.isWindows) ? 10 : 100), () async { + _resizeTimer = Timer( + Duration(milliseconds: (kReleaseMode || Platform.isWindows) ? 10 : 100), + () async { if (!mounted) { return; } while (_resizing) { await Future.delayed(const Duration(milliseconds: 20)); } - + _resizing = true; - await widget.controller.resize(_rect); + await widget.controller.setDimensions(_rect, _pixelRatio); + await widget.controller.resize(); _resizeTimer = null; setState(() {}); _resizing = false;