diff --git a/README.md b/README.md index 57a9f382..8605e998 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,38 @@ # Flutter Filament -Cross-platform, Physically-based rendering inside Flutter applications. +Cross-platform, 3D PBR rendering and animation for Flutter. -Flutter plugin wrapping the Filament renderer https://github.com/google/filament. +Wraps the [the Filament rendering library](https://github.com/google/filament). Powers the Polyvox and odd-io engines. -# Sponsors +|Feature|Supported| +|---|---| +|glTF|Partial - see Known Issues| +|glb|✅| +|Camera movement (mouse/finger gesture)|✅| +|Skinning|✅| +|Morph targets|✅| +|Runtime material changes|Partial| +|Viewport entity selection|✅| +|Entity transform manipulation (mouse/finger gesture)|Partial| +|Entity/transform parenting|Planned| +|Entity material manipulation|Partial| -Thank you to odd-io for sponsoring work on supporting Windows, raycasting, testing and documentation. +Special thanks to odd-io for sponsoring work on supporting Windows, raycasting, testing and documentation. -# Overview +PRs are welcome but please create a placeholder PR to discuss before writing any code. This will help avoid -## Versioning +## 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: -Last tested on Flutter `3.15.0-15.2.pre`. This is on the Flutter beta channel, so run: ``` flutter channel beta flutter upgrade ``` -||Android|iOS|MacOS|Windows|Linux|WebGL -|---|---|---|---|---|---|| -|Filament|v1.43.1 (arm64/armeabi-v7a/x86/x86_64)|v1.43.1* (arm64)|v1.43.1 (arm64)|v1.32.4 (x86_64)|TODO**|TODO***| -|Flutter||3.15.0-15.2.pre|3.15.0-15.2.pre|3.15.0-15.2.pre - -* iOS release build has a skybox bug so the debug versions are currently shipped on iOS -** (Waiting for https://github.com/google/filament/issues/7078 to be resolved before upgrading, not sure exactly when the bug was introduced but it was somewhere between v1.32.4 and v1.40.0) -*** Texture widget not currently supported on web in Flutter. - -## Features - -|Feature|Supported| -|---|---| -|glTF|Y| -|glb|Y| - -# Basic Setup - -## Clone flutter_filament - -This plugin is not yet published to pub.dev. To use in your project, simply clone the repository and pull the latest binaries from Git LFS: +Next, clone this repository and pull the latest binaries from Git LFS: ``` cd $HOME @@ -48,17 +40,17 @@ git clone && cd flutter_filament git lfs pull ``` -You *do not need to build Filament yourself*. The repository is bundled with all necessary headers/static libraries (`windows/lib`, `ios/lib`, `macos/lib` and `linux/lib`) and the Flutter plugin has been configured to link at build time. +(these instructions will be updated after the plugin is published to pub.dev). +> [!NOTE] +> You *do not need to build Filament yourself*. The repository is bundled with all necessary headers/static libraries (`windows/lib`, `ios/lib`, `macos/lib` and `linux/lib`) and the Flutter plugin has been configured to link at build time. -If you want to run the example project to check: +Run the example project to check: ``` cd example && flutter run -d ``` -## Add dependency - -Add the plugin as a dependency in the pubspec.yaml for your application: +To use the plugin in your own project, add the plugin to your pubspec.yaml: ``` name: your_project @@ -71,11 +63,11 @@ dependencies: path: ``` -# Basic Usage +## Basic Usage See the `example` project for a complete sample of the below steps. -## Creating the viewport widget and controller +### Creating the viewport widget and controller To embed a viewport in your app, create an instance of `FilamentControllerFFI` somewhere in your app: @@ -147,12 +139,12 @@ You can also pass a URI to indicate that the glTF file should be loaded from the var entity = _filamentController.loadGlb("file:///tmp/bob.glb"); ``` -The return type `FilamentEntity` is simply an integer handle that be used to manipulate the entity in the scene. - -For example, to remove the asset: +The return type `FilamentEntity` is simply an integer handle that be used to manipulate the entity in the scene. For example, to remove the asset: ``` _filamentController.removeAsset(entity); +entity = null; // see note* below ``` +* Removing an entity from the scene will invalidate the corresponding `FilamentEntity` handle, so ensure you don't retain any references to it after calling `removeAsset` or `clearAssets`. Removing one `FilamentEntity` does not invalidate/change any other `FilamentEntity` handles; you can continue to safely manipulate these via the `FilamentController`. To set the world space position of the asset: ``` @@ -161,7 +153,6 @@ _filamentController.setPositon(entity, 1.0, 1.0, 1.0); On desktop, you can also click any renderable object in the viewport to retrieve its associated FilamentEntity (see below). -Whenever an entity is removed from the scene (or you've called another method that clears all entities), the corresponding `FilamentEntity` handle(s) are invalidated and should not be used. ### Camera movement @@ -257,7 +248,36 @@ uberz -TSHADINGMODEL=lit -TBLENDING=opaque -o lit_opaque_43.uberz lit_opaque (note that the number in the filename corresponds to the Material version, not the Filament version. Not every Filament version requires a new Material version). +## Versioning +||Android|iOS|MacOS|Windows|Linux|WebGL| +|---|---|---|---|---|---|| +|Filament|v1.43.1 (arm64/armeabi-v7a/x86/x86_64)|v1.43.1* (arm64)|v1.43.1 (arm64)|v1.32.4 (x86_64)|TODO**|TODO***| +|Flutter||3.15.0-15.2.pre|3.15.0-15.2.pre|3.15.0-15.2.pre + +* iOS release build has a skybox bug so the debug versions are currently shipped on iOS +** (Waiting for https://github.com/google/filament/issues/7078 to be resolved before upgrading, not sure exactly when the bug was introduced but it was somewhere between v1.32.4 and v1.40.0) +*** Texture widget not currently supported on web in Flutter. + +## Testing + +We automate testing by running the example project on actual iOS/Android/MacOS/Windows devices and executing various operations. + +Eventually we want to compare screenshots after each operation to a set of goldens for every platform. + +Currently this is only possible on iOS (see https://github.com/flutter/flutter/issues/123063 and https://github.com/flutter/flutter/issues/127306). + +To re-generate the golden screenshots for a given device: + +``` +./regenerate_goldens.sh +``` +To run the tests and compare against those goldens: +``` +./compare_goldens.sh +``` + +The results will depend on the actual device used to generate the golden, therefore if you are using a different device (which is likely), your results may not be the same. This is expected. # Building Filament from source diff --git a/example/.gitattributes b/example/.gitattributes index 2f26b9aa..9b96d81d 100644 --- a/example/.gitattributes +++ b/example/.gitattributes @@ -3,3 +3,19 @@ assets/BusterDrone filter=lfs diff=lfs merge=lfs -text assets/FlightHelmet filter=lfs diff=lfs merge=lfs -text assets/lit_opaque_32.uberz filter=lfs diff=lfs merge=lfs -text windows/lib/**/*.* filter=lfs diff=lfs merge=lfs -text +integration_test/goldens/ios/12_Settonemappingtolinear.png filter=lfs diff=lfs merge=lfs -text +integration_test/goldens/ios/3_loadIBL.png filter=lfs diff=lfs merge=lfs -text +integration_test/goldens/ios/6_zoomin.png filter=lfs diff=lfs merge=lfs -text +integration_test/goldens/ios/13_Movecameratoasset.png filter=lfs diff=lfs merge=lfs -text +integration_test/goldens/ios/14_movecamerato.png filter=lfs diff=lfs merge=lfs -text +integration_test/goldens/ios/7_rotate.png filter=lfs diff=lfs merge=lfs -text +integration_test/goldens/ios/9_transformtounitcube.png filter=lfs diff=lfs merge=lfs -text +integration_test/goldens/ios/0_fresh.png filter=lfs diff=lfs merge=lfs -text +integration_test/goldens/ios/10_setshapespositionto.png filter=lfs diff=lfs merge=lfs -text +integration_test/goldens/ios/2_loadskybox.png filter=lfs diff=lfs merge=lfs -text +integration_test/goldens/ios/4_Renderingfalse.png filter=lfs diff=lfs merge=lfs -text +integration_test/goldens/ios/8_pan.png filter=lfs diff=lfs merge=lfs -text +integration_test/goldens/ios/11_Disablefrustumculling.png filter=lfs diff=lfs merge=lfs -text +integration_test/goldens/ios/15_setcameratofirstcamerainshapesGLB.png filter=lfs diff=lfs merge=lfs -text +integration_test/goldens/ios/1_createviewerdefaultubershader.png filter=lfs diff=lfs merge=lfs -text +integration_test/goldens/ios/5_loadshapesGLB.png filter=lfs diff=lfs merge=lfs -text diff --git a/example/compare_goldens.sh b/example/compare_goldens.sh new file mode 100644 index 00000000..3ed87412 --- /dev/null +++ b/example/compare_goldens.sh @@ -0,0 +1,9 @@ +#!/bin/bash +device=$1 +if [ -z "$device" ]; then + echo "Usage: $0 " + exit 1; +fi + +rm integration_test/goldens/{ios,macos,windows,android}/diffs/*.png +flutter drive --driver=test_driver/integration_test.dart -d $1 --target=integration_test/plugin_integration_test.dart diff --git a/example/integration_test/goldens/ios/0_fresh.png b/example/integration_test/goldens/ios/0_fresh.png new file mode 100644 index 00000000..327aeaae --- /dev/null +++ b/example/integration_test/goldens/ios/0_fresh.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:556955b3ea80e461ab1d19de856276b286e964ea6ffbf5b4f788b03b4d1f1d41 +size 258315 diff --git a/example/integration_test/goldens/ios/10_setshapespositionto.png b/example/integration_test/goldens/ios/10_setshapespositionto.png new file mode 100644 index 00000000..be4013e8 --- /dev/null +++ b/example/integration_test/goldens/ios/10_setshapespositionto.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7eaf3e2e5faab0fff2947e6747f44a29a27441fed30dfa5af586f5d18e4c313f +size 1876179 diff --git a/example/integration_test/goldens/ios/11_Disablefrustumculling.png b/example/integration_test/goldens/ios/11_Disablefrustumculling.png new file mode 100644 index 00000000..eb99c29c --- /dev/null +++ b/example/integration_test/goldens/ios/11_Disablefrustumculling.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1abf2fbc19655ce0d99f15a5659268a16c1ef6d2cf2554e07b70fc06505c22e1 +size 1867991 diff --git a/example/integration_test/goldens/ios/12_Settonemappingtolinear.png b/example/integration_test/goldens/ios/12_Settonemappingtolinear.png new file mode 100644 index 00000000..3147c69f --- /dev/null +++ b/example/integration_test/goldens/ios/12_Settonemappingtolinear.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:91f82e50232a672de1604da9555c4a62996c577a9ec9d7a037dfcc989b27a2c2 +size 1743987 diff --git a/example/integration_test/goldens/ios/13_Movecameratoasset.png b/example/integration_test/goldens/ios/13_Movecameratoasset.png new file mode 100644 index 00000000..21686686 --- /dev/null +++ b/example/integration_test/goldens/ios/13_Movecameratoasset.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3c4408b417f5e6fd119c0d7edc6cfd66d44fb1d6d1f03943b7485ab131d3fcbb +size 2283182 diff --git a/example/integration_test/goldens/ios/14_movecamerato.png b/example/integration_test/goldens/ios/14_movecamerato.png new file mode 100644 index 00000000..ab0312ba --- /dev/null +++ b/example/integration_test/goldens/ios/14_movecamerato.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4347cade24c7c55b0ca449b3a346fa4bae0fc8c43ca0018d622b8393ad4d7f5c +size 1957365 diff --git a/example/integration_test/goldens/ios/15_setcameratofirstcamerainshapesGLB.png b/example/integration_test/goldens/ios/15_setcameratofirstcamerainshapesGLB.png new file mode 100644 index 00000000..b32bf2bb --- /dev/null +++ b/example/integration_test/goldens/ios/15_setcameratofirstcamerainshapesGLB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb73e1d9d7c5dfa5cef180d1f1d930da417f9b164e237399c8a7394dd60415e8 +size 1892497 diff --git a/example/integration_test/goldens/ios/1_createviewerdefaultubershader.png b/example/integration_test/goldens/ios/1_createviewerdefaultubershader.png new file mode 100644 index 00000000..15f53c1c --- /dev/null +++ b/example/integration_test/goldens/ios/1_createviewerdefaultubershader.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f7d232df983168399a0aae4b5424d6493ce460794101015f05c07a929268698 +size 310421 diff --git a/example/integration_test/goldens/ios/2_loadskybox.png b/example/integration_test/goldens/ios/2_loadskybox.png new file mode 100644 index 00000000..eb0868a3 --- /dev/null +++ b/example/integration_test/goldens/ios/2_loadskybox.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:891223fabbace04e3394dd15852180e443460111cf96a92b7f3a45d5600318cf +size 306407 diff --git a/example/integration_test/goldens/ios/3_loadIBL.png b/example/integration_test/goldens/ios/3_loadIBL.png new file mode 100644 index 00000000..049950b7 --- /dev/null +++ b/example/integration_test/goldens/ios/3_loadIBL.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:df8157a13a07023837f36645a517a77e837fa5983ddd3a92b883cf96b8098f65 +size 304380 diff --git a/example/integration_test/goldens/ios/4_Renderingfalse.png b/example/integration_test/goldens/ios/4_Renderingfalse.png new file mode 100644 index 00000000..ffba1911 --- /dev/null +++ b/example/integration_test/goldens/ios/4_Renderingfalse.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4e85d3916e42afe77a9854c3297d5d85ce1fa8f973883f5d48bf5efd99bf60e9 +size 309699 diff --git a/example/integration_test/goldens/ios/5_loadshapesGLB.png b/example/integration_test/goldens/ios/5_loadshapesGLB.png new file mode 100644 index 00000000..16145006 --- /dev/null +++ b/example/integration_test/goldens/ios/5_loadshapesGLB.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad4b4bd59ff6ffa870d3fffb0f9151ccc51392906a6fbdc8832e31b4965c64d6 +size 300230 diff --git a/example/integration_test/goldens/ios/6_zoomin.png b/example/integration_test/goldens/ios/6_zoomin.png new file mode 100644 index 00000000..7b9af793 --- /dev/null +++ b/example/integration_test/goldens/ios/6_zoomin.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a297bad3e356e06fb68c53b8718a72924dfd88ca2c5ed1f486fffe5780c77084 +size 1944342 diff --git a/example/integration_test/goldens/ios/7_rotate.png b/example/integration_test/goldens/ios/7_rotate.png new file mode 100644 index 00000000..5288e45b --- /dev/null +++ b/example/integration_test/goldens/ios/7_rotate.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19bc90955c575a28d9b8e4112c24fe9b65f3ba6df7a3579f036761fe6a54d065 +size 1937031 diff --git a/example/integration_test/goldens/ios/8_pan.png b/example/integration_test/goldens/ios/8_pan.png new file mode 100644 index 00000000..6683ad72 --- /dev/null +++ b/example/integration_test/goldens/ios/8_pan.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72b73825e31bf5d6ae3c880a17fe578f9351749d68a9b1fc710b838985b833fa +size 1938760 diff --git a/example/integration_test/goldens/ios/9_transformtounitcube.png b/example/integration_test/goldens/ios/9_transformtounitcube.png new file mode 100644 index 00000000..27fdaa03 --- /dev/null +++ b/example/integration_test/goldens/ios/9_transformtounitcube.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a3a78e7ec65d33d62105938de4a14f50a91ed4009beed588f17379ad3d7df9b6 +size 1941399 diff --git a/example/integration_test/plugin_integration_test.dart b/example/integration_test/plugin_integration_test.dart index acde9fdf..fcf4528a 100644 --- a/example/integration_test/plugin_integration_test.dart +++ b/example/integration_test/plugin_integration_test.dart @@ -1,25 +1,152 @@ -// This is a basic Flutter integration test. -// -// Since integration tests run in a full Flutter application, they can interact -// with the host side of a plugin implementation, unlike Dart unit tests. -// -// For more information about Flutter integration tests, please see -// https://docs.flutter.dev/cookbook/testing/integration/introduction - +import 'dart:async'; +import 'dart:io'; +import 'dart:ui'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; - -import 'package:polyvox_filament/polyvox_filament.dart'; +import 'package:golden_toolkit/golden_toolkit.dart'; +import 'package:polyvox_filament/widgets/filament_widget.dart'; +import '../lib/main.dart' as app; void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized() + as IntegrationTestWidgetsFlutterBinding; - testWidgets('getPlatformVersion test', (WidgetTester tester) async { - final PolyvoxFilament plugin = PolyvoxFilament(); - final String? version = await plugin.getPlatformVersion(); - // The version string depends on the host platform running the test, so - // just assert that some non-empty string is returned. - expect(version?.isNotEmpty, true); + late String platformIdentifier; + if (Platform.isIOS) { + platformIdentifier = "ios"; + } else if (Platform.isAndroid) { + platformIdentifier = "android"; + } else if (Platform.isMacOS) { + platformIdentifier = "macos"; + } else if (Platform.isWindows) { + platformIdentifier = "windows"; + } else if (Platform.isLinux) { + platformIdentifier = "linux"; + } else { + throw Exception("Unexpected platform"); + } + + int _counter = 0; + + Future _snapshot(WidgetTester tester, Device device, String label, + [int seconds = 0]) async { + for (int i = 0; i < seconds; i++) { + await Future.delayed(Duration(seconds: 1)); + await tester.pumpAndSettle(); + } + await Future.delayed(Duration(milliseconds: 100)); + await tester.pumpAndSettle(); + var screenshotPath = '$platformIdentifier/${_counter}_$label'; + if (Platform.isIOS) { + // this is currently hanging on Android + // see https://github.com/flutter/flutter/issues/127306 + // it is also not yet implemented on Windows or MacOS + await binding.convertFlutterSurfaceToImage(); + final bytes = await binding.takeScreenshot(screenshotPath); + } + _counter++; + } + + late Device device; + + Future tap(WidgetTester tester, String label, [int seconds = 0]) async { + var target = find.text(label).first; + await tester.dragUntilVisible( + target, + find.byType(SingleChildScrollView), + // widget you want to scroll + const Offset(0, 500), // delta to move + duration: Duration(milliseconds: 10)); + await tester.tap(target); + await _snapshot( + tester, device, label.replaceAll(RegExp("[ -:]"), ""), seconds); + } + + Future pumpUntilFound( + WidgetTester tester, + Finder finder, { + Duration timeout = const Duration(seconds: 30), + }) async { + bool timerDone = false; + final timer = Timer( + timeout, () => throw TimeoutException("Pump until has timed out")); + while (timerDone != true) { + await tester.pump(); + + final found = tester.any(finder); + if (found) { + timerDone = true; + } + } + timer.cancel(); + } + + testGoldens('test', (WidgetTester tester) async { + app.main(); + await pumpUntilFound(tester, find.byType(app.ExampleWidget)); + device = Device(size: Size(800, 600), name: "desktop"); + await _snapshot(tester, device, "fresh"); + await tap(tester, "create viewer (default ubershader)", 4); + + await tap(tester, "load skybox"); + await tap(tester, "load IBL"); + await tap(tester, "Rendering: false"); + await tap(tester, "load shapes GLB"); + + final Offset pointerLocation = + tester.getCenter(find.byType(FilamentWidget)); + TestPointer testPointer = TestPointer(1, PointerDeviceKind.mouse); + + // scroll/zoom + testPointer.hover(pointerLocation); + await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 1.0))); + await tester.pumpAndSettle(); + await tester.sendEventToBinding(testPointer.scroll(const Offset(0.0, 1.0))); + await tester.pumpAndSettle(); + await _snapshot(tester, device, "zoomin"); + + // rotate + testPointer = + TestPointer(1, PointerDeviceKind.mouse, null, kTertiaryButton); + testPointer.hover(pointerLocation); + await tester.sendEventToBinding(testPointer.down(pointerLocation)); + await tester.pumpAndSettle(); + await tester.sendEventToBinding( + testPointer.move(pointerLocation + Offset(10.0, 10.0))); + await tester.pumpAndSettle(); + await tester.sendEventToBinding( + testPointer.move(pointerLocation + Offset(20.0, 20.0))); + await tester.pumpAndSettle(); + await tester.sendEventToBinding(testPointer.up()); + + await _snapshot(tester, device, "rotate", 2); + + // pan + testPointer = TestPointer(1, PointerDeviceKind.mouse, null, kPrimaryButton); + testPointer.hover(pointerLocation); + await tester.sendEventToBinding(testPointer.down(pointerLocation)); + await tester + .sendEventToBinding(testPointer.move(pointerLocation + Offset(0, 1.0))); + + for (int i = 0; i < 60; i++) { + await tester.sendEventToBinding(testPointer + .move(pointerLocation + Offset(i.toDouble() * 2, i.toDouble() * 2))); + await tester.pumpAndSettle(); + } + await tester.sendEventToBinding(testPointer.up()); + await tester.pumpAndSettle(); + + await _snapshot(tester, device, "pan"); + + await tap(tester, "transform to unit cube"); + await tap(tester, "set shapes position to 1, 1, -1"); + await tap(tester, "Disable frustum culling"); + await tap(tester, "Set tone mapping to linear"); + await tap(tester, "Move camera to asset"); + await tap(tester, "move camera to 1, 1, -1"); + await tap(tester, 'set camera to first camera in shapes GLB'); }); } diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 8efc8651..2f81940b 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1,21 +1,34 @@ PODS: - Flutter (1.0.0) + - integration_test (0.0.1): + - Flutter + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS - polyvox_filament (0.0.1): - Flutter DEPENDENCIES: - Flutter (from `Flutter`) + - integration_test (from `.symlinks/plugins/integration_test/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - polyvox_filament (from `.symlinks/plugins/polyvox_filament/ios`) EXTERNAL SOURCES: Flutter: :path: Flutter + integration_test: + :path: ".symlinks/plugins/integration_test/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" polyvox_filament: :path: ".symlinks/plugins/polyvox_filament/ios" SPEC CHECKSUMS: Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 - polyvox_filament: 99047b2e0d56e073f5db603dd6152a1598c2a345 + integration_test: 13825b8a9334a850581300559b8839134b124670 + path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 + polyvox_filament: 35fece7761e74c973afd80fe3aa0ca225eaace32 PODFILE CHECKSUM: 7adbc9d59f05e1b01f554ea99b6c79e97f2214a2 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 43ac31b2..fca39eb4 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -138,6 +138,7 @@ 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + F9FAB8A67CF505858CCDA424 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -249,6 +250,23 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + F9FAB8A67CF505858CCDA424 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ diff --git a/example/lib/main.dart b/example/lib/main.dart index b95277b4..c37a5f7f 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -48,7 +48,7 @@ class _ExampleWidgetState extends State { List? _animations; FilamentEntity? _light; - late StreamSubscription _pickResultListener; + StreamSubscription? _pickResultListener; String? picked; final weights = List.filled(255, 0.0); @@ -63,19 +63,10 @@ class _ExampleWidgetState extends State { bool _coneHidden = false; bool _frustumCulling = true; - @override - void initState() { - getApplicationSupportDirectory().then((dir) { - print(dir); - }); - - super.initState(); - } - @override void dispose() { super.dispose(); - _pickResultListener.cancel(); + _pickResultListener?.cancel(); } Widget _item(void Function() onTap, String text) { @@ -136,7 +127,7 @@ class _ExampleWidgetState extends State { _rendering = !_rendering; _filamentController!.setRendering(_rendering); }); - }, "Rendering: $_rendering "), + }, "Rendering: $_rendering"), _item(() { setState(() { _framerate = _framerate == 60 ? 30 : 60; @@ -207,8 +198,8 @@ class _ExampleWidgetState extends State { _filamentController!.setPosition(_shapes!, 1.0, 1.0, -1.0); }, 'set shapes position to 1, 1, -1'), _item(() async { - _filamentController!.setPosition(_shapes!, 1.0, 1.0, -1.0); - }, 'move camera to shapes position'), + _filamentController!.setCameraPosition(1.0, 1.0, -1.0); + }, 'move camera to 1, 1, -1'), _item(() async { var frameData = Float32List.fromList( List.generate(120, (i) => i / 120).expand((x) { @@ -350,7 +341,7 @@ class _ExampleWidgetState extends State { right: 0, height: 200, child: Container( - color: Colors.white, + color: Colors.white.withOpacity(0.75), child: SingleChildScrollView( child: Wrap(children: children // _item(24 () async { 'rotate by pi around Y axis'), diff --git a/example/macos/Podfile.lock b/example/macos/Podfile.lock index b452d7ae..dc01329a 100644 --- a/example/macos/Podfile.lock +++ b/example/macos/Podfile.lock @@ -1,21 +1,28 @@ PODS: - FlutterMacOS (1.0.0) + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS - polyvox_filament (0.0.1): - FlutterMacOS DEPENDENCIES: - FlutterMacOS (from `Flutter/ephemeral`) + - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - polyvox_filament (from `Flutter/ephemeral/.symlinks/plugins/polyvox_filament/macos`) EXTERNAL SOURCES: FlutterMacOS: :path: Flutter/ephemeral + path_provider_foundation: + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin polyvox_filament: :path: Flutter/ephemeral/.symlinks/plugins/polyvox_filament/macos SPEC CHECKSUMS: FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - polyvox_filament: 59a161de3df49c867bc2003d4ff73b8a304d2feb + path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 + polyvox_filament: 9aa36ae5e5654ff1576534086cbd618b239b75d6 PODFILE CHECKSUM: 9cc8fc8fc62b1d9a89fd6f974ad4157b35254030 diff --git a/example/macos/Runner/DebugProfile.entitlements b/example/macos/Runner/DebugProfile.entitlements index dddb8a30..9f56413f 100644 --- a/example/macos/Runner/DebugProfile.entitlements +++ b/example/macos/Runner/DebugProfile.entitlements @@ -3,7 +3,7 @@ com.apple.security.app-sandbox - + com.apple.security.cs.allow-jit com.apple.security.network.server diff --git a/example/macos/Runner/Info.plist b/example/macos/Runner/Info.plist index 4789daa6..75d20137 100644 --- a/example/macos/Runner/Info.plist +++ b/example/macos/Runner/Info.plist @@ -28,5 +28,7 @@ MainMenu NSPrincipalClass NSApplication + com.apple.security.app-sandbox + diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 50520574..bc7e077e 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -26,7 +26,12 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + integration_test: + sdk: flutter flutter_lints: ^1.0.0 + golden_toolkit: ^0.15.0 + crypto: + image_compare: ^1.1.2 # For information on the generic Dart part of this file, see the diff --git a/example/regenerate_goldens.sh b/example/regenerate_goldens.sh new file mode 100644 index 00000000..97a2e1f1 --- /dev/null +++ b/example/regenerate_goldens.sh @@ -0,0 +1,8 @@ +#!/bin/bash +device=$1 +if [ -z "$device" ]; then + echo "Usage: $0 " + exit 1; +fi + +flutter drive --driver=test_driver/integration_test_update_goldens.dart -d $1 --target=integration_test/plugin_integration_test.dart diff --git a/example/test_driver/integration_test.dart b/example/test_driver/integration_test.dart new file mode 100644 index 00000000..e0e828b7 --- /dev/null +++ b/example/test_driver/integration_test.dart @@ -0,0 +1,58 @@ +import 'dart:io'; + +import 'package:integration_test/integration_test_driver_extended.dart'; +import 'package:image_compare/image_compare.dart'; + +Future main() async { + await integrationDriver( + onScreenshot: ( + String screenshotName, + List screenshotBytes, [ + Map? args, + ]) async { + final dir = screenshotName.split("/")[0]; + final name = screenshotName.split("/")[1]; + final File golden = await File('integration_test/goldens/$dir/$name.png'); + + if (!golden.existsSync()) { + throw Exception( + "Golden image doesn't exist yet. Make sure you have run integraton_test_update_goldens.dart first"); + } + + var result = await compareImages( + src1: screenshotBytes, + src2: golden.readAsBytesSync(), + algorithm: ChiSquareDistanceHistogram()); + + print(result); + + // TODO - it would be preferable to use Flutter's GoldenFileComparator here, e.g. + // + // ```var comparator = LocalFileComparator(testImage.uri); + // comparator.compare(imageBytes, golden) + // comparator.getFailureFile(failure, golden, basedir) + // var result = await comparator.compare( + // Uint8List.fromList(screenshotBytes), golden.uri); + // if (!result.passed) { + // for (var key in result.diffs!.keys) { + // var byteData = await result.diffs![key]!.toByteData(); + // File('integration_test/goldens/$dir/diffs/$name.png') + // .writeAsBytesSync( + // byteData!.buffer.asUint8List(byteData!.offsetInBytes)); + // } + // return false; + // }``` + // but this is only available via a Flutter shell which is currently unavailable (this script is run as a plain Dart file I guess). + // let's revisit if/when this changes + // see https://github.com/flutter/flutter/issues/51890 and https://github.com/flutter/flutter/issues/103222 + + if (result > 0.005) { + File('integration_test/goldens/$dir/diffs/$name.png') + .writeAsBytesSync(screenshotBytes); + return false; + } + + return true; + }, + ); +} diff --git a/example/test_driver/integration_test_update_goldens.dart b/example/test_driver/integration_test_update_goldens.dart new file mode 100644 index 00000000..e32febe2 --- /dev/null +++ b/example/test_driver/integration_test_update_goldens.dart @@ -0,0 +1,22 @@ +import 'dart:io'; +import 'package:crypto/crypto.dart'; +import 'package:integration_test/integration_test_driver_extended.dart'; + +Future main() async { + await integrationDriver( + onScreenshot: ( + String screenshotName, + List screenshotBytes, [ + Map? args, + ]) async { + final dir = screenshotName.split("/")[0]; + final name = screenshotName.split("/")[1]; + final File image = await File('integration_test/goldens/$dir/$name.png') + .create(recursive: true); + + image.writeAsBytesSync(screenshotBytes); + + return true; + }, + ); +} diff --git a/lib/filament_controller_ffi.dart b/lib/filament_controller_ffi.dart index ef8427d6..a2afc4f1 100644 --- a/lib/filament_controller_ffi.dart +++ b/lib/filament_controller_ffi.dart @@ -198,9 +198,16 @@ class FilamentControllerFFI extends FilamentController { /// @override Future resize(int width, int height, {double scaleFactor = 1.0}) async { + if (_textureId == null) { + print("No texture created, ignoring call to resize."); + return; + } _resizing = true; setRendering(false); - _lib.destroy_swap_chain(_viewer!); + if (_viewer != null) { + _lib.destroy_swap_chain(_viewer!); + } + await destroyTexture(); size = ui.Size(width * _pixelRatio, height * _pixelRatio); diff --git a/lib/widgets/filament_widget.dart b/lib/widgets/filament_widget.dart index 590151c6..db0a016a 100644 --- a/lib/widgets/filament_widget.dart +++ b/lib/widgets/filament_widget.dart @@ -183,6 +183,7 @@ class _FilamentWidgetState extends State { _resizeTimer?.cancel(); _resizeTimer = Timer(Duration(milliseconds: 500), () async { + print("Resizing to $newSize"); await widget.controller .resize(newSize.width.toInt(), newSize.height.toInt()); WidgetsBinding.instance.addPostFrameCallback((_) async {