Compare commits

...

11 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
Nick Fisher
d3f84f156a update README 2023-10-16 21:46:21 +08:00
Nick Fisher
4fe79e3b92 remove golden_toolkit from test dependency 2023-10-17 00:29:21 +11:00
Nick Fisher
2a5c863506 remove golden_toolkit from test dependency 2023-10-17 00:26:11 +11:00
Nick Fisher
837a2cebc7 different timeouts for resize in debug/release 2023-10-17 00:25:30 +11:00
Nick Fisher
6ab814114e rewrite resize handler for better support on Windows 2023-10-17 00:13:44 +11:00
Nick Fisher
818d75b493 specify minimum Flutter version in README 2023-10-16 18:24:45 +11:00
11 changed files with 364 additions and 260 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,18 +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.15.0-15.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
``` ```
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:
@@ -115,6 +115,9 @@ When a `FilamentWidget` is added to the widget hierarchy:
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. 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.
> 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.
### Load a background ### Load a background
@@ -358,6 +361,7 @@ Separately, we also force the Filament gltfio library to load assets via in-memo
``` ```
git checkout flutter-filament-windows git checkout flutter-filament-windows
mkdir out && cd out mkdir out && cd out
"C:\Program Files\Microsoft Visual Studio\2022\Community\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe" --build . --target gltf_viewer --config Debug
``` ```
Building notes: Building notes:

View File

@@ -6,7 +6,6 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart'; import 'package:integration_test/integration_test.dart';
import 'package:golden_toolkit/golden_toolkit.dart';
import 'package:polyvox_filament/widgets/filament_widget.dart'; import 'package:polyvox_filament/widgets/filament_widget.dart';
import '../lib/main.dart' as app; import '../lib/main.dart' as app;
@@ -31,7 +30,7 @@ void main() {
int _counter = 0; int _counter = 0;
Future _snapshot(WidgetTester tester, Device device, String label, Future _snapshot(WidgetTester tester, String label,
[int seconds = 0]) async { [int seconds = 0]) async {
await tester.pumpAndSettle(Duration(milliseconds: 16)); await tester.pumpAndSettle(Duration(milliseconds: 16));
for (int i = 0; i < seconds; i++) { for (int i = 0; i < seconds; i++) {
@@ -50,8 +49,6 @@ void main() {
_counter++; _counter++;
} }
late Device device;
Future tap(WidgetTester tester, String label, [int seconds = 0]) async { Future tap(WidgetTester tester, String label, [int seconds = 0]) async {
var target = find.text(label).first; var target = find.text(label).first;
await tester.dragUntilVisible( await tester.dragUntilVisible(
@@ -62,7 +59,7 @@ void main() {
duration: Duration(milliseconds: 10)); duration: Duration(milliseconds: 10));
await tester.tap(target); await tester.tap(target);
await _snapshot( await _snapshot(
tester, device, label.replaceAll(RegExp("[ -:]"), ""), seconds); tester, label.replaceAll(RegExp("[ -:]"), ""), seconds);
} }
Future<void> pumpUntilFound( Future<void> pumpUntilFound(
@@ -84,14 +81,13 @@ void main() {
timer.cancel(); timer.cancel();
} }
testGoldens('test', (WidgetTester tester) async { testWidgets('test', (WidgetTester tester) async {
app.main(); app.main();
await pumpUntilFound(tester, find.byType(app.ExampleWidget)); await pumpUntilFound(tester, find.byType(app.ExampleWidget));
device = Device(size: Size(800, 600), name: "desktop");
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await _snapshot(tester, device, "fresh"); await _snapshot(tester, "fresh");
await tap(tester, "create viewer (default ubershader)", 4); await tap(tester, "create viewer (default ubershader)", 4);
@@ -111,7 +107,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 1.0))); await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 1.0)));
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await _snapshot(tester, device, "zoomin"); await _snapshot(tester, "zoomin");
// rotate // rotate
testPointer = testPointer =
@@ -127,7 +123,7 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await tester.sendEventToBinding(testPointer.up()); await tester.sendEventToBinding(testPointer.up());
await _snapshot(tester, device, "rotate", 2); await _snapshot(tester, "rotate", 2);
// pan // pan
testPointer = TestPointer(1, PointerDeviceKind.mouse, null, kPrimaryButton); testPointer = TestPointer(1, PointerDeviceKind.mouse, null, kPrimaryButton);
@@ -144,7 +140,7 @@ void main() {
await tester.sendEventToBinding(testPointer.up()); await tester.sendEventToBinding(testPointer.up());
await tester.pumpAndSettle(); await tester.pumpAndSettle();
await _snapshot(tester, device, "pan"); await _snapshot(tester, "pan");
await tap(tester, "transform to unit cube"); await tap(tester, "transform to unit cube");
await tap(tester, "set shapes position to 1, 1, -1"); await tap(tester, "set shapes position to 1, 1, -1");
@@ -154,9 +150,9 @@ void main() {
await tap(tester, "move camera to 1, 1, -1"); await tap(tester, "move camera to 1, 1, -1");
await tap(tester, 'set camera to first camera in shapes GLB'); await tap(tester, 'set camera to first camera in shapes GLB');
await tap(tester, 'resize'); await tap(tester, 'resize', 1);
await tap(tester, 'resize'); await tap(tester, 'resize', 1);
await tap(tester, 'resize'); await tap(tester, 'resize', 1);
await tap(tester, 'resize'); await tap(tester, 'resize', 1);
}); });
} }

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

@@ -6,7 +6,7 @@ description: Demonstrates how to use the polyvox_filament plugin.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev publish_to: 'none' # Remove this line if you wish to publish to pub.dev
environment: environment:
sdk: ">=2.12.0 <4.0.0" sdk: ">=3.0.0 <4.0.0"
# Dependencies specify other packages that your package needs in order to work. # Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions # To automatically upgrade your package dependencies to the latest versions
@@ -29,7 +29,6 @@ dev_dependencies:
integration_test: integration_test:
sdk: flutter sdk: flutter
flutter_lints: ^1.0.0 flutter_lints: ^1.0.0
golden_toolkit: ^0.15.0
crypto: crypto:
image_compare: ^1.1.2 image_compare: ^1.1.2

View File

@@ -8,19 +8,31 @@ 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 {
// the current target size of the viewport, in logical pixels ///
ui.Size size = ui.Size.zero; /// 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;
Future get isReadyForScene; ///
/// 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).
@@ -29,6 +41,11 @@ abstract class FilamentController {
/// ///
Stream<FilamentEntity?> get pickResult; Stream<FilamentEntity?> get pickResult;
///
/// Whether the controller is currently rendering at [framerate].
///
bool get rendering;
/// ///
/// Set to true to continuously render the scene at the framerate specified by [setFrameRate] (60 fps by default). /// Set to true to continuously render the scene at the framerate specified by [setFrameRate] (60 fps by default).
/// ///
@@ -61,9 +78,8 @@ abstract class FilamentController {
/// ///
Future destroyViewer(); Future destroyViewer();
/// ///
/// Destroys the 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.
/// ///
Future destroyTexture(); Future destroyTexture();
@@ -77,15 +93,15 @@ 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.
/// ///
Future<TextureDetails> createViewer(int width, int height); void createViewer(int width, int height);
/// ///
/// Resize the viewport & backing texture. /// Resize the viewport & backing texture.
/// This is called by FilamentWidget; you shouldn't need to invoke this manually. /// This is called by FilamentWidget; you shouldn't need to invoke this manually.
/// ///
Future<TextureDetails> resize(int width, int height, {double scaleFactor = 1.0}); Future resize(int width, int height, {double scaleFactor = 1.0});
/// ///
/// Set the background image to [path] (which should have a file extension .png, .jpg, or .ktx). /// Set the background image to [path] (which should have a file extension .png, .jpg, or .ktx).

View File

@@ -16,24 +16,23 @@ class FilamentControllerFFI extends FilamentController {
double _pixelRatio = 1.0; double _pixelRatio = 1.0;
int? _textureId;
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;
bool _resizing = false;
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();
@override
TextureDetails? textureDetails;
/// ///
/// This controller uses platform channels to bridge Dart with the C/C++ code for the Filament API. /// This controller uses platform channels to bridge Dart with the C/C++ code for the Filament API.
/// Setting up the context/texture (since this is platform-specific) and the render ticker are platform-specific; all other methods are passed through by the platform channel to the methods specified in PolyvoxFilamentApi.h. /// Setting up the context/texture (since this is platform-specific) and the render ticker are platform-specific; all other methods are passed through by the platform channel to the methods specified in PolyvoxFilamentApi.h.
@@ -52,10 +51,11 @@ class FilamentControllerFFI extends FilamentController {
} }
bool _rendering = false; bool _rendering = false;
bool get rendering => _rendering;
@override @override
Future setRendering(bool render) async { Future setRendering(bool render) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_rendering = render; _rendering = render;
@@ -64,7 +64,7 @@ class FilamentControllerFFI extends FilamentController {
@override @override
Future render() async { Future render() async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.render_ffi(_viewer!); _lib.render_ffi(_viewer!);
@@ -89,7 +89,7 @@ class FilamentControllerFFI extends FilamentController {
@override @override
Future destroyViewer() async { Future destroyViewer() async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
var viewer = _viewer; var viewer = _viewer;
@@ -98,48 +98,43 @@ 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 (_textureId != null) { if (textureDetails != null) {
throw Exception("No texture available"); await _channel.invokeMethod("destroyTexture", textureDetails!.textureId);
} }
print("Destroying texture");
// we need to flush all references to the previous texture ID before calling destroy, otherwise the Texture widget will attempt to render a non-existent texture and crash.
// however, this is not a synchronous stream, so we need to ensure the Texture widget has been removed from the hierarchy before destroying
_textureId = null;
await _channel.invokeMethod("destroyTexture", _textureId!);
print("Texture destroyed"); print("Texture destroyed");
} }
/// ///
/// Called by `FilamentWidget`. You do not need to call this yourself. /// Called by `FilamentWidget`. You do not need to call this yourself.
/// ///
Future<TextureDetails> createViewer(int width, int height) async { void createViewer(int width, int height) async {
if (_viewer != null) { if (_viewer != null) {
throw Exception( throw Exception(
"Viewer already exists, make sure you call destroyViewer first"); "Viewer already exists, make sure you call destroyViewer first");
} }
if (_isReadyForScene.isCompleted) { if (textureDetails != null) {
throw Exception( throw Exception(
"Do not call createViewer when a viewer has already been created without calling destroyViewer"); "Texture already exists, make sure you call destroyTexture first");
} }
var loader = Pointer<ResourceLoaderWrapper>.fromAddress( var loader = Pointer<ResourceLoaderWrapper>.fromAddress(
await _channel.invokeMethod("getResourceLoaderWrapper")); await _channel.invokeMethod("getResourceLoaderWrapper"));
if (loader == nullptr) { if (loader == nullptr) {
throw Exception("Failed to get resource loader"); throw Exception("Failed to get resource loader");
} }
size = ui.Size(width * _pixelRatio, height * _pixelRatio); var size = ui.Size(width * _pixelRatio, height * _pixelRatio);
print("Creating viewer with size $size"); print("Creating viewer with size $size");
var textures = var textures =
await _channel.invokeMethod("createTexture", [size.width, size.height]); await _channel.invokeMethod("createTexture", [size.width, size.height]);
var flutterTextureId = textures[0]; var flutterTextureId = textures[0];
_textureId = flutterTextureId;
// 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(
return TextureDetails(textureId: _textureId!, width: width, height: height); textureId: flutterTextureId!, width: width, height: height);
_hasViewerController.add(true);
} }
/// ///
@@ -260,32 +256,35 @@ class FilamentControllerFFI extends FilamentController {
/// ############################################################################ /// ############################################################################
/// ///
///
///
/// Other options:
/// 1) never destroy the texture, simply allocate a large (4k?) texture and crop as needed
/// 2) double-buffering?
@override @override
Future<TextureDetails> resize(int width, int height, Future resize(int width, int height, {double scaleFactor = 1.0}) async {
{double scaleFactor = 1.0}) async { // we defer to the FilamentWidget to ensure that every call to [resize] is synchronized
if (_textureId == null) { // so this exception should never be thrown (right?)
throw Exception("No texture created, ignoring call to resize."); if (textureDetails == null) {
throw Exception("Resize currently underway, ignoring");
} }
var textureId = _textureId;
_textureId = null; var _textureDetails = textureDetails;
textureDetails = null;
_lib.set_rendering_ffi(_viewer!, false); _lib.set_rendering_ffi(_viewer!, false);
if (_viewer != null) { if (_textureDetails != null) {
_lib.destroy_swap_chain_ffi(_viewer!); if (_viewer != null) {
_lib.destroy_swap_chain_ffi(_viewer!);
}
await _channel.invokeMethod("destroyTexture", _textureDetails!.textureId);
print("Destroyed texture ${_textureDetails!.textureId}");
} }
await _channel.invokeMethod("destroyTexture", textureId); var newSize = ui.Size(width * _pixelRatio, height * _pixelRatio);
size = ui.Size(width * _pixelRatio, height * _pixelRatio); print("Size after pixel ratio : $width x $height ");
var textures = var textures = await _channel
await _channel.invokeMethod("createTexture", [size.width, size.height]); .invokeMethod("createTexture", [newSize.width, newSize.height]);
// 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;
@@ -296,28 +295,26 @@ class FilamentControllerFFI extends FilamentController {
_lib.create_swap_chain_ffi( _lib.create_swap_chain_ffi(
_viewer!, _viewer!,
Pointer<Void>.fromAddress(surfaceAddress), Pointer<Void>.fromAddress(surfaceAddress),
size.width.toInt(), newSize.width.toInt(),
size.height.toInt()); newSize.height.toInt());
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, size.width.toInt(), size.height.toInt()); newSize.width.toInt(), newSize.height.toInt());
} }
_lib.update_viewport_and_camera_projection_ffi( _lib.update_viewport_and_camera_projection_ffi(
_viewer!, size.width.toInt(), size.height.toInt(), 1.0); _viewer!, newSize.width.toInt(), newSize.height.toInt(), 1.0);
await setRendering(_rendering); await setRendering(_rendering);
textureDetails =
_textureId = textures[0]; TextureDetails(textureId: textures[0]!, width: width, height: height);
return TextureDetails(textureId: _textureId!, width: width, height: height);
} }
@override @override
Future clearBackgroundImage() async { Future clearBackgroundImage() async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.clear_background_image_ffi(_viewer!); _lib.clear_background_image_ffi(_viewer!);
@@ -325,7 +322,7 @@ class FilamentControllerFFI extends FilamentController {
@override @override
Future setBackgroundImage(String path, {bool fillHeight = false}) async { Future setBackgroundImage(String path, {bool fillHeight = false}) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.set_background_image_ffi( _lib.set_background_image_ffi(
@@ -334,7 +331,7 @@ class FilamentControllerFFI extends FilamentController {
@override @override
Future setBackgroundColor(Color color) async { Future setBackgroundColor(Color color) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.set_background_color_ffi( _lib.set_background_color_ffi(
@@ -348,7 +345,7 @@ class FilamentControllerFFI extends FilamentController {
@override @override
Future setBackgroundImagePosition(double x, double y, Future setBackgroundImagePosition(double x, double y,
{bool clamp = false}) async { {bool clamp = false}) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.set_background_image_position_ffi(_viewer!, x, y, clamp); _lib.set_background_image_position_ffi(_viewer!, x, y, clamp);
@@ -356,7 +353,7 @@ class FilamentControllerFFI extends FilamentController {
@override @override
Future loadSkybox(String skyboxPath) async { Future loadSkybox(String skyboxPath) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.load_skybox_ffi(_viewer!, skyboxPath.toNativeUtf8().cast<Char>()); _lib.load_skybox_ffi(_viewer!, skyboxPath.toNativeUtf8().cast<Char>());
@@ -364,7 +361,7 @@ class FilamentControllerFFI extends FilamentController {
@override @override
Future loadIbl(String lightingPath, {double intensity = 30000}) async { Future loadIbl(String lightingPath, {double intensity = 30000}) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.load_ibl_ffi( _lib.load_ibl_ffi(
@@ -373,7 +370,7 @@ class FilamentControllerFFI extends FilamentController {
@override @override
Future removeSkybox() async { Future removeSkybox() async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.remove_skybox_ffi(_viewer!); _lib.remove_skybox_ffi(_viewer!);
@@ -381,7 +378,7 @@ class FilamentControllerFFI extends FilamentController {
@override @override
Future removeIbl() async { Future removeIbl() async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.remove_ibl_ffi(_viewer!); _lib.remove_ibl_ffi(_viewer!);
@@ -399,7 +396,7 @@ class FilamentControllerFFI extends FilamentController {
double dirY, double dirY,
double dirZ, double dirZ,
bool castShadows) async { bool castShadows) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
var entity = _lib.add_light_ffi(_viewer!, type, colour, intensity, posX, var entity = _lib.add_light_ffi(_viewer!, type, colour, intensity, posX,
@@ -409,7 +406,7 @@ class FilamentControllerFFI extends FilamentController {
@override @override
Future removeLight(FilamentEntity light) async { Future removeLight(FilamentEntity light) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.remove_light_ffi(_viewer!, light); _lib.remove_light_ffi(_viewer!, light);
@@ -417,7 +414,7 @@ class FilamentControllerFFI extends FilamentController {
@override @override
Future clearLights() async { Future clearLights() async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.clear_lights_ffi(_viewer!); _lib.clear_lights_ffi(_viewer!);
@@ -425,7 +422,7 @@ class FilamentControllerFFI extends FilamentController {
@override @override
Future<FilamentEntity> loadGlb(String path, {bool unlit = false}) async { Future<FilamentEntity> loadGlb(String path, {bool unlit = false}) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
if (unlit) { if (unlit) {
@@ -446,7 +443,7 @@ class FilamentControllerFFI extends FilamentController {
throw Exception( throw Exception(
"loadGltf has a race condition on Windows which is likely to crash your program. If you really want to try, pass force=true to loadGltf"); "loadGltf has a race condition on Windows which is likely to crash your program. If you really want to try, pass force=true to loadGltf");
} }
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
var asset = _lib.load_gltf_ffi( var asset = _lib.load_gltf_ffi(
@@ -461,7 +458,7 @@ class FilamentControllerFFI extends FilamentController {
@override @override
Future panStart(double x, double y) async { Future panStart(double x, double y) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.grab_begin(_viewer!, x * _pixelRatio, y * _pixelRatio, true); _lib.grab_begin(_viewer!, x * _pixelRatio, y * _pixelRatio, true);
@@ -469,7 +466,7 @@ class FilamentControllerFFI extends FilamentController {
@override @override
Future panUpdate(double x, double y) async { Future panUpdate(double x, double y) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.grab_update(_viewer!, x * _pixelRatio, y * _pixelRatio); _lib.grab_update(_viewer!, x * _pixelRatio, y * _pixelRatio);
@@ -477,7 +474,7 @@ class FilamentControllerFFI extends FilamentController {
@override @override
Future panEnd() async { Future panEnd() async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.grab_end(_viewer!); _lib.grab_end(_viewer!);
@@ -485,7 +482,7 @@ class FilamentControllerFFI extends FilamentController {
@override @override
Future rotateStart(double x, double y) async { Future rotateStart(double x, double y) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.grab_begin(_viewer!, x * _pixelRatio, y * _pixelRatio, false); _lib.grab_begin(_viewer!, x * _pixelRatio, y * _pixelRatio, false);
@@ -493,7 +490,7 @@ class FilamentControllerFFI extends FilamentController {
@override @override
Future rotateUpdate(double x, double y) async { Future rotateUpdate(double x, double y) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.grab_update(_viewer!, x * _pixelRatio, y * _pixelRatio); _lib.grab_update(_viewer!, x * _pixelRatio, y * _pixelRatio);
@@ -501,7 +498,7 @@ class FilamentControllerFFI extends FilamentController {
@override @override
Future rotateEnd() async { Future rotateEnd() async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.grab_end(_viewer!); _lib.grab_end(_viewer!);
@@ -510,7 +507,7 @@ class FilamentControllerFFI extends FilamentController {
@override @override
Future setMorphTargetWeights( Future setMorphTargetWeights(
FilamentEntity asset, String meshName, List<double> weights) async { FilamentEntity asset, String meshName, List<double> weights) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
var weightsPtr = calloc<Float>(weights.length); var weightsPtr = calloc<Float>(weights.length);
@@ -526,7 +523,7 @@ class FilamentControllerFFI extends FilamentController {
@override @override
Future<List<String>> getMorphTargetNames( Future<List<String>> getMorphTargetNames(
FilamentEntity asset, String meshName) async { FilamentEntity asset, String meshName) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
var names = <String>[]; var names = <String>[];
@@ -544,7 +541,7 @@ class FilamentControllerFFI extends FilamentController {
@override @override
Future<List<String>> getAnimationNames(FilamentEntity asset) async { Future<List<String>> getAnimationNames(FilamentEntity asset) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
var animationCount = _lib.get_animation_count(_assetManager!, asset); var animationCount = _lib.get_animation_count(_assetManager!, asset);
@@ -561,7 +558,7 @@ class FilamentControllerFFI extends FilamentController {
@override @override
Future<double> getAnimationDuration( Future<double> getAnimationDuration(
FilamentEntity asset, int animationIndex) async { FilamentEntity asset, int animationIndex) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
var duration = var duration =
@@ -573,7 +570,7 @@ class FilamentControllerFFI extends FilamentController {
@override @override
Future setMorphAnimationData( Future setMorphAnimationData(
FilamentEntity asset, MorphAnimationData animation) async { FilamentEntity asset, MorphAnimationData animation) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
@@ -603,7 +600,7 @@ class FilamentControllerFFI extends FilamentController {
@override @override
Future setBoneAnimation( Future setBoneAnimation(
FilamentEntity asset, BoneAnimationData animation) async { FilamentEntity asset, BoneAnimationData animation) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
// var data = calloc<Float>(animation.frameData.length); // var data = calloc<Float>(animation.frameData.length);
@@ -640,7 +637,7 @@ class FilamentControllerFFI extends FilamentController {
@override @override
Future removeAsset(FilamentEntity asset) async { Future removeAsset(FilamentEntity asset) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.remove_asset_ffi(_viewer!, asset); _lib.remove_asset_ffi(_viewer!, asset);
@@ -648,7 +645,7 @@ class FilamentControllerFFI extends FilamentController {
@override @override
Future clearAssets() async { Future clearAssets() async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.clear_assets_ffi(_viewer!); _lib.clear_assets_ffi(_viewer!);
@@ -656,7 +653,7 @@ class FilamentControllerFFI extends FilamentController {
@override @override
Future zoomBegin() async { Future zoomBegin() async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.scroll_begin(_viewer!); _lib.scroll_begin(_viewer!);
@@ -664,7 +661,7 @@ class FilamentControllerFFI extends FilamentController {
@override @override
Future zoomUpdate(double x, double y, double z) async { Future zoomUpdate(double x, double y, double z) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.scroll_update(_viewer!, x, y, z); _lib.scroll_update(_viewer!, x, y, z);
@@ -672,7 +669,7 @@ class FilamentControllerFFI extends FilamentController {
@override @override
Future zoomEnd() async { Future zoomEnd() async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.scroll_end(_viewer!); _lib.scroll_end(_viewer!);
@@ -684,7 +681,7 @@ class FilamentControllerFFI extends FilamentController {
bool reverse = false, bool reverse = false,
bool replaceActive = true, bool replaceActive = true,
double crossfade = 0.0}) async { double crossfade = 0.0}) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.play_animation_ffi( _lib.play_animation_ffi(
@@ -693,14 +690,14 @@ class FilamentControllerFFI extends FilamentController {
Future setAnimationFrame( Future setAnimationFrame(
FilamentEntity asset, int index, int animationFrame) async { FilamentEntity asset, int index, int animationFrame) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.set_animation_frame(_assetManager!, asset, index, animationFrame); _lib.set_animation_frame(_assetManager!, asset, index, animationFrame);
} }
Future stopAnimation(FilamentEntity asset, int animationIndex) async { Future stopAnimation(FilamentEntity asset, int animationIndex) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.stop_animation(_assetManager!, asset, animationIndex); _lib.stop_animation(_assetManager!, asset, animationIndex);
@@ -708,7 +705,7 @@ class FilamentControllerFFI extends FilamentController {
@override @override
Future setCamera(FilamentEntity asset, String? name) async { Future setCamera(FilamentEntity asset, String? name) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
var result = _lib.set_camera( var result = _lib.set_camera(
@@ -720,7 +717,7 @@ class FilamentControllerFFI extends FilamentController {
@override @override
Future setToneMapping(ToneMapper mapper) async { Future setToneMapping(ToneMapper mapper) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
@@ -729,7 +726,7 @@ class FilamentControllerFFI extends FilamentController {
@override @override
Future setPostProcessing(bool enabled) async { Future setPostProcessing(bool enabled) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
@@ -738,35 +735,35 @@ class FilamentControllerFFI extends FilamentController {
@override @override
Future setBloom(double bloom) async { Future setBloom(double bloom) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.set_bloom_ffi(_viewer!, bloom); _lib.set_bloom_ffi(_viewer!, bloom);
} }
Future setCameraFocalLength(double focalLength) async { Future setCameraFocalLength(double focalLength) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.set_camera_focal_length(_viewer!, focalLength); _lib.set_camera_focal_length(_viewer!, focalLength);
} }
Future setCameraFocusDistance(double focusDistance) async { Future setCameraFocusDistance(double focusDistance) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.set_camera_focus_distance(_viewer!, focusDistance); _lib.set_camera_focus_distance(_viewer!, focusDistance);
} }
Future setCameraPosition(double x, double y, double z) async { Future setCameraPosition(double x, double y, double z) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.set_camera_position(_viewer!, x, y, z); _lib.set_camera_position(_viewer!, x, y, z);
} }
Future moveCameraToAsset(FilamentEntity asset) async { Future moveCameraToAsset(FilamentEntity asset) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.move_camera_to_asset(_viewer!, asset); _lib.move_camera_to_asset(_viewer!, asset);
@@ -774,7 +771,7 @@ class FilamentControllerFFI extends FilamentController {
@override @override
Future setViewFrustumCulling(bool enabled) async { Future setViewFrustumCulling(bool enabled) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.set_view_frustum_culling(_viewer!, enabled); _lib.set_view_frustum_culling(_viewer!, enabled);
@@ -782,21 +779,21 @@ class FilamentControllerFFI extends FilamentController {
Future setCameraExposure( Future setCameraExposure(
double aperture, double shutterSpeed, double sensitivity) async { double aperture, double shutterSpeed, double sensitivity) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.set_camera_exposure(_viewer!, aperture, shutterSpeed, sensitivity); _lib.set_camera_exposure(_viewer!, aperture, shutterSpeed, sensitivity);
} }
Future setCameraRotation(double rads, double x, double y, double z) async { Future setCameraRotation(double rads, double x, double y, double z) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.set_camera_rotation(_viewer!, rads, x, y, z); _lib.set_camera_rotation(_viewer!, rads, x, y, z);
} }
Future setCameraModelMatrix(List<double> matrix) async { Future setCameraModelMatrix(List<double> matrix) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
assert(matrix.length == 16); assert(matrix.length == 16);
@@ -810,7 +807,7 @@ class FilamentControllerFFI extends FilamentController {
Future setMaterialColor(FilamentEntity asset, String meshName, Future setMaterialColor(FilamentEntity asset, String meshName,
int materialIndex, Color color) async { int materialIndex, Color color) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
var result = _lib.set_material_color( var result = _lib.set_material_color(
@@ -828,21 +825,21 @@ class FilamentControllerFFI extends FilamentController {
} }
Future transformToUnitCube(FilamentEntity asset) async { Future transformToUnitCube(FilamentEntity asset) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.transform_to_unit_cube(_assetManager!, asset); _lib.transform_to_unit_cube(_assetManager!, asset);
} }
Future setPosition(FilamentEntity asset, double x, double y, double z) async { Future setPosition(FilamentEntity asset, double x, double y, double z) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.set_position(_assetManager!, asset, x, y, z); _lib.set_position(_assetManager!, asset, x, y, z);
} }
Future setScale(FilamentEntity asset, double scale) async { Future setScale(FilamentEntity asset, double scale) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.set_scale(_assetManager!, asset, scale); _lib.set_scale(_assetManager!, asset, scale);
@@ -850,14 +847,14 @@ class FilamentControllerFFI extends FilamentController {
Future setRotation( Future setRotation(
FilamentEntity asset, double rads, double x, double y, double z) async { FilamentEntity asset, double rads, double x, double y, double z) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
_lib.set_rotation(_assetManager!, asset, rads, x, y, z); _lib.set_rotation(_assetManager!, asset, rads, x, y, z);
} }
Future hide(FilamentEntity asset, String meshName) async { Future hide(FilamentEntity asset, String meshName) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
if (_lib.hide_mesh( if (_lib.hide_mesh(
@@ -866,7 +863,7 @@ class FilamentControllerFFI extends FilamentController {
} }
Future reveal(FilamentEntity asset, String meshName) async { Future reveal(FilamentEntity asset, String meshName) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
if (_lib.reveal_mesh( if (_lib.reveal_mesh(
@@ -885,13 +882,13 @@ class FilamentControllerFFI extends FilamentController {
} }
void pick(int x, int y) async { void pick(int x, int y) async {
if (_viewer == null || _resizing) { if (_viewer == null) {
throw Exception("No viewer available, ignoring"); throw Exception("No viewer available, ignoring");
} }
final outPtr = calloc<EntityId>(1); final outPtr = calloc<EntityId>(1);
outPtr.value = 0; outPtr.value = 0;
_lib.pick_ffi(_viewer!, x, size.height.toInt() - y, outPtr); _lib.pick_ffi(_viewer!, x, textureDetails!.height - y, outPtr);
int wait = 0; int wait = 0;
while (outPtr.value == 0) { while (outPtr.value == 0) {
await Future.delayed(Duration(milliseconds: 50)); await Future.delayed(Duration(milliseconds: 50));

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,4 +670,16 @@ class FilamentControllerMethodChannel extends FilamentController {
// TODO: implement getNameForEntity // TODO: implement getNameForEntity
throw UnimplementedError(); throw UnimplementedError();
} }
@override
// TODO: implement textureDetails
TextureDetails? get textureDetails => throw UnimplementedError();
@override
// TODO: implement rendering
bool get rendering => throw UnimplementedError();
@override
// TODO: implement hasViewer
Stream<bool> get hasViewer => throw UnimplementedError();
} }

View File

@@ -9,7 +9,7 @@ import 'package:polyvox_filament/filament_controller.dart';
import 'dart:async'; import 'dart:async';
typedef ResizeCallback = void Function(Size oldSize, Size newSize); typedef ResizeCallback = void Function(Size newSize);
class ResizeObserver extends SingleChildRenderObjectWidget { class ResizeObserver extends SingleChildRenderObjectWidget {
final ResizeCallback onResized; final ResizeCallback onResized;
@@ -36,14 +36,14 @@ class _RenderResizeObserver extends RenderProxyBox {
required this.onLayoutChangedCallback, required this.onLayoutChangedCallback,
}) : super(child); }) : super(child);
late var _oldSize = size; Size _oldSize = Size.zero;
@override @override
void performLayout() { void performLayout() async {
super.performLayout(); super.performLayout();
if (size != _oldSize) { if (size.width != _oldSize.width || size.height != _oldSize.height) {
onLayoutChangedCallback(_oldSize, size); onLayoutChangedCallback(size);
_oldSize = size; _oldSize = Size(size.width, size.height);
} }
} }
} }
@@ -65,62 +65,11 @@ class FilamentWidget extends StatefulWidget {
} }
class _FilamentWidgetState extends State<FilamentWidget> { class _FilamentWidgetState extends State<FilamentWidget> {
TextureDetails? _textureDetails;
late final AppLifecycleListener _listener;
AppLifecycleState? _lastState;
String? _error;
int? _width; int? _width;
int? _height; int? _height;
void _handleStateChange(AppLifecycleState state) async {
switch (state) {
case AppLifecycleState.detached:
print("Detached");
_textureDetails = null;
await widget.controller.destroyViewer();
await widget.controller.destroyTexture();
break;
case AppLifecycleState.hidden:
print("Hidden");
if (Platform.isIOS) {
_textureDetails = null;
await widget.controller.destroyViewer();
await widget.controller.destroyTexture();
}
break;
case AppLifecycleState.inactive:
print("Inactive");
break;
case AppLifecycleState.paused:
print("Paused");
break;
case AppLifecycleState.resumed:
print("Resumed");
if (!Platform.isWindows) {
if (_textureDetails == null) {
var size = ((context.findRenderObject()) as RenderBox).size;
print("Size after resuming : $size");
_height = size.height.ceil();
_width = size.width.ceil();
await widget.controller.createViewer(_width!, _height!);
print("Created viewer Size after resuming");
}
}
break;
}
_lastState = state;
}
@override @override
void initState() { void initState() {
_listener = AppLifecycleListener(
onStateChange: _handleStateChange,
);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
// when attaching a debugger via Android Studio on startup, this can delay presentation of the widget // when attaching a debugger via Android Studio on startup, this can delay presentation of the widget
// (meaning the widget may attempt to create a viewer with size 0x0). // (meaning the widget may attempt to create a viewer with size 0x0).
@@ -131,26 +80,176 @@ class _FilamentWidgetState extends State<FilamentWidget> {
var size = ((context.findRenderObject()) as RenderBox).size; var size = ((context.findRenderObject()) as RenderBox).size;
_width = size.width.ceil(); _width = size.width.ceil();
_height = size.height.ceil(); _height = size.height.ceil();
try { setState(() {});
_textureDetails = });
await widget.controller.createViewer(_width!, _height!); super.initState();
} catch (err) { }
setState(() {
_error = err.toString(); @override
}); Widget build(BuildContext context) {
if (_width == null || _height == null) {
return widget.initial ?? Container(color: Colors.red);
}
return ResizeObserver(
onResized: (newSize) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
setState(() {
_width = newSize.width.ceil();
_height = newSize.height.ceil();
});
});
},
child: _SizedFilamentWidget(
initial: widget.initial,
width: _width!,
height: _height!,
controller: widget.controller,
));
}
}
class _SizedFilamentWidget extends StatefulWidget {
final int width;
final int height;
final Widget? initial;
final FilamentController controller;
const _SizedFilamentWidget(
{super.key,
required this.width,
required this.height,
this.initial,
required this.controller});
@override
State<StatefulWidget> createState() => _SizedFilamentWidgetState();
}
class _SizedFilamentWidgetState extends State<_SizedFilamentWidget> {
String? _error;
late final AppLifecycleListener _appLifecycleListener;
AppLifecycleState? _lastState;
@override
void initState() {
_appLifecycleListener = AppLifecycleListener(
onStateChange: _handleStateChange,
);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
// when attaching a debugger via Android Studio on startup, this can delay presentation of the widget
// (meaning the widget may attempt to create a viewer with size 0x0).
// we just add a small delay here which should avoid this
if (!kReleaseMode) {
await Future.delayed(Duration(seconds: 2));
} }
try {
widget.controller.createViewer(widget.width, widget.height);
} catch (err) {
_error = err.toString();
}
setState(() {});
}); });
super.initState(); super.initState();
} }
Timer? _resizeTimer;
bool _resizing = false;
Future _resize() {
final completer = Completer();
// resizing the window can be sluggish (particular in debug mode), exacerbated when simultaneously recreating the swapchain and resize the window.
// to address this, whenever the widget is resized, we set a timer for Xms in the future.
// this timer will call [resize] with the widget size at that point in time.
// any subsequent widget resizes will cancel the timer and replace with a new one.
// debug mode does need a longer timeout.
_resizeTimer?.cancel();
_resizeTimer =
Timer(const Duration(milliseconds: kReleaseMode ? 20 : 100), () async {
if (!mounted) {
return;
}
var size = ((context.findRenderObject()) as RenderBox).size;
var width = size.width.ceil();
var height = size.height.ceil();
while (_resizing) {
await Future.delayed(Duration(milliseconds: 20));
}
_resizing = true;
await widget.controller.resize(width, height);
_resizeTimer = null;
setState(() {});
_resizing = false;
completer.complete();
});
return completer.future;
}
@override
void didUpdateWidget(_SizedFilamentWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.height != widget.height || oldWidget.width != widget.width) {
_resize();
}
}
@override @override
void dispose() { void dispose() {
_listener.dispose(); _appLifecycleListener.dispose();
super.dispose(); super.dispose();
} }
Timer? _resizeTimer; bool _wasRenderingOnInactive = false;
void _handleStateChange(AppLifecycleState state) async {
switch (state) {
case AppLifecycleState.detached:
print("Detached");
if (!_wasRenderingOnInactive) {
_wasRenderingOnInactive = widget.controller.rendering;
}
await widget.controller.setRendering(false);
break;
case AppLifecycleState.hidden:
print("Hidden");
if (!_wasRenderingOnInactive) {
_wasRenderingOnInactive = widget.controller.rendering;
}
await widget.controller.setRendering(false);
break;
case AppLifecycleState.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.
// 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).
await widget.controller.setRendering(false);
break;
case AppLifecycleState.paused:
print("Paused");
if (!_wasRenderingOnInactive) {
_wasRenderingOnInactive = widget.controller.rendering;
}
await widget.controller.setRendering(false);
break;
case AppLifecycleState.resumed:
print("Resumed");
await widget.controller.setRendering(_wasRenderingOnInactive);
await _resize();
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {});
});
break;
}
_lastState = state;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -164,51 +263,26 @@ class _FilamentWidgetState extends State<FilamentWidget> {
])); ]));
} }
// if no texture ID is available, display the [initial] widget (solid red by default) if (widget.controller.textureDetails == null || _resizeTimer != null) {
late Widget content; return widget.initial ?? Container(color: Colors.red);
if (_textureDetails == null ||
_textureDetails!.height != _height ||
_textureDetails!.width != _width) {
content = widget.initial ?? Container(color: Colors.red);
} else {
content = Texture(
key: ObjectKey("texture_${_textureDetails!.textureId}"),
textureId: _textureDetails!.textureId,
filterQuality: FilterQuality.none,
freeze: false,
);
} }
// see [FilamentControllerFFI.resize] for an explanation of how we deal with resizing // see [FilamentControllerFFI.resize] for an explanation of how we deal with resizing
return ResizeObserver( var texture = Texture(
onResized: (Size oldSize, Size newSize) async { key: ObjectKey("texture_${widget.controller.textureDetails!.textureId}"),
_resizeTimer?.cancel(); textureId: widget.controller.textureDetails!.textureId,
filterQuality: FilterQuality.none,
freeze: false,
);
_resizeTimer = Timer(const Duration(milliseconds: 50), () async { return Stack(children: [
var newWidth = newSize.width.ceil(); Positioned.fill(
var newHeight = newSize.height.ceil(); child: Platform.isLinux || Platform.isWindows
try { ? Transform(
_textureDetails = alignment: Alignment.center,
await widget.controller.resize(newWidth, newHeight); transform: Matrix4.rotationX(
setState(() { pi), // TODO - this rotation is due to OpenGL texture coordinate working in a different space from Flutter, can we move this to the C++ side somewhere?
_width = newWidth; child: texture)
_height = newHeight; : texture)
}); ]);
} catch (err) {
print(err);
}
});
},
child: Stack(children: [
Positioned.fill(
child: Platform.isLinux || Platform.isWindows
? Transform(
alignment: Alignment.center,
transform: Matrix4.rotationX(
pi), // TODO - this rotation is due to OpenGL texture coordinate working in a different space from Flutter, can we move this to the C++ side somewhere?
child: content)
: content)
]));
} }
} }

View File

@@ -1,11 +1,11 @@
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:
sdk: ">=3.0.0 <3.11.0" sdk: ">=3.0.0 <4.0.0"
flutter: ">=1.20.0" flutter: ">=3.16.0-0.2.pre"
dependencies: dependencies:
flutter: flutter:

View File

@@ -70,12 +70,14 @@ OpenGLTextureBuffer::OpenGLTextureBuffer(
std::make_unique<flutter::TextureVariant>(flutter::PixelBufferTexture( std::make_unique<flutter::TextureVariant>(flutter::PixelBufferTexture(
[=](size_t width, [=](size_t width,
size_t height) -> const FlutterDesktopPixelBuffer * { size_t height) -> const FlutterDesktopPixelBuffer * {
if (width != this->_width || height != this->_height) { if (width != this->_width || height != this->_height) {
std::cout << "Front-end widget has been resized, you need to " if(!this->logged) {
"teardown/rebuild the swapchain. This pixel buffer " std::cout << "Front-end widget expects " << width << "x" << height << " but this is " << this->_width << "x" << this->_height
"will be discarded." << std::endl;
<< std::endl; this->logged = true;
return nullptr; }
return nullptr;
} }
uint8_t *data = (uint8_t *)pixelData.get(); uint8_t *data = (uint8_t *)pixelData.get();