separate Gltf/Morph/BoneAnimationComponentManager definitions

move gltf animation instantiation to GltfAnimationComponentManager (this helps ensure we are creating the component on the correct entity)
This commit is contained in:
Nick Fisher
2025-05-20 14:57:26 +08:00
parent d61723dee2
commit 1fb68b20e9
24 changed files with 629 additions and 354 deletions

View File

@@ -51,24 +51,25 @@ class _MyHomePageState extends State<MyHomePage> {
await _thermionViewer!.destroyAsset(_asset!);
_asset = null;
}
if (uri == null) {
return;
}
_asset = await _thermionViewer!.loadGltf(uri);
await _asset!.transformToUnitCube();
final animations = await _asset!.getGltfAnimationNames();
final durations = await Future.wait(List.generate(
animations.length, (i) => _asset!.getGltfAnimationDuration(i)));
final labels = animations
.asMap()
.map((index, animation) =>
MapEntry(index, "$animation (${durations[index]}s"))
.values;
gltfAnimations.clear();
gltfAnimations.addAll(labels);
selectedGltfAnimation = 0;
_loaded = uri;
if (uri != null) {
_asset = await _thermionViewer!.loadGltf(uri);
await _asset!.transformToUnitCube();
final animations = await _asset!.getGltfAnimationNames();
final durations = await Future.wait(List.generate(
animations.length, (i) => _asset!.getGltfAnimationDuration(i)));
final labels = animations
.asMap()
.map((index, animation) =>
MapEntry(index, "$animation (${durations[index]}s"))
.values;
gltfAnimations.clear();
gltfAnimations.addAll(labels);
selectedGltfAnimation = 0;
}
setState(() {});
}
@@ -123,19 +124,19 @@ class _MyHomePageState extends State<MyHomePage> {
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
child: Row(children: [
Text("Asset: "),
DropdownButton<String>(
const Text("Asset: "),
DropdownButton<String?>(
value: _loaded,
items: [_droneUri, _cubeUri]
.map((uri) => DropdownMenuItem<String>(
items: [_droneUri, _cubeUri, null]
.map((uri) => DropdownMenuItem<String?>(
value: uri,
child: Text(
uri,
style: TextStyle(fontSize: 12),
uri ?? "None",
style: const TextStyle(fontSize: 12),
)))
.toList(),
onChanged: _load),
Text("Animation: "),
const Text("Animation: "),
DropdownButton<String>(
value: selectedGltfAnimation == -1
? null
@@ -143,9 +144,9 @@ class _MyHomePageState extends State<MyHomePage> {
items: gltfAnimations
.map((animation) => DropdownMenuItem<String>(
value: animation,
child: Text(
child: Text(
animation,
style: TextStyle(fontSize: 12),
style: const TextStyle(fontSize: 12),
)))
.toList(),
onChanged: (value) {
@@ -160,7 +161,7 @@ class _MyHomePageState extends State<MyHomePage> {
icon: const Icon(Icons.play_arrow)),
IconButton(
onPressed: _stopGltfAnimation, icon: const Icon(Icons.stop))
])))
]))),
]);
}
}

View File

@@ -19,19 +19,23 @@
<title>quickstart</title>
<link rel="manifest" href="manifest.json">
<script>
// The value below is injected by flutter build, do not touch.
const serviceWorkerVersion = null;
</script>
<script src="flutter.js" defer></script>
<script type="text/javascript" src="./thermion_dart.js"></script>
<script type="module">
<script type="module">
try {
window.thermion_dart = await thermion_dart();
console.log("set thermion_dart");
} catch(err) {
console.error(err);
}
</script>
<script>
// The value below is injected by flutter build, do not touch.
const serviceWorkerVersion = null;
</script>
<script src="flutter.js" defer></script>
</head>
<body>

View File

@@ -3394,9 +3394,9 @@ external bool AnimationManager_setMorphTargetWeights(
);
@ffi.Native<
ffi.Void Function(ffi.Pointer<TAnimationManager>, ffi.Pointer<TSceneAsset>,
ffi.Bool Function(ffi.Pointer<TAnimationManager>, ffi.Pointer<TSceneAsset>,
ffi.Int, ffi.Int)>(isLeaf: true)
external void AnimationManager_setGltfAnimationFrame(
external bool AnimationManager_setGltfAnimationFrame(
ffi.Pointer<TAnimationManager> tAnimationManager,
ffi.Pointer<TSceneAsset> tSceneAsset,
int animationIndex,

View File

@@ -255,7 +255,7 @@ sealed class Struct extends NativeType {
Struct(this._address);
static create<T extends Struct>() {
switch (T) {
switch (T) {
case double4x4:
final ptr = double4x4.stackAlloc();
final arr1 =
@@ -267,7 +267,7 @@ sealed class Struct extends NativeType {
final arr4 =
Array<Float64>._((numElements: 4, addr: ptr.cast<Float64>() + 96));
return double4x4(arr1, arr2, arr3, arr4, ptr) as T;
}
}
}
}
@@ -1831,7 +1831,6 @@ extension type NativeLibrary(JSObject _) implements JSObject {
);
external void _GltfResourceLoader_createRenderThread(
Pointer<TEngine> tEngine,
Pointer<Char> relativeResourcePath,
Pointer<
self
.NativeFunction<void Function(PointerClass<TGltfResourceLoader>)>>
@@ -1895,7 +1894,6 @@ extension type NativeLibrary(JSObject _) implements JSObject {
);
external Pointer<TGltfResourceLoader> _GltfResourceLoader_create(
Pointer<TEngine> tEngine,
Pointer<Char> relativeResourcePath,
);
external void _GltfResourceLoader_destroy(
Pointer<TEngine> tEngine,
@@ -2069,14 +2067,30 @@ extension type NativeLibrary(JSObject _) implements JSObject {
Pointer<TAnimationManager> tAnimationManager,
JSBigInt frameTimeInNanos,
);
external void _AnimationManager_addAnimationComponent(
external bool _AnimationManager_addGltfAnimationComponent(
Pointer<TAnimationManager> tAnimationManager,
Pointer<TSceneAsset> tSceneAsset,
);
external bool _AnimationManager_removeGltfAnimationComponent(
Pointer<TAnimationManager> tAnimationManager,
Pointer<TSceneAsset> tSceneAsset,
);
external void _AnimationManager_addMorphAnimationComponent(
Pointer<TAnimationManager> tAnimationManager,
EntityId entityId,
);
external void _AnimationManager_removeAnimationComponent(
external void _AnimationManager_removeMorphAnimationComponent(
Pointer<TAnimationManager> tAnimationManager,
EntityId entityId,
);
external bool _AnimationManager_addBoneAnimationComponent(
Pointer<TAnimationManager> tAnimationManager,
Pointer<TSceneAsset> tSceneAsset,
);
external bool _AnimationManager_removeBoneAnimationComponent(
Pointer<TAnimationManager> tAnimationManager,
Pointer<TSceneAsset> tSceneAsset,
);
external bool _AnimationManager_setMorphAnimation(
Pointer<TAnimationManager> tAnimationManager,
EntityId entityId,
@@ -2094,7 +2108,7 @@ extension type NativeLibrary(JSObject _) implements JSObject {
Pointer<TAnimationManager> tAnimationManager,
Pointer<TSceneAsset> sceneAsset,
);
external void _AnimationManager_addBoneAnimation(
external bool _AnimationManager_addBoneAnimation(
Pointer<TAnimationManager> tAnimationManager,
Pointer<TSceneAsset> tSceneAsset,
int skinIndex,
@@ -2126,9 +2140,9 @@ extension type NativeLibrary(JSObject _) implements JSObject {
int boneIndex,
Pointer<Float32> out,
);
external void _AnimationManager_playAnimation(
external bool _AnimationManager_playGltfAnimation(
Pointer<TAnimationManager> tAnimationManager,
Pointer<TSceneAsset> sceneAsset,
Pointer<TSceneAsset> tSceneAsset,
int index,
bool loop,
bool reverse,
@@ -2136,21 +2150,21 @@ extension type NativeLibrary(JSObject _) implements JSObject {
double crossfade,
double startOffset,
);
external void _AnimationManager_stopAnimation(
external bool _AnimationManager_stopGltfAnimation(
Pointer<TAnimationManager> tAnimationManager,
Pointer<TSceneAsset> sceneAsset,
int index,
);
external double _AnimationManager_getAnimationDuration(
external double _AnimationManager_getGltfAnimationDuration(
Pointer<TAnimationManager> tAnimationManager,
Pointer<TSceneAsset> sceneAsset,
int animationIndex,
);
external int _AnimationManager_getAnimationCount(
external int _AnimationManager_getGltfAnimationCount(
Pointer<TAnimationManager> tAnimationManager,
Pointer<TSceneAsset> sceneAsset,
);
external void _AnimationManager_getAnimationName(
external void _AnimationManager_getGltfAnimationName(
Pointer<TAnimationManager> tAnimationManager,
Pointer<TSceneAsset> sceneAsset,
Pointer<Char> outPtr,
@@ -2189,7 +2203,7 @@ extension type NativeLibrary(JSObject _) implements JSObject {
Pointer<Float32> morphData,
int numWeights,
);
external void _AnimationManager_setGltfAnimationFrame(
external bool _AnimationManager_setGltfAnimationFrame(
Pointer<TAnimationManager> tAnimationManager,
Pointer<TSceneAsset> tSceneAsset,
int animationIndex,
@@ -5133,12 +5147,11 @@ void GltfAssetLoader_createRenderThread(
void GltfResourceLoader_createRenderThread(
self.Pointer<TEngine> tEngine,
self.Pointer<Char> relativeResourcePath,
self.Pointer<self.NativeFunction<void Function(Pointer<TGltfResourceLoader>)>>
callback,
) {
final result = _lib._GltfResourceLoader_createRenderThread(
tEngine.cast(), relativeResourcePath, callback.cast());
tEngine.cast(), callback.cast());
return result;
}
@@ -5263,10 +5276,8 @@ void Gizmo_createRenderThread(
self.Pointer<TGltfResourceLoader> GltfResourceLoader_create(
self.Pointer<TEngine> tEngine,
self.Pointer<Char> relativeResourcePath,
) {
final result =
_lib._GltfResourceLoader_create(tEngine.cast(), relativeResourcePath);
final result = _lib._GltfResourceLoader_create(tEngine.cast());
return self.Pointer<TGltfResourceLoader>(result);
}
@@ -5641,24 +5652,60 @@ void AnimationManager_update(
return result;
}
void AnimationManager_addAnimationComponent(
bool AnimationManager_addGltfAnimationComponent(
self.Pointer<TAnimationManager> tAnimationManager,
self.Pointer<TSceneAsset> tSceneAsset,
) {
final result = _lib._AnimationManager_addGltfAnimationComponent(
tAnimationManager.cast(), tSceneAsset.cast());
return result;
}
bool AnimationManager_removeGltfAnimationComponent(
self.Pointer<TAnimationManager> tAnimationManager,
self.Pointer<TSceneAsset> tSceneAsset,
) {
final result = _lib._AnimationManager_removeGltfAnimationComponent(
tAnimationManager.cast(), tSceneAsset.cast());
return result;
}
void AnimationManager_addMorphAnimationComponent(
self.Pointer<TAnimationManager> tAnimationManager,
DartEntityId entityId,
) {
final result = _lib._AnimationManager_addAnimationComponent(
final result = _lib._AnimationManager_addMorphAnimationComponent(
tAnimationManager.cast(), entityId);
return result;
}
void AnimationManager_removeAnimationComponent(
void AnimationManager_removeMorphAnimationComponent(
self.Pointer<TAnimationManager> tAnimationManager,
DartEntityId entityId,
) {
final result = _lib._AnimationManager_removeAnimationComponent(
final result = _lib._AnimationManager_removeMorphAnimationComponent(
tAnimationManager.cast(), entityId);
return result;
}
bool AnimationManager_addBoneAnimationComponent(
self.Pointer<TAnimationManager> tAnimationManager,
self.Pointer<TSceneAsset> tSceneAsset,
) {
final result = _lib._AnimationManager_addBoneAnimationComponent(
tAnimationManager.cast(), tSceneAsset.cast());
return result;
}
bool AnimationManager_removeBoneAnimationComponent(
self.Pointer<TAnimationManager> tAnimationManager,
self.Pointer<TSceneAsset> tSceneAsset,
) {
final result = _lib._AnimationManager_removeBoneAnimationComponent(
tAnimationManager.cast(), tSceneAsset.cast());
return result;
}
bool AnimationManager_setMorphAnimation(
self.Pointer<TAnimationManager> tAnimationManager,
DartEntityId entityId,
@@ -5697,7 +5744,7 @@ void AnimationManager_resetToRestPose(
return result;
}
void AnimationManager_addBoneAnimation(
bool AnimationManager_addBoneAnimation(
self.Pointer<TAnimationManager> tAnimationManager,
self.Pointer<TSceneAsset> tSceneAsset,
int skinIndex,
@@ -5758,9 +5805,9 @@ void AnimationManager_getInverseBindMatrix(
return result;
}
void AnimationManager_playAnimation(
bool AnimationManager_playGltfAnimation(
self.Pointer<TAnimationManager> tAnimationManager,
self.Pointer<TSceneAsset> sceneAsset,
self.Pointer<TSceneAsset> tSceneAsset,
int index,
bool loop,
bool reverse,
@@ -5768,9 +5815,9 @@ void AnimationManager_playAnimation(
double crossfade,
double startOffset,
) {
final result = _lib._AnimationManager_playAnimation(
final result = _lib._AnimationManager_playGltfAnimation(
tAnimationManager.cast(),
sceneAsset.cast(),
tSceneAsset.cast(),
index,
loop,
reverse,
@@ -5780,42 +5827,42 @@ void AnimationManager_playAnimation(
return result;
}
void AnimationManager_stopAnimation(
bool AnimationManager_stopGltfAnimation(
self.Pointer<TAnimationManager> tAnimationManager,
self.Pointer<TSceneAsset> sceneAsset,
int index,
) {
final result = _lib._AnimationManager_stopAnimation(
final result = _lib._AnimationManager_stopGltfAnimation(
tAnimationManager.cast(), sceneAsset.cast(), index);
return result;
}
double AnimationManager_getAnimationDuration(
double AnimationManager_getGltfAnimationDuration(
self.Pointer<TAnimationManager> tAnimationManager,
self.Pointer<TSceneAsset> sceneAsset,
int animationIndex,
) {
final result = _lib._AnimationManager_getAnimationDuration(
final result = _lib._AnimationManager_getGltfAnimationDuration(
tAnimationManager.cast(), sceneAsset.cast(), animationIndex);
return result;
}
int AnimationManager_getAnimationCount(
int AnimationManager_getGltfAnimationCount(
self.Pointer<TAnimationManager> tAnimationManager,
self.Pointer<TSceneAsset> sceneAsset,
) {
final result = _lib._AnimationManager_getAnimationCount(
final result = _lib._AnimationManager_getGltfAnimationCount(
tAnimationManager.cast(), sceneAsset.cast());
return result;
}
void AnimationManager_getAnimationName(
void AnimationManager_getGltfAnimationName(
self.Pointer<TAnimationManager> tAnimationManager,
self.Pointer<TSceneAsset> sceneAsset,
self.Pointer<Char> outPtr,
int index,
) {
final result = _lib._AnimationManager_getAnimationName(
final result = _lib._AnimationManager_getGltfAnimationName(
tAnimationManager.cast(), sceneAsset.cast(), outPtr, index);
return result;
}
@@ -5883,7 +5930,7 @@ bool AnimationManager_setMorphTargetWeights(
return result;
}
void AnimationManager_setGltfAnimationFrame(
bool AnimationManager_setGltfAnimationFrame(
self.Pointer<TAnimationManager> tAnimationManager,
self.Pointer<TSceneAsset> tSceneAsset,
int animationIndex,

View File

@@ -621,8 +621,12 @@ class FFIAsset extends ThermionAsset {
///
@override
Future<double> getGltfAnimationDuration(int animationIndex) async {
return AnimationManager_getGltfAnimationDuration(
final duration = AnimationManager_getGltfAnimationDuration(
animationManager, asset, animationIndex);
if (duration < 0) {
throw Exception("Failed to get glTF animation duration");
}
return duration;
}
///
@@ -1030,6 +1034,18 @@ class FFIAsset extends ThermionAsset {
///
///
Future removeAnimationComponent() async {
AnimationManager_removeGltfAnimationComponent(animationManager, this.asset);
if (!AnimationManager_removeGltfAnimationComponent(
animationManager, asset)) {
throw Exception("Failed to remove glTF animation component");
}
if (!AnimationManager_removeBoneAnimationComponent(
animationManager, asset)) {
throw Exception("Failed to remove bone animation component");
}
AnimationManager_removeMorphAnimationComponent(animationManager, entity);
for (final child in await getChildEntities()) {
AnimationManager_removeMorphAnimationComponent(animationManager, child);
}
}
}

View File

@@ -116,14 +116,14 @@ class ThermionViewerFFI extends ThermionViewer {
///
@override
Future render() async {
await withVoidCallback((requestId,cb) =>
RenderTicker_renderRenderThread(app.renderTicker, 0.toBigInt, requestId,cb));
await withVoidCallback((requestId, cb) => RenderTicker_renderRenderThread(
app.renderTicker, 0.toBigInt, requestId, cb));
if (FILAMENT_SINGLE_THREADED) {
await withVoidCallback(
(requestId,cb) => Engine_executeRenderThread(app.engine, requestId,cb));
await withVoidCallback((requestId, cb) =>
Engine_executeRenderThread(app.engine, requestId, cb));
} else {
await withVoidCallback(
(requestId,cb) => Engine_flushAndWaitRenderThread(app.engine, requestId,cb));
await withVoidCallback((requestId, cb) =>
Engine_flushAndWaitRenderThread(app.engine, requestId, cb));
}
}
@@ -300,8 +300,8 @@ class ThermionViewerFFI extends ThermionViewer {
}
if (skybox != null) {
await withVoidCallback(
(requestId,cb) => Engine_destroySkyboxRenderThread(app.engine, skybox!, requestId,cb));
await withVoidCallback((requestId, cb) =>
Engine_destroySkyboxRenderThread(app.engine, skybox!, requestId, cb));
skybox = null;
}
}
@@ -313,8 +313,9 @@ class ThermionViewerFFI extends ThermionViewer {
Future removeIbl() async {
if (indirectLight != null) {
Scene_setIndirectLight(scene.scene, nullptr);
await withVoidCallback((requestId,cb) => Engine_destroyIndirectLightRenderThread(
app.engine, indirectLight!, requestId,cb));
await withVoidCallback((requestId, cb) =>
Engine_destroyIndirectLightRenderThread(
app.engine, indirectLight!, requestId, cb));
indirectLight = null;
}
}
@@ -439,7 +440,9 @@ class ThermionViewerFFI extends ThermionViewer {
///
@override
Future destroyAsset(covariant FFIAsset asset) async {
await asset.removeAnimationComponent();
await scene.remove(asset);
if (asset.boundingBoxAsset != null) {
await scene.remove(asset.boundingBoxAsset! as FFIAsset);
await FilamentApp.instance!.destroyAsset(asset.boundingBoxAsset!);

View File

@@ -129,7 +129,7 @@ extern "C"
const float *const morphData,
int numWeights);
EMSCRIPTEN_KEEPALIVE void AnimationManager_setGltfAnimationFrame(
EMSCRIPTEN_KEEPALIVE bool AnimationManager_setGltfAnimationFrame(
TAnimationManager *tAnimationManager,
TSceneAsset *tSceneAsset,
int animationIndex,

View File

@@ -301,7 +301,7 @@ namespace thermion
void AnimationManager_resetToRestPoseRenderThread(TAnimationManager *tAnimationManager, EntityId entityId, uint32_t requestId, VoidCallback onComplete);
void GltfAssetLoader_createRenderThread(TEngine *tEngine, TMaterialProvider *tMaterialProvider, void (*callback)(TGltfAssetLoader *));
void GltfResourceLoader_createRenderThread(TEngine *tEngine, const char* relativeResourcePath, void (*callback)(TGltfResourceLoader *));
void GltfResourceLoader_createRenderThread(TEngine *tEngine, void (*callback)(TGltfResourceLoader *));
void GltfResourceLoader_destroyRenderThread(TEngine *tEngine, TGltfResourceLoader *tResourceLoader, uint32_t requestId, VoidCallback onComplete);
void GltfResourceLoader_loadResourcesRenderThread(TGltfResourceLoader *tGltfResourceLoader, TFilamentAsset *tFilamentAsset, void (*callback)(bool));
void GltfResourceLoader_addResourceDataRenderThread(TGltfResourceLoader *tGltfResourceLoader, const char *uri, uint8_t *data, size_t length, uint32_t requestId, VoidCallback onComplete);

View File

@@ -0,0 +1,19 @@
#pragma once
#include <chrono>
namespace thermion
{
using namespace std::chrono;
typedef std::chrono::time_point<std::chrono::high_resolution_clock> time_point_t;
struct Animation
{
time_point_t start = time_point_t::max();
float startOffset;
bool loop = false;
bool reverse = false;
float durationInSecs = 0;
};
}

View File

@@ -1,157 +0,0 @@
#pragma once
#include <chrono>
#include <variant>
#include <filament/Engine.h>
#include <filament/RenderableManager.h>
#include <filament/Renderer.h>
#include <filament/Scene.h>
#include <filament/Texture.h>
#include <filament/TransformManager.h>
#include <math/vec3.h>
#include <math/vec4.h>
#include <math/mat3.h>
#include <math/norm.h>
#include <gltfio/Animator.h>
#include <gltfio/math.h>
#include <utils/SingleInstanceComponentManager.h>
#include "Log.hpp"
template class std::vector<float>;
namespace thermion
{
using namespace filament;
using namespace filament::gltfio;
using namespace utils;
using namespace std::chrono;
typedef std::chrono::time_point<std::chrono::high_resolution_clock> time_point_t;
struct Animation
{
time_point_t start = time_point_t::max();
float startOffset;
bool loop = false;
bool reverse = false;
float durationInSecs = 0;
};
/// @brief
/// The status of an animation embedded in a glTF object.
/// @param index refers to the index of the animation in the animations property of the underlying object.
///
struct GltfAnimation : Animation
{
int index = -1;
};
//
// The status of a morph target animation created dynamically at runtime (not glTF embedded).
//
struct MorphAnimation : Animation
{
int lengthInFrames;
float frameLengthInMs = 0;
std::vector<float> frameData;
std::vector<int> morphIndices;
};
struct BoneAnimation : Animation {
int lengthInFrames;
size_t boneIndex;
size_t skinIndex = 0;
float frameLengthInMs = 0;
std::vector<math::mat4f> frameData;
float fadeOutInSecs = 0;
float fadeInInSecs = 0;
float maxDelta = 1.0f;
};
/// @brief
///
///
struct BoneAnimationComponent
{
FilamentInstance * target;
std::vector<BoneAnimation> animations;
};
/// @brief
///
///
struct MorphAnimationComponent
{
std::vector<MorphAnimation> animations;
};
/// @brief
///
///
struct GltfAnimationComponent
{
FilamentInstance * target;
// the index of the last active glTF animation,
// used to cross-fade
int fadeGltfAnimationIndex = -1;
float fadeDuration = 0.0f;
float fadeOutAnimationStart = 0.0f;
std::vector<GltfAnimation> animations;
};
class GltfAnimationComponentManager : public utils::SingleInstanceComponentManager<GltfAnimationComponent> {
public:
GltfAnimationComponentManager(
filament::TransformManager &transformManager,
filament::RenderableManager &renderableManager) :
mTransformManager(transformManager), mRenderableManager(renderableManager) {};
~GltfAnimationComponentManager() = default;
void addAnimationComponent(FilamentInstance *target);
void removeAnimationComponent(FilamentInstance *target);
void update();
private:
filament::TransformManager &mTransformManager;
filament::RenderableManager &mRenderableManager;
};
class BoneAnimationComponentManager : public utils::SingleInstanceComponentManager<BoneAnimationComponent> {
public:
BoneAnimationComponentManager(
filament::TransformManager &transformManager,
filament::RenderableManager &renderableManager) :
mTransformManager(transformManager), mRenderableManager(renderableManager) {};
~BoneAnimationComponentManager() {};
void addAnimationComponent(FilamentInstance *target);
void removeAnimationComponent(FilamentInstance *target);
void update();
private:
filament::TransformManager &mTransformManager;
filament::RenderableManager &mRenderableManager;
};
class MorphAnimationComponentManager : public utils::SingleInstanceComponentManager<MorphAnimationComponent> {
public:
MorphAnimationComponentManager(
filament::TransformManager &transformManager,
filament::RenderableManager &renderableManager) :
mTransformManager(transformManager), mRenderableManager(renderableManager) {};
~MorphAnimationComponentManager() {};
void addAnimationComponent(Entity entity);
void removeAnimationComponent(Entity entity);
void update();
private:
filament::TransformManager &mTransformManager;
filament::RenderableManager &mRenderableManager;
};
}

View File

@@ -0,0 +1,35 @@
#pragma once
#include <chrono>
#include <vector>
#include <gltfio/FilamentInstance.h>
#include <math/vec3.h>
#include <math/vec4.h>
#include <math/mat3.h>
#include <math/norm.h>
#include "Log.hpp"
namespace thermion
{
using namespace filament;
using namespace std::chrono;
typedef std::chrono::time_point<std::chrono::high_resolution_clock> time_point_t;
struct Animation
{
time_point_t start = time_point_t::max();
float startOffset;
bool loop = false;
bool reverse = false;
float durationInSecs = 0;
};
}

View File

@@ -0,0 +1,68 @@
#pragma once
#include <filament/Engine.h>
#include <filament/RenderableManager.h>
#include <filament/Renderer.h>
#include <filament/Scene.h>
#include <filament/Texture.h>
#include <filament/TransformManager.h>
#include <math/vec3.h>
#include <math/vec4.h>
#include <math/mat3.h>
#include <math/norm.h>
#include <gltfio/Animator.h>
#include <gltfio/math.h>
#include <utils/SingleInstanceComponentManager.h>
#include "Log.hpp"
#include "components/Animation.hpp"
namespace thermion
{
using namespace filament;
using namespace filament::gltfio;
using namespace utils;
struct BoneAnimation : Animation {
int lengthInFrames;
size_t boneIndex;
size_t skinIndex = 0;
float frameLengthInMs = 0;
std::vector<math::mat4f> frameData;
float fadeOutInSecs = 0;
float fadeInInSecs = 0;
float maxDelta = 1.0f;
};
/// @brief
///
///
struct BoneAnimationComponent
{
filament::gltfio::FilamentInstance * target;
std::vector<BoneAnimation> animations;
};
class BoneAnimationComponentManager : public utils::SingleInstanceComponentManager<BoneAnimationComponent> {
public:
BoneAnimationComponentManager(
filament::TransformManager &transformManager,
filament::RenderableManager &renderableManager) :
mTransformManager(transformManager), mRenderableManager(renderableManager) {};
~BoneAnimationComponentManager() {};
void addAnimationComponent(FilamentInstance *target);
void removeAnimationComponent(FilamentInstance *target);
void update();
private:
filament::TransformManager &mTransformManager;
filament::RenderableManager &mRenderableManager;
};
}

View File

@@ -0,0 +1,74 @@
#pragma once
#include <filament/Engine.h>
#include <filament/RenderableManager.h>
#include <filament/Renderer.h>
#include <filament/Scene.h>
#include <filament/Texture.h>
#include <filament/TransformManager.h>
#include <math/vec3.h>
#include <math/vec4.h>
#include <math/mat3.h>
#include <math/norm.h>
#include <gltfio/Animator.h>
#include <gltfio/math.h>
#include <utils/SingleInstanceComponentManager.h>
#include "Log.hpp"
#include "scene/GltfSceneAssetInstance.hpp"
#include "components/Animation.hpp"
template class std::vector<float>;
namespace thermion
{
using namespace filament;
using namespace filament::gltfio;
using namespace utils;
using namespace std::chrono;
/// @brief
/// The status of an animation embedded in a glTF object.
/// @param index refers to the index of the animation in the animations property of the underlying object.
///
struct GltfAnimation : Animation
{
int index = -1;
};
/// @brief
///
///
struct GltfAnimationComponent
{
filament::gltfio::FilamentInstance * target;
// the index of the last active glTF animation,
// used to cross-fade
int fadeGltfAnimationIndex = -1;
float fadeDuration = 0.0f;
float fadeOutAnimationStart = 0.0f;
std::vector<GltfAnimation> animations;
};
class GltfAnimationComponentManager : public utils::SingleInstanceComponentManager<GltfAnimationComponent> {
public:
GltfAnimationComponentManager(
filament::TransformManager &transformManager,
filament::RenderableManager &renderableManager) :
mTransformManager(transformManager), mRenderableManager(renderableManager) {};
~GltfAnimationComponentManager() = default;
void addAnimationComponent(FilamentInstance *target);
void removeAnimationComponent(FilamentInstance *target);
bool addGltfAnimation(FilamentInstance *target, int index, bool loop, bool reverse, bool replaceActive, float crossfade, float startOffset);
// GltfAnimationComponent getAnimationComponentInstance(FilamentInstance *target);
void update();
private:
filament::TransformManager &mTransformManager;
filament::RenderableManager &mRenderableManager;
};
}

View File

@@ -0,0 +1,67 @@
#pragma once
#include <filament/Engine.h>
#include <filament/RenderableManager.h>
#include <filament/Renderer.h>
#include <filament/Scene.h>
#include <filament/Texture.h>
#include <filament/TransformManager.h>
#include <math/vec3.h>
#include <math/vec4.h>
#include <math/mat3.h>
#include <math/norm.h>
#include <gltfio/Animator.h>
#include <gltfio/math.h>
#include <utils/SingleInstanceComponentManager.h>
#include "Log.hpp"
#include "components/Animation.hpp"
namespace thermion
{
using namespace filament;
using namespace filament::gltfio;
using namespace utils;
using namespace std::chrono;
//
// The status of a morph target animation created dynamically at runtime (not glTF embedded).
//
struct MorphAnimation : Animation
{
int lengthInFrames;
float frameLengthInMs = 0;
std::vector<float> frameData;
std::vector<int> morphIndices;
};
/// @brief
///
///
struct MorphAnimationComponent
{
std::vector<MorphAnimation> animations;
};
class MorphAnimationComponentManager : public utils::SingleInstanceComponentManager<MorphAnimationComponent> {
public:
MorphAnimationComponentManager(
filament::TransformManager &transformManager,
filament::RenderableManager &renderableManager) :
mTransformManager(transformManager), mRenderableManager(renderableManager) {};
~MorphAnimationComponentManager() {};
void addAnimationComponent(Entity entity);
void removeAnimationComponent(Entity entity);
void update();
private:
filament::TransformManager &mTransformManager;
filament::RenderableManager &mRenderableManager;
};
}

View File

@@ -9,10 +9,12 @@
#include "c_api/APIBoundaryTypes.h"
#include "components/CollisionComponentManager.hpp"
#include "components/AnimationComponentManager.hpp"
#include "GltfSceneAssetInstance.hpp"
#include "GltfSceneAsset.hpp"
#include "SceneAsset.hpp"
#include "components/GltfAnimationComponentManager.hpp"
#include "components/MorphAnimationComponentManager.hpp"
#include "components/BoneAnimationComponentManager.hpp"
#include "scene/GltfSceneAssetInstance.hpp"
#include "scene/GltfSceneAsset.hpp"
#include "scene/SceneAsset.hpp"
namespace thermion
{

View File

@@ -14,7 +14,6 @@
#include <utils/NameComponentManager.h>
#include "scene/GltfSceneAssetInstance.hpp"
#include "components/AnimationComponentManager.hpp"
#include "components/CollisionComponentManager.hpp"
#include "scene/SceneAsset.hpp"

View File

@@ -31,49 +31,93 @@ extern "C"
EMSCRIPTEN_KEEPALIVE bool AnimationManager_addGltfAnimationComponent(TAnimationManager *tAnimationManager, TSceneAsset *tSceneAsset)
{
auto sceneAsset = reinterpret_cast<SceneAsset *>(tSceneAsset);
if(sceneAsset->getType() != SceneAsset::SceneAssetType::Gltf || !sceneAsset->isInstance()) {
if(sceneAsset->getType() != SceneAsset::SceneAssetType::Gltf) {
return false;
}
GltfSceneAssetInstance *instance;
if (sceneAsset->isInstance())
{
instance = reinterpret_cast<GltfSceneAssetInstance *>(sceneAsset);
} else {
instance = reinterpret_cast<GltfSceneAssetInstance *>(sceneAsset->getInstanceAt(0));
}
auto animationManager = reinterpret_cast<AnimationManager *>(tAnimationManager);
animationManager->addGltfAnimationComponent(reinterpret_cast<GltfSceneAssetInstance *>(sceneAsset));
animationManager->addGltfAnimationComponent(instance);
return true;
}
EMSCRIPTEN_KEEPALIVE bool AnimationManager_removeGltfAnimationComponent(TAnimationManager *tAnimationManager, TSceneAsset *tSceneAsset)
{
auto sceneAsset = reinterpret_cast<SceneAsset *>(tSceneAsset);
if(sceneAsset->getType() != SceneAsset::SceneAssetType::Gltf || !sceneAsset->isInstance()) {
if(sceneAsset->getType() != SceneAsset::SceneAssetType::Gltf) {
return false;
}
GltfSceneAssetInstance *instance;
if (sceneAsset->isInstance())
{
instance = reinterpret_cast<GltfSceneAssetInstance *>(sceneAsset);
} else {
instance = reinterpret_cast<GltfSceneAssetInstance *>(sceneAsset->getInstanceAt(0));
}
auto animationManager = reinterpret_cast<AnimationManager *>(tAnimationManager);
animationManager->removeGltfAnimationComponent(reinterpret_cast<GltfSceneAssetInstance *>(sceneAsset));
animationManager->removeGltfAnimationComponent(instance);
return true;
}
EMSCRIPTEN_KEEPALIVE bool AnimationManager_addBoneAnimationComponent(TAnimationManager *tAnimationManager, TSceneAsset *tSceneAsset)
{
auto sceneAsset = reinterpret_cast<SceneAsset *>(tSceneAsset);
if(sceneAsset->getType() != SceneAsset::SceneAssetType::Gltf || !sceneAsset->isInstance()) {
if(sceneAsset->getType() != SceneAsset::SceneAssetType::Gltf) {
return false;
}
GltfSceneAssetInstance *instance;
if (sceneAsset->isInstance())
{
instance = reinterpret_cast<GltfSceneAssetInstance *>(sceneAsset);
} else {
instance = reinterpret_cast<GltfSceneAssetInstance *>(sceneAsset->getInstanceAt(0));
}
auto animationManager = reinterpret_cast<AnimationManager *>(tAnimationManager);
animationManager->addBoneAnimationComponent(reinterpret_cast<GltfSceneAssetInstance *>(sceneAsset));
animationManager->addBoneAnimationComponent(instance);
return true;
}
EMSCRIPTEN_KEEPALIVE bool AnimationManager_removeBoneAnimationComponent(TAnimationManager *tAnimationManager, TSceneAsset *tSceneAsset)
{
auto sceneAsset = reinterpret_cast<SceneAsset *>(tSceneAsset);
if(sceneAsset->getType() != SceneAsset::SceneAssetType::Gltf || !sceneAsset->isInstance()) {
if(sceneAsset->getType() != SceneAsset::SceneAssetType::Gltf) {
return false;
}
if(sceneAsset->getType() != SceneAsset::SceneAssetType::Gltf) {
return false;
}
GltfSceneAssetInstance *instance;
if (sceneAsset->isInstance())
{
instance = reinterpret_cast<GltfSceneAssetInstance *>(sceneAsset);
} else {
instance = reinterpret_cast<GltfSceneAssetInstance *>(sceneAsset->getInstanceAt(0));
}
auto animationManager = reinterpret_cast<AnimationManager *>(tAnimationManager);
animationManager->removeBoneAnimationComponent(reinterpret_cast<GltfSceneAssetInstance *>(sceneAsset));
animationManager->removeBoneAnimationComponent(instance);
return true;
}
@@ -124,15 +168,27 @@ extern "C"
return true;
}
EMSCRIPTEN_KEEPALIVE void AnimationManager_resetToRestPose(TAnimationManager *tAnimationManager, TSceneAsset *sceneAsset)
EMSCRIPTEN_KEEPALIVE void AnimationManager_resetToRestPose(TAnimationManager *tAnimationManager, TSceneAsset *tSceneAsset)
{
auto *animationManager = reinterpret_cast<AnimationManager *>(tAnimationManager);
auto asset = reinterpret_cast<SceneAsset *>(sceneAsset);
if (asset->getType() == SceneAsset::SceneAssetType::Gltf && asset->isInstance())
{
auto *instance = reinterpret_cast<GltfSceneAssetInstance *>(asset);
animationManager->resetToRestPose(instance);
auto sceneAsset = reinterpret_cast<SceneAsset *>(tSceneAsset);
if (sceneAsset->getType() != SceneAsset::SceneAssetType::Gltf) {
Log("Error - incorrect asset type, cannot reset to reset pose");
return;
}
GltfSceneAssetInstance *instance;
if (sceneAsset->isInstance())
{
instance = reinterpret_cast<GltfSceneAssetInstance *>(sceneAsset);
} else {
instance = reinterpret_cast<GltfSceneAssetInstance *>(sceneAsset->getInstanceAt(0));
}
animationManager->resetToRestPose(instance);
}
EMSCRIPTEN_KEEPALIVE bool AnimationManager_addBoneAnimation(
@@ -297,19 +353,31 @@ extern "C"
return true;
}
EMSCRIPTEN_KEEPALIVE void AnimationManager_setGltfAnimationFrame(
EMSCRIPTEN_KEEPALIVE bool AnimationManager_setGltfAnimationFrame(
TAnimationManager *tAnimationManager,
TSceneAsset *tSceneAsset,
int animationIndex,
int frame)
{
auto *animationManager = reinterpret_cast<AnimationManager *>(tAnimationManager);
auto asset = reinterpret_cast<SceneAsset *>(tSceneAsset);
if (asset->getType() == SceneAsset::SceneAssetType::Gltf && asset->isInstance())
{
auto *instance = reinterpret_cast<GltfSceneAssetInstance *>(asset);
animationManager->setGltfAnimationFrame(instance, animationIndex, frame);
auto sceneAsset = reinterpret_cast<SceneAsset *>(tSceneAsset);
if (sceneAsset->getType() != SceneAsset::SceneAssetType::Gltf) {
return false;
}
GltfSceneAssetInstance *instance;
if (sceneAsset->isInstance())
{
instance = reinterpret_cast<GltfSceneAssetInstance *>(sceneAsset);
} else {
instance = reinterpret_cast<GltfSceneAssetInstance *>(sceneAsset->getInstanceAt(0));
}
animationManager->setGltfAnimationFrame(instance, animationIndex, frame);
return true;
}
EMSCRIPTEN_KEEPALIVE float AnimationManager_getGltfAnimationDuration(
@@ -320,7 +388,7 @@ extern "C"
auto sceneAsset = reinterpret_cast<SceneAsset *>(tSceneAsset);
if(sceneAsset->getType() != SceneAsset::SceneAssetType::Gltf) {
return false;
return -1.0;
}
auto animationManager = reinterpret_cast<AnimationManager *>(tAnimationManager);

View File

@@ -1,7 +1,7 @@
#include <chrono>
#include <variant>
#include "components/AnimationComponentManager.hpp"
#include "components/BoneAnimationComponentManager.hpp"
#include "Log.hpp"

View File

@@ -1,7 +1,7 @@
#include <chrono>
#include <variant>
#include "components/AnimationComponentManager.hpp"
#include "components/GltfAnimationComponentManager.hpp"
#include "Log.hpp"
@@ -15,9 +15,75 @@ namespace thermion
}
}
bool GltfAnimationComponentManager::addGltfAnimation(FilamentInstance *target, int index, bool loop, bool reverse, bool replaceActive, float crossfade, float startOffset) {
EntityInstanceBase::Type componentInstance = getInstance(target->getRoot());
auto &animationComponent = this->elementAt<0>(componentInstance);
animationComponent.target = target;
if (replaceActive)
{
if (animationComponent.animations.size() > 0)
{
auto &last = animationComponent.animations.back();
animationComponent.fadeGltfAnimationIndex = last.index;
animationComponent.fadeDuration = crossfade;
auto now = high_resolution_clock::now();
auto elapsedInSecs = float(std::chrono::duration_cast<std::chrono::milliseconds>(now - last.start).count()) / 1000.0f;
animationComponent.fadeOutAnimationStart = elapsedInSecs;
animationComponent.animations.clear();
}
else
{
animationComponent.fadeGltfAnimationIndex = -1;
animationComponent.fadeDuration = 0.0f;
}
}
else if (crossfade > 0)
{
Log("ERROR: crossfade only supported when replaceActive is true.");
return false;
}
else
{
animationComponent.fadeGltfAnimationIndex = -1;
animationComponent.fadeDuration = 0.0f;
}
GltfAnimation animation;
animation.startOffset = startOffset;
animation.index = index;
animation.start = std::chrono::high_resolution_clock::now();
animation.loop = loop;
animation.reverse = reverse;
animation.durationInSecs = target->getAnimator()->getAnimationDuration(index);
bool found = false;
// don't play the animation if it's already running
for (int i = 0; i < animationComponent.animations.size(); i++)
{
if (animationComponent.animations[i].index == index)
{
found = true;
break;
}
}
if (!found)
{
animationComponent.animations.push_back(animation);
}
return true;
}
void GltfAnimationComponentManager::removeAnimationComponent(FilamentInstance *target) {
if(hasComponent(target->getRoot())) {
removeComponent(target->getRoot());
TRACE("Found component, component removed");
} else {
TRACE("Component not found, skipping removal");
}
}

View File

@@ -1,7 +1,7 @@
#include <chrono>
#include <variant>
#include "components/AnimationComponentManager.hpp"
#include "components/MorphAnimationComponentManager.hpp"
#include "Log.hpp"

View File

@@ -11,8 +11,6 @@
#include "Log.hpp"
#include "components/AnimationComponentManager.hpp"
#include "components/AnimationComponentManager.hpp"
#include "scene/AnimationManager.hpp"
#include "scene/SceneAsset.hpp"
#include "scene/GltfSceneAssetInstance.hpp"
@@ -334,71 +332,7 @@ namespace thermion
return;
}
if (!_gltfAnimationComponentManager->hasComponent(instance->getEntity()))
{
_gltfAnimationComponentManager->addComponent(instance->getEntity());
Log("ERROR: specified entity is not animatable (has no animation component attached).");
return;
}
auto animationComponentInstance = _gltfAnimationComponentManager->getInstance(instance->getEntity());
auto &animationComponent = _gltfAnimationComponentManager->elementAt<0>(animationComponentInstance);
animationComponent.target = instance->getInstance();
if (replaceActive)
{
if (animationComponent.animations.size() > 0)
{
auto &last = animationComponent.animations.back();
animationComponent.fadeGltfAnimationIndex = last.index;
animationComponent.fadeDuration = crossfade;
auto now = high_resolution_clock::now();
auto elapsedInSecs = float(std::chrono::duration_cast<std::chrono::milliseconds>(now - last.start).count()) / 1000.0f;
animationComponent.fadeOutAnimationStart = elapsedInSecs;
animationComponent.animations.clear();
}
else
{
animationComponent.fadeGltfAnimationIndex = -1;
animationComponent.fadeDuration = 0.0f;
}
}
else if (crossfade > 0)
{
Log("ERROR: crossfade only supported when replaceActive is true.");
return;
}
else
{
animationComponent.fadeGltfAnimationIndex = -1;
animationComponent.fadeDuration = 0.0f;
}
GltfAnimation animation;
animation.startOffset = startOffset;
animation.index = index;
animation.start = std::chrono::high_resolution_clock::now();
animation.loop = loop;
animation.reverse = reverse;
animation.durationInSecs = instance->getInstance()->getAnimator()->getAnimationDuration(index);
bool found = false;
// don't play the animation if it's already running
for (int i = 0; i < animationComponent.animations.size(); i++)
{
if (animationComponent.animations[i].index == index)
{
found = true;
break;
}
}
if (!found)
{
animationComponent.animations.push_back(animation);
}
_gltfAnimationComponentManager->addGltfAnimation(instance->getInstance(), index, loop, reverse, replaceActive, crossfade, startOffset);
}
void AnimationManager::stopGltfAnimation(GltfSceneAssetInstance *instance, int index)
@@ -515,35 +449,47 @@ namespace thermion
bool AnimationManager::addGltfAnimationComponent(GltfSceneAssetInstance *instance)
{
std::lock_guard lock(_mutex);
_gltfAnimationComponentManager->addAnimationComponent(instance->getInstance());
TRACE("Added glTF animation component");
return true;
}
void AnimationManager::removeGltfAnimationComponent(GltfSceneAssetInstance *instance)
{
std::lock_guard lock(_mutex);
_gltfAnimationComponentManager->removeAnimationComponent(instance->getInstance());
TRACE("Removed glTF animation component");
}
bool AnimationManager::addBoneAnimationComponent(GltfSceneAssetInstance *instance)
{
std::lock_guard lock(_mutex);
_boneAnimationComponentManager->addAnimationComponent(instance->getInstance());
TRACE("Added bone animation component");
return true;
}
void AnimationManager::removeBoneAnimationComponent(GltfSceneAssetInstance *instance)
{
std::lock_guard lock(_mutex);
_boneAnimationComponentManager->removeAnimationComponent(instance->getInstance());
TRACE("Removed bone animation component");
}
bool AnimationManager::addMorphAnimationComponent(utils::Entity entity)
{
std::lock_guard lock(_mutex);
_morphAnimationComponentManager->addAnimationComponent(entity);
TRACE("Added morph animation component");
return true;
}
void AnimationManager::removeMorphAnimationComponent(utils::Entity entity)
{
std::lock_guard lock(_mutex);
_morphAnimationComponentManager->removeAnimationComponent(entity);
TRACE("Removed morph animation component");
}
}

View File

@@ -12,19 +12,18 @@
#include <filament/VertexBuffer.h>
#include <filament/IndexBuffer.h>
#include <gltfio/AssetLoader.h>
#include <gltfio/Animator.h>
#include <gltfio/FilamentAsset.h>
#include <gltfio/FilamentInstance.h>
#include <gltfio/MaterialProvider.h>
#include <utils/NameComponentManager.h>
#include "scene/GltfSceneAssetInstance.hpp"
#include "components/AnimationComponentManager.hpp"
#include "components/CollisionComponentManager.hpp"
#include "scene/SceneAsset.hpp"
namespace thermion
{

View File

@@ -38,52 +38,69 @@ void main() async {
test('set morph target weights', () async {
await testHelper.withViewer((viewer) async {
final cube = await viewer.loadGltf(
"${testHelper.testDir}/assets/cube_with_morph_targets.glb");
final cube = await viewer
.loadGltf("${testHelper.testDir}/assets/cube_with_morph_targets.glb");
await viewer.addToScene(cube);
await testHelper.capture(viewer.view, "cube_no_morph");
await cube.setMorphTargetWeights((await cube.getChildEntities()).first, [1.0]);
await cube
.setMorphTargetWeights((await cube.getChildEntities()).first, [1.0]);
await testHelper.capture(viewer.view, "cube_with_morph");
}, bg:kRed, cameraPosition: Vector3(3, 2, 6));
}, bg: kRed, cameraPosition: Vector3(3, 2, 6));
});
test('set morph target animation', () async {
await testHelper.withViewer((viewer) async {
final cube = await viewer.loadGltf(
"${testHelper.testDir}/assets/cube_with_morph_targets.glb");
final cube = await viewer
.loadGltf("${testHelper.testDir}/assets/cube_with_morph_targets.glb");
await viewer.addToScene(cube);
await testHelper.capture(viewer.view, "cube_morph_animation_reset");
var morphData = MorphAnimationData(Float32List.fromList([1.0]), ["Key 1"],
frameLengthInMs: 1000.0 / 60.0);
await cube.setMorphAnimationData(morphData);
await viewer.render();
await testHelper.capture(viewer.view, "cube_with_morph_animation");
}, bg:kRed, cameraPosition: Vector3(3, 2, -6));
await testHelper.capture(viewer.view, "cube_morph_animation_playing");
}, bg: kRed, cameraPosition: Vector3(3, 2, -6));
});
test('get gltf animation names', () async {
test('play/stop gltf animation', () async {
await testHelper.withViewer((viewer) async {
final cube = await viewer
.loadGltf("${testHelper.testDir}/assets/cube_with_morph_targets.glb");
await viewer.addToScene(cube);
await testHelper.capture(viewer.view, "gltf_animation_stopped");
final animationNames = await cube.getGltfAnimationNames();
expect(animationNames.first, "CubeAction");
await testHelper.capture(viewer.view, "gltf_animation_rest");
await viewer.render();
await cube.playGltfAnimation(0);
await Future.delayed(Duration(seconds: 1));
await Future.delayed(Duration(milliseconds: 750));
await viewer.render();
await testHelper.capture(viewer.view, "gltf_animation_started");
await viewer.render();
await Future.delayed(Duration(milliseconds: 1000));
await viewer.render();
await cube.stopGltfAnimation(0);
await viewer.render();
await testHelper.capture(viewer.view, "gltf_animation_stopped");
await viewer.destroyAsset(cube);
await viewer.render();
await testHelper.capture(viewer.view, "gltf_asset_destroyed");
}, bg: kRed);
});
}

View File

@@ -0,0 +1 @@
../../../examples/assets/BusterDrone