Compare commits

...

5 Commits

Author SHA1 Message Date
Nick Fisher
53b8d352da Merge branch 'develop' of github.com:nmfisher/polyvox_filament into develop 2023-10-17 08:57:49 +08:00
Nick Fisher
2553d854e9 replace isReadyForScene with hasViewer stream and update version number/CHANGELOG 2023-10-17 08:57:00 +08:00
Nick Fisher
7f9c5a0f2d (re)set rendering on all lifecycle changes 2023-10-17 08:55:49 +08:00
Nick Fisher
7718885781 update README 2023-10-17 00:55:51 +11:00
Nick Fisher
5bf21ceaf9 update README 2023-10-17 00:54:19 +11:00
8 changed files with 81 additions and 68 deletions

View File

@@ -1,3 +1,4 @@
## 0.0.1 ## 0.5.0
* TODO: Describe initial release. * Replaced `isReadyForScene` Future in `FilamentController` with the `Stream<bool>` `hasViewer`.
* Rendering is set to false when the app is hidden, inactive or paused; on resume, this will be set to the value it held prior to being hidden/inactive/paused.

View File

@@ -4,11 +4,10 @@ Cross-platform, 3D PBR rendering and animation for [Flutter](https://github.com/
Wraps the [the Filament rendering library](https://github.com/google/filament). Wraps the [the Filament rendering library](https://github.com/google/filament).
Powers the Polyvox and odd-io engines. Powers the [Polyvox](https://polyvox.app) and [odd-io](https://github.com/odd-io/) engines.
This is still in beta: bugs/missing features are to be expected. This is still in beta: bugs/missing features are to be expected.
https://github.com/nmfisher/polyvox_filament/assets/7238578/abaed1c8-c97b-4999-97b2-39e85e0fa7dd https://github.com/nmfisher/polyvox_filament/assets/7238578/abaed1c8-c97b-4999-97b2-39e85e0fa7dd
@@ -21,19 +20,19 @@ https://github.com/nmfisher/polyvox_filament/assets/7238578/abaed1c8-c97b-4999-9
|Animation|✅ Embedded glTF skinning animations<br/>✅ Embedded glTF morph animations<br/> ✅ Runtime/dynamic morph animations<br/> ⚠️ Runtime/dynamic skinning animations <br/> |Animation|✅ Embedded glTF skinning animations<br/>✅ Embedded glTF morph animations<br/> ✅ Runtime/dynamic morph animations<br/> ⚠️ Runtime/dynamic skinning animations <br/>
|Entity manipulation|✅ Viewport selection<br/>⚠️ Entity/transform parenting (planned)<br/> ⚠️ Transform manipulation (mouse/gesture to rotate/translate/scale object) (partial)<br/>⚠️ Runtime material changes (planned)| |Entity manipulation|✅ Viewport selection<br/>⚠️ Entity/transform parenting (planned)<br/> ⚠️ Transform manipulation (mouse/gesture to rotate/translate/scale object) (partial)<br/>⚠️ Runtime material changes (planned)|
Special thanks to odd-io for sponsoring work on supporting Windows, raycasting, testing and documentation. Special thanks to [odd-io](https://github.com/odd-io/) for sponsoring work on supporting Windows, raycasting, testing and documentation.
PRs are welcome but please create a placeholder PR to discuss before writing any code. This will help with feature planning, avoid clashes with existing work and keep the project structure consistent. PRs are welcome but please create a placeholder PR to discuss before writing any code. This will help with feature planning, avoid clashes with existing work and keep the project structure consistent.
## Getting Started ## Getting Started
This package is currently only tested on Flutter >= `3.16.0-0.2.pre`, so you will need to first switch to the `beta` channel: This package requires Flutter >= `3.16.0-0.2.pre`, so you will need to first switch to the `beta` channel:
``` ```
flutter channel beta flutter channel beta
flutter upgrade flutter upgrade
``` ```
Earlier versions have specific issues that will prevent them from working on Windows/MacOS! There are specific issues with earlier versions on Windows/MacOS (mobile should actually be fine, so if you want to experiment on your own you're free to remove the minimum version from `pubspec.yaml`).
Next, clone this repository and pull the latest binaries from Git LFS: Next, clone this repository and pull the latest binaries from Git LFS:

View File

@@ -42,7 +42,6 @@ class ExampleWidget extends StatefulWidget {
class _ExampleWidgetState extends State<ExampleWidget> { class _ExampleWidgetState extends State<ExampleWidget> {
FilamentController? _filamentController; FilamentController? _filamentController;
FilamentEntity? _shapes; FilamentEntity? _shapes;
FilamentEntity? _flightHelmet; FilamentEntity? _flightHelmet;
List<String>? _animations; List<String>? _animations;
@@ -65,10 +64,13 @@ class _ExampleWidgetState extends State<ExampleWidget> {
bool _coneHidden = false; bool _coneHidden = false;
bool _frustumCulling = true; bool _frustumCulling = true;
StreamSubscription? _hasViewerListener;
@override @override
void dispose() { void dispose() {
super.dispose(); super.dispose();
_pickResultListener?.cancel(); _pickResultListener?.cancel();
_hasViewerListener?.cancel();
} }
Widget _item(void Function() onTap, String text) { Widget _item(void Function() onTap, String text) {
@@ -92,9 +94,10 @@ class _ExampleWidgetState extends State<ExampleWidget> {
picked = _filamentController!.getNameForEntity(entityId!); picked = _filamentController!.getNameForEntity(entityId!);
}); });
}); });
_filamentController!.isReadyForScene.then((readyForScene) { _hasViewerListener =
_filamentController!.hasViewer.listen((bool hasViewer) {
setState(() { setState(() {
_readyForScene = readyForScene; _readyForScene = hasViewer;
}); });
}); });
} }

View File

@@ -8,20 +8,32 @@ typedef FilamentEntity = int;
enum ToneMapper { ACES, FILMIC, LINEAR } enum ToneMapper { ACES, FILMIC, LINEAR }
class TextureDetails { class TextureDetails {
final int textureId; final int textureId;
final int width; final int width;
final int height; final int height;
TextureDetails({required this.textureId, required this.width, required this.height}); TextureDetails(
{required this.textureId, required this.width, required this.height});
} }
abstract class FilamentController { abstract class FilamentController {
///
Future get isReadyForScene; /// The Flutter texture ID and dimensions for current texture in use.
/// This is only used by [FilamentWidget]; you shouldn't need to access directly yourself.
///
TextureDetails? get textureDetails; TextureDetails? get textureDetails;
///
/// 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<bool> get hasViewer;
/// ///
/// The result(s) of calling [pick] (see below). /// 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]. /// This may be a broadcast stream, so you should ensure you have subscribed to this stream before calling [pick].
@@ -66,7 +78,6 @@ abstract class FilamentController {
/// ///
Future destroyViewer(); Future destroyViewer();
/// ///
/// Destroys the specified backing texture. You probably want to call [destroy] instead of this; this is exposed mostly for lifecycle changes which are handled by FilamentWidget. /// Destroys the specified backing texture. You probably want to call [destroy] instead of this; this is exposed mostly for lifecycle changes which are handled by FilamentWidget.
/// ///
@@ -82,7 +93,7 @@ abstract class FilamentController {
/// 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) . /// 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 /// 4) The FilamentController will notify FilamentWidget that a texture is available
/// 5) The FilamentWidget will replace the empty Container with a Texture widget /// 5) The FilamentWidget will replace the empty Container with a Texture widget
/// If you need to wait until a FilamentViewer has been created, [await] the [isReadyForScene] Future. /// If you need to wait until a FilamentViewer has been created, listen to the [viewer] stream.
/// ///
void createViewer(int width, int height); void createViewer(int width, int height);

View File

@@ -16,18 +16,17 @@ class FilamentControllerFFI extends FilamentController {
double _pixelRatio = 1.0; double _pixelRatio = 1.0;
Completer _isReadyForScene = Completer();
Future get isReadyForScene => _isReadyForScene.future;
late Pointer<Void>? _assetManager; late Pointer<Void>? _assetManager;
late NativeLibrary _lib; late NativeLibrary _lib;
Pointer<Void>? _viewer; Pointer<Void>? _viewer;
final String? uberArchivePath; final String? uberArchivePath;
Stream<bool> get hasViewer => _hasViewerController.stream;
final _hasViewerController = StreamController<bool>();
Stream<FilamentEntity> get pickResult => _pickResultController.stream; Stream<FilamentEntity> get pickResult => _pickResultController.stream;
final _pickResultController = StreamController<FilamentEntity>.broadcast(); final _pickResultController = StreamController<FilamentEntity>.broadcast();
@@ -99,12 +98,12 @@ class FilamentControllerFFI extends FilamentController {
_assetManager = null; _assetManager = null;
_lib.destroy_filament_viewer_ffi(viewer!); _lib.destroy_filament_viewer_ffi(viewer!);
_isReadyForScene = Completer(); _hasViewerController.add(false);
} }
@override @override
Future destroyTexture() async { Future destroyTexture() async {
if(textureDetails != null) { if (textureDetails != null) {
await _channel.invokeMethod("destroyTexture", textureDetails!.textureId); await _channel.invokeMethod("destroyTexture", textureDetails!.textureId);
} }
print("Texture destroyed"); print("Texture destroyed");
@@ -118,14 +117,11 @@ class FilamentControllerFFI extends FilamentController {
throw Exception( throw Exception(
"Viewer already exists, make sure you call destroyViewer first"); "Viewer already exists, make sure you call destroyViewer first");
} }
if(textureDetails != null) { if (textureDetails != null) {
throw Exception( throw Exception(
"Texture already exists, make sure you call destroyTexture first"); "Texture already exists, make sure you call destroyTexture first");
} }
if (_isReadyForScene.isCompleted) {
throw Exception(
"Do not call createViewer when a viewer has already been created without calling destroyViewer");
}
var loader = Pointer<ResourceLoaderWrapper>.fromAddress( var loader = Pointer<ResourceLoaderWrapper>.fromAddress(
await _channel.invokeMethod("getResourceLoaderWrapper")); await _channel.invokeMethod("getResourceLoaderWrapper"));
if (loader == nullptr) { if (loader == nullptr) {
@@ -140,7 +136,6 @@ class FilamentControllerFFI extends FilamentController {
await _channel.invokeMethod("createTexture", [size.width, size.height]); await _channel.invokeMethod("createTexture", [size.width, size.height]);
var flutterTextureId = textures[0]; var flutterTextureId = textures[0];
// void* on iOS (pointer to pixel buffer), void* on Android (pointer to native window), null on Windows/macOS // void* on iOS (pointer to pixel buffer), void* on Android (pointer to native window), null on Windows/macOS
var surfaceAddress = textures[1] as int? ?? 0; var surfaceAddress = textures[1] as int? ?? 0;
@@ -192,8 +187,9 @@ class FilamentControllerFFI extends FilamentController {
_assetManager = _lib.get_asset_manager(_viewer!); _assetManager = _lib.get_asset_manager(_viewer!);
_isReadyForScene.complete(true); textureDetails = TextureDetails(
textureDetails = TextureDetails(textureId: flutterTextureId!, width: width, height: height); textureId: flutterTextureId!, width: width, height: height);
_hasViewerController.add(true);
} }
/// ///
@@ -259,23 +255,22 @@ class FilamentControllerFFI extends FilamentController {
/// # Given we don't do this on other platforms, I'm OK to stick with the existing solution for the time being. /// # Given we don't do this on other platforms, I'm OK to stick with the existing solution for the time being.
/// ############################################################################ /// ############################################################################
/// ///
@override @override
Future resize(int width, int height, {double scaleFactor = 1.0}) async { Future resize(int width, int height, {double scaleFactor = 1.0}) async {
// we defer to the FilamentWidget to ensure that every call to [resize] is synchronized
// we defer to the FilamentWidget to ensure that every call to [resize] is synchronized
// so this exception should never be thrown (right?) // so this exception should never be thrown (right?)
if(textureDetails == null) { if (textureDetails == null) {
throw Exception("Resize currently underway, ignoring"); throw Exception("Resize currently underway, ignoring");
} }
var _textureDetails = textureDetails; var _textureDetails = textureDetails;
textureDetails = null; textureDetails = null;
_lib.set_rendering_ffi(_viewer!, false); _lib.set_rendering_ffi(_viewer!, false);
if(_textureDetails != null) { if (_textureDetails != null) {
if (_viewer != null) { if (_viewer != null) {
_lib.destroy_swap_chain_ffi(_viewer!); _lib.destroy_swap_chain_ffi(_viewer!);
} }
@@ -288,7 +283,6 @@ class FilamentControllerFFI extends FilamentController {
print("Size after pixel ratio : $width x $height "); print("Size after pixel ratio : $width x $height ");
var textures = await _channel var textures = await _channel
.invokeMethod("createTexture", [newSize.width, newSize.height]); .invokeMethod("createTexture", [newSize.width, newSize.height]);
@@ -306,19 +300,18 @@ class FilamentControllerFFI extends FilamentController {
if (nativeTexture != 0) { if (nativeTexture != 0) {
assert(surfaceAddress == 0); assert(surfaceAddress == 0);
print("Creating render target from native texture $nativeTexture"); print("Creating render target from native texture $nativeTexture");
_lib.create_render_target_ffi( _lib.create_render_target_ffi(_viewer!, nativeTexture,
_viewer!, nativeTexture, newSize.width.toInt(), newSize.height.toInt()); newSize.width.toInt(), newSize.height.toInt());
} }
_lib.update_viewport_and_camera_projection_ffi( _lib.update_viewport_and_camera_projection_ffi(
_viewer!, newSize.width.toInt(), newSize.height.toInt(), 1.0); _viewer!, newSize.width.toInt(), newSize.height.toInt(), 1.0);
await setRendering(_rendering); await setRendering(_rendering);
textureDetails = TextureDetails(textureId: textures[0]!, width: width, height: height); textureDetails =
TextureDetails(textureId: textures[0]!, width: width, height: height);
} }
@override @override
Future clearBackgroundImage() async { Future clearBackgroundImage() async {
if (_viewer == null) { if (_viewer == null) {

View File

@@ -120,8 +120,8 @@ class FilamentControllerMethodChannel extends FilamentController {
bool _resizing = false; bool _resizing = false;
Future<TextureDetails> resize(int width, int height,
Future<TextureDetails> resize(int width, int height, {double scaleFactor = 1.0}) async { {double scaleFactor = 1.0}) async {
throw Exception(); throw Exception();
_resizing = true; _resizing = true;
_textureId = await _channel.invokeMethod( _textureId = await _channel.invokeMethod(
@@ -670,12 +670,16 @@ class FilamentControllerMethodChannel extends FilamentController {
// TODO: implement getNameForEntity // TODO: implement getNameForEntity
throw UnimplementedError(); throw UnimplementedError();
} }
@override @override
// TODO: implement textureDetails // TODO: implement textureDetails
TextureDetails? get textureDetails => throw UnimplementedError(); TextureDetails? get textureDetails => throw UnimplementedError();
@override @override
// TODO: implement rendering // TODO: implement rendering
bool get rendering => throw UnimplementedError(); bool get rendering => throw UnimplementedError();
@override
// TODO: implement hasViewer
Stream<bool> get hasViewer => throw UnimplementedError();
} }

View File

@@ -168,8 +168,9 @@ class _SizedFilamentWidgetState extends State<_SizedFilamentWidget> {
// debug mode does need a longer timeout. // debug mode does need a longer timeout.
_resizeTimer?.cancel(); _resizeTimer?.cancel();
_resizeTimer = Timer(const Duration(milliseconds: kReleaseMode ? 20 : 100), () async { _resizeTimer =
if(!mounted) { Timer(const Duration(milliseconds: kReleaseMode ? 20 : 100), () async {
if (!mounted) {
return; return;
} }
var size = ((context.findRenderObject()) as RenderBox).size; var size = ((context.findRenderObject()) as RenderBox).size;
@@ -209,41 +210,42 @@ class _SizedFilamentWidgetState extends State<_SizedFilamentWidget> {
switch (state) { switch (state) {
case AppLifecycleState.detached: case AppLifecycleState.detached:
print("Detached"); print("Detached");
if (!_wasRenderingOnInactive) {
if (widget.controller.textureDetails != null) { _wasRenderingOnInactive = widget.controller.rendering;
await widget.controller.destroyViewer();
await widget.controller.destroyTexture();
} }
await widget.controller.setRendering(false);
break; break;
case AppLifecycleState.hidden: case AppLifecycleState.hidden:
print("Hidden"); print("Hidden");
if (Platform.isIOS && widget.controller.textureDetails != null) { if (!_wasRenderingOnInactive) {
await widget.controller.destroyViewer(); _wasRenderingOnInactive = widget.controller.rendering;
await widget.controller.destroyTexture();
} }
await widget.controller.setRendering(false);
break; break;
case AppLifecycleState.inactive: case AppLifecycleState.inactive:
print("Inactive"); print("Inactive");
if (!_wasRenderingOnInactive) {
_wasRenderingOnInactive = widget.controller.rendering;
}
// on Windows in particular, restoring a window after minimizing stalls the renderer (and the whole application) for a considerable length of time. // on Windows in particular, restoring a window after minimizing stalls the renderer (and the whole application) for a considerable length of time.
// disabling rendering on minimize seems to fix the issue (so I wonder if there's some kind of command buffer that's filling up while the window is minimized). // disabling rendering on minimize seems to fix the issue (so I wonder if there's some kind of command buffer that's filling up while the window is minimized).
_wasRenderingOnInactive = widget.controller.rendering;
await widget.controller.setRendering(false); await widget.controller.setRendering(false);
break; break;
case AppLifecycleState.paused: case AppLifecycleState.paused:
print("Paused"); print("Paused");
if (!_wasRenderingOnInactive) {
_wasRenderingOnInactive = widget.controller.rendering;
}
await widget.controller.setRendering(false);
break; break;
case AppLifecycleState.resumed: case AppLifecycleState.resumed:
print("Resumed"); print("Resumed");
if (!Platform.isWindows) {
if (widget.controller.textureDetails == null) {
var size = ((context.findRenderObject()) as RenderBox).size;
widget.controller
.createViewer(size.width.ceil(), size.height.ceil());
}
} else {
await _resize();
}
await widget.controller.setRendering(_wasRenderingOnInactive); await widget.controller.setRendering(_wasRenderingOnInactive);
await _resize();
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {});
});
break; break;
} }
_lastState = state; _lastState = state;
@@ -274,7 +276,7 @@ class _SizedFilamentWidgetState extends State<_SizedFilamentWidget> {
return Stack(children: [ return Stack(children: [
Positioned.fill( Positioned.fill(
child: Platform.isLinux || Platform.isWindows child: Platform.isLinux || Platform.isWindows
? Transform( ? Transform(
alignment: Alignment.center, alignment: Alignment.center,
transform: Matrix4.rotationX( transform: Matrix4.rotationX(

View File

@@ -1,6 +1,6 @@
name: polyvox_filament name: polyvox_filament
description: A Flutter plugin to wrap the Filament rendering engine. description: A Flutter plugin to wrap the Filament rendering engine.
version: 0.0.1 version: 0.5.0
homepage: homepage:
environment: environment: