Просмотр исходного кода

resource: add MeshAnimationResource

Part-of: #276
Daniele Bartolini 11 месяцев назад
Родитель
Сommit
38ac821271

+ 2 - 0
src/device/device.cpp

@@ -45,6 +45,7 @@
 #include "resource/material_resource.h"
 #include "resource/mesh_resource.h"
 #include "resource/mesh_skeleton_resource.h"
+#include "resource/mesh_animation_resource.h"
 #include "resource/package_resource.h"
 #include "resource/physics_resource.h"
 #include "resource/resource_id.inl"
@@ -586,6 +587,7 @@ void Device::run()
 	_resource_manager->register_type(RESOURCE_TYPE_MATERIAL,         RESOURCE_VERSION_MATERIAL,         NULL,      NULL,        mtr::online, mtr::offline);
 	_resource_manager->register_type(RESOURCE_TYPE_MESH,             RESOURCE_VERSION_MESH,             mhr::load, mhr::unload, mhr::online, mhr::offline);
 	_resource_manager->register_type(RESOURCE_TYPE_MESH_SKELETON,    RESOURCE_VERSION_MESH_SKELETON,    NULL,      NULL,        NULL,        NULL);
+	_resource_manager->register_type(RESOURCE_TYPE_MESH_ANIMATION,   RESOURCE_VERSION_MESH_ANIMATION,   NULL,      NULL,        NULL,        NULL);
 	_resource_manager->register_type(RESOURCE_TYPE_PACKAGE,          RESOURCE_VERSION_PACKAGE,          NULL,      NULL,        NULL,        NULL);
 	_resource_manager->register_type(RESOURCE_TYPE_PHYSICS_CONFIG,   RESOURCE_VERSION_PHYSICS_CONFIG,   NULL,      NULL,        NULL,        NULL);
 	_resource_manager->register_type(RESOURCE_TYPE_SCRIPT,           RESOURCE_VERSION_SCRIPT,           NULL,      NULL,        NULL,        NULL);

+ 2 - 0
src/resource/data_compiler.cpp

@@ -37,6 +37,7 @@
 #include "resource/material_resource.h"
 #include "resource/mesh_resource.h"
 #include "resource/mesh_skeleton_resource.h"
+#include "resource/mesh_animation_resource.h"
 #include "resource/package_resource.h"
 #include "resource/physics_resource.h"
 #include "resource/resource_id.inl"
@@ -1542,6 +1543,7 @@ int main_data_compiler(const DeviceOptions &opts)
 	dc->register_compiler("material",         RESOURCE_VERSION_MATERIAL,         material_resource_internal::compile);
 	dc->register_compiler("mesh",             RESOURCE_VERSION_MESH,             mesh_resource_internal::compile);
 	dc->register_compiler("mesh_skeleton",    RESOURCE_VERSION_MESH_SKELETON,    mesh_skeleton_resource_internal::compile);
+	dc->register_compiler("mesh_animation",   RESOURCE_VERSION_MESH_ANIMATION,   mesh_animation_resource_internal::compile);
 	dc->register_compiler("package",          RESOURCE_VERSION_PACKAGE,          package_resource_internal::compile);
 	dc->register_compiler("physics_config",   RESOURCE_VERSION_PHYSICS_CONFIG,   physics_config_resource_internal::compile);
 	dc->register_compiler("lua",              RESOURCE_VERSION_SCRIPT,           lua_resource_internal::compile);

+ 190 - 0
src/resource/mesh_animation.cpp

@@ -0,0 +1,190 @@
+/*
+ * Copyright (c) 2012-2025 Daniele Bartolini et al.
+ * SPDX-License-Identifier: MIT
+ */
+
+#include "resource/mesh_animation.h"
+
+#if CROWN_CAN_COMPILE
+#   include "core/error/error.inl"
+#   include "core/json/json_object.inl"
+#   include "core/json/sjson.h"
+#   include "core/memory/temp_allocator.inl"
+#   include "core/strings/dynamic_string.inl"
+#   include "core/strings/string_id.inl"
+#   include "device/log.h"
+#   include "resource/compile_options.inl"
+#   include "resource/mesh_animation_fbx.h"
+#   include "resource/mesh_skeleton.h"
+#   include <algorithm> // std::sort
+
+#define DUMP_KEYS 0
+
+LOG_SYSTEM(MESH_ANIMATION, "mesh_animation")
+
+namespace crown
+{
+namespace mesh_animation
+{
+#if DUMP_KEYS
+	static void dump_keys(AnimationKey *begin, AnimationKey *end)
+	{
+		char buf[256];
+
+		for (auto cur = begin; cur != end; ++cur)
+			logi(MESH_ANIMATION, "b %hu t %hu type %hu val %s"
+				, cur->h.track_id
+				, cur->h.time
+				, cur->h.type
+				, cur->h.type == 0
+				? to_string(buf, sizeof(buf), cur->p.value)
+				: to_string(buf, sizeof(buf), cur->r.value)
+				);
+	}
+#endif
+
+	u16 track_id(MeshAnimation &a, u16 bone_id, u16 parameter_type)
+	{
+		CE_ENSURE(bone_id < MESH_SKELETON_MAX_BONES);
+		CE_ENSURE(parameter_type < AnimationKeyHeader::Type::COUNT);
+
+		u16 t = (bone_id << 2) | u16(parameter_type);
+
+		u16 track_id_not_found = UINT16_MAX;
+		u16 track_id = hash_map::get(a.track_ids, t, track_id_not_found);
+		if (track_id == track_id_not_found) {
+			track_id = array::size(a.bone_ids);
+			array::push_back(a.bone_ids, bone_id);
+			hash_map::set(a.track_ids, t, track_id);
+		}
+
+		return track_id;
+	}
+
+	static s32 generate_sorted_keys(MeshAnimation &ma)
+	{
+#if 0
+		// Test data.
+		array::clear(ma.keys);
+		array::clear(ma.indices);
+
+		array::push_back(ma.indices, { { 0, 0, 0 }, array::size(ma.keys), 2 });
+		array::push_back(ma.keys, { { 0, 0,  0 } });
+		array::push_back(ma.keys, { { 0, 0, 10 } });
+
+		array::push_back(ma.indices, { { 0, 1, 0 }, array::size(ma.keys), 4 });
+		array::push_back(ma.keys, { { 0, 1,  0 } });
+		array::push_back(ma.keys, { { 0, 1,  1 } });
+		array::push_back(ma.keys, { { 0, 1,  5 } });
+		array::push_back(ma.keys, { { 0, 1, 10 } });
+
+		array::push_back(ma.indices, { { 0, 2, 0 }, array::size(ma.keys), 4 });
+		array::push_back(ma.keys, { { 0, 2,  0 } });
+		array::push_back(ma.keys, { { 0, 2,  6 } });
+		array::push_back(ma.keys, { { 0, 2,  8 } });
+		array::push_back(ma.keys, { { 0, 2, 10 } });
+#endif // if 0
+
+		// Sort indices by track ID. This ensures that when we encounter multiple keys
+		// with matching times, we choose the key with the smallest track ID first.
+		std::sort(array::begin(ma.indices)
+			, array::end(ma.indices)
+			, [](const AnimationKeyIndex &a, const AnimationKeyIndex &b) {
+				return a.h.track_id < b.h.track_id;
+			});
+
+		// Generate a list of animation keys sorted by key access time.
+		// Start by getting the first two keys for each track.
+		for (u32 i = 0; i < array::size(ma.indices); ++i) {
+			AnimationKeyIndex &idx = ma.indices[i];
+
+			array::push_back(ma.sorted_keys, ma.keys[idx.offset + idx.cur++]);
+			array::push_back(ma.sorted_keys, ma.keys[idx.offset + idx.cur++]);
+		}
+
+		while (array::size(ma.sorted_keys) != array::size(ma.keys)) {
+			AnimationKeyIndex *next_key = NULL;
+			// For each track, choose the key that will be needed next.
+			for (u32 i = 0; i < array::size(ma.indices); ++i) {
+				AnimationKeyIndex &idx = ma.indices[i];
+				// There are no more keys in this track. Skip it.
+				if (idx.cur > idx.num - 1)
+					continue;
+
+				// Select this as the next key if none have been selected so far.
+				if (next_key == NULL) {
+					next_key = &idx;
+					continue;
+				} else {
+					// If next key's previous time is greater than current
+					// key's previous time, then we need to get this key next.
+					auto next_prev_time = ma.keys[next_key->offset + next_key->cur - 1].h.time;
+					auto this_prev_time = ma.keys[idx.offset + idx.cur - 1].h.time;
+					if (next_prev_time > this_prev_time)
+						next_key = &idx;
+				}
+			}
+
+			CE_ENSURE(next_key != NULL);
+			array::push_back(ma.sorted_keys, ma.keys[next_key->offset + next_key->cur]);
+			++next_key->cur;
+		}
+
+#if DUMP_KEYS
+		dump_keys(array::begin(ma.sorted_keys), array::end(ma.sorted_keys));
+#endif
+		return 0;
+	}
+
+	s32 parse(MeshAnimation &ma, Buffer &buf, CompileOptions &opts)
+	{
+		TempAllocator4096 ta;
+		JsonObject obj(ta);
+		RETURN_IF_ERROR(sjson::parse(obj, buf), opts);
+
+		// Parse skeleton.
+		DynamicString target_skeleton(ta);
+		RETURN_IF_ERROR(sjson::parse_string(target_skeleton, obj["target_skeleton"]), opts);
+		RETURN_IF_RESOURCE_MISSING("mesh_skeleton", target_skeleton.c_str(), opts);
+		opts.add_requirement("mesh_skeleton", target_skeleton.c_str());
+		ma.target_skeleton = RETURN_IF_ERROR(sjson::parse_resource_name(obj["target_skeleton"]), opts);
+
+		// Parse animations.
+		RETURN_IF_ERROR(sjson::parse_string(ma.stack_name, obj["stack_name"]), opts);
+
+		DynamicString source(ta);
+		if (json_object::has(obj, "source")) {
+			RETURN_IF_ERROR(sjson::parse_string(source, obj["source"]), opts);
+
+			RETURN_IF_FILE_MISSING(source.c_str(), opts);
+			Buffer fbx_buf = opts.read(source.c_str());
+			s32 err = fbx::parse(ma, fbx_buf, opts);
+			ENSURE_OR_RETURN(err == 0, opts);
+		} else {
+			RETURN_IF_FALSE(false
+				, opts
+				, "Unknown source mesh '%s'"
+				, source.c_str()
+				);
+		}
+
+		return generate_sorted_keys(ma);
+	}
+
+} // namespace mesh_animation
+
+MeshAnimation::MeshAnimation(Allocator &a)
+	: sorted_keys(a)
+	, keys(a)
+	, indices(a)
+	, num_bones(0u)
+	, total_time(0.0f)
+	, target_skeleton(u64(0u))
+	, stack_name(a)
+	, track_ids(a)
+	, bone_ids(a)
+{
+}
+
+} // namespace crown
+#endif // if CROWN_CAN_COMPILE

+ 54 - 0
src/resource/mesh_animation.h

@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2012-2025 Daniele Bartolini et al.
+ * SPDX-License-Identifier: MIT
+ */
+
+#pragma once
+
+#include "config.h"
+
+#if CROWN_CAN_COMPILE
+#   include "core/memory/types.h"
+#   include "core/strings/dynamic_string.h"
+#   include "resource/mesh_animation_resource.h"
+#   include "resource/types.h"
+
+namespace crown
+{
+struct AnimationKeyIndex
+{
+	AnimationKeyHeader h;
+	u32 offset; ///< Offset to first key.
+	u32 num;    ///< Number of keys.
+	u32 cur;    ///< Current key.
+};
+
+struct MeshAnimation
+{
+	Array<AnimationKey> sorted_keys;  ///< Animation keys sorted by access time.
+	Array<AnimationKey> keys;         ///< Unordered animation keys.
+	Array<AnimationKeyIndex> indices; ///< Indices into keys, sorted first by track_id then by type.
+	u32 num_bones;                    ///< Number of bones affected by the animation.
+	f32 total_time;                   ///< Animation duration in seconds.
+	StringId64 target_skeleton;       ///< Reference to the animated skeleton.
+	DynamicString stack_name;         ///< Animation name.
+	HashMap<u16, u16> track_ids;      ///< From (bone_id, parameter_type) to track_id.
+	Array<u16> bone_ids;              ///< From track_id to bone_id
+
+	///
+	explicit MeshAnimation(Allocator &a);
+};
+
+namespace mesh_animation
+{
+	/// Returns the track ID for the pair (bone_id, parameter_type).
+	u16 track_id(MeshAnimation &a, u16 bone_id, u16 parameter_type);
+
+	///
+	s32 parse(MeshAnimation &ma, Buffer &buf, CompileOptions &opts);
+
+} // namespace mesh_animation
+
+} // namespace crown
+
+#endif // if CROWN_CAN_COMPILE

+ 121 - 0
src/resource/mesh_animation_fbx.cpp

@@ -0,0 +1,121 @@
+/*
+ * Copyright (c) 2012-2025 Daniele Bartolini et al.
+ * SPDX-License-Identifier: MIT
+ */
+
+#include "resource/mesh_animation_resource.h"
+
+#if CROWN_CAN_COMPILE
+#   include "core/memory/globals.h"
+#   include "core/strings/dynamic_string.inl"
+#   include "resource/compile_options.inl"
+#   include "resource/fbx_document.h"
+#   include "resource/mesh_animation.h"
+#   include "resource/mesh_skeleton.h"
+#   include <ufbx.h>
+
+namespace crown
+{
+namespace fbx
+{
+	static s32 parse_animation(MeshAnimation &ma, FBXDocument &fbx, ufbx_anim *anim, CompileOptions &opts)
+	{
+		ufbx_error bake_error;
+		ufbx_baked_anim *bake = ufbx_bake_anim(fbx.scene, anim, NULL, &bake_error);
+		RETURN_IF_FALSE(fbx.scene != NULL
+			, opts
+			, "ufbx: %s"
+			, bake_error.description.data
+			);
+
+		ma.total_time = bake->playback_duration;
+
+		for (size_t i = 0; i < bake->nodes.count; i++) {
+			ufbx_baked_node *bake_node = &bake->nodes.data[i];
+			ufbx_node *scene_node = fbx.scene->nodes.data[bake_node->typed_id];
+
+			u16 bone_id = fbx::bone_id(fbx, scene_node->name.data);
+			RETURN_IF_FALSE(bone_id != UINT16_MAX
+				, opts
+				, "Bone '%s' not found in FBX source"
+				, scene_node->name.data
+				);
+			RETURN_IF_FALSE(bone_id < MESH_SKELETON_MAX_BONES
+				, opts
+				, "Maximum number of bones reached %u"
+				, MESH_SKELETON_MAX_BONES
+				);
+
+			AnimationKeyIndex ki;
+			ki.h.type = AnimationKeyHeader::POSITION;
+			ki.h.track_id = mesh_animation::track_id(ma, bone_id, ki.h.type);
+			ki.offset = array::size(ma.keys);
+			ki.num = (u32)bake_node->translation_keys.count;
+			ki.cur = 0;
+			array::push_back(ma.indices, ki);
+
+			for (size_t j = 0; j < bake_node->translation_keys.count; ++j) {
+				ufbx_baked_vec3 *bake_vec3 = &bake_node->translation_keys.data[j];
+
+				AnimationKey key;
+				key.h.type = AnimationKeyHeader::POSITION;
+				key.h.track_id = mesh_animation::track_id(ma, bone_id, key.h.type);
+				key.h.time = u16(bake_vec3->time * 1000.0f);
+				key.p.value.x = (f32)bake_vec3->value.x;
+				key.p.value.y = (f32)bake_vec3->value.y;
+				key.p.value.z = (f32)bake_vec3->value.z;
+				array::push_back(ma.keys, key);
+			}
+
+			ki.h.type = AnimationKeyHeader::ROTATION;
+			ki.h.track_id = mesh_animation::track_id(ma, bone_id, ki.h.type);
+			ki.offset = array::size(ma.keys);
+			ki.num = (u32)bake_node->rotation_keys.count;
+			ki.cur = 0;
+			array::push_back(ma.indices, ki);
+
+			for (size_t j = 0; j < bake_node->rotation_keys.count; ++j) {
+				ufbx_baked_quat *bake_quat = &bake_node->rotation_keys.data[j];
+
+				AnimationKey key;
+				key.h.type = AnimationKeyHeader::ROTATION;
+				key.h.track_id = mesh_animation::track_id(ma, bone_id, key.h.type);
+				key.h.time = u16(bake_quat->time * 1000.0f);
+				key.r.value.x = (f32)bake_quat->value.x;
+				key.r.value.y = (f32)bake_quat->value.y;
+				key.r.value.z = (f32)bake_quat->value.z;
+				key.r.value.w = (f32)bake_quat->value.w;
+				array::push_back(ma.keys, key);
+			}
+		}
+
+		ufbx_free_baked_anim(bake);
+		return 0;
+	}
+
+	static s32 parse_animations(MeshAnimation &ma, FBXDocument &fbx, CompileOptions &opts)
+	{
+		// Find matching animation in FBX file.
+		for (size_t i = 0; i < fbx.scene->anim_stacks.count; ++i) {
+			ufbx_anim_stack *stack = fbx.scene->anim_stacks.data[i];
+			if (ma.stack_name == stack->name.data)
+				return parse_animation(ma, fbx, stack->anim, opts);
+		}
+
+		opts.error("No matching animation '%s' in FBX source", ma.stack_name.c_str());
+		return -1;
+	}
+
+	s32 parse(MeshAnimation &ma, Buffer &buf, CompileOptions &opts)
+	{
+		FBXDocument fbx(default_allocator());
+		s32 err = fbx::parse(fbx, buf, opts);
+		ENSURE_OR_RETURN(err == 0, opts);
+
+		return parse_animations(ma, fbx, opts);
+	}
+
+} // namespace fbx
+
+} // namespace crown
+#endif // if CROWN_CAN_COMPILE

+ 18 - 0
src/resource/mesh_animation_fbx.h

@@ -0,0 +1,18 @@
+/*
+ * Copyright (c) 2012-2025 Daniele Bartolini et al.
+ * SPDX-License-Identifier: MIT
+ */
+
+#include "config.h"
+
+#if CROWN_CAN_COMPILE
+namespace crown
+{
+namespace fbx
+{
+	s32 parse(MeshAnimation &ma, Buffer &buf, CompileOptions &opts);
+
+} // namespace fbx
+
+} // namespace crown
+#endif

+ 81 - 0
src/resource/mesh_animation_resource.cpp

@@ -0,0 +1,81 @@
+/*
+ * Copyright (c) 2012-2025 Daniele Bartolini et al.
+ * SPDX-License-Identifier: MIT
+ */
+
+#include "resource/mesh_animation_resource.h"
+
+#if CROWN_CAN_COMPILE
+#   include "core/json/json_object.inl"
+#   include "core/json/sjson.h"
+#   include "core/memory/globals.h"
+#   include "core/strings/dynamic_string.inl"
+#   include "core/strings/string_id.inl"
+#   include "resource/mesh_skeleton.h"
+#   include "resource/compile_options.inl"
+#   include "resource/mesh_animation.h"
+
+namespace crown
+{
+namespace mesh_animation_resource_internal
+{
+	static s32 write(MeshAnimation &ma, CompileOptions &opts)
+	{
+		MeshAnimationResource mar;
+		mar.version = RESOURCE_VERSION_MESH_ANIMATION;
+		mar.num_tracks = hash_map::size(ma.track_ids);
+		mar.total_time = ma.total_time;
+		mar.num_keys = array::size(ma.sorted_keys);
+		mar.keys_offset = sizeof(mar);
+		mar._pad0 = 0u;
+		mar.target_skeleton = ma.target_skeleton;
+		mar.num_bones = array::size(ma.bone_ids);
+		mar.bone_ids_offset = mar.keys_offset + mar.num_keys * sizeof(AnimationKey);
+
+		opts.write(mar.version);
+		opts.write(mar.num_tracks);
+		opts.write(mar.total_time);
+		opts.write(mar.num_keys);
+		opts.write(mar.keys_offset);
+		opts.write(mar._pad0);
+		opts.write(mar.target_skeleton);
+		opts.write(mar.num_bones);
+		opts.write(mar.bone_ids_offset);
+
+		for (u32 i = 0; i < array::size(ma.sorted_keys); ++i)
+			opts.write(ma.sorted_keys[i]);
+
+		for (u32 i = 0; i < array::size(ma.bone_ids); ++i)
+			opts.write(ma.bone_ids[i]);
+
+		return 0;
+	}
+
+	s32 compile(CompileOptions &opts)
+	{
+		Buffer buf = opts.read();
+		MeshAnimation ma(default_allocator());
+
+		s32 err = mesh_animation::parse(ma, buf, opts);
+		ENSURE_OR_RETURN(err == 0, opts);
+		return write(ma, opts);
+	}
+
+} // namespace mesh_animation_resource_internal
+
+namespace mesh_animation_resource
+{
+	const AnimationKey *animation_keys(const MeshAnimationResource *mar)
+	{
+		return (AnimationKey *)((char *)mar + mar->keys_offset);
+	}
+
+	const u16 *bone_ids(const MeshAnimationResource *mar)
+	{
+		return (u16 *)((char *)mar + mar->bone_ids_offset);
+	}
+
+} // namespace mesh_animation_resource
+
+} // namespace crown
+#endif // if CROWN_CAN_COMPILE

+ 84 - 0
src/resource/mesh_animation_resource.h

@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2012-2025 Daniele Bartolini et al.
+ * SPDX-License-Identifier: MIT
+ */
+
+#pragma once
+
+#include "config.h"
+#include "core/math/types.h"
+#include "core/strings/string_id.h"
+#include "resource/types.h"
+
+namespace crown
+{
+struct AnimationKeyHeader
+{
+	enum Type
+	{
+		POSITION = 0, ///< Position data.
+		ROTATION = 1, ///< Rotation data.
+
+		COUNT
+	};
+
+	u32 type : 1;      ///< AnimationKeyHeader::Type
+	u32 track_id : 10; ///< Track ID.
+	u32 time : 16;     ///< Timestamp in milliseconds.
+};
+CE_STATIC_ASSERT(sizeof(AnimationKeyHeader) == 4);
+
+struct PositionKey
+{
+	AnimationKeyHeader h;
+	Vector3 value;
+};
+
+struct RotationKey
+{
+	AnimationKeyHeader h;
+	Quaternion value;
+};
+
+union AnimationKey
+{
+	AnimationKeyHeader h;
+	PositionKey p;
+	RotationKey r;
+};
+
+struct MeshAnimationResource
+{
+	u32 version;
+	u32 num_tracks;
+	f32 total_time;
+	u32 num_keys;
+	u32 keys_offset;
+	u32 _pad0;
+	StringId64 target_skeleton;
+	u32 num_bones;
+	u32 bone_ids_offset;
+	// AnimationKey animation_keys[num_keys]
+	// u16 bone_ids[num_bones]
+};
+
+#if CROWN_CAN_COMPILE
+namespace mesh_animation_resource_internal
+{
+	///
+	s32 compile(CompileOptions &opts);
+
+} // namespace mesh_animation_resource_internal
+#endif
+
+namespace mesh_animation_resource
+{
+	///
+	const AnimationKey *animation_keys(const MeshAnimationResource *mar);
+
+	///
+	const u16 *bone_ids(const MeshAnimationResource *mar);
+
+} // namespace mesh_animation_resource
+
+} // namespace crown

+ 3 - 0
src/resource/types.h

@@ -26,6 +26,7 @@ struct LuaResource;
 struct MaterialResource;
 struct MeshResource;
 struct MeshSkeletonResource;
+struct MeshAnimationResource;
 struct PackageResource;
 struct PhysicsConfigResource;
 struct ShaderResource;
@@ -62,6 +63,7 @@ struct Platform
 #define RESOURCE_TYPE_MATERIAL         STRING_ID_64("material",         UINT64_C(0xeac0b497876adedf))
 #define RESOURCE_TYPE_MESH             STRING_ID_64("mesh",             UINT64_C(0x48ff313713a997a1))
 #define RESOURCE_TYPE_MESH_SKELETON    STRING_ID_64("mesh_skeleton",    UINT64_C(0x2597bb272931eded))
+#define RESOURCE_TYPE_MESH_ANIMATION   STRING_ID_64("mesh_animation",   UINT64_C(0x7369558b842d5314))
 #define RESOURCE_TYPE_PACKAGE          STRING_ID_64("package",          UINT64_C(0xad9c6d9ed1e5e77a))
 #define RESOURCE_TYPE_PHYSICS_CONFIG   STRING_ID_64("physics_config",   UINT64_C(0x72e3cc03787a11a1))
 #define RESOURCE_TYPE_SCRIPT           STRING_ID_64("lua",              UINT64_C(0xa14e8dfa2cd117e2))
@@ -84,6 +86,7 @@ struct Platform
 #define RESOURCE_VERSION_MATERIAL         RESOURCE_VERSION(6)
 #define RESOURCE_VERSION_MESH             RESOURCE_VERSION(6)
 #define RESOURCE_VERSION_MESH_SKELETON    RESOURCE_VERSION(1)
+#define RESOURCE_VERSION_MESH_ANIMATION   RESOURCE_VERSION(1)
 #define RESOURCE_VERSION_PACKAGE          RESOURCE_VERSION(7)
 #define RESOURCE_VERSION_PHYSICS_CONFIG   RESOURCE_VERSION(3)
 #define RESOURCE_VERSION_SCRIPT           RESOURCE_VERSION(4)