diff --git a/example/lib/menus/asset_submenu.dart b/example/lib/menus/asset_submenu.dart index 752fd7c6..ab757a71 100644 --- a/example/lib/menus/asset_submenu.dart +++ b/example/lib/menus/asset_submenu.dart @@ -43,13 +43,16 @@ class _AssetSubmenuState extends State { await widget.controller.addBoneAnimation( ExampleWidgetState.assets.last, BoneAnimationData( - "Bone", + ["Bone"], ["Cylinder"], List.generate( 60, - (idx) => v.Quaternion.axisAngle( - v.Vector3(0, 0, 1), pi * 8 * (idx / 60)) - .normalized()), + (idx) => [ + v.Quaternion.axisAngle( + v.Vector3(0, 0, 1), pi * 8 * (idx / 60)) + .normalized() + ]), + List.generate(60, (idx) => [v.Vector3.zero()]), 1000.0 / 60.0)); }, child: const Text('Set bone transform animation for Cylinder')), diff --git a/ios/src/FlutterFilamentFFIApi.cpp b/ios/src/FlutterFilamentFFIApi.cpp index 7bad2b3f..1da05d88 100644 --- a/ios/src/FlutterFilamentFFIApi.cpp +++ b/ios/src/FlutterFilamentFFIApi.cpp @@ -21,9 +21,6 @@ #include #include -using namespace polyvox; -using namespace std::chrono_literals; - #include #include @@ -31,9 +28,11 @@ extern "C" { extern FLUTTER_PLUGIN_EXPORT EMSCRIPTEN_WEBGL_CONTEXT_HANDLE flutter_filament_web_create_gl_context(); } - -#endif #include +#endif + +using namespace polyvox; +using namespace std::chrono_literals; class RenderLoop { public: diff --git a/lib/animations/animation_builder.dart b/lib/animations/animation_builder.dart index d87712a1..83aca99c 100644 --- a/lib/animations/animation_builder.dart +++ b/lib/animations/animation_builder.dart @@ -47,7 +47,7 @@ class AnimationBuilder { } } return MorphAnimationData( - meshName, + [meshName], morphData, _morphTargets.map((i) => availableMorphs[i]).toList(), _frameLengthInMs); diff --git a/lib/animations/animation_data.dart b/lib/animations/animation_data.dart index 76fc715c..42c72178 100644 --- a/lib/animations/animation_data.dart +++ b/lib/animations/animation_data.dart @@ -1,5 +1,3 @@ -import 'dart:typed_data'; - import 'package:vector_math/vector_math_64.dart'; /// @@ -43,10 +41,21 @@ class MorphAnimationData { /// [frameData] is laid out as [locX, locY, locZ, rotW, rotX, rotY, rotZ] /// class BoneAnimationData { - final String boneName; + final List bones; final List meshNames; - final List frameData; + final List> rotationFrameData; + final List> translationFrameData; double frameLengthInMs; - BoneAnimationData( - this.boneName, this.meshNames, this.frameData, this.frameLengthInMs); + final bool isModelSpace; + BoneAnimationData(this.bones, this.meshNames, this.rotationFrameData, + this.translationFrameData, this.frameLengthInMs, + {this.isModelSpace = false}); + + int get numFrames => rotationFrameData.length; + + BoneAnimationData frame(int frame) { + return BoneAnimationData(bones, meshNames, [rotationFrameData[frame]], + [translationFrameData[frame]], frameLengthInMs, + isModelSpace: isModelSpace); + } } diff --git a/lib/animations/bvh.dart b/lib/animations/bvh.dart new file mode 100644 index 00000000..b2d6583f --- /dev/null +++ b/lib/animations/bvh.dart @@ -0,0 +1,139 @@ +import 'dart:io'; +import 'dart:math'; +import 'dart:ui'; +import 'package:flutter_filament/animations/animation_data.dart'; +import 'package:vector_math/vector_math_64.dart'; + +enum RotationMode { ZYX, XYZ } + +enum Axes { Filament, Blender } + +Map _permutations = { + RotationMode.XYZ: Matrix3.identity() + // RotationMode.ZYX:Matrix3.columns([],[],[]), +}; + +class BVHParser { + static Map parseARPRemap(String arpRemapData) { + final remap = {}; + var data = arpRemapData.split("\n"); + for (int i = 0; i < data.length;) { + var srcBone = data[i].split("%")[0]; + if (srcBone.isNotEmpty && srcBone != "None") { + var targetBone = data[i + 1].trim(); + remap[targetBone] = srcBone; + } + i += 5; + } + return remap; + } + + static BoneAnimationData parse(String data, List meshNames, + {Map? remap, + RegExp? boneRegex, + RotationMode rotationMode = RotationMode.ZYX, + Vector3? rootTranslationOffset, + axes = Axes.Filament}) { + // parse the list/hierarchy of bones + final bones = []; + double frameLengthInMs = 0.0; + var iter = data.split("\n").iterator; + while (iter.moveNext()) { + final line = iter.current.trim(); + if (line.startsWith('ROOT') || line.startsWith('JOINT')) { + var bone = line.split(' ')[1]; + if (remap?.containsKey(bone) == true) { + print("Remapping $bone to ${remap![bone]!}"); + bone = remap![bone]!; + } + bones.add(bone); + } else if (line.startsWith('Frame Time:')) { + var frameTime = line.split(' ').last.trim(); + frameLengthInMs = + double.parse(frameTime) * 1000; // Convert to milliseconds + print("Got frame time $frameTime frameLengthInMs $frameLengthInMs"); + break; + } + } + + // filter out any bones that don't match the regexp (if provided) + final boneIndices = boneRegex == null + ? List.generate(bones.length, (index) => index) + : bones.indexed + .where((bone) => boneRegex.hasMatch(bone.$2)) + .map((bone) => bone.$1); + + // the remaining lines contain the actual animation data + // we assume the first six are LOCX LOCY LOCZ ROTZ ROTY ROTX for the root bone, then ROTZ ROTY ROTX for the remainder + final translationData = >[]; + final rotationData = >[]; + while (iter.moveNext()) { + final line = iter.current; + if (line.isEmpty) { + break; + } + var parseResult = _parseFrameData(line, + axes: axes, rootTranslationOffset: rootTranslationOffset); + + rotationData.add( + boneIndices.map((idx) => parseResult.rotationData[idx]).toList()); + translationData.add([parseResult.translationData] + + List.filled(bones.length - 1, Vector3.zero())); + } + + return BoneAnimationData(boneIndices.map((idx) => bones[idx]).toList(), + meshNames, rotationData, translationData, frameLengthInMs, + isModelSpace: true); + } + + static ({List rotationData, Vector3 translationData}) + _parseFrameData(String frameLine, + {Vector3? rootTranslationOffset, axes = Axes.Filament}) { + final frameValues = []; + for (final entry in frameLine.split(RegExp(r'\s+'))) { + if (entry.isNotEmpty) { + frameValues.add(double.parse(entry)); + } + } + // first 3 values are root node position (translation), remainder are ZYX rotatons + // this is hardcoded assumption for BVH files generated by momask only; won't work for any other animations in general + + // Blender exports BVH using same coordinate system (i.e. Z is up, Y points into screen) + // but Filament uses Y-up/Z forward + late Vector3 rootTranslation; + late Vector3 Z, Y, X; + if (axes == Axes.Blender) { + rootTranslation = + Vector3(frameValues[0], frameValues[2], -frameValues[1]); + Z = Vector3(0, 1, 0); + Y = Vector3(0, 0, -1); + X = Vector3(1, 0, 0); + } else { + rootTranslation = Vector3( + frameValues[0], + frameValues[1], + frameValues[2], + ); + Z = Vector3(0, 0, 1); + Y = Vector3(0, 1, 0); + X = Vector3(1, 0, 0); + } + if (rootTranslationOffset != null) { + rootTranslation -= rootTranslationOffset; + } + + List frameData = []; + for (int i = 3; i < frameValues.length; i += 3) { + var raw = frameValues.sublist(i, i + 3); + var z = Quaternion.axisAngle(Z, radians(raw[0])); + var y = Quaternion.axisAngle(Y, radians(raw[1])); + var x = Quaternion.axisAngle(X, radians(raw[2])); + var rotation = z * y * x; + + frameData.add(rotation.normalized()); + } + return (rotationData: frameData, translationData: rootTranslation); + } + + static double radians(double degrees) => degrees * (pi / 180.0); +} diff --git a/lib/filament_controller.dart b/lib/filament_controller.dart index 71dab108..c2afaf04 100644 --- a/lib/filament_controller.dart +++ b/lib/filament_controller.dart @@ -38,12 +38,6 @@ abstract class FilamentController { Stream get onUnload; - /// - /// A [ValueNotifier] that holds the current dimensions (in physical pixels, after multiplying by pixel ratio) of the FilamentWidget. - /// If you need to perform work as early as possible, add a listener to this property before a [FilamentWidget] has been inserted into the widget hierarchy. - /// - ValueNotifier get rect; - /// /// A [ValueNotifier] to indicate whether a FilamentViewer is currently available. /// (FilamentViewer is a C++ type, hence why it is not referenced) here. @@ -168,6 +162,11 @@ abstract class FilamentController { /// Future loadIbl(String lightingPath, {double intensity = 30000}); + /// + /// Rotates the IBL & skybox. + /// + Future rotateIbl(Matrix3 rotation); + /// /// Removes the image-based light from the scene. /// @@ -273,6 +272,12 @@ abstract class FilamentController { Future setMorphAnimationData( FilamentEntity entity, MorphAnimationData animation); + /// + /// Resets all bones in the given entity to their rest pose. + /// This should be done before every call to addBoneAnimation. + /// + Future resetBones(FilamentEntity entity); + /// /// Starts animating a bone (joint) according to the specified [animation]. /// @@ -503,6 +508,11 @@ abstract class FilamentController { Future getChildEntity( FilamentEntity parent, String childName); + /// + /// Lists all child meshes under the given entity. + /// + Future> getMeshNames(FilamentEntity entity, {bool async = true}); + /// /// If [recording] is set to true, each frame the framebuffer/texture will be written to /tmp/output_*.png. /// This will impact performance; handle with care. diff --git a/lib/filament_controller_ffi.dart b/lib/filament_controller_ffi.dart index 465e493e..953cdabe 100644 --- a/lib/filament_controller_ffi.dart +++ b/lib/filament_controller_ffi.dart @@ -42,7 +42,8 @@ class FilamentControllerFFI extends FilamentController { Pointer _driver = nullptr.cast(); @override - final rect = ValueNotifier(null); + final _rect = ValueNotifier(null); + final _rectCompleter = Completer(); @override final hasViewer = ValueNotifier(false); @@ -65,6 +66,13 @@ class FilamentControllerFFI extends FilamentController { final _onUnloadController = StreamController.broadcast(); Stream get onUnload => _onUnloadController.stream; + final allocator = calloc; + + void _using(Pointer ptr, Future Function(Pointer ptr) function) async { + await function.call(ptr); + allocator.free(ptr); + } + /// /// 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 FlutterFilamentApi.h. @@ -83,7 +91,7 @@ class FilamentControllerFFI extends FilamentController { _resizingWidth = call.arguments[0]; _resizingHeight = call.arguments[1]; _resizeTimer = Timer(const Duration(milliseconds: 500), () async { - rect.value = Offset.zero & + _rect.value = Offset.zero & ui.Size(_resizingWidth!.toDouble(), _resizingHeight!.toDouble()); await resize(); }); @@ -132,12 +140,15 @@ class FilamentControllerFFI extends FilamentController { @override Future setDimensions(Rect rect, double pixelRatio) async { - this.rect.value = Rect.fromLTWH( + this._rect.value = Rect.fromLTWH( (rect.left * _pixelRatio).floor().toDouble(), rect.top * _pixelRatio.floor().toDouble(), (rect.width * _pixelRatio).ceil().toDouble(), (rect.height * _pixelRatio).ceil().toDouble()); _pixelRatio = pixelRatio; + if (!_rectCompleter.isCompleted) { + _rectCompleter.complete(this._rect.value); + } } @override @@ -169,12 +180,23 @@ class FilamentControllerFFI extends FilamentController { dev.log("Texture destroyed"); } + bool _creating = false; + /// /// Called by `FilamentWidget`. You do not need to call this yourself. /// @override Future createViewer() async { - if (rect.value == null) { + if (_creating) { + throw Exception( + "An existing call to createViewer is pending completion."); + } + _creating = true; + print("Waiting for widget dimensions to become available.."); + await _rectCompleter.future; + print("Got widget dimensions : ${_rect.value}"); + + if (_rect.value == null) { throw Exception( "Dimensions have not yet been set by FilamentWidget. You need to wait for at least one frame after FilamentWidget has been inserted into the hierarchy"); } @@ -209,13 +231,17 @@ class FilamentControllerFFI extends FilamentController { dev.log("Got rendering surface"); + final uberarchivePtr = + uberArchivePath?.toNativeUtf8().cast() ?? nullptr; + _viewer = create_filament_viewer_ffi( Pointer.fromAddress(renderingSurface.sharedContext), _driver, - uberArchivePath?.toNativeUtf8().cast() ?? nullptr, + uberarchivePtr, loader, renderCallback, renderCallbackOwner); + allocator.free(uberarchivePtr); dev.log("Created viewer"); if (_viewer!.address == 0) { throw Exception("Failed to create viewer. Check logs for details"); @@ -224,31 +250,32 @@ class FilamentControllerFFI extends FilamentController { _assetManager = get_asset_manager(_viewer!); create_swap_chain_ffi(_viewer!, renderingSurface.surface, - rect.value!.width.toInt(), rect.value!.height.toInt()); + _rect.value!.width.toInt(), _rect.value!.height.toInt()); dev.log("Created swap chain"); if (renderingSurface.textureHandle != 0) { dev.log( "Creating render target from native texture ${renderingSurface.textureHandle}"); create_render_target_ffi(_viewer!, renderingSurface.textureHandle, - rect.value!.width.toInt(), rect.value!.height.toInt()); + _rect.value!.width.toInt(), _rect.value!.height.toInt()); } textureDetails.value = TextureDetails( textureId: renderingSurface.flutterTextureId, - width: rect.value!.width.toInt(), - height: rect.value!.height.toInt()); + width: _rect.value!.width.toInt(), + height: _rect.value!.height.toInt()); dev.log("texture details ${textureDetails.value}"); update_viewport_and_camera_projection_ffi( - _viewer!, rect.value!.width.toInt(), rect.value!.height.toInt(), 1.0); + _viewer!, _rect.value!.width.toInt(), _rect.value!.height.toInt(), 1.0); hasViewer.value = true; + _creating = false; } Future _createRenderingSurface() async { return RenderingSurface.from(await _channel.invokeMethod("createTexture", [ - rect.value!.width, - rect.value!.height, - rect.value!.left, - rect.value!.top + _rect.value!.width, + _rect.value!.height, + _rect.value!.left, + _rect.value!.top ])); } @@ -342,12 +369,12 @@ class FilamentControllerFFI extends FilamentController { "destroyTexture", textureDetails.value!.textureId); } } else if (Platform.isWindows) { - dev.log("Resizing window with rect $rect"); + dev.log("Resizing window with rect ${_rect.value}"); await _channel.invokeMethod("resizeWindow", [ - rect.value!.width, - rect.value!.height, - rect.value!.left, - rect.value!.top + _rect.value!.width, + _rect.value!.height, + _rect.value!.left, + _rect.value!.top ]); } @@ -361,23 +388,23 @@ class FilamentControllerFFI extends FilamentController { if (!_usesBackingWindow) { create_swap_chain_ffi(_viewer!, renderingSurface.surface, - rect.value!.width.toInt(), rect.value!.height.toInt()); + _rect.value!.width.toInt(), _rect.value!.height.toInt()); } if (renderingSurface.textureHandle != 0) { dev.log( "Creating render target from native texture ${renderingSurface.textureHandle}"); create_render_target_ffi(_viewer!, renderingSurface.textureHandle, - rect.value!.width.toInt(), rect.value!.height.toInt()); + _rect.value!.width.toInt(), _rect.value!.height.toInt()); } textureDetails.value = TextureDetails( textureId: renderingSurface.flutterTextureId, - width: rect.value!.width.toInt(), - height: rect.value!.height.toInt()); + width: _rect.value!.width.toInt(), + height: _rect.value!.height.toInt()); - update_viewport_and_camera_projection_ffi( - _viewer!, rect.value!.width.toInt(), rect.value!.height.toInt(), 1.0); + update_viewport_and_camera_projection_ffi(_viewer!, + _rect.value!.width.toInt(), _rect.value!.height.toInt(), 1.0); await setRendering(_rendering); } finally { @@ -398,8 +425,10 @@ class FilamentControllerFFI extends FilamentController { if (_viewer == null) { throw Exception("No viewer available, ignoring"); } - set_background_image_ffi( - _viewer!, path.toNativeUtf8().cast(), fillHeight); + final pathPtr = path.toNativeUtf8().cast(); + + set_background_image_ffi(_viewer!, pathPtr, fillHeight); + allocator.free(pathPtr); } @override @@ -429,7 +458,8 @@ class FilamentControllerFFI extends FilamentController { if (_viewer == null) { throw Exception("No viewer available, ignoring"); } - load_skybox_ffi(_viewer!, skyboxPath.toNativeUtf8().cast()); + final pathPtr = skyboxPath.toNativeUtf8().cast(); + load_skybox_ffi(_viewer!, pathPtr); } @override @@ -437,7 +467,21 @@ class FilamentControllerFFI extends FilamentController { if (_viewer == null) { throw Exception("No viewer available, ignoring"); } - load_ibl_ffi(_viewer!, lightingPath.toNativeUtf8().cast(), intensity); + final pathPtr = lightingPath.toNativeUtf8().cast(); + load_ibl_ffi(_viewer!, pathPtr, intensity); + } + + @override + Future rotateIbl(Matrix3 rotationMatrix) async { + if (_viewer == nullptr) { + throw Exception("No viewer available, ignoring"); + } + var floatPtr = allocator(9); + for (int i = 0; i < 9; i++) { + floatPtr.elementAt(i).value = rotationMatrix.storage[i]; + } + rotate_ibl(_viewer!, floatPtr); + allocator.free(floatPtr); } @override @@ -508,8 +552,9 @@ class FilamentControllerFFI extends FilamentController { if (unlit) { throw Exception("Not yet implemented"); } - var entity = - load_glb_ffi(_assetManager!, path.toNativeUtf8().cast(), unlit); + final pathPtr = path.toNativeUtf8().cast(); + var entity = load_glb_ffi(_assetManager!, pathPtr, unlit); + allocator.free(pathPtr); if (entity == _FILAMENT_ASSET_ERROR) { throw Exception("An error occurred loading the asset at $path"); } @@ -528,8 +573,13 @@ class FilamentControllerFFI extends FilamentController { if (_viewer == null) { throw Exception("No viewer available, ignoring"); } - var entity = load_gltf_ffi(_assetManager!, path.toNativeUtf8().cast(), - relativeResourcePath.toNativeUtf8().cast()); + final pathPtr = path.toNativeUtf8().cast(); + final relativeResourcePathPtr = + relativeResourcePath.toNativeUtf8().cast(); + var entity = + load_gltf_ffi(_assetManager!, pathPtr, relativeResourcePathPtr); + allocator.free(pathPtr); + allocator.free(relativeResourcePathPtr); if (entity == _FILAMENT_ASSET_ERROR) { throw Exception("An error occurred loading the asset at $path"); } @@ -592,16 +642,16 @@ class FilamentControllerFFI extends FilamentController { if (_viewer == null) { throw Exception("No viewer available, ignoring"); } - var weightsPtr = calloc(weights.length); + var weightsPtr = allocator(weights.length); for (int i = 0; i < weights.length; i++) { weightsPtr.elementAt(i).value = weights[i]; } - var meshNamePtr = meshName.toNativeUtf8(allocator: calloc).cast(); + var meshNamePtr = meshName.toNativeUtf8(allocator: allocator).cast(); set_morph_target_weights_ffi( _assetManager!, entity, meshNamePtr, weightsPtr, weights.length); - calloc.free(weightsPtr); - calloc.free(meshNamePtr); + allocator.free(weightsPtr); + allocator.free(meshNamePtr); } @override @@ -611,15 +661,16 @@ class FilamentControllerFFI extends FilamentController { throw Exception("No viewer available, ignoring"); } var names = []; - var count = get_morph_target_name_count_ffi( - _assetManager!, entity, meshName.toNativeUtf8().cast()); - var outPtr = calloc(255); + var meshNamePtr = meshName.toNativeUtf8().cast(); + var count = + get_morph_target_name_count_ffi(_assetManager!, entity, meshNamePtr); + var outPtr = allocator(255); for (int i = 0; i < count; i++) { - get_morph_target_name(_assetManager!, entity, - meshName.toNativeUtf8().cast(), outPtr, i); + get_morph_target_name(_assetManager!, entity, meshNamePtr, outPtr, i); names.add(outPtr.cast().toDartString()); } - calloc.free(outPtr); + allocator.free(outPtr); + allocator.free(meshNamePtr); return names.cast(); } @@ -630,7 +681,7 @@ class FilamentControllerFFI extends FilamentController { } var animationCount = get_animation_count(_assetManager!, entity); var names = []; - var outPtr = calloc(255); + var outPtr = allocator(255); for (int i = 0; i < animationCount; i++) { get_animation_name_ffi(_assetManager!, entity, outPtr, i); names.add(outPtr.cast().toDartString()); @@ -658,12 +709,12 @@ class FilamentControllerFFI extends FilamentController { throw Exception("No viewer available, ignoring"); } - var dataPtr = calloc(animation.data.length); + var dataPtr = allocator(animation.data.length); for (int i = 0; i < animation.data.length; i++) { dataPtr.elementAt(i).value = animation.data[i]; } - Pointer idxPtr = calloc(animation.morphTargets.length); + Pointer idxPtr = allocator(animation.morphTargets.length); for (var meshName in animation.meshNames) { // the morph targets in [animation] might be a subset of those that actually exist in the mesh (and might not have the same order) @@ -674,15 +725,16 @@ class FilamentControllerFFI extends FilamentController { for (int i = 0; i < animation.numMorphTargets; i++) { var index = meshMorphTargets.indexOf(animation.morphTargets[i]); if (index == -1) { - calloc.free(dataPtr); - calloc.free(idxPtr); + allocator.free(dataPtr); + allocator.free(idxPtr); throw Exception( "Morph target ${animation.morphTargets[i]} is specified in the animation but could not be found in the mesh $meshName under entity $entity"); } idxPtr.elementAt(i).value = index; } - var meshNamePtr = meshName.toNativeUtf8(allocator: calloc).cast(); + var meshNamePtr = + meshName.toNativeUtf8(allocator: allocator).cast(); set_morph_animation( _assetManager!, @@ -693,47 +745,72 @@ class FilamentControllerFFI extends FilamentController { animation.numMorphTargets, animation.numFrames, (animation.frameLengthInMs)); - calloc.free(meshNamePtr); + allocator.free(meshNamePtr); } - calloc.free(dataPtr); - calloc.free(idxPtr); + allocator.free(dataPtr); + allocator.free(idxPtr); } @override Future addBoneAnimation( - FilamentEntity entity, BoneAnimationData animation) async { + FilamentEntity entity, + BoneAnimationData animation, + ) async { if (_viewer == null) { throw Exception("No viewer available, ignoring"); } - var numFrames = animation.frameData.length; + var numFrames = animation.rotationFrameData.length; - var meshNames = calloc>(animation.meshNames.length); + var meshNames = allocator>(animation.meshNames.length); for (int i = 0; i < animation.meshNames.length; i++) { - meshNames.elementAt(i).value = - animation.meshNames[i].toNativeUtf8().cast(); + meshNames.elementAt(i).value = animation.meshNames[i] + .toNativeUtf8(allocator: allocator) + .cast(); } - var data = calloc(numFrames * 4); + var data = allocator(numFrames * 16); + DateTime start = DateTime.now(); - for (int i = 0; i < numFrames; i++) { - data.elementAt(i * 4).value = animation.frameData[i].w; - data.elementAt((i * 4) + 1).value = animation.frameData[i].x; - data.elementAt((i * 4) + 2).value = animation.frameData[i].y; - data.elementAt((i * 4) + 3).value = animation.frameData[i].z; + for (var boneIndex = 0; boneIndex < animation.bones.length; boneIndex++) { + var bone = animation.bones[boneIndex]; + var boneNamePtr = bone.toNativeUtf8(allocator: allocator).cast(); + + for (int i = 0; i < numFrames; i++) { + var rotation = animation.rotationFrameData[i][boneIndex]; + var translation = animation.translationFrameData[i][boneIndex]; + var mat4 = Matrix4.compose(translation, rotation, Vector3.all(1.0)); + for (int j = 0; j < 16; j++) { + data.elementAt((i * 16) + j).value = mat4.storage[j]; + } + } + + add_bone_animation_ffi( + _assetManager!, + entity, + data, + numFrames, + boneNamePtr, + meshNames, + animation.meshNames.length, + animation.frameLengthInMs, + animation.isModelSpace); + + allocator.free(boneNamePtr); } + allocator.free(data); + for (int i = 0; i < animation.meshNames.length; i++) { + allocator.free(meshNames.elementAt(i).value); + } + allocator.free(meshNames); + } - add_bone_animation( - _assetManager!, - entity, - data, - numFrames, - animation.boneName.toNativeUtf8().cast(), - meshNames, - animation.meshNames.length, - animation.frameLengthInMs); - calloc.free(data); - calloc.free(meshNames); + @override + Future resetBones(FilamentEntity entity) async { + if (_viewer == nullptr) { + throw Exception("No viewer available, ignoring"); + } + reset_to_rest_pose_ffi(_assetManager!, entity); } @override @@ -818,8 +895,9 @@ class FilamentControllerFFI extends FilamentController { if (_viewer == null) { throw Exception("No viewer available, ignoring"); } - var result = set_camera( - _viewer!, entity, name?.toNativeUtf8().cast() ?? nullptr); + var cameraNamePtr = name?.toNativeUtf8().cast() ?? nullptr; + var result = set_camera(_viewer!, entity, cameraNamePtr); + allocator.free(cameraNamePtr); if (!result) { throw Exception("Failed to set camera"); } @@ -932,12 +1010,12 @@ class FilamentControllerFFI extends FilamentController { throw Exception("No viewer available, ignoring"); } assert(matrix.length == 16); - var ptr = calloc(16); + var ptr = allocator(16); for (int i = 0; i < 16; i++) { ptr.elementAt(i).value = matrix[i]; } set_camera_model_matrix(_viewer!, ptr); - calloc.free(ptr); + allocator.free(ptr); } @override @@ -946,15 +1024,17 @@ class FilamentControllerFFI extends FilamentController { if (_viewer == null) { throw Exception("No viewer available, ignoring"); } + var meshNamePtr = meshName.toNativeUtf8().cast(); var result = set_material_color( _assetManager!, entity, - meshName.toNativeUtf8().cast(), + meshNamePtr, materialIndex, color.red.toDouble() / 255.0, color.green.toDouble() / 255.0, color.blue.toDouble() / 255.0, color.alpha.toDouble() / 255.0); + allocator.free(meshNamePtr); if (!result) { throw Exception("Failed to set material color"); } @@ -999,9 +1079,9 @@ class FilamentControllerFFI extends FilamentController { if (_viewer == null) { throw Exception("No viewer available, ignoring"); } - if (hide_mesh( - _assetManager!, entity, meshName.toNativeUtf8().cast()) != - 1) {} + final meshNamePtr = meshName.toNativeUtf8().cast(); + if (hide_mesh(_assetManager!, entity, meshNamePtr) != 1) {} + allocator.free(meshNamePtr); } @override @@ -1009,9 +1089,11 @@ class FilamentControllerFFI extends FilamentController { if (_viewer == null) { throw Exception("No viewer available, ignoring"); } - if (reveal_mesh( - _assetManager!, entity, meshName.toNativeUtf8().cast()) != - 1) { + + final meshNamePtr = meshName.toNativeUtf8().cast(); + final result = reveal_mesh(_assetManager!, entity, meshNamePtr) != 1; + allocator.free(meshNamePtr); + if (!result) { throw Exception("Failed to reveal mesh $meshName"); } } @@ -1030,7 +1112,7 @@ class FilamentControllerFFI extends FilamentController { if (_viewer == null) { throw Exception("No viewer available, ignoring"); } - final outPtr = calloc(1); + final outPtr = allocator(1); outPtr.value = 0; pick_ffi(_viewer!, x, textureDetails.value!.height - y, outPtr); @@ -1039,13 +1121,13 @@ class FilamentControllerFFI extends FilamentController { await Future.delayed(const Duration(milliseconds: 32)); wait++; if (wait > 10) { - calloc.free(outPtr); + allocator.free(outPtr); throw Exception("Failed to get picking result"); } } var entityId = outPtr.value; _pickResultController.add(entityId); - calloc.free(outPtr); + allocator.free(outPtr); } @override @@ -1055,7 +1137,7 @@ class FilamentControllerFFI extends FilamentController { } var arrayPtr = get_camera_view_matrix(_viewer!); var viewMatrix = Matrix4.fromList(arrayPtr.asTypedList(16)); - calloc.free(arrayPtr); + allocator.free(arrayPtr); return viewMatrix; } @@ -1066,7 +1148,7 @@ class FilamentControllerFFI extends FilamentController { } var arrayPtr = get_camera_model_matrix(_viewer!); var modelMatrix = Matrix4.fromList(arrayPtr.asTypedList(16)); - calloc.free(arrayPtr); + allocator.free(arrayPtr); return modelMatrix; } @@ -1179,20 +1261,20 @@ class FilamentControllerFFI extends FilamentController { @override Future setBoneTransform(FilamentEntity entity, String meshName, String boneName, Matrix4 data) async { - var ptr = calloc(16); + var ptr = allocator(16); for (int i = 0; i < 16; i++) { ptr.elementAt(i).value = data.storage[i]; } - var meshNamePtr = meshName.toNativeUtf8(allocator: calloc).cast(); - var boneNamePtr = boneName.toNativeUtf8(allocator: calloc).cast(); + var meshNamePtr = meshName.toNativeUtf8(allocator: allocator).cast(); + var boneNamePtr = boneName.toNativeUtf8(allocator: allocator).cast(); var result = set_bone_transform_ffi( _assetManager!, entity, meshNamePtr, ptr, boneNamePtr); - calloc.free(ptr); - calloc.free(meshNamePtr); - calloc.free(boneNamePtr); + allocator.free(ptr); + allocator.free(meshNamePtr); + allocator.free(boneNamePtr); if (!result) { throw Exception("Failed to set bone transform. See logs for details"); } @@ -1201,18 +1283,31 @@ class FilamentControllerFFI extends FilamentController { @override Future getChildEntity( FilamentEntity parent, String childName) async { - var childNamePtr = childName.toNativeUtf8(allocator: calloc).cast(); - try { - var childEntity = - find_child_entity_by_name(_assetManager!, parent, childNamePtr); - if (childEntity == _FILAMENT_ASSET_ERROR) { - throw Exception( - "Could not find child ${childName} under the specified entity"); - } - return childEntity; - } finally { - calloc.free(childNamePtr); + var childNamePtr = + childName.toNativeUtf8(allocator: allocator).cast(); + + var childEntity = + find_child_entity_by_name(_assetManager!, parent, childNamePtr); + allocator.free(childNamePtr); + if (childEntity == _FILAMENT_ASSET_ERROR) { + throw Exception( + "Could not find child ${childName} under the specified entity"); } + return childEntity; + } + + Future> getMeshNames(FilamentEntity entity, + {bool async = false}) async { + var count = get_entity_count(_assetManager!, entity, true); + var names = []; + for (int i = 0; i < count; i++) { + var name = get_entity_name_at(_assetManager!, entity, i, true); + if (name == nullptr) { + throw Exception("Failed to find mesh at index $i"); + } + names.add(name.cast().toDartString()); + } + return names; } @override @@ -1222,8 +1317,8 @@ class FilamentControllerFFI extends FilamentController { @override Future setRecordingOutputDirectory(String outputDir) async { - var pathPtr = outputDir.toNativeUtf8(allocator: calloc); + var pathPtr = outputDir.toNativeUtf8(allocator: allocator); set_recording_output_directory(_viewer!, pathPtr.cast()); - calloc.free(pathPtr); + allocator.free(pathPtr); } } diff --git a/lib/generated_bindings.dart b/lib/generated_bindings.dart index aa139ff6..ee99d594 100644 --- a/lib/generated_bindings.dart +++ b/lib/generated_bindings.dart @@ -122,6 +122,13 @@ external void load_ibl( double intensity, ); +@ffi.Native, ffi.Pointer)>( + symbol: 'rotate_ibl', assetId: 'flutter_filament_plugin') +external void rotate_ibl( + ffi.Pointer viewer, + ffi.Pointer rotationMatrix, +); + @ffi.Native)>( symbol: 'remove_skybox', assetId: 'flutter_filament_plugin') external void remove_skybox( @@ -364,6 +371,13 @@ external bool set_morph_animation( double frameLengthInMs, ); +@ffi.Native, EntityId)>( + symbol: 'reset_to_rest_pose', assetId: 'flutter_filament_plugin') +external void reset_to_rest_pose( + ffi.Pointer assetManager, + int asset, +); + @ffi.Native< ffi.Void Function( ffi.Pointer, @@ -373,7 +387,8 @@ external bool set_morph_animation( ffi.Pointer, ffi.Pointer>, ffi.Int, - ffi.Float)>( + ffi.Float, + ffi.Bool)>( symbol: 'add_bone_animation', assetId: 'flutter_filament_plugin') external void add_bone_animation( ffi.Pointer assetManager, @@ -384,6 +399,7 @@ external void add_bone_animation( ffi.Pointer> meshNames, int numMeshTargets, double frameLengthInMs, + bool isModelSpace, ); @ffi.Native< @@ -758,6 +774,25 @@ external int find_child_entity_by_name( ffi.Pointer name, ); +@ffi.Native, EntityId, ffi.Bool)>( + symbol: 'get_entity_count', assetId: 'flutter_filament_plugin') +external int get_entity_count( + ffi.Pointer assetManager, + int target, + bool renderableOnly, +); + +@ffi.Native< + ffi.Pointer Function( + ffi.Pointer, EntityId, ffi.Int, ffi.Bool)>( + symbol: 'get_entity_name_at', assetId: 'flutter_filament_plugin') +external ffi.Pointer get_entity_name_at( + ffi.Pointer assetManager, + int target, + int index, + bool renderableOnly, +); + @ffi.Native, ffi.Bool)>( symbol: 'set_recording', assetId: 'flutter_filament_plugin') external void set_recording( @@ -1141,6 +1176,28 @@ external void set_morph_target_weights_ffi( int numWeights, ); +@ffi.Native< + ffi.Bool Function( + ffi.Pointer, + EntityId, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ffi.Int, + ffi.Int, + ffi.Float)>( + symbol: 'set_morph_animation_ffi', assetId: 'flutter_filament_plugin') +external bool set_morph_animation_ffi( + ffi.Pointer assetManager, + int asset, + ffi.Pointer entityName, + ffi.Pointer morphData, + ffi.Pointer morphIndices, + int numMorphTargets, + int numFrames, + double frameLengthInMs, +); + @ffi.Native< ffi.Bool Function( ffi.Pointer, @@ -1157,6 +1214,30 @@ external bool set_bone_transform_ffi( ffi.Pointer boneName, ); +@ffi.Native< + ffi.Void Function( + ffi.Pointer, + EntityId, + ffi.Pointer, + ffi.Int, + ffi.Pointer, + ffi.Pointer>, + ffi.Int, + ffi.Float, + ffi.Bool)>( + symbol: 'add_bone_animation_ffi', assetId: 'flutter_filament_plugin') +external void add_bone_animation_ffi( + ffi.Pointer assetManager, + int asset, + ffi.Pointer frameData, + int numFrames, + ffi.Pointer boneName, + ffi.Pointer> meshNames, + int numMeshTargets, + double frameLengthInMs, + bool isModelSpace, +); + @ffi.Native, ffi.Bool)>( symbol: 'set_post_processing_ffi', assetId: 'flutter_filament_plugin') external void set_post_processing_ffi( @@ -1175,6 +1256,13 @@ external void pick_ffi( ffi.Pointer entityId, ); +@ffi.Native, EntityId)>( + symbol: 'reset_to_rest_pose_ffi', assetId: 'flutter_filament_plugin') +external void reset_to_rest_pose_ffi( + ffi.Pointer assetManager, + int entityId, +); + @ffi.Native( symbol: 'ios_dummy_ffi', assetId: 'flutter_filament_plugin') external void ios_dummy_ffi(); diff --git a/lib/widgets/ibl_rotation_slider.dart b/lib/widgets/ibl_rotation_slider.dart new file mode 100644 index 00000000..2efde291 --- /dev/null +++ b/lib/widgets/ibl_rotation_slider.dart @@ -0,0 +1,31 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_filament/filament_controller.dart'; +import 'package:vector_math/vector_math_64.dart' as v; + +class IblRotationSliderWidget extends StatefulWidget { + final FilamentController controller; + + const IblRotationSliderWidget({super.key, required this.controller}); + @override + State createState() => _IblRotationSliderWidgetState(); +} + +class _IblRotationSliderWidgetState extends State { + double _iblRotation = 0; + @override + Widget build(BuildContext context) { + return Slider( + value: _iblRotation, + onChanged: (value) { + _iblRotation = value; + setState(() {}); + print(value); + var rotation = v.Matrix3.identity(); + Matrix4.rotationY(value * 2 * pi).copyRotation(rotation); + widget.controller.rotateIbl(rotation); + }); + } +} diff --git a/lib/widgets/light_slider.dart b/lib/widgets/light_slider.dart new file mode 100644 index 00000000..b9e9f9a2 --- /dev/null +++ b/lib/widgets/light_slider.dart @@ -0,0 +1,182 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_filament/filament_controller.dart'; +import 'package:vector_math/vector_math_64.dart' as v; + +class LightSliderWidget extends StatefulWidget { + final FilamentController controller; + + late final v.Vector3 initialPosition; + late final v.Vector3 initialDirection; + final int initialType; + final double initialColor; + final double initialIntensity; + final bool initialCastShadows; + final bool showControls; + + LightSliderWidget( + {super.key, + required this.controller, + this.initialType = 0, + this.initialColor = 6500, + this.initialIntensity = 100000, + this.initialCastShadows = true, + this.showControls = false, + v.Vector3? initialDirection, + v.Vector3? initialPosition}) { + this.initialDirection = initialDirection ?? v.Vector3(0, 0.5, -1); + this.initialPosition = initialPosition ?? v.Vector3(0, 0.5, 1); + } + + @override + State createState() => _IblRotationSliderWidgetState(); +} + +class _IblRotationSliderWidgetState extends State { + v.Vector3 lightPos = v.Vector3(1, 0.1, 1); + v.Vector3 lightDir = v.Vector3(-1, 0.1, 0); + bool castShadows = true; + int type = 0; + double color = 6500; + double intensity = 100000; + FilamentEntity? _light; + + @override + void initState() { + type = widget.initialType; + castShadows = widget.initialCastShadows; + color = widget.initialColor; + lightPos = widget.initialPosition; + lightDir = widget.initialDirection; + intensity = widget.initialIntensity; + _set(); + super.initState(); + } + + Future _set() async { + if (_light != null) await widget.controller.removeLight(_light!); + + _light = await widget.controller.addLight( + type, + color, + intensity, + lightPos.x, + lightPos.y, + lightPos.z, + lightDir.x, + lightDir.y, + lightDir.z, + castShadows, + async: false); + + setState(() {}); + } + + @override + Widget build(BuildContext context) { + if (_light == null || !widget.showControls) { + return Container(); + } + return Theme( + data: ThemeData(platform: TargetPlatform.android), + child: Container( + decoration: BoxDecoration(color: Colors.white.withOpacity(0.5)), + child: Column(children: [ + Slider( + label: "POSX", + value: lightPos.x, + min: -10.0, + max: 10.0, + onChanged: (value) { + lightPos.x = value; + _set(); + }), + Slider( + label: "POSY", + value: lightPos.y, + min: -10.0, + max: 10.0, + onChanged: (value) { + lightPos.y = value; + _set(); + }), + Slider( + label: "POSZ", + value: lightPos.z, + min: -10.0, + max: 10.0, + onChanged: (value) { + lightPos.z = value; + _set(); + }), + Slider( + label: "DIRX", + value: lightDir.x, + min: -10.0, + max: 10.0, + onChanged: (value) { + lightDir.x = value; + _set(); + }), + Slider( + label: "DIRY", + value: lightDir.y, + min: -10.0, + max: 10.0, + onChanged: (value) { + lightDir.y = value; + _set(); + }), + Slider( + label: "DIRZ", + value: lightDir.z, + min: -10.0, + max: 10.0, + onChanged: (value) { + lightDir.z = value; + _set(); + }), + Slider( + label: "Color", + value: color, + min: 0, + max: 16000, + onChanged: (value) { + color = value; + _set(); + }), + Slider( + label: "Intensity", + value: intensity, + min: 0, + max: 1000000, + onChanged: (value) { + intensity = value; + _set(); + }), + DropdownButton( + onChanged: (v) { + this.type = v; + _set(); + }, + value: type, + items: List.generate( + 5, + (idx) => DropdownMenuItem( + value: idx, + child: Text("$idx"), + ))), + Row(children: [ + Text("Shadows: $castShadows"), + Checkbox( + value: castShadows, + onChanged: (v) { + this.castShadows = v!; + _set(); + }) + ]) + ]))); + } +}