Merge pull request #35 from nmfisher/feature/bone-animation-blending

Blend between glTF and dynamic bone animations
This commit is contained in:
Nick Fisher
2024-06-16 15:13:12 +10:00
committed by GitHub
12 changed files with 84 additions and 33 deletions

View File

@@ -273,8 +273,20 @@ abstract class AbstractFilamentViewer {
/// Currently, only [Space.ParentBone] and [Space.Model] are supported; if you want /// Currently, only [Space.ParentBone] and [Space.Model] are supported; if you want
/// to transform to another space, you will need to do so manually. /// to transform to another space, you will need to do so manually.
/// ///
/// [fadeInInSecs]/[fadeOutInSecs]/[maxDelta] are used to cross-fade between
/// the current active glTF animation ("animation1") and the animation you
/// set via this method ("animation2"). The bone orientations will be
/// linearly interpolated between animation1 and animation2; at time 0,
/// the orientation will be 100% animation1, at time [fadeInInSecs], the
/// animation will be ((1 - maxDelta) * animation1) + (maxDelta * animation2).
/// This will be applied in reverse after [fadeOutInSecs].
///
///
Future addBoneAnimation(FilamentEntity entity, BoneAnimationData animation, Future addBoneAnimation(FilamentEntity entity, BoneAnimationData animation,
{int skinIndex = 0, double fadeInInSecs=0.0, double fadeOutInSecs=0.0}); {int skinIndex = 0,
double fadeInInSecs = 0.0,
double fadeOutInSecs = 0.0,
double maxDelta = 1.0});
/// ///
/// Gets the entity representing the bone at [boneIndex]/[skinIndex]. /// Gets the entity representing the bone at [boneIndex]/[skinIndex].
@@ -307,10 +319,10 @@ abstract class AbstractFilamentViewer {
Future setTransform(FilamentEntity entity, Matrix4 transform); Future setTransform(FilamentEntity entity, Matrix4 transform);
/// ///
/// Updates the bone matrices for [entity] (which must be the FilamentEntity /// Updates the bone matrices for [entity] (which must be the FilamentEntity
/// returned by [loadGlb/loadGltf]). /// returned by [loadGlb/loadGltf]).
/// Under the hood, this just calls [updateBoneMatrices] on the Animator /// Under the hood, this just calls [updateBoneMatrices] on the Animator
/// instance of the relevant FilamentInstance (which uses the local /// instance of the relevant FilamentInstance (which uses the local
/// bone transform and the inverse bind matrix to set the bone matrix). /// bone transform and the inverse bind matrix to set the bone matrix).
/// ///
Future updateBoneMatrices(FilamentEntity entity); Future updateBoneMatrices(FilamentEntity entity);
@@ -320,7 +332,8 @@ abstract class AbstractFilamentViewer {
/// Don't call this manually unless you know what you're doing. /// Don't call this manually unless you know what you're doing.
/// ///
Future setBoneTransform( Future setBoneTransform(
FilamentEntity entity, int boneIndex, Matrix4 transform, { int skinIndex=0}); FilamentEntity entity, int boneIndex, Matrix4 transform,
{int skinIndex = 0});
/// ///
/// Removes/destroys the specified entity from the scene. /// Removes/destroys the specified entity from the scene.

View File

@@ -460,8 +460,17 @@ external void reset_to_rest_pose(
); );
@ffi.Native< @ffi.Native<
ffi.Void Function(ffi.Pointer<ffi.Void>, EntityId, ffi.Int, ffi.Int, ffi.Void Function(
ffi.Pointer<ffi.Float>, ffi.Int, ffi.Float, ffi.Float, ffi.Float)>( ffi.Pointer<ffi.Void>,
EntityId,
ffi.Int,
ffi.Int,
ffi.Pointer<ffi.Float>,
ffi.Int,
ffi.Float,
ffi.Float,
ffi.Float,
ffi.Float)>(
symbol: 'add_bone_animation', symbol: 'add_bone_animation',
assetId: 'package:dart_filament/dart_filament.dart') assetId: 'package:dart_filament/dart_filament.dart')
external void add_bone_animation( external void add_bone_animation(
@@ -474,6 +483,7 @@ external void add_bone_animation(
double frameLengthInMs, double frameLengthInMs,
double fadeOutInSecs, double fadeOutInSecs,
double fadeInInSecs, double fadeInInSecs,
double maxDelta,
); );
@ffi.Native< @ffi.Native<
@@ -1804,8 +1814,10 @@ typedef LoadFilamentResourceIntoOutPointerFunction = ffi.Void Function(
typedef DartLoadFilamentResourceIntoOutPointerFunction = void Function( typedef DartLoadFilamentResourceIntoOutPointerFunction = void Function(
ffi.Pointer<ffi.Char> uri, ffi.Pointer<ResourceBuffer> out); ffi.Pointer<ffi.Char> uri, ffi.Pointer<ResourceBuffer> out);
/// This header replicates most of the methods in FlutterFilamentApi.h, and is only intended to be used to generate client FFI bindings. /// This header replicates most of the methods in DartFilamentApi.h.
/// The intention is that calling one of these methods will call its respective method in FlutterFilamentApi.h, but wrapped in some kind of thread runner to ensure thread safety. /// It represents the interface for:
/// - invoking those methods that must be called on the main Filament engine thread
/// - setting up a render loop
typedef EntityId = ffi.Int32; typedef EntityId = ffi.Int32;
typedef DartEntityId = int; typedef DartEntityId = int;
typedef _ManipulatorMode = ffi.Int32; typedef _ManipulatorMode = ffi.Int32;

View File

@@ -548,6 +548,7 @@ external void reset_to_rest_pose(
ffi.Int, ffi.Int,
ffi.Float, ffi.Float,
ffi.Float, ffi.Float,
ffi.Float,
ffi.Float)>(symbol: '_add_bone_animation', assetId: 'dart_filament') ffi.Float)>(symbol: '_add_bone_animation', assetId: 'dart_filament')
external void add_bone_animation( external void add_bone_animation(
ffi.Pointer<ffi.Void> sceneManager, ffi.Pointer<ffi.Void> sceneManager,
@@ -559,6 +560,7 @@ external void add_bone_animation(
double frameLengthInMs, double frameLengthInMs,
double fadeOutInSecs, double fadeOutInSecs,
double fadeInInSecs, double fadeInInSecs,
double maxDelta,
); );
@ffi.Native< @ffi.Native<
@@ -1788,8 +1790,10 @@ typedef FreeFilamentResourceFromOwnerFunction = ffi.Void Function(
typedef DartFreeFilamentResourceFromOwnerFunction = void Function( typedef DartFreeFilamentResourceFromOwnerFunction = void Function(
ResourceBuffer, ffi.Pointer<ffi.Void>); ResourceBuffer, ffi.Pointer<ffi.Void>);
/// This header replicates most of the methods in FlutterFilamentApi.h, and is only intended to be used to generate client FFI bindings. /// This header replicates most of the methods in DartFilamentApi.h.
/// The intention is that calling one of these methods will call its respective method in FlutterFilamentApi.h, but wrapped in some kind of thread runner to ensure thread safety. /// It represents the interface for:
/// - invoking those methods that must be called on the main Filament engine thread
/// - setting up a render loop
typedef EntityId = ffi.Int32; typedef EntityId = ffi.Int32;
typedef DartEntityId = int; typedef DartEntityId = int;
typedef _ManipulatorMode = ffi.Int32; typedef _ManipulatorMode = ffi.Int32;

View File

@@ -70,8 +70,10 @@ class DartFilamentJSExportViewer {
JSPromise removeSkybox() => viewer.removeSkybox().toJS; JSPromise removeSkybox() => viewer.removeSkybox().toJS;
@JSExport() @JSExport()
JSPromise loadIbl(String lightingPath, {double intensity = 30000}) => JSPromise loadIbl(String lightingPath, double intensity) {
viewer.loadIbl(lightingPath, intensity: intensity).toJS; print("Loading IBL from $lightingPath with intensity $intensity");
return viewer.loadIbl(lightingPath, intensity: intensity).toJS;
}
@JSExport() @JSExport()
JSPromise rotateIbl(JSArray<JSNumber> rotation) { JSPromise rotateIbl(JSArray<JSNumber> rotation) {
@@ -266,7 +268,8 @@ class DartFilamentJSExportViewer {
JSNumber spaceEnum, JSNumber spaceEnum,
JSNumber skinIndex, JSNumber skinIndex,
JSNumber fadeInInSecs, JSNumber fadeInInSecs,
JSNumber fadeOutInSecs) { JSNumber fadeOutInSecs,
JSNumber maxDelta) {
var frameDataDart = frameData.toDart var frameDataDart = frameData.toDart
.map((frame) => frame.toDart .map((frame) => frame.toDart
.map((v) { .map((v) {
@@ -463,23 +466,29 @@ class DartFilamentJSExportViewer {
JSPromise moveCameraToAsset(FilamentEntity entity) => JSPromise moveCameraToAsset(FilamentEntity entity) =>
throw UnimplementedError(); throw UnimplementedError();
// viewer.moveCameraToAsset(entity)).toJS; // viewer.moveCameraToAsset(entity)).toJS;
@JSExport() @JSExport()
JSPromise setViewFrustumCulling(JSBoolean enabled) => JSPromise setViewFrustumCulling(JSBoolean enabled) =>
throw UnimplementedError(); throw UnimplementedError();
// viewer.setViewFrustumCulling(enabled).toJS; // viewer.setViewFrustumCulling(enabled).toJS;
@JSExport() @JSExport()
JSPromise setCameraExposure( JSPromise setCameraExposure(
double aperture, double shutterSpeed, double sensitivity) => double aperture, double shutterSpeed, double sensitivity) =>
viewer.setCameraExposure(aperture, shutterSpeed, sensitivity).toJS; viewer.setCameraExposure(aperture, shutterSpeed, sensitivity).toJS;
@JSExport() @JSExport()
JSPromise setCameraRotation(JSArray<JSNumber> quaternion) { JSPromise setCameraRotation(JSArray<JSNumber> quaternion) {
var dartVals = quaternion.toDart; var dartVals = quaternion.toDart;
return viewer.setCameraRotation(v64.Quaternion(dartVals[0].toDartDouble, dartVals[1].toDartDouble, dartVals[2].toDartDouble, dartVals[3].toDartDouble)).toJS; return viewer
.setCameraRotation(v64.Quaternion(
dartVals[0].toDartDouble,
dartVals[1].toDartDouble,
dartVals[2].toDartDouble,
dartVals[3].toDartDouble))
.toJS;
} }
@JSExport() @JSExport()
JSPromise setCameraModelMatrix(JSArray<JSNumber> matrix) { JSPromise setCameraModelMatrix(JSArray<JSNumber> matrix) {
throw UnimplementedError(); throw UnimplementedError();

View File

@@ -151,7 +151,8 @@ extension type DartFilamentJSShim(JSObject _) implements JSObject {
JSNumber spaceEnum, JSNumber spaceEnum,
JSNumber skinIndex, JSNumber skinIndex,
JSNumber fadeInInSecs, JSNumber fadeInInSecs,
JSNumber fadeOutInSecs); JSNumber fadeOutInSecs,
JSNumber maxDelta);
@JS('removeEntity') @JS('removeEntity')
external JSPromise removeEntity(FilamentEntity entity); external JSPromise removeEntity(FilamentEntity entity);

View File

@@ -289,7 +289,8 @@ class JsInteropFilamentViewer implements AbstractFilamentViewer {
FilamentEntity entity, BoneAnimationData animation, FilamentEntity entity, BoneAnimationData animation,
{int skinIndex = 0, {int skinIndex = 0,
double fadeInInSecs = 0.0, double fadeInInSecs = 0.0,
double fadeOutInSecs = 0.0}) async { double fadeOutInSecs = 0.0,
double maxDelta=1.0}) async {
var boneNames = animation.bones.map((n) => n.toJS).toList().toJS; var boneNames = animation.bones.map((n) => n.toJS).toList().toJS;
var frameData = animation.frameData var frameData = animation.frameData
.map((frame) => frame .map((frame) => frame
@@ -316,7 +317,8 @@ class JsInteropFilamentViewer implements AbstractFilamentViewer {
animation.space.index.toJS, animation.space.index.toJS,
skinIndex.toJS, skinIndex.toJS,
fadeInInSecs.toJS, fadeInInSecs.toJS,
fadeOutInSecs.toJS) fadeOutInSecs.toJS,
maxDelta)
.toDart; .toDart;
} }

View File

@@ -677,7 +677,8 @@ class FilamentViewer extends AbstractFilamentViewer {
Future addBoneAnimation(FilamentEntity entity, BoneAnimationData animation, Future addBoneAnimation(FilamentEntity entity, BoneAnimationData animation,
{int skinIndex = 0, {int skinIndex = 0,
double fadeOutInSecs = 0.0, double fadeOutInSecs = 0.0,
double fadeInInSecs = 0.0}) async { double fadeInInSecs = 0.0,
double maxDelta=1.0}) async {
if (animation.space != Space.Bone && if (animation.space != Space.Bone &&
animation.space != Space.ParentWorldRotation) { animation.space != Space.ParentWorldRotation) {
throw UnimplementedError("TODO - support ${animation.space}"); throw UnimplementedError("TODO - support ${animation.space}");
@@ -758,7 +759,8 @@ class FilamentViewer extends AbstractFilamentViewer {
numFrames, numFrames,
animation.frameLengthInMs, animation.frameLengthInMs,
fadeOutInSecs, fadeOutInSecs,
fadeInInSecs); fadeInInSecs,
maxDelta);
} }
allocator.free(data); allocator.free(data);
} }

View File

@@ -149,7 +149,8 @@ extern "C"
int numFrames, int numFrames,
float frameLengthInMs, float frameLengthInMs,
float fadeOutInSecs, float fadeOutInSecs,
float fadeInInSecs); float fadeInInSecs,
float maxDelta);
EMSCRIPTEN_KEEPALIVE void get_local_transform(void *sceneManager, EMSCRIPTEN_KEEPALIVE void get_local_transform(void *sceneManager,
EntityId entityId, float* const); EntityId entityId, float* const);
EMSCRIPTEN_KEEPALIVE void get_rest_local_transforms(void *sceneManager, EMSCRIPTEN_KEEPALIVE void get_rest_local_transforms(void *sceneManager,

View File

@@ -118,8 +118,9 @@ namespace flutter_filament
int numFrames, int numFrames,
float frameLengthInMs, float frameLengthInMs,
float fadeOutInSecs, float fadeOutInSecs,
float fadeInInSecs float fadeInInSecs,
); float maxDelta
);
std::unique_ptr<std::vector<math::mat4f>> getBoneRestTranforms(EntityId entityId, int skinIndex); std::unique_ptr<std::vector<math::mat4f>> getBoneRestTranforms(EntityId entityId, int skinIndex);
void resetBones(EntityId entityId); void resetBones(EntityId entityId);

View File

@@ -82,6 +82,7 @@ namespace flutter_filament
std::vector<math::mat4f> frameData; std::vector<math::mat4f> frameData;
float fadeOutInSecs = 0; float fadeOutInSecs = 0;
float fadeInInSecs = 0; float fadeInInSecs = 0;
float maxDelta = 1.0f;
}; };
struct AnimationComponent struct AnimationComponent
@@ -290,11 +291,13 @@ namespace flutter_filament
// if we're fading in, this will be 0.0 at the start of the fade and 1.0 at the end // if we're fading in, this will be 0.0 at the start of the fade and 1.0 at the end
auto fadeDelta = elapsedInSecs / animationStatus.fadeInInSecs; auto fadeDelta = elapsedInSecs / animationStatus.fadeInInSecs;
// if we're fading out, this will be 1.0 at the start of the fade and 0.0 at the end // // if we're fading out, this will be 1.0 at the start of the fade and 0.0 at the end
if(fadeDelta > 1.0f) { if(fadeDelta > 1.0f) {
fadeDelta = 1 - ((elapsedInSecs - animationStatus.durationInSecs - animationStatus.fadeInInSecs) / animationStatus.fadeOutInSecs); fadeDelta = 1 - ((elapsedInSecs - animationStatus.durationInSecs - animationStatus.fadeInInSecs) / animationStatus.fadeOutInSecs);
} }
fadeDelta = std::clamp(fadeDelta, 0.0f, animationStatus.maxDelta);
auto jointTransform = _transformManager.getInstance(joint); auto jointTransform = _transformManager.getInstance(joint);
// linearly interpolate this animation between its current (interpolated) frame and the current transform (i.e. as set by the gltf frame) // linearly interpolate this animation between its current (interpolated) frame and the current transform (i.e. as set by the gltf frame)

View File

@@ -429,9 +429,10 @@ extern "C"
int numFrames, int numFrames,
float frameLengthInMs, float frameLengthInMs,
float fadeOutInSecs, float fadeOutInSecs,
float fadeInInSecs) float fadeInInSecs,
float maxDelta)
{ {
((SceneManager *)sceneManager)->addBoneAnimation(asset, skinIndex, boneIndex, frameData, numFrames, frameLengthInMs, fadeOutInSecs, fadeInInSecs); ((SceneManager *)sceneManager)->addBoneAnimation(asset, skinIndex, boneIndex, frameData, numFrames, frameLengthInMs, fadeOutInSecs, fadeInInSecs, maxDelta);
} }
EMSCRIPTEN_KEEPALIVE void set_post_processing(void *const viewer, bool enabled) EMSCRIPTEN_KEEPALIVE void set_post_processing(void *const viewer, bool enabled)

View File

@@ -1024,7 +1024,9 @@ namespace flutter_filament
const float *const frameData, const float *const frameData,
int numFrames, int numFrames,
float frameLengthInMs, float frameLengthInMs,
float fadeOutInSecs, float fadeInInSecs) float fadeOutInSecs,
float fadeInInSecs,
float maxDelta)
{ {
std::lock_guard lock(_mutex); std::lock_guard lock(_mutex);
@@ -1072,7 +1074,6 @@ namespace flutter_filament
} }
animation.frameLengthInMs = frameLengthInMs; animation.frameLengthInMs = frameLengthInMs;
animation.start = std::chrono::high_resolution_clock::now(); animation.start = std::chrono::high_resolution_clock::now();
animation.reverse = false; animation.reverse = false;
animation.durationInSecs = (frameLengthInMs * numFrames) / 1000.0f; animation.durationInSecs = (frameLengthInMs * numFrames) / 1000.0f;
@@ -1080,6 +1081,7 @@ namespace flutter_filament
animation.frameLengthInMs = frameLengthInMs; animation.frameLengthInMs = frameLengthInMs;
animation.fadeOutInSecs = fadeOutInSecs; animation.fadeOutInSecs = fadeOutInSecs;
animation.fadeInInSecs = fadeInInSecs; animation.fadeInInSecs = fadeInInSecs;
animation.maxDelta = maxDelta;
animation.skinIndex = skinIndex; animation.skinIndex = skinIndex;
if(!_animationComponentManager->hasComponent(instance->getRoot())) { if(!_animationComponentManager->hasComponent(instance->getRoot())) {
Log("ERROR: specified entity is not animatable (has no animation component attached)."); Log("ERROR: specified entity is not animatable (has no animation component attached).");