From ad0499f99d5b5225da72114a0eb19562e5ac9119 Mon Sep 17 00:00:00 2001 From: Daniele Bartolini Date: Thu, 27 Feb 2025 13:46:05 +0100 Subject: [PATCH 1/9] resource: cleanup --- src/resource/data_compiler.cpp | 47 +++++++++++----------------------- 1 file changed, 15 insertions(+), 32 deletions(-) diff --git a/src/resource/data_compiler.cpp b/src/resource/data_compiler.cpp index 7e446985e..cdf6e419d 100644 --- a/src/resource/data_compiler.cpp +++ b/src/resource/data_compiler.cpp @@ -1534,39 +1534,22 @@ int main_data_compiler(const DeviceOptions &opts) if (opts._server) console_server()->listen(CROWN_DEFAULT_COMPILER_PORT, opts._wait_console); - namespace cor = config_resource_internal; - namespace ftr = font_resource_internal; - namespace lur = lua_resource_internal; - namespace lvr = level_resource_internal; - namespace mhr = mesh_resource_internal; - namespace mtr = material_resource_internal; - namespace pcr = physics_config_resource_internal; - namespace phr = physics_resource_internal; - namespace pkr = package_resource_internal; - namespace sar = sprite_animation_resource_internal; - namespace sdr = sound_resource_internal; - namespace shr = shader_resource_internal; - namespace smr = state_machine_internal; - namespace spr = sprite_resource_internal; - namespace txr = texture_resource_internal; - namespace utr = unit_resource_internal; - DataCompiler *dc = CE_NEW(default_allocator(), DataCompiler)(opts, *console_server()); - dc->register_compiler("config", RESOURCE_VERSION_CONFIG, cor::compile); - dc->register_compiler("font", RESOURCE_VERSION_FONT, ftr::compile); - dc->register_compiler("level", RESOURCE_VERSION_LEVEL, lvr::compile); - dc->register_compiler("material", RESOURCE_VERSION_MATERIAL, mtr::compile); - dc->register_compiler("mesh", RESOURCE_VERSION_MESH, mhr::compile); - dc->register_compiler("package", RESOURCE_VERSION_PACKAGE, pkr::compile); - dc->register_compiler("physics_config", RESOURCE_VERSION_PHYSICS_CONFIG, pcr::compile); - dc->register_compiler("lua", RESOURCE_VERSION_SCRIPT, lur::compile); - dc->register_compiler("shader", RESOURCE_VERSION_SHADER, shr::compile); - dc->register_compiler("sound", RESOURCE_VERSION_SOUND, sdr::compile); - dc->register_compiler("sprite", RESOURCE_VERSION_SPRITE, spr::compile); - dc->register_compiler("sprite_animation", RESOURCE_VERSION_SPRITE_ANIMATION, sar::compile); - dc->register_compiler("state_machine", RESOURCE_VERSION_STATE_MACHINE, smr::compile); - dc->register_compiler("texture", RESOURCE_VERSION_TEXTURE, txr::compile); - dc->register_compiler("unit", RESOURCE_VERSION_UNIT, utr::compile); + dc->register_compiler("config", RESOURCE_VERSION_CONFIG, config_resource_internal::compile); + dc->register_compiler("font", RESOURCE_VERSION_FONT, font_resource_internal::compile); + dc->register_compiler("level", RESOURCE_VERSION_LEVEL, level_resource_internal::compile); + 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("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); + dc->register_compiler("shader", RESOURCE_VERSION_SHADER, shader_resource_internal::compile); + dc->register_compiler("sound", RESOURCE_VERSION_SOUND, sound_resource_internal::compile); + dc->register_compiler("sprite", RESOURCE_VERSION_SPRITE, sprite_resource_internal::compile); + dc->register_compiler("sprite_animation", RESOURCE_VERSION_SPRITE_ANIMATION, sprite_animation_resource_internal::compile); + dc->register_compiler("state_machine", RESOURCE_VERSION_STATE_MACHINE, state_machine_internal::compile); + dc->register_compiler("texture", RESOURCE_VERSION_TEXTURE, texture_resource_internal::compile); + dc->register_compiler("unit", RESOURCE_VERSION_UNIT, unit_resource_internal::compile); dc->map_source_dir("", opts._source_dir.c_str()); From 8a6a46e776fb4bd95a2d1f0d1e50d2d975e1277c Mon Sep 17 00:00:00 2001 From: Daniele Bartolini Date: Mon, 17 Feb 2025 13:44:31 +0100 Subject: [PATCH 2/9] resource: add FBXDocument Part-of: #276 --- src/resource/fbx_document.cpp | 75 +++++++++++++++++++++++++++++++++++ src/resource/fbx_document.h | 45 +++++++++++++++++++++ src/resource/mesh_fbx.cpp | 49 +++++------------------ 3 files changed, 130 insertions(+), 39 deletions(-) create mode 100644 src/resource/fbx_document.cpp create mode 100644 src/resource/fbx_document.h diff --git a/src/resource/fbx_document.cpp b/src/resource/fbx_document.cpp new file mode 100644 index 000000000..802bd8237 --- /dev/null +++ b/src/resource/fbx_document.cpp @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2012-2025 Daniele Bartolini et al. + * SPDX-License-Identifier: MIT + */ + +#include "resource/fbx_document.h" + +#if CROWN_CAN_COMPILE +# include "core/containers/array.inl" +# include "core/containers/hash_map.inl" +# include "core/strings/string_id.inl" +# include "resource/compile_options.inl" +# include + +namespace crown +{ +namespace fbx +{ + s32 parse(FBXDocument &fbx, Buffer &buf, CompileOptions &opts) + { + // Keep in sync with mesh_resource_fbx.vala! + ufbx_load_opts load_opts = {}; + load_opts.target_camera_axes = + { + UFBX_COORDINATE_AXIS_POSITIVE_X, + UFBX_COORDINATE_AXIS_POSITIVE_Z, + UFBX_COORDINATE_AXIS_NEGATIVE_Y + }; + load_opts.target_light_axes = + { + UFBX_COORDINATE_AXIS_POSITIVE_X, + UFBX_COORDINATE_AXIS_POSITIVE_Y, + UFBX_COORDINATE_AXIS_POSITIVE_Z + }; + load_opts.target_axes = ufbx_axes_right_handed_z_up; + load_opts.target_unit_meters = 1.0f; + load_opts.space_conversion = UFBX_SPACE_CONVERSION_TRANSFORM_ROOT; + + ufbx_error error; + fbx.scene = ufbx_load_memory(array::begin(buf) + , array::size(buf) + , &load_opts + , &error + ); + RETURN_IF_FALSE(fbx.scene != NULL + , opts + , "ufbx: %s" + , error.description.data + ); + return 0; + } + + /// + s32 parse(FBXDocument &fbx, const char *path, CompileOptions &opts) + { + RETURN_IF_FILE_MISSING(path, opts); + Buffer buf = opts.read(path); + return parse(fbx, buf, opts); + } + +} // namespace fbx + +FBXDocument::FBXDocument(Allocator &a) + : scene(NULL) +{ +} + +FBXDocument::~FBXDocument() +{ + ufbx_free_scene(scene); +} + +} // namespace crown + +#endif // if CROWN_CAN_COMPILE diff --git a/src/resource/fbx_document.h b/src/resource/fbx_document.h new file mode 100644 index 000000000..1338b6689 --- /dev/null +++ b/src/resource/fbx_document.h @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2012-2025 Daniele Bartolini et al. + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include "config.h" + +#if CROWN_CAN_COMPILE +# include "core/filesystem/types.h" +# include "core/math/types.h" +# include "core/memory/types.h" +# include "core/strings/dynamic_string.h" +# include "resource/types.h" + +struct ufbx_scene; // Avoids #include +struct ufbx_node; // Ditto. + +namespace crown +{ +struct FBXDocument +{ + ufbx_scene *scene; + + /// + explicit FBXDocument(Allocator &a); + + /// + ~FBXDocument(); +}; + +namespace fbx +{ + /// + s32 parse(FBXDocument &fbx, Buffer &buf, CompileOptions &opts); + + /// + s32 parse(FBXDocument &fbx, const char *path, CompileOptions &opts); + +} // namespace fbx + +} // namespace crown + +#endif // if CROWN_CAN_COMPILE diff --git a/src/resource/mesh_fbx.cpp b/src/resource/mesh_fbx.cpp index 8dd5ae6cd..a218abca4 100644 --- a/src/resource/mesh_fbx.cpp +++ b/src/resource/mesh_fbx.cpp @@ -15,6 +15,7 @@ #include "core/strings/dynamic_string.inl" #include "device/log.h" #include "resource/compile_options.inl" +#include "resource/fbx_document.h" #include "resource/mesh_fbx.h" #include #include @@ -43,7 +44,7 @@ namespace fbx } /// See: https://ufbx.github.io/elements/meshes/#example - size_t convert_mesh_part(Geometry &g, const ufbx_mesh *mesh, const ufbx_mesh_part *part) + size_t convert_mesh_part(Geometry &g, FBXDocument &fbx, const ufbx_mesh *mesh, const ufbx_mesh_part *part) { size_t num_triangles = part->num_triangles; @@ -101,12 +102,12 @@ namespace fbx return num_triangles * 3; } - s32 parse_geometry(Geometry &g, const ufbx_mesh *mesh) + s32 parse_geometry(Geometry &g, FBXDocument &fbx, const ufbx_mesh *mesh, CompileOptions &opts) { size_t num_indices = 0; for (size_t i = 0; i < mesh->material_parts.count; ++i) { const ufbx_mesh_part *mesh_part = &mesh->material_parts.data[i]; - num_indices += convert_mesh_part(g, mesh, mesh_part); + num_indices += convert_mesh_part(g, fbx, mesh, mesh_part); } array::resize(g._position_indices, (u32)num_indices); @@ -135,13 +136,13 @@ namespace fbx return 0; } - s32 parse_geometries(Mesh &m, const ufbx_mesh_list *meshes, CompileOptions &opts) + s32 parse_geometries(Mesh &m, FBXDocument &fbx, const ufbx_mesh_list *meshes, CompileOptions &opts) { for (size_t i = 0; i < meshes->count; ++i) { const ufbx_mesh *mesh = meshes->data[i]; Geometry geo(default_allocator()); - s32 err = fbx::parse_geometry(geo, mesh); + s32 err = fbx::parse_geometry(geo, fbx, mesh, opts); ENSURE_OR_RETURN(err == 0, opts); DynamicString geometry_name(default_allocator()); @@ -206,44 +207,14 @@ namespace fbx s32 parse(Mesh &m, Buffer &buf, CompileOptions &opts) { - // Keep in sync with mesh_resource_fbx.vala! - ufbx_load_opts load_opts = {}; - load_opts.target_camera_axes = - { - UFBX_COORDINATE_AXIS_POSITIVE_X, - UFBX_COORDINATE_AXIS_POSITIVE_Z, - UFBX_COORDINATE_AXIS_NEGATIVE_Y - }; - load_opts.target_light_axes = - { - UFBX_COORDINATE_AXIS_POSITIVE_X, - UFBX_COORDINATE_AXIS_POSITIVE_Y, - UFBX_COORDINATE_AXIS_POSITIVE_Z - }; - load_opts.target_axes = ufbx_axes_right_handed_z_up; - load_opts.target_unit_meters = 1.0f; - load_opts.space_conversion = UFBX_SPACE_CONVERSION_TRANSFORM_ROOT; - - ufbx_error error; - ufbx_scene *scene = ufbx_load_memory(array::begin(buf) - , array::size(buf) - , &load_opts - , &error - ); - RETURN_IF_FALSE(scene != NULL - , opts - , "ufbx: %s" - , error.description.data - ); - - s32 err = parse_geometries(m, &scene->meshes, opts); + FBXDocument fbx(default_allocator()); + s32 err = fbx::parse(fbx, buf, opts); ENSURE_OR_RETURN(err == 0, opts); - err = parse_nodes(m, &scene->nodes, opts); + err = parse_geometries(m, fbx, &fbx.scene->meshes, opts); ENSURE_OR_RETURN(err == 0, opts); - ufbx_free_scene(scene); - return 0; + return parse_nodes(m, &fbx.scene->nodes, opts); } } // namespace fbx From 8920dda837b5d4e17b2de46db4ed308d44fd5b12 Mon Sep 17 00:00:00 2001 From: Daniele Bartolini Date: Sun, 16 Feb 2025 11:34:44 +0100 Subject: [PATCH 3/9] resource: add bones and weights data Part-of: #276 --- src/resource/mesh.cpp | 33 +++++++++++++++++++++++++++++++++ src/resource/mesh.h | 9 +++++++++ 2 files changed, 42 insertions(+) diff --git a/src/resource/mesh.cpp b/src/resource/mesh.cpp index 0c9c850ab..f4fd826c4 100644 --- a/src/resource/mesh.cpp +++ b/src/resource/mesh.cpp @@ -62,6 +62,8 @@ namespace mesh array::clear(g._uvs); array::clear(g._tangents); array::clear(g._bitangents); + array::clear(g._bones); + array::clear(g._weights); array::clear(g._position_indices); array::clear(g._normal_indices); @@ -93,12 +95,18 @@ namespace mesh return array::size(g._bitangents) != 0; } + bool has_bones(Geometry &g) + { + return array::size(g._bones) != 0; + } + static u32 vertex_stride(Geometry &g) { u32 stride = 0; stride += 3 * sizeof(f32); stride += (has_normals(g) ? 3 * sizeof(f32) : 0); stride += (has_uvs(g) ? 2 * sizeof(f32) : 0); + stride += (has_bones(g) ? 8 * sizeof(f32) : 0); return stride; } @@ -116,6 +124,10 @@ namespace mesh if (has_uvs(g)) { layout.add(bgfx::Attrib::TexCoord0, 2, bgfx::AttribType::Float); } + if (has_bones(g)) { + layout.add(bgfx::Attrib::Indices, 4, bgfx::AttribType::Float); + layout.add(bgfx::Attrib::Weight, 4, bgfx::AttribType::Float); + } layout.end(); return layout; @@ -151,6 +163,23 @@ namespace mesh uv.y = g._uvs[t_idx + 1]; array::push(g._vertex_buffer, (char *)&uv, sizeof(uv)); } + if (has_bones(g)) { + const u16 b_idx = g._bone_indices[i] * 4; + Vector4 b; + b.x = g._bones[b_idx + 0]; + b.y = g._bones[b_idx + 1]; + b.z = g._bones[b_idx + 2]; + b.w = g._bones[b_idx + 3]; + array::push(g._vertex_buffer, (char *)&b, sizeof(b)); + + Vector4 w; + const u16 w_idx = g._weight_indices[i] * 4; + w.x = g._weights[w_idx + 0]; + w.y = g._weights[w_idx + 1]; + w.z = g._weights[w_idx + 2]; + w.w = g._weights[w_idx + 3]; + array::push(g._vertex_buffer, (char *)&w, sizeof(w)); + } } } @@ -271,11 +300,15 @@ Geometry::Geometry(Allocator &a) , _uvs(a) , _tangents(a) , _bitangents(a) + , _bones(a) + , _weights(a) , _position_indices(a) , _normal_indices(a) , _uv_indices(a) , _tangent_indices(a) , _bitangent_indices(a) + , _bone_indices(a) + , _weight_indices(a) , _vertex_buffer(a) , _index_buffer(a) { diff --git a/src/resource/mesh.h b/src/resource/mesh.h index 26c6aea2f..bd35a7055 100644 --- a/src/resource/mesh.h +++ b/src/resource/mesh.h @@ -31,17 +31,23 @@ struct Geometry { ALLOCATOR_AWARE; + enum { MAX_BONE_WEIGHTS = 4 }; + Array _positions; Array _normals; Array _uvs; Array _tangents; Array _bitangents; + Array _bones; + Array _weights; Array _position_indices; Array _normal_indices; Array _uv_indices; Array _tangent_indices; Array _bitangent_indices; + Array _bone_indices; + Array _weight_indices; Array _vertex_buffer; Array _index_buffer; @@ -73,6 +79,9 @@ namespace mesh /// bool has_bitangents(Geometry &g); + /// + bool has_bones(Geometry &g); + /// s32 parse(Mesh &m, const char *path, CompileOptions &opts); From 1f6c5dfad42f8166e841f64bf9dbf4e33f5236eb Mon Sep 17 00:00:00 2001 From: Daniele Bartolini Date: Mon, 17 Feb 2025 15:44:21 +0100 Subject: [PATCH 4/9] resource: drop unused parameter --- src/resource/mesh_fbx.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/resource/mesh_fbx.cpp b/src/resource/mesh_fbx.cpp index a218abca4..d0bc108a9 100644 --- a/src/resource/mesh_fbx.cpp +++ b/src/resource/mesh_fbx.cpp @@ -102,7 +102,7 @@ namespace fbx return num_triangles * 3; } - s32 parse_geometry(Geometry &g, FBXDocument &fbx, const ufbx_mesh *mesh, CompileOptions &opts) + s32 parse_geometry(Geometry &g, FBXDocument &fbx, const ufbx_mesh *mesh) { size_t num_indices = 0; for (size_t i = 0; i < mesh->material_parts.count; ++i) { @@ -142,7 +142,7 @@ namespace fbx const ufbx_mesh *mesh = meshes->data[i]; Geometry geo(default_allocator()); - s32 err = fbx::parse_geometry(geo, fbx, mesh, opts); + s32 err = fbx::parse_geometry(geo, fbx, mesh); ENSURE_OR_RETURN(err == 0, opts); DynamicString geometry_name(default_allocator()); From c23f3864b49eebab0edd28ad6779efef4ecd007b Mon Sep 17 00:00:00 2001 From: Daniele Bartolini Date: Mon, 17 Feb 2025 15:24:52 +0100 Subject: [PATCH 5/9] resource: fbx: read bone IDs and weights Part-of: #276 --- src/resource/fbx_document.cpp | 65 +++++++++++++++++++++++++++++++++++ src/resource/fbx_document.h | 8 +++++ src/resource/mesh_fbx.cpp | 39 +++++++++++++++++++++ 3 files changed, 112 insertions(+) diff --git a/src/resource/fbx_document.cpp b/src/resource/fbx_document.cpp index 802bd8237..43baf3607 100644 --- a/src/resource/fbx_document.cpp +++ b/src/resource/fbx_document.cpp @@ -16,6 +16,55 @@ namespace crown { namespace fbx { + static ufbx_node *find_first_non_bone_parent(ufbx_node *bone_node) + { + while (bone_node && bone_node->bone != NULL) + bone_node = bone_node->parent; + + return bone_node; + } + + static ufbx_node *find_skeleton_root(ufbx_node *node) + { + if (node->bone != NULL) + return node; + + for (size_t i = 0; i < node->children.count; ++i) { + ufbx_node *n = find_skeleton_root(node->children.data[i]); + if (n != NULL) + return find_first_non_bone_parent(n); + } + + return NULL; + } + + u16 bone_id(const FBXDocument &fbx, StringId32 bone_name) + { + u16 deffault_bone_id = UINT16_MAX; + return hash_map::get(fbx.bone_ids, bone_name, deffault_bone_id); + } + + u16 bone_id(const FBXDocument &fbx, const char *bone_name) + { + return bone_id(fbx, StringId32(bone_name)); + } + + static s32 populate_bone_ids(FBXDocument &fbx, ufbx_node *bone, CompileOptions &opts, u32 *debug_num_bones) + { + u16 bone_id = (u16)hash_map::size(fbx.bone_ids); + StringId32 bone_name = StringId32(bone->name.data); + hash_map::set(fbx.bone_ids, bone_name, bone_id); + (*debug_num_bones)++; + + for (size_t i = 0; i < bone->children.count; ++i) { + ufbx_node *child = bone->children.data[i]; + s32 err = populate_bone_ids(fbx, child, opts, debug_num_bones); + ENSURE_OR_RETURN(err == 0, opts); + } + + return 0; + } + s32 parse(FBXDocument &fbx, Buffer &buf, CompileOptions &opts) { // Keep in sync with mesh_resource_fbx.vala! @@ -47,6 +96,20 @@ namespace fbx , "ufbx: %s" , error.description.data ); + + fbx.skeleton_root_node = find_skeleton_root(fbx.scene->root_node); + if (fbx.skeleton_root_node != NULL) { + u32 debug_num_bones = 0; + s32 err = populate_bone_ids(fbx, fbx.skeleton_root_node, opts, &debug_num_bones); + ENSURE_OR_RETURN(err == 0, opts); + RETURN_IF_FALSE(debug_num_bones == hash_map::size(fbx.bone_ids) + , opts + , "Bone mismatch expected/actual %u/%u" + , debug_num_bones + , hash_map::size(fbx.bone_ids) + ); + } + return 0; } @@ -62,6 +125,8 @@ namespace fbx FBXDocument::FBXDocument(Allocator &a) : scene(NULL) + , skeleton_root_node(NULL) + , bone_ids(a) { } diff --git a/src/resource/fbx_document.h b/src/resource/fbx_document.h index 1338b6689..ace53f325 100644 --- a/src/resource/fbx_document.h +++ b/src/resource/fbx_document.h @@ -22,6 +22,8 @@ namespace crown struct FBXDocument { ufbx_scene *scene; + ufbx_node *skeleton_root_node; + HashMap bone_ids; /// explicit FBXDocument(Allocator &a); @@ -32,6 +34,12 @@ struct FBXDocument namespace fbx { + /// Returns the node ID for @a bone_name. + u16 bone_id(const FBXDocument &fbx, StringId32 bone_name); + + /// Returns the node ID for @a bone_name. + u16 bone_id(const FBXDocument &fbx, const char *bone_name); + /// s32 parse(FBXDocument &fbx, Buffer &buf, CompileOptions &opts); diff --git a/src/resource/mesh_fbx.cpp b/src/resource/mesh_fbx.cpp index d0bc108a9..36bd4803a 100644 --- a/src/resource/mesh_fbx.cpp +++ b/src/resource/mesh_fbx.cpp @@ -68,6 +68,38 @@ namespace fbx array::push_back(g._positions, (f32)v.y); array::push_back(g._positions, (f32)v.z); + // See: https://ufbx.github.io/elements/deformers/ + if (mesh->skin_deformers.count == 1) { + ufbx_skin_deformer *skin = mesh->skin_deformers.data[0]; + + uint32_t vertex = mesh->vertex_indices.data[index]; + ufbx_skin_vertex skin_vertex = skin->vertices.data[vertex]; + f32 bones[Geometry::MAX_BONE_WEIGHTS] = { 0 }; + f32 weights[Geometry::MAX_BONE_WEIGHTS] = { 0.0f }; + u32 num_weights = min(u32(skin_vertex.num_weights), u32(Geometry::MAX_BONE_WEIGHTS)); + + // Read bone ID and weight. + f32 total_weight = 0.0f; + for (u32 i = 0; i < num_weights; i++) { + ufbx_skin_weight skin_weight = skin->weights.data[skin_vertex.weight_begin + i]; + ufbx_skin_cluster *cluster = skin->clusters.data[skin_weight.cluster_index]; + + bones[i] = (f32)fbx::bone_id(fbx, cluster->bone_node->name.data); + weights[i] = (f32)skin_weight.weight; + total_weight += weights[i]; + } + + // FBX does not guarantee that skin weights are normalized, and we may even + // be dropping some, so we must renormalize them. + for (u32 i = 0; i < num_weights; i++) + weights[i] /= total_weight; + + for (u32 i = 0; i < countof(bones); ++i) { + array::push_back(g._bones, bones[i]); + array::push_back(g._weights, weights[i]); + } + } + if (mesh->vertex_normal.exists) { ufbx_vec3 v = ufbx_get_vertex_vec3(&mesh->vertex_normal, index); array::push_back(g._normals, (f32)v.x); @@ -133,6 +165,13 @@ namespace fbx generate_indices(g._bitangent_indices, g._bitangents, sizeof(f32)*3); } + if (mesh::has_bones(g)) { + array::resize(g._bone_indices, (u32)num_indices); + array::resize(g._weight_indices, (u32)num_indices); + generate_indices(g._bone_indices, g._bones, sizeof(f32)*4); + generate_indices(g._weight_indices, g._weights, sizeof(f32)*4); + } + return 0; } From 7bc6ddf770a661b66c555adcc5ee37cc0168c63a Mon Sep 17 00:00:00 2001 From: Daniele Bartolini Date: Thu, 20 Feb 2025 09:51:25 +0100 Subject: [PATCH 6/9] resource: add MeshSkeletonResource Part-of: #276 --- src/device/device.cpp | 2 + src/resource/data_compiler.cpp | 2 + src/resource/mesh_skeleton.cpp | 57 ++++++++++++ src/resource/mesh_skeleton.h | 41 +++++++++ src/resource/mesh_skeleton_fbx.cpp | 111 ++++++++++++++++++++++++ src/resource/mesh_skeleton_fbx.h | 25 ++++++ src/resource/mesh_skeleton_resource.cpp | 76 ++++++++++++++++ src/resource/mesh_skeleton_resource.h | 58 +++++++++++++ src/resource/types.h | 3 + 9 files changed, 375 insertions(+) create mode 100644 src/resource/mesh_skeleton.cpp create mode 100644 src/resource/mesh_skeleton.h create mode 100644 src/resource/mesh_skeleton_fbx.cpp create mode 100644 src/resource/mesh_skeleton_fbx.h create mode 100644 src/resource/mesh_skeleton_resource.cpp create mode 100644 src/resource/mesh_skeleton_resource.h diff --git a/src/device/device.cpp b/src/device/device.cpp index e6fbd253b..8baba1b4f 100644 --- a/src/device/device.cpp +++ b/src/device/device.cpp @@ -44,6 +44,7 @@ #include "resource/lua_resource.h" #include "resource/material_resource.h" #include "resource/mesh_resource.h" +#include "resource/mesh_skeleton_resource.h" #include "resource/package_resource.h" #include "resource/physics_resource.h" #include "resource/resource_id.inl" @@ -584,6 +585,7 @@ void Device::run() _resource_manager->register_type(RESOURCE_TYPE_LEVEL, RESOURCE_VERSION_LEVEL, NULL, NULL, NULL, NULL); _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_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); diff --git a/src/resource/data_compiler.cpp b/src/resource/data_compiler.cpp index cdf6e419d..9495ad5d7 100644 --- a/src/resource/data_compiler.cpp +++ b/src/resource/data_compiler.cpp @@ -36,6 +36,7 @@ #include "resource/lua_resource.h" #include "resource/material_resource.h" #include "resource/mesh_resource.h" +#include "resource/mesh_skeleton_resource.h" #include "resource/package_resource.h" #include "resource/physics_resource.h" #include "resource/resource_id.inl" @@ -1540,6 +1541,7 @@ int main_data_compiler(const DeviceOptions &opts) dc->register_compiler("level", RESOURCE_VERSION_LEVEL, level_resource_internal::compile); 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("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); diff --git a/src/resource/mesh_skeleton.cpp b/src/resource/mesh_skeleton.cpp new file mode 100644 index 000000000..07a1cb0d5 --- /dev/null +++ b/src/resource/mesh_skeleton.cpp @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2012-2025 Daniele Bartolini et al. + * SPDX-License-Identifier: MIT + */ + +#include "resource/mesh_skeleton.h" + +#if CROWN_CAN_COMPILE +# include "core/json/json_object.inl" +# include "core/json/sjson.h" +# include "core/memory/temp_allocator.inl" +# include "core/strings/dynamic_string.inl" +# include "resource/mesh_skeleton_fbx.h" +# include "resource/compile_options.inl" + +namespace crown +{ +namespace mesh_skeleton +{ + static s32 parse_internal(AnimationSkeleton &s, Buffer &buf, CompileOptions &opts) + { + TempAllocator4096 ta; + JsonObject obj(ta); + RETURN_IF_ERROR(sjson::parse(obj, buf), opts); + + DynamicString source(ta); + 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()); + return fbx::parse(s, fbx_buf, opts); + } + + s32 parse(AnimationSkeleton &s, const char *path, CompileOptions &opts) + { + RETURN_IF_FILE_MISSING(path, opts); + Buffer buf = opts.read(path); + return parse_internal(s, buf, opts); + } + + s32 parse(AnimationSkeleton &s, CompileOptions &opts) + { + Buffer buf = opts.read(); + return parse_internal(s, buf, opts); + } + +} // namespace mesh_skeleton + +AnimationSkeleton::AnimationSkeleton(Allocator &a) + : local_transforms(a) + , parents(a) + , binding_matrices(a) +{ +} + +} // namespace crown +#endif // if CROWN_CAN_COMPILE diff --git a/src/resource/mesh_skeleton.h b/src/resource/mesh_skeleton.h new file mode 100644 index 000000000..948e7a0d1 --- /dev/null +++ b/src/resource/mesh_skeleton.h @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2012-2025 Daniele Bartolini et al. + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include "config.h" + +#if CROWN_CAN_COMPILE +# include "core/containers/types.h" +# include "resource/mesh_skeleton_resource.h" + +namespace crown +{ +struct AnimationSkeleton +{ + Array local_transforms; + Array parents; + Array binding_matrices; + + /// + explicit AnimationSkeleton(Allocator &a); +}; + +namespace mesh_skeleton +{ + /// + s32 parse(AnimationSkeleton &s, const char *path, CompileOptions &opts); + + /// + s32 parse(AnimationSkeleton &s, CompileOptions &opts); + + /// + s32 write(AnimationSkeleton &s, CompileOptions &opts); + +} // namespace mesh_skeleton + +} // namespace crown + +#endif // if CROWN_CAN_COMPILE diff --git a/src/resource/mesh_skeleton_fbx.cpp b/src/resource/mesh_skeleton_fbx.cpp new file mode 100644 index 000000000..d1bb8ddaa --- /dev/null +++ b/src/resource/mesh_skeleton_fbx.cpp @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2012-2025 Daniele Bartolini et al. + * SPDX-License-Identifier: MIT + */ + +#include "resource/mesh_skeleton_fbx.h" + +#if CROWN_CAN_COMPILE +# include "core/containers/array.inl" +# include "core/containers/hash_map.inl" +# include "core/math/matrix4x4.inl" +# include "core/math/vector2.inl" +# include "core/math/vector3.inl" +# include "core/memory/temp_allocator.inl" +# include "core/strings/dynamic_string.inl" +# include "core/strings/string_id.inl" +# include "resource/mesh_skeleton.h" +# include "resource/compile_options.inl" +# include "resource/fbx_document.h" +# include + +namespace crown +{ +namespace fbx +{ + static ufbx_skin_cluster *find_cluster(ufbx_scene *scene, ufbx_node *node) + { + for (size_t i = 0; i < scene->skin_clusters.count; ++i) { + ufbx_skin_cluster *cluster = scene->skin_clusters.data[i]; + if (cluster->bone_node == node) + return cluster; + } + + return NULL; + } + + static s32 parse_skeleton(AnimationSkeleton &as, FBXDocument &fbx, ufbx_node *bone, CompileOptions &opts) + { + Vector3 pos; + pos.x = bone->local_transform.translation.x; + pos.y = bone->local_transform.translation.y; + pos.z = bone->local_transform.translation.z; + + Quaternion rot; + rot.x = bone->local_transform.rotation.x; + rot.y = bone->local_transform.rotation.y; + rot.z = bone->local_transform.rotation.z; + rot.w = bone->local_transform.rotation.w; + + Vector3 scl; + scl.x = bone->local_transform.scale.x; + scl.y = bone->local_transform.scale.y; + scl.z = bone->local_transform.scale.z; + + u16 parent_bone_id = bone_id(fbx, bone->parent->name.data); + array::push_back(as.local_transforms, { pos, rot, scl }); + array::push_back(as.parents, (u32)parent_bone_id); + + ufbx_skin_cluster *cluster = find_cluster(fbx.scene, bone); + if (cluster != NULL) { + Matrix4x4 m; + m.x.x = cluster->geometry_to_bone.cols[0].x; + m.x.y = cluster->geometry_to_bone.cols[0].y; + m.x.z = cluster->geometry_to_bone.cols[0].z; + m.x.w = 0.0f; + m.y.x = cluster->geometry_to_bone.cols[1].x; + m.y.y = cluster->geometry_to_bone.cols[1].y; + m.y.z = cluster->geometry_to_bone.cols[1].z; + m.y.w = 0.0f; + m.z.x = cluster->geometry_to_bone.cols[2].x; + m.z.y = cluster->geometry_to_bone.cols[2].y; + m.z.z = cluster->geometry_to_bone.cols[2].z; + m.z.w = 0.0f; + m.t.x = cluster->geometry_to_bone.cols[3].x; + m.t.y = cluster->geometry_to_bone.cols[3].y; + m.t.z = cluster->geometry_to_bone.cols[3].z; + m.t.w = 1.0f; + array::push_back(as.binding_matrices, m); + } else { + array::push_back(as.binding_matrices, MATRIX4X4_IDENTITY); + } + + CE_ENSURE(bone_id(fbx, bone->name.data) == array::size(as.binding_matrices) - 1); + + for (size_t i = 0; i < bone->children.count; ++i) { + ufbx_node *child = bone->children.data[i]; + s32 err = parse_skeleton(as, fbx, child, opts); + ENSURE_OR_RETURN(err == 0, opts); + } + + return 0; + } + + s32 parse(AnimationSkeleton &as, Buffer &buf, CompileOptions &opts) + { + FBXDocument fbx(default_allocator()); + s32 err = fbx::parse(fbx, buf, opts); + ENSURE_OR_RETURN(err == 0, opts); + RETURN_IF_FALSE(fbx.skeleton_root_node != NULL + , opts + , "No skeleton in FBX source" + ); + + return parse_skeleton(as, fbx, fbx.skeleton_root_node, opts); + } + +} // namespace fbx + +} // namespace crown + +#endif // if CROWN_CAN_COMPILE diff --git a/src/resource/mesh_skeleton_fbx.h b/src/resource/mesh_skeleton_fbx.h new file mode 100644 index 000000000..cb9de4747 --- /dev/null +++ b/src/resource/mesh_skeleton_fbx.h @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2012-2025 Daniele Bartolini et al. + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include "config.h" + +#if CROWN_CAN_COMPILE +# include "resource/mesh_skeleton.h" +# include "resource/types.h" + +namespace crown +{ +namespace fbx +{ + /// + s32 parse(AnimationSkeleton &s, Buffer &buf, CompileOptions &opts); + +} // namespace fbx + +} // namespace crown + +#endif // if CROWN_CAN_COMPILE diff --git a/src/resource/mesh_skeleton_resource.cpp b/src/resource/mesh_skeleton_resource.cpp new file mode 100644 index 000000000..436cb2104 --- /dev/null +++ b/src/resource/mesh_skeleton_resource.cpp @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2012-2025 Daniele Bartolini et al. + * SPDX-License-Identifier: MIT + */ + +#include "resource/mesh_skeleton_resource.h" +#include "core/containers/array.inl" +#include "core/json/json_object.inl" +#include "core/json/sjson.h" +#include "core/memory/temp_allocator.inl" +#include "resource/mesh_skeleton_fbx.h" +#include "resource/compile_options.inl" + +namespace crown +{ +#if CROWN_CAN_COMPILE +namespace mesh_skeleton_resource_internal +{ + s32 write(AnimationSkeleton &s, CompileOptions &opts) + { + MeshSkeletonResource asr; + asr.version = RESOURCE_VERSION_MESH_SKELETON; + asr.num_bones = array::size(s.local_transforms); + asr.local_transforms_offset = sizeof(asr); + asr.parents_offset = asr.local_transforms_offset + sizeof(BoneTransform) * asr.num_bones; + asr.binding_matrices_offset = asr.parents_offset + sizeof(u32) * asr.num_bones; + + opts.write(asr.version); + opts.write(asr.num_bones); + opts.write(asr.local_transforms_offset); + opts.write(asr.parents_offset); + opts.write(asr.binding_matrices_offset); + + for (u32 i = 0; i < asr.num_bones; ++i) + opts.write(s.local_transforms[i]); + + for (u32 i = 0; i < asr.num_bones; ++i) + opts.write(s.parents[i]); + + for (u32 i = 0; i < asr.num_bones; ++i) + opts.write(s.binding_matrices[i]); + + return 0; + } + + s32 compile(CompileOptions &opts) + { + AnimationSkeleton s(default_allocator()); + s32 err = mesh_skeleton::parse(s, opts); + ENSURE_OR_RETURN(err == 0, opts); + return write(s, opts); + } + +} // namespace mesh_skeleton_resource_internal +#endif // if CROWN_CAN_COMPILE + +namespace mesh_skeleton_resource +{ + const BoneTransform *local_transforms(const MeshSkeletonResource *asr) + { + return (BoneTransform *)((char *)asr + asr->local_transforms_offset); + } + + const u32 *parents(const MeshSkeletonResource *asr) + { + return (u32 *)((char *)asr + asr->parents_offset); + } + + const Matrix4x4 *binding_matrices(const MeshSkeletonResource *asr) + { + return (Matrix4x4 *)((char *)asr + asr->binding_matrices_offset); + } + +} // namespace mesh_skeleton_resource + +} // namespace crown diff --git a/src/resource/mesh_skeleton_resource.h b/src/resource/mesh_skeleton_resource.h new file mode 100644 index 000000000..645e77548 --- /dev/null +++ b/src/resource/mesh_skeleton_resource.h @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2012-2025 Daniele Bartolini et al. + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include "config.h" +#include "core/math/types.h" +#include "core/types.h" +#include "resource/types.h" + +namespace crown +{ +#define MESH_SKELETON_MAX_BONES 1024 + +struct BoneTransform +{ + Vector3 position; + Quaternion rotation; + Vector3 scale; +}; + +struct MeshSkeletonResource +{ + u32 version; + u32 num_bones; ///< Number of bones in the skeleton. + u32 local_transforms_offset; ///< Offset to first local transform. + u32 parents_offset; ///< Offset to first parent of first transform. + u32 binding_matrices_offset; ///< Offset to first binding matrix. + // BoneTransform local_transforms[num_bones]; + // u32 parents[num_bones]; + // Matrix4x4 binding_matrices[num_bones]; +}; + +#if CROWN_CAN_COMPILE +namespace mesh_skeleton_resource_internal +{ + /// + s32 compile(CompileOptions &opts); + +} // namespace mesh_skeleton_resource_internal +#endif + +namespace mesh_skeleton_resource +{ + /// + const BoneTransform *local_transforms(const MeshSkeletonResource *asr); + + /// + const u32 *parents(const MeshSkeletonResource *asr); + + /// + const Matrix4x4 *binding_matrices(const MeshSkeletonResource *asr); + +} // namespace mesh_skeleton_resource + +} // namespace crown diff --git a/src/resource/types.h b/src/resource/types.h index 5eeaecbc5..475c47a87 100644 --- a/src/resource/types.h +++ b/src/resource/types.h @@ -25,6 +25,7 @@ struct LevelResource; struct LuaResource; struct MaterialResource; struct MeshResource; +struct MeshSkeletonResource; struct PackageResource; struct PhysicsConfigResource; struct ShaderResource; @@ -60,6 +61,7 @@ struct Platform #define RESOURCE_TYPE_LEVEL STRING_ID_64("level", UINT64_C(0x2a690fd348fe9ac5)) #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_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)) @@ -81,6 +83,7 @@ struct Platform #define RESOURCE_VERSION_LEVEL (RESOURCE_VERSION_UNIT + 4) //!< Level embeds UnitResource #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_PACKAGE RESOURCE_VERSION(7) #define RESOURCE_VERSION_PHYSICS_CONFIG RESOURCE_VERSION(3) #define RESOURCE_VERSION_SCRIPT RESOURCE_VERSION(4) From 97cbf6ca77ff0dae5e1f635a0db4d84442e99b1c Mon Sep 17 00:00:00 2001 From: Daniele Bartolini Date: Fri, 21 Feb 2025 09:44:39 +0100 Subject: [PATCH 7/9] resource: add MeshAnimationResource Part-of: #276 --- src/device/device.cpp | 2 + src/resource/data_compiler.cpp | 2 + src/resource/mesh_animation.cpp | 190 +++++++++++++++++++++++ src/resource/mesh_animation.h | 54 +++++++ src/resource/mesh_animation_fbx.cpp | 121 +++++++++++++++ src/resource/mesh_animation_fbx.h | 18 +++ src/resource/mesh_animation_resource.cpp | 81 ++++++++++ src/resource/mesh_animation_resource.h | 84 ++++++++++ src/resource/types.h | 3 + 9 files changed, 555 insertions(+) create mode 100644 src/resource/mesh_animation.cpp create mode 100644 src/resource/mesh_animation.h create mode 100644 src/resource/mesh_animation_fbx.cpp create mode 100644 src/resource/mesh_animation_fbx.h create mode 100644 src/resource/mesh_animation_resource.cpp create mode 100644 src/resource/mesh_animation_resource.h diff --git a/src/device/device.cpp b/src/device/device.cpp index 8baba1b4f..1ce59719b 100644 --- a/src/device/device.cpp +++ b/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); diff --git a/src/resource/data_compiler.cpp b/src/resource/data_compiler.cpp index 9495ad5d7..971858083 100644 --- a/src/resource/data_compiler.cpp +++ b/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); diff --git a/src/resource/mesh_animation.cpp b/src/resource/mesh_animation.cpp new file mode 100644 index 000000000..2ad83212d --- /dev/null +++ b/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 // 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 diff --git a/src/resource/mesh_animation.h b/src/resource/mesh_animation.h new file mode 100644 index 000000000..a4d09773a --- /dev/null +++ b/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 sorted_keys; ///< Animation keys sorted by access time. + Array keys; ///< Unordered animation keys. + Array 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 track_ids; ///< From (bone_id, parameter_type) to track_id. + Array 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 diff --git a/src/resource/mesh_animation_fbx.cpp b/src/resource/mesh_animation_fbx.cpp new file mode 100644 index 000000000..1357ce239 --- /dev/null +++ b/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 + +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 diff --git a/src/resource/mesh_animation_fbx.h b/src/resource/mesh_animation_fbx.h new file mode 100644 index 000000000..632b3021a --- /dev/null +++ b/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 diff --git a/src/resource/mesh_animation_resource.cpp b/src/resource/mesh_animation_resource.cpp new file mode 100644 index 000000000..0ae1762d9 --- /dev/null +++ b/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 diff --git a/src/resource/mesh_animation_resource.h b/src/resource/mesh_animation_resource.h new file mode 100644 index 000000000..43a1621fe --- /dev/null +++ b/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 diff --git a/src/resource/types.h b/src/resource/types.h index 475c47a87..f212f4e7d 100644 --- a/src/resource/types.h +++ b/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) From 100e21c96c42a3786b80fa5312748641e3208187 Mon Sep 17 00:00:00 2001 From: Daniele Bartolini Date: Fri, 21 Feb 2025 10:51:48 +0100 Subject: [PATCH 8/9] samples: core: add skinning to mesh shader Part-of: #276 --- samples/core/shaders/common.shader | 2 +- samples/core/shaders/default.shader | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/samples/core/shaders/common.shader b/samples/core/shaders/common.shader index acacc5443..9bab50418 100644 --- a/samples/core/shaders/common.shader +++ b/samples/core/shaders/common.shader @@ -44,7 +44,7 @@ bgfx_shaders = { #define BGFX_SHADER_H_HEADER_GUARD #if !defined(BGFX_CONFIG_MAX_BONES) - # define BGFX_CONFIG_MAX_BONES 32 + # define BGFX_CONFIG_MAX_BONES 192 #endif // !defined(BGFX_CONFIG_MAX_BONES) #ifndef __cplusplus diff --git a/samples/core/shaders/default.shader b/samples/core/shaders/default.shader index 177d9c2a6..6bd2702c6 100644 --- a/samples/core/shaders/default.shader +++ b/samples/core/shaders/default.shader @@ -218,19 +218,36 @@ bgfx_shaders = { vec3 a_position : POSITION; vec3 a_normal : NORMAL; vec2 a_texcoord0 : TEXCOORD0; + vec4 a_indices : BLENDINDICES; + vec4 a_weight : BLENDWEIGHT; """ vs_input_output = """ + #if defined(SKINNING) + $input a_position, a_normal, a_texcoord0, a_indices, a_weight + #else $input a_position, a_normal, a_texcoord0 + #endif $output v_normal, v_view, v_texcoord0 """ vs_code = """ void main() { + #if defined(SKINNING) + mat4 model; + model = a_weight.x * u_model[int(a_indices.x)]; + model += a_weight.y * u_model[int(a_indices.y)]; + model += a_weight.z * u_model[int(a_indices.z)]; + model += a_weight.w * u_model[int(a_indices.w)]; + gl_Position = mul(mul(u_modelViewProj, model), vec4(a_position, 1.0)); + v_view = mul(mul(u_modelView, model), vec4(a_position, 1.0)); + v_normal = normalize(mul(mul(u_modelView, model), vec4(a_normal, 0.0)).xyz); + #else gl_Position = mul(u_modelViewProj, vec4(a_position, 1.0)); v_view = mul(u_modelView, vec4(a_position, 1.0)); v_normal = normalize(mul(u_modelView, vec4(a_normal, 0.0)).xyz); + #endif v_texcoord0 = a_texcoord0; } @@ -561,6 +578,8 @@ static_compile = [ { shader = "sprite" defines = [] } { shader = "mesh" defines = [] } { shader = "mesh" defines = ["DIFFUSE_MAP"] } + { shader = "mesh" defines = ["SKINNING"] } + { shader = "mesh" defines = ["DIFFUSE_MAP" "SKINNING"] } { shader = "mesh" defines = ["DIFFUSE_MAP" "NO_LIGHT"] } { shader = "selection" defines = [] } { shader = "outline" defines = [] } From 3125d867e04b91b037e563e2ebf11d454c549155 Mon Sep 17 00:00:00 2001 From: Daniele Bartolini Date: Sat, 22 Feb 2025 10:50:57 +0100 Subject: [PATCH 9/9] tools: fbx: import skeleton and animations Part-of: #276 --- scripts/uncrustify/format-all.sh | 1 + tools/resource/mesh_resource_fbx.vala | 214 +++++++++++++++++++++++--- tools/resource/types.vala | 3 + 3 files changed, 196 insertions(+), 22 deletions(-) diff --git a/scripts/uncrustify/format-all.sh b/scripts/uncrustify/format-all.sh index b896f2b94..c39c1cb09 100755 --- a/scripts/uncrustify/format-all.sh +++ b/scripts/uncrustify/format-all.sh @@ -24,6 +24,7 @@ format_tools_vala () { | grep -v 'project_browser.vala' \ | grep -v 'resource_chooser.vala' \ | grep -v 'user.vala' \ + | grep -v 'mesh_resource_fbx.vala' \ | tr '\n' '\0' \ | xargs -0 -n1 -P"$1" ./scripts/uncrustify/uncrustify-wrapper.sh scripts/uncrustify/vala.cfg } diff --git a/tools/resource/mesh_resource_fbx.vala b/tools/resource/mesh_resource_fbx.vala index b8663d1db..4dd15c0f4 100644 --- a/tools/resource/mesh_resource_fbx.vala +++ b/tools/resource/mesh_resource_fbx.vala @@ -73,11 +73,14 @@ public class FBXImportOptions public CheckBox create_textures_folder; public CheckBox import_materials; public CheckBox create_materials_folder; - public CheckBox import_skeleton; - public CheckBox import_animations; + + public CheckBox import_animation; + public CheckBox new_skeleton; + public ResourceChooserButton target_skeleton; + public CheckBox import_clips; public CheckBox create_animations_folder; - public FBXImportOptions() + public FBXImportOptions(ProjectStore project_store) { import_units = new CheckBox(); import_units.value = true; @@ -96,10 +99,17 @@ public class FBXImportOptions import_materials.value_changed.connect(on_import_materials_changed); create_materials_folder = new CheckBox(); create_materials_folder.value = true; - import_skeleton = new CheckBox(); - import_skeleton.value = false; - import_animations = new CheckBox(); - import_animations.value = false; + import_animation = new CheckBox(); + import_animation.value = true; + import_animation.value_changed.connect(on_import_skeleton_and_animations_changed); + new_skeleton = new CheckBox(); + new_skeleton.value = true; + new_skeleton.value_changed.connect(on_new_skeleton_changed); + target_skeleton = new ResourceChooserButton(project_store, OBJECT_TYPE_MESH_SKELETON); + target_skeleton.sensitive = false; + import_clips = new CheckBox(); + import_clips.value = true; + import_clips.value_changed.connect(on_import_animations_changed); create_animations_folder = new CheckBox(); create_animations_folder.value = true; } @@ -124,6 +134,24 @@ public class FBXImportOptions create_materials_folder.set_sensitive(import_materials.value); } + public void on_import_animations_changed() + { + create_animations_folder.set_sensitive(import_clips.value); + } + + public void on_import_skeleton_and_animations_changed() + { + new_skeleton.sensitive = import_animation.value; + target_skeleton.sensitive = import_animation.value; + import_clips.sensitive = import_animation.value; + create_animations_folder.sensitive = import_animation.value; + } + + public void on_new_skeleton_changed() + { + target_skeleton.sensitive = !new_skeleton.value; + } + public void decode(Hashtable json) { json.foreach((g) => { @@ -139,10 +167,12 @@ public class FBXImportOptions import_materials.value = (bool)g.value; else if (g.key == "create_materials_folder") create_materials_folder.value = (bool)g.value; - else if (g.key == "import_skeleton") - import_skeleton.value = (bool)g.value; - else if (g.key == "import_animations") - import_animations.value = (bool)g.value; + else if (g.key == "new_skeleton") + new_skeleton.value = (bool)g.value; + else if (g.key == "target_skeleton") + target_skeleton.value = (string)g.value; + else if (g.key == "import_clips") + import_clips.value = (bool)g.value; else if (g.key == "create_animations_folder") create_animations_folder.value = (bool)g.value; else @@ -157,11 +187,17 @@ public class FBXImportOptions || import_materials.value ; import_units.value_changed(); + + import_animation.value = new_skeleton.value + || import_clips.value + ; + import_animation.value_changed(); } public Hashtable encode() { bool skip_units = !import_units.value; + bool skip_anims = !import_animation.value; Hashtable obj = new Hashtable(); @@ -171,9 +207,10 @@ public class FBXImportOptions obj.set("create_textures_folder", skip_units ? false : create_textures_folder.value); obj.set("import_materials", skip_units ? false : import_materials.value); obj.set("create_materials_folder", skip_units ? false : create_materials_folder.value); - obj.set("import_skeleton", import_skeleton.value); - obj.set("import_animations", import_animations.value); - obj.set("create_animations_folder", create_animations_folder.value); + obj.set("new_skeleton", skip_anims ? false : new_skeleton.value); + obj.set("target_skeleton", skip_anims ? "" : target_skeleton.value); + obj.set("import_clips", skip_anims ? false : import_clips.value); + obj.set("create_animations_folder", skip_anims ? false : create_animations_folder.value); return obj; } @@ -208,7 +245,7 @@ public class FBXImportDialog : Gtk.Window _general_set = new PropertyGridSet(); _general_set.border_width = 12; - _options = new FBXImportOptions(); + _options = new FBXImportOptions(project_store); GLib.File file_dst; string resource_path; get_destination_file(out file_dst, destination_dir, File.new_for_path(_filenames[0])); @@ -232,14 +269,13 @@ public class FBXImportDialog : Gtk.Window cv.add_row("Create Materials Folder", _options.create_materials_folder); _general_set.add_property_grid_optional(cv, "Units", _options.import_units); -#if 0 cv = new PropertyGrid(); cv.column_homogeneous = true; - cv.add_row("Import Skeleton", _options.import_skeleton); - cv.add_row("Import Animations", _options.import_animations); + cv.add_row("New Skeleton", _options.new_skeleton); + cv.add_row("Target Skeleton", _options.target_skeleton); + cv.add_row("Import Animations", _options.import_clips); cv.add_row("Create Animations Folder", _options.create_animations_folder); - _general_set.add_property_grid(cv, "Skeleton and Animations"); -#endif + _general_set.add_property_grid_optional(cv, "Animation", _options.import_animation); _box = new Gtk.Box(Gtk.Orientation.VERTICAL, 0); _box.pack_start(_general_set, false, false); @@ -258,6 +294,11 @@ public class FBXImportDialog : Gtk.Window _header_bar.pack_start(_cancel); _header_bar.pack_end(_import); + _options.import_units.value_changed.connect(on_import_options_changed); + _options.import_animation.value_changed.connect(on_import_options_changed); + _options.new_skeleton.value_changed.connect(on_import_options_changed); + _options.target_skeleton.value_changed.connect(on_import_options_changed); + this.set_titlebar(_header_bar); this.add(_box); } @@ -275,6 +316,19 @@ public class FBXImportDialog : Gtk.Window _import_result(res); close(); } + + void on_import_options_changed() + { + bool target_skeleton_is_valid = _options.new_skeleton.value + || _options.target_skeleton.value != "" + ; + + bool enable_import_button = (_options.import_units.value + || _options.import_animation.value) + && target_skeleton_is_valid + ; + _import.set_sensitive(enable_import_button); + } } public class FBXImporter @@ -391,8 +445,7 @@ public class FBXImporter unit.set_component_property_double(component_id, "data.near_range", (double)node.camera.near_plane); } } else if (node.bone != null) { - if (!options.import_skeleton.value) - return; + return; } else { Unit unit = Unit(db, unit_id); db.create(unit_id, OBJECT_TYPE_UNIT); @@ -425,6 +478,52 @@ public class FBXImporter } } + public static unowned ufbx.Node? find_first_non_bone_parent(ufbx.Node? bone_node) + { + assert(bone_node != null); + + while (bone_node.bone != null) + bone_node = bone_node.parent; + + return bone_node; + } + + public static unowned ufbx.Node? find_skeleton_root(ufbx.Node? node) + { + if (node.bone != null) + return node; + + for (size_t i = 0; i < node.children.data.length; ++i) { + unowned ufbx.Node? n = find_skeleton_root(node.children.data[i]); + if (n != null) + return find_first_non_bone_parent(n); + } + + return null; + } + + public static void import_skeleton(FBXImportOptions options + , Database db + , Guid parent_bone_id + , Guid bone_id + , ufbx.Node node + ) + { + db.create(bone_id, OBJECT_TYPE_MESH_BONE); + db.set_property_string(bone_id, "name", (string)node.name.data); + if (parent_bone_id != GUID_ZERO) + db.add_to_set(parent_bone_id, "children", bone_id); + + for (size_t i = 0; i < node.children.data.length; ++i) { + import_skeleton(options + , db + , bone_id + , Guid.new_guid() + , node.children.data[i] + ); + } + } + public static ImportResult do_import(FBXImportOptions options, Project project, string destination_dir, Gee.ArrayList filenames) { foreach (string filename_i in filenames) { @@ -436,6 +535,7 @@ public class FBXImporter if (get_resource_path(out resource_path, file_dst, project) != 0) return ImportResult.ERROR; string resource_name = ResourceId.name(resource_path); + string resource_basename = GLib.File.new_for_path(resource_name).get_basename(); // Copy FBX file. try { @@ -634,6 +734,76 @@ public class FBXImporter } } + string target_skeleton = options.target_skeleton.value; + + // Import skeleton. + if (options.import_animation.value && options.new_skeleton.value) { + // Create .animation_skeleton resource. + unowned ufbx.Node? skeleton_root_node = find_skeleton_root(scene.root_node); + if (skeleton_root_node != null) { + Guid skeleton_hierarchy_id = Guid.new_guid(); + import_skeleton(options + , db + , GUID_ZERO + , skeleton_hierarchy_id + , skeleton_root_node + ); + + Guid animation_skeleton_id = Guid.new_guid(); + db.create(animation_skeleton_id, OBJECT_TYPE_MESH_SKELETON); + db.set_property_string(animation_skeleton_id, "source", resource_path); + db.add_to_set(animation_skeleton_id, "skeleton", skeleton_hierarchy_id); + db.save(project.absolute_path(resource_name) + "." + OBJECT_TYPE_MESH_SKELETON, animation_skeleton_id); + target_skeleton = resource_name; + } + } + + // Import animations. + if (target_skeleton == "") { + logw("Animation must have a target skeleton."); + } else { + if (options.import_animation.value && options.import_clips.value) { + // Create 'animations' folder. + string directory_name = "animations"; + string animations_path = destination_dir; + if (options.create_animations_folder.value && scene.anim_stacks.data.length != 0) { + GLib.File animations_file = File.new_for_path(Path.build_filename(destination_dir, directory_name)); + try { + animations_file.make_directory(); + } catch (GLib.IOError.EXISTS e) { + // Ignore. + } catch (GLib.Error e) { + loge(e.message); + return ImportResult.ERROR; + } + + animations_path = animations_file.get_path(); + } + + // Extract clips. + if (scene.anim_stacks.data.length > 0) { + unowned ufbx.AnimStack anim_stack = scene.anim_stacks.data[0]; + unowned ufbx.Anim anim = anim_stack.anim; + + string anim_filename = Path.build_filename(animations_path, resource_basename + "." + OBJECT_TYPE_MESH_ANIMATION); + GLib.File anim_file = GLib.File.new_for_path(anim_filename); + string anim_path = anim_file.get_path(); + + string anim_resource_filename = project.resource_filename(anim_path); + string anim_resource_path = ResourceId.normalize(anim_resource_filename); + string anim_resource_name = ResourceId.name(anim_resource_path); + + // Create .mesh_animation resource. + Guid anim_id = Guid.new_guid(); + db.create(anim_id, "mesh_animation"); + db.set_property_string(anim_id, "source", resource_path); + db.set_property_string(anim_id, "target_skeleton", target_skeleton); + db.set_property_string(anim_id, "stack_name", (string)anim_stack.name.data); + db.save(project.absolute_path(anim_resource_name) + "." + OBJECT_TYPE_MESH_ANIMATION, anim_id); + } + } + } + if (options.import_units.value) { // Generate or modify existing .unit. Guid unit_id = Guid.new_guid(); diff --git a/tools/resource/types.vala b/tools/resource/types.vala index fa8d736d7..24854f93e 100644 --- a/tools/resource/types.vala +++ b/tools/resource/types.vala @@ -13,6 +13,9 @@ const string OBJECT_TYPE_FILE = "file"; const string OBJECT_TYPE_LEVEL = "level"; const string OBJECT_TYPE_LIGHT = "light"; const string OBJECT_TYPE_MESH_RENDERER = "mesh_renderer"; +const string OBJECT_TYPE_MESH_BONE = "mesh_bone"; +const string OBJECT_TYPE_MESH_SKELETON = "mesh_skeleton"; +const string OBJECT_TYPE_MESH_ANIMATION = "mesh_animation"; const string OBJECT_TYPE_SCRIPT = "script"; const string OBJECT_TYPE_SOUND_SOURCE = "sound_source"; const string OBJECT_TYPE_SPRITE_RENDERER = "sprite_renderer";