From 04e1aa96e00e665a8fbe5551430bd78f993717cf Mon Sep 17 00:00:00 2001 From: Jaremie Romer <33360084+jaremieromer@users.noreply.github.com> Date: Thu, 21 Dec 2023 10:05:24 -0600 Subject: [PATCH] Skeletal Animation asset support (#33) --- docs/AssetFormats.md | 55 ++++++-- docs/SourceFileRequirements.md | 37 ++++++ docs/image.png | Bin 0 -> 10309 bytes include/ncasset/AssetType.h | 1 + include/ncasset/Assets.h | 37 ++++++ include/ncasset/AssetsFwd.h | 2 + include/ncasset/Import.h | 6 + include/ncasset/NcaHeader.h | 1 + source/ncasset/Deserialize.cpp | 5 + source/ncasset/Deserialize.h | 3 + source/ncasset/Import.cpp | 12 ++ .../ncconvert/builder/BuildInstructions.cpp | 1 + .../ncconvert/builder/BuildOrchestrator.cpp | 3 +- source/ncconvert/builder/Builder.cpp | 6 + source/ncconvert/builder/Inspect.cpp | 27 +++- source/ncconvert/builder/Manifest.cpp | 4 +- source/ncconvert/builder/Serialize.cpp | 5 + source/ncconvert/builder/Serialize.h | 3 + .../converters/GeometryConverter.cpp | 122 +++++++++++++++--- .../ncconvert/converters/GeometryConverter.h | 5 +- source/ncconvert/utility/BlobSize.cpp | 32 +++++ source/ncconvert/utility/BlobSize.h | 3 + source/ncconvert/utility/EnumExtensions.cpp | 26 +--- test/collateral/CollateralGeometry.h | 6 + test/collateral/manifest.json | 11 ++ test/collateral/simple_cube_animation.fbx | Bin 0 -> 47740 bytes .../BuildAndImport_integration_tests.cpp | 33 +++++ .../NcConvert_integration_tests.cpp | 1 + .../Serialize_integration_tests.cpp | 109 ++++++++++++++++ test/ncconvert/EnumExtensions_unit_tests.cpp | 2 + .../GeometryConverter_unit_tests.cpp | 39 ++++-- 31 files changed, 533 insertions(+), 64 deletions(-) create mode 100644 docs/image.png create mode 100644 test/collateral/simple_cube_animation.fbx diff --git a/docs/AssetFormats.md b/docs/AssetFormats.md index 7d436a5..0575fb2 100644 --- a/docs/AssetFormats.md +++ b/docs/AssetFormats.md @@ -14,6 +14,7 @@ This document describes the data layouts for NcEngine assets and asset file type - [HullCollider](#hullcollider-blob-format) - [Mesh](#mesh-blob-format) - [Shader](#shader-blob-format) + - [SkeletalAnimation](#skeletalanimation-blob-format) - [Texture](#texture-blob-format) ## Nc Asset @@ -94,21 +95,57 @@ CubeMap faces in pixel data array are ordered: front, back, up, down, right, lef ### Mesh Blob Format > Magic Number: 'MESH' -| Name | Type | Size | -|--------------|---------------------|-------------------| -| extents | Vector3 | 12 | -| max extent | float | 4 | -| vertex count | u64 | 8 | -| index count | u64 | 8 | -| vertex list | MeshVertex[] | vertex count * 88 | -| indices | int[] | index count * 4 | -| bones data | optional | 56 | +| Name | Type | Size | Note +|----------------------|--------------------------------------|-------------------|------------- +| extents | Vector3 | 12 | +| max extent | float | 4 | +| vertex count | u64 | 8 | +| index count | u64 | 8 | +| vertex list | MeshVertex[] | vertex count * 88 | +| indices | int[] | index count * 4 | +| bones data has value | bool | 1 | +| BonesData | BonesData | | [BonesData](#bones-data-blob-format) + +### Bones Data Blob Format + +| Name | Type | Size | Note +|------------------------------|-----------------------------------------------------|---------------------------------------------------------|------------- +| vertexSpaceToBoneSpace count | u64 | 8 | +| boneSpaceToParentSpace count | u64 | 8 | +| boneMapping | (u64 + string + u32) * vertexSpaceToBoneSpace count | (12 + sizeof(boneName)) * vertexSpaceToBoneSpace count | +| vertexSpaceToBoneSpace | VertexSpaceToBoneSpace[] | (72 + sizeof(boneName)) * vertexSpaceToBoneSpace count | +| boneSpaceToParentSpace | BoneSpaceToParentSpace[] | (136 + sizeof(boneName)) * vertexSpaceToBoneSpace count | ### Shader Blob Format > Magic Number: 'SHAD' TODO +### SkeletalAnimation Blob Format +> Magic Number: 'SKEL' + +| Name | Type | Size | Note +|----------------------------|------------------|---------------------------|------ +| name size | u64 | 8 | +| name | string | name.size() | +| durationInTicks | u32 | 4 | +| ticksPerSecond | float | 4 | +| framesPerBone count | u64 | 8 | +| framesPerBone list | FramesPerBone[] | | [FramesPerBone](#FramesPerBone-blob-format) + +### FramesPerBone Blob Format + +| Name | Type | Size | Note +|----------------------|------------------|---------------------------|------ +| name size | u64 | 8 | +| name | string | name.size() | +| position frames size | u64 | 8 | +| position frames | PositionFrames[] | position frames size * 20 | +| rotation frames size | u64 | 8 | +| rotation frames | RotationFrames[] | rotation frames size * 24 | +| scale frames size | u64 | 8 | +| scale frames | ScaleFrames[] | scale frames size * 20 | + ### Texture Blob Format > Magic Number: 'TEXT' diff --git a/docs/SourceFileRequirements.md b/docs/SourceFileRequirements.md index 4c3839f..ad39ca3 100644 --- a/docs/SourceFileRequirements.md +++ b/docs/SourceFileRequirements.md @@ -21,6 +21,27 @@ This makes `Transform` operations within NcEngine less surprising. Additionally, NcEngine will treat the origin as the object's center of mass for physics calculations. +If the mesh is intended for animation, it needs to have the following properties: + +- An armature with bones +- All bone weights for each vertex must be normalized to sum 1.0 +- No more than four bone influences per vertices + +These properties can be set in the modeling software. To set them in Blender, for example: + +Normalizing bone weights: +1. Select the mesh +2. Change mode to Weight Paint +3. Select all vertices +4. Choose Weights -> Normalize All + +Limiting bone influences to four: +1. Select the mesh +2. Change mode to Weight Paint +3. Select all vertices +4. Choose Weights -> Limit Total +5. Set the limit to 4 in the popup menu + Geometry used for `hull-collider` generation should be convex. ## Image Conversion @@ -64,3 +85,19 @@ Vertical cross layout (3:4): [-Y] [-Z] ``` + +## Skeletal Animation Conversion +> Supported file types: .fbx + +`skeletal-animation` assets can be converted from .fbx files with animation data. +When exporting the .fbx from the modeling software, the following settings must be observed. (Using Blender as an example): +- Rotate the mesh -90 degrees on the X-axis +- Set "Apply Scalings" to "FBX Units Scale" +- Check the "Apply Unit" box +- Check the "Use Space Transform" box +- Set "Forward" to "-Z Forward" +- Set "Up" to "Y Up" + +![Alt text](image.png) + +There is also support for external animations such as Mixamo. diff --git a/docs/image.png b/docs/image.png new file mode 100644 index 0000000000000000000000000000000000000000..e97892a31136c027f29572ea452f75d3e864ee98 GIT binary patch literal 10309 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 #include #include +#include #include namespace nc::asset @@ -33,6 +34,7 @@ struct BoneSpaceToParentSpace struct BonesData { + std::unordered_map boneMapping; std::vector vertexSpaceToBoneSpace; std::vector boneSpaceToParentSpace; }; @@ -81,6 +83,41 @@ struct Shader { }; +struct PositionFrame +{ + float timeInTicks; + Vector3 position; +}; + +struct RotationFrame +{ + float timeInTicks; + Quaternion rotation; +}; + +struct ScaleFrame +{ + float timeInTicks; + Vector3 scale; +}; + +struct SkeletalAnimationFrames +{ + // Vectors are not guaranteed to be the same length. + // There could be no rotation data for a frame, for example. + std::vector positionFrames; + std::vector rotationFrames; + std::vector scaleFrames; +}; + +struct SkeletalAnimation +{ + std::string name; + uint32_t durationInTicks; + float ticksPerSecond; + std::unordered_map framesPerBone; +}; + struct Texture { static constexpr uint32_t numChannels = 4u; diff --git a/include/ncasset/AssetsFwd.h b/include/ncasset/AssetsFwd.h index 1fa8721..301c997 100644 --- a/include/ncasset/AssetsFwd.h +++ b/include/ncasset/AssetsFwd.h @@ -3,10 +3,12 @@ namespace nc::asset { struct AudioClip; +struct BonesData; struct ConcaveCollider; struct CubeMap; struct HullCollider; struct Mesh; struct MeshVertex; +struct SkeletalAnimation; struct Texture; } // namespace nc::asset diff --git a/include/ncasset/Import.h b/include/ncasset/Import.h index 114070c..2c76cb5 100644 --- a/include/ncasset/Import.h +++ b/include/ncasset/Import.h @@ -38,6 +38,12 @@ auto ImportMesh(const std::filesystem::path& ncaPath) -> Mesh; /** @brief Read a Mesh asset from a binary stream. */ auto ImportMesh(std::istream& data) -> Mesh; +/** @brief Read a SkeletalAnimation asset from an .nca file. */ +auto ImportSkeletalAnimation(const std::filesystem::path& ncaPath) -> SkeletalAnimation; + +/** @brief Read a SkeletalAnimation asset from a binary stream. */ +auto ImportSkeletalAnimation(std::istream& data) -> SkeletalAnimation; + /** @brief Read a Texture asset from an .nca file. */ auto ImportTexture(const std::filesystem::path& ncaPath) -> Texture; diff --git a/include/ncasset/NcaHeader.h b/include/ncasset/NcaHeader.h index b68bd17..e115182 100644 --- a/include/ncasset/NcaHeader.h +++ b/include/ncasset/NcaHeader.h @@ -17,6 +17,7 @@ struct MagicNumber static constexpr auto hullCollider = std::string_view{"HULL"}; static constexpr auto mesh = std::string_view{"MESH"}; static constexpr auto shader = std::string_view{"SHAD"}; + static constexpr auto skeletalAnimation = std::string_view{"SKEL"}; static constexpr auto texture = std::string_view{"TEXT"}; }; diff --git a/source/ncasset/Deserialize.cpp b/source/ncasset/Deserialize.cpp index c8e4b28..7f4a327 100644 --- a/source/ncasset/Deserialize.cpp +++ b/source/ncasset/Deserialize.cpp @@ -74,6 +74,11 @@ auto DeserializeMesh(std::istream& stream) -> DeserializedResult return DeserializeImpl(stream, MagicNumber::mesh); } +auto DeserializeSkeletalAnimation(std::istream& stream) -> DeserializedResult +{ + return DeserializeImpl(stream, MagicNumber::skeletalAnimation); +} + auto DeserializeTexture(std::istream& stream) -> DeserializedResult { return DeserializeImpl(stream, MagicNumber::texture); diff --git a/source/ncasset/Deserialize.h b/source/ncasset/Deserialize.h index 99e22c7..85a06e7 100644 --- a/source/ncasset/Deserialize.h +++ b/source/ncasset/Deserialize.h @@ -34,6 +34,9 @@ auto DeserializeHullCollider(std::istream& stream) -> DeserializedResult DeserializedResult; +/** @brief Construct a SkeletalAnimation from data in a binary stream. */ +auto DeserializeSkeletalAnimation(std::istream& stream) -> DeserializedResult; + /** @brief Construct a Texture from data in a binary stream. */ auto DeserializeTexture(std::istream& stream) -> DeserializedResult; } // nc::asset diff --git a/source/ncasset/Import.cpp b/source/ncasset/Import.cpp index 1049f1d..36cdab7 100644 --- a/source/ncasset/Import.cpp +++ b/source/ncasset/Import.cpp @@ -98,6 +98,18 @@ auto ImportMesh(const std::filesystem::path& ncaPath) -> Mesh return ImportMesh(file); } +auto ImportSkeletalAnimation(std::istream& data) -> SkeletalAnimation +{ + auto [header, asset] = DeserializeSkeletalAnimation(data); + return asset; +} + +auto ImportSkeletalAnimation(const std::filesystem::path& ncaPath) -> SkeletalAnimation +{ + auto file = ::OpenNca(ncaPath); + return ImportSkeletalAnimation(file); +} + auto ImportTexture(std::istream& data) -> Texture { auto [header, asset] = DeserializeTexture(data); diff --git a/source/ncconvert/builder/BuildInstructions.cpp b/source/ncconvert/builder/BuildInstructions.cpp index b35ae1b..706c91c 100644 --- a/source/ncconvert/builder/BuildInstructions.cpp +++ b/source/ncconvert/builder/BuildInstructions.cpp @@ -17,6 +17,7 @@ auto BuildTargetMap() -> std::unordered_map{}); out.emplace(nc::asset::AssetType::HullCollider, std::vector{}); out.emplace(nc::asset::AssetType::Mesh, std::vector{}); + out.emplace(nc::asset::AssetType::SkeletalAnimation, std::vector{}); out.emplace(nc::asset::AssetType::Texture, std::vector{}); return out; } diff --git a/source/ncconvert/builder/BuildOrchestrator.cpp b/source/ncconvert/builder/BuildOrchestrator.cpp index a7ea7a3..551d705 100644 --- a/source/ncconvert/builder/BuildOrchestrator.cpp +++ b/source/ncconvert/builder/BuildOrchestrator.cpp @@ -14,12 +14,13 @@ namespace { -constexpr auto assetTypes = std::array{ +constexpr auto assetTypes = std::array{ nc::asset::AssetType::AudioClip, nc::asset::AssetType::CubeMap, nc::asset::AssetType::ConcaveCollider, nc::asset::AssetType::HullCollider, nc::asset::AssetType::Mesh, + nc::asset::AssetType::SkeletalAnimation, nc::asset::AssetType::Texture }; } diff --git a/source/ncconvert/builder/Builder.cpp b/source/ncconvert/builder/Builder.cpp index f079c90..d71bf62 100644 --- a/source/ncconvert/builder/Builder.cpp +++ b/source/ncconvert/builder/Builder.cpp @@ -95,6 +95,12 @@ auto Builder::Build(asset::AssetType type, const Target& target) -> bool { throw NcError("Not implemented"); } + case asset::AssetType::SkeletalAnimation: + { + const auto asset = m_geometryConverter->ImportSkeletalAnimation(target.sourcePath, target.subResourceName); + convert::Serialize(outFile, asset, assetId); + return true; + } case asset::AssetType::Texture: { const auto asset = m_textureConverter->ImportTexture(target.sourcePath); diff --git a/source/ncconvert/builder/Inspect.cpp b/source/ncconvert/builder/Inspect.cpp index b3c5993..9d13f32 100644 --- a/source/ncconvert/builder/Inspect.cpp +++ b/source/ncconvert/builder/Inspect.cpp @@ -37,10 +37,19 @@ R"(Data constexpr auto meshTemplate = R"(Data - extents {}, {}, {} - max extent {} - vertex count {} - index count {})"; + extents {}, {}, {} + max extent {} + vertex count {} + index count {} + bones data vertex to bone count {} + bones data bone to parent count {})"; + +constexpr auto skeletalAnimationTemplate = +R"(Data + name {} + duration in ticks {} + ticks per seconds {} + frames per bone {})"; constexpr auto textureTemplate = R"(Data @@ -86,7 +95,9 @@ void Inspect(const std::filesystem::path& ncaPath) case asset::AssetType::Mesh: { const auto asset = asset::ImportMesh(ncaPath); - LOG(meshTemplate, asset.extents.x, asset.extents.y, asset.extents.z, asset.maxExtent, asset.vertices.size(), asset.indices.size()); + auto vertexSpaceSize = asset.bonesData.has_value()? asset.bonesData.value().vertexSpaceToBoneSpace.size() : 0; + auto boneSpaceSize = asset.bonesData.has_value()? asset.bonesData.value().boneSpaceToParentSpace.size() : 0; + LOG(meshTemplate, asset.extents.x, asset.extents.y, asset.extents.z, asset.maxExtent, asset.vertices.size(), asset.indices.size(), vertexSpaceSize, boneSpaceSize); break; } case asset::AssetType::Shader: @@ -94,6 +105,12 @@ void Inspect(const std::filesystem::path& ncaPath) LOG("Shader not supported"); break; } + case asset::AssetType::SkeletalAnimation: + { + const auto asset = asset::ImportSkeletalAnimation(ncaPath); + LOG(skeletalAnimationTemplate, asset.name, asset.durationInTicks, asset.ticksPerSecond, asset.framesPerBone.size()); + break; + } case asset::AssetType::Texture: { const auto asset = asset::ImportTexture(ncaPath); diff --git a/source/ncconvert/builder/Manifest.cpp b/source/ncconvert/builder/Manifest.cpp index a1fe21b..693a600 100644 --- a/source/ncconvert/builder/Manifest.cpp +++ b/source/ncconvert/builder/Manifest.cpp @@ -11,8 +11,8 @@ namespace { -const auto jsonAssetArrayTags = std::array { - "audio-clip", "concave-collider", "cube-map", "hull-collider", "mesh", "texture" +const auto jsonAssetArrayTags = std::array { + "audio-clip", "concave-collider", "cube-map", "hull-collider", "mesh", "skeletal-animation", "texture" }; struct GlobalManifestOptions diff --git a/source/ncconvert/builder/Serialize.cpp b/source/ncconvert/builder/Serialize.cpp index 15f86fd..f263c86 100644 --- a/source/ncconvert/builder/Serialize.cpp +++ b/source/ncconvert/builder/Serialize.cpp @@ -47,6 +47,11 @@ void Serialize(std::ostream& stream, const asset::Mesh& data, size_t assetId) SerializeImpl(stream, data, asset::MagicNumber::mesh, assetId); } +void Serialize(std::ostream& stream, const asset::SkeletalAnimation& data, size_t assetId) +{ + SerializeImpl(stream, data, asset::MagicNumber::skeletalAnimation, assetId); +} + void Serialize(std::ostream& stream, const asset::Texture& data, size_t assetId) { SerializeImpl(stream, data, asset::MagicNumber::texture, assetId); diff --git a/source/ncconvert/builder/Serialize.h b/source/ncconvert/builder/Serialize.h index 390a8bd..a1f2a90 100644 --- a/source/ncconvert/builder/Serialize.h +++ b/source/ncconvert/builder/Serialize.h @@ -21,6 +21,9 @@ void Serialize(std::ostream& stream, const asset::HullCollider& data, size_t ass /** @brief Write a Mesh to a binary stream. */ void Serialize(std::ostream& stream, const asset::Mesh& data, size_t assetId); +/** @brief Write a SkeletalAnimation to a binary stream. */ +void Serialize(std::ostream& stream, const asset::SkeletalAnimation& data, size_t assetId); + /** @brief Write a Texture to a binary stream. */ void Serialize(std::ostream& stream, const asset::Texture& data, size_t assetId); } // nc::convert diff --git a/source/ncconvert/converters/GeometryConverter.cpp b/source/ncconvert/converters/GeometryConverter.cpp index 2323547..8a831bb 100644 --- a/source/ncconvert/converters/GeometryConverter.cpp +++ b/source/ncconvert/converters/GeometryConverter.cpp @@ -16,12 +16,14 @@ #include #include #include +#include namespace { constexpr auto concaveColliderFlags = aiProcess_Triangulate | aiProcess_ConvertToLeftHanded; constexpr auto hullColliderFlags = concaveColliderFlags | aiProcess_JoinIdenticalVertices; constexpr auto meshFlags = hullColliderFlags | aiProcess_GenNormals | aiProcess_CalcTangentSpace; +constexpr auto skeletalAnimationFlags = meshFlags | aiProcess_LimitBoneWeights; const auto supportedFileExtensions = std::array {".fbx", ".obj"}; auto ReadFbx(const std::filesystem::path& path, Assimp::Importer* importer, unsigned flags) -> const aiScene* @@ -52,28 +54,53 @@ auto ReadFbx(const std::filesystem::path& path, Assimp::Importer* importer, unsi return scene; } -auto GetMeshFromScene(const aiScene* scene, const std::optional& subResourceName = std::nullopt) -> aiMesh* +template +[[noreturn]] void SubResourceErrorHandler(const std::string& resource, std::span items) { - aiMesh* mesh = nullptr; + auto ss = std::ostringstream{}; + ss << "A sub-resource name was provided but no sub-resource was found by that name: " << resource << ".\nNo asset will created. Found sub-resources: \n"; + std::ranges::for_each(items, [&ss](auto&& item){ ss << item->mName.C_Str() << ", "; }); + throw nc::NcError(ss.str()); +} +auto GetMeshFromScene(const aiScene* scene, const std::optional& subResourceName = std::nullopt) -> aiMesh* +{ NC_ASSERT(scene->mNumMeshes != 0, "No meshes found in scene."); if (!subResourceName.has_value()) { return scene->mMeshes[0]; } - - for (auto* sceneMesh : std::span(scene->mMeshes, scene->mNumMeshes)) + + auto target = aiString{subResourceName.value()}; + auto meshes = std::span(scene->mMeshes, scene->mNumMeshes); + auto pos = std::ranges::find(meshes, target, [](auto&& m) { return m->mName; }); + if (pos != std::cend(meshes)) { - if (std::string{sceneMesh->mName.C_Str()} == subResourceName) - { - mesh = sceneMesh; - break; - } + return *pos; + } + + SubResourceErrorHandler(subResourceName.value(), meshes); +} + +auto GetAnimationFromMesh(const aiScene* scene, const std::optional& subResourceName = std::nullopt) -> aiAnimation* +{ + NC_ASSERT(scene->mNumAnimations != 0, "No animations found in scene."); + + if (!subResourceName.has_value()) + { + return scene->mAnimations[0]; + } + + auto target = aiString{subResourceName.value()}; + auto animations = std::span(scene->mAnimations, scene->mNumAnimations); + auto pos = std::ranges::find(animations, target, [](auto&& m) { return m->mName; }); + if (pos != std::cend(animations)) + { + return *pos; } - if (mesh == nullptr) throw nc::NcError("A sub-resource name was provided but no mesh was found by that name: {}. No asset will be created.", subResourceName.value()); - return mesh; + SubResourceErrorHandler(subResourceName.value(), animations); } auto ToVector3(const aiVector3D& in) -> nc::Vector3 @@ -137,13 +164,13 @@ auto ConvertToTriangles(std::span faces, std::span DirectX::XMMATRIX { - return DirectX::XMMATRIX + return DirectX::XMMatrixTranspose(DirectX::XMMATRIX { inputMatrix->a1, inputMatrix->a2, inputMatrix->a3, inputMatrix->a4, inputMatrix->b1, inputMatrix->b2, inputMatrix->b3, inputMatrix->b4, inputMatrix->c1, inputMatrix->c2, inputMatrix->c3, inputMatrix->c4, inputMatrix->d1, inputMatrix->d2, inputMatrix->d3, inputMatrix->d4 - }; + }); } auto GetBoneWeights(const aiMesh* mesh) -> std::unordered_map @@ -257,12 +284,30 @@ auto GetVertexToBoneSpaceMatrices(const aiMesh* mesh) -> std::vector& vertexSpaceToBoneSpaceMatrices) -> std::unordered_map +{ + auto boneMapping = std::unordered_map{}; + boneMapping.reserve(vertexSpaceToBoneSpaceMatrices.size()); + + for (auto i = 0u; i < vertexSpaceToBoneSpaceMatrices.size(); i++) + { + boneMapping.emplace(vertexSpaceToBoneSpaceMatrices[i].boneName, i); + } + + return boneMapping; +} + auto GetBonesData(const aiMesh* mesh, const aiNode* rootNode) -> nc::asset::BonesData { + auto vertexSpaceToBoneSpaces = GetVertexToBoneSpaceMatrices(mesh); + auto boneSpaceToParentSpaces = GetBoneSpaceToParentSpaceMatrices(rootNode); + auto boneMapping = GetBoneMapping(vertexSpaceToBoneSpaces); + return nc::asset::BonesData { - GetVertexToBoneSpaceMatrices(mesh), - GetBoneSpaceToParentSpaceMatrices(rootNode) + std::move(boneMapping), + std::move(vertexSpaceToBoneSpaces), + std::move(boneSpaceToParentSpaces) }; } @@ -305,6 +350,41 @@ auto ConvertToMeshVertices(const aiMesh* mesh) -> std::vector nc::asset::SkeletalAnimation +{ + auto skeletalAnimation = nc::asset::SkeletalAnimation{}; + skeletalAnimation.name = std::string(animationClip->mName.C_Str()); + skeletalAnimation.ticksPerSecond = animationClip->mTicksPerSecond == 0 ? 25.0f : static_cast(animationClip->mTicksPerSecond); // Ticks per second is not required to be set in animation software. + skeletalAnimation.durationInTicks = static_cast(animationClip->mDuration); + skeletalAnimation.framesPerBone.reserve(animationClip->mNumChannels); + + // A single channel represents one bone and all of its transformations for the animation clip. + for (const auto* channel : std::span(animationClip->mChannels, animationClip->mNumChannels)) + { + auto frames = nc::asset::SkeletalAnimationFrames{}; + frames.positionFrames.reserve(channel->mNumPositionKeys); + frames.rotationFrames.reserve(channel->mNumRotationKeys); + frames.scaleFrames.reserve(channel->mNumScalingKeys); + + for (const auto& positionKey : std::span(channel->mPositionKeys, channel->mNumPositionKeys)) + { + frames.positionFrames.emplace_back(static_cast(positionKey.mTime), nc::Vector3(positionKey.mValue.x, positionKey.mValue.y, positionKey.mValue.z)); + } + + for (const auto& rotationKey : std::span(channel->mRotationKeys, channel->mNumRotationKeys)) + { + frames.rotationFrames.emplace_back(static_cast(rotationKey.mTime), nc::Quaternion(rotationKey.mValue.x, rotationKey.mValue.y, rotationKey.mValue.z, rotationKey.mValue.w)); + } + + for (const auto& scaleKey : std::span(channel->mScalingKeys, channel->mNumScalingKeys)) + { + frames.scaleFrames.emplace_back(static_cast(scaleKey.mTime), nc::Vector3(scaleKey.mValue.x, scaleKey.mValue.y, scaleKey.mValue.z)); + } + skeletalAnimation.framesPerBone.emplace(std::string(channel->mNodeName.C_Str()), std::move(frames)); + } + return skeletalAnimation; +} } // anonymous namespace namespace nc::convert @@ -381,6 +461,13 @@ class GeometryConverter::impl }; } + auto ImportSkeletalAnimation(const std::filesystem::path& path, const std::optional& subResourceName) -> asset::SkeletalAnimation + { + const auto scene = ::ReadFbx(path, &m_importer, skeletalAnimationFlags); + auto animation = GetAnimationFromMesh(scene, subResourceName); + return ::ConvertToSkeletalAnimation(animation); + } + private: Assimp::Importer m_importer; }; @@ -407,4 +494,9 @@ auto GeometryConverter::ImportMesh(const std::filesystem::path& path, const std: return m_impl->ImportMesh(path, subResourceName); } +auto GeometryConverter::ImportSkeletalAnimation(const std::filesystem::path& path, const std::optional& subResourceName) -> asset::SkeletalAnimation +{ + return m_impl->ImportSkeletalAnimation(path, subResourceName); +} + } // namespace nc::convert diff --git a/source/ncconvert/converters/GeometryConverter.h b/source/ncconvert/converters/GeometryConverter.h index 9493b83..269bca6 100644 --- a/source/ncconvert/converters/GeometryConverter.h +++ b/source/ncconvert/converters/GeometryConverter.h @@ -15,7 +15,7 @@ class GeometryConverter GeometryConverter(); ~GeometryConverter() noexcept; - /** Process an Fbx file as geometry for a concave collider. */ + /** Process an fbx file as geometry for a concave collider. */ auto ImportConcaveCollider(const std::filesystem::path& path) -> asset::ConcaveCollider; /** Process an fbx file as geometry for a hull collider. */ @@ -24,6 +24,9 @@ class GeometryConverter /** Process an fbx file as geometry for a mesh renderer. Supply a subResourceName of the mesh to extract if there are multiple meshes in the fbx file. */ auto ImportMesh(const std::filesystem::path& path, const std::optional& subResourceName = std::nullopt) -> asset::Mesh; + /** Process an fbx file into a skeletal animation clip. Supply a subResourceName of the clip to extract if there are multiple clips in the fbx file. */ + auto ImportSkeletalAnimation(const std::filesystem::path& path, const std::optional& subResourceName = std::nullopt) -> asset::SkeletalAnimation; + private: class impl; std::unique_ptr m_impl; diff --git a/source/ncconvert/utility/BlobSize.cpp b/source/ncconvert/utility/BlobSize.cpp index d6478a6..f956f5a 100644 --- a/source/ncconvert/utility/BlobSize.cpp +++ b/source/ncconvert/utility/BlobSize.cpp @@ -14,6 +14,11 @@ auto GetBonesSize(const std::optional& bonesData) -> size_ out += sizeof(size_t); out += sizeof(size_t); + for (const auto& [boneName, index] : bonesData.value().boneMapping) + { + out += sizeof(size_t) + boneName.size() + sizeof(uint32_t); + } + for (const auto& vertexSpaceToBoneSpace : bonesData.value().vertexSpaceToBoneSpace) { out += sizeof(size_t) + vertexSpaceToBoneSpace.boneName.size() + matrixSize; @@ -26,6 +31,28 @@ auto GetBonesSize(const std::optional& bonesData) -> size_ } return out; } + +auto GetSkeletalAnimationSize(const nc::asset::SkeletalAnimation& asset) -> size_t +{ + auto baseSize = sizeof(size_t) + // name size + asset.name.size() + // name + sizeof(uint32_t) + // durationInTicks + sizeof(float) + // ticksPerSecond + sizeof(size_t); // framesPerBone count + + for (const auto& [name, frames] : asset.framesPerBone) + { + baseSize += sizeof(size_t); + baseSize += name.size(); + baseSize += sizeof(size_t); + baseSize += frames.positionFrames.size() * sizeof(nc::asset::PositionFrame); + baseSize += sizeof(size_t); + baseSize += frames.rotationFrames.size() * sizeof(nc::asset::RotationFrame); + baseSize += sizeof(size_t); + baseSize += frames.scaleFrames.size() * sizeof(nc::asset::ScaleFrame); + } + return baseSize; +} } // anonymous namespace namespace nc::convert @@ -60,6 +87,11 @@ auto GetBlobSize(const asset::Mesh& asset) -> size_t return baseSize + asset.vertices.size() * sizeof(asset::MeshVertex) + asset.indices.size() * sizeof(uint32_t) + sizeof(bool) + GetBonesSize(asset.bonesData); } +auto GetBlobSize(const asset::SkeletalAnimation& asset) -> size_t +{ + return GetSkeletalAnimationSize(asset); +} + auto GetBlobSize(const asset::Texture& asset) -> size_t { constexpr auto baseSize = sizeof(asset::Texture::width) + sizeof(asset::Texture::height); diff --git a/source/ncconvert/utility/BlobSize.h b/source/ncconvert/utility/BlobSize.h index 8801429..dd5c217 100644 --- a/source/ncconvert/utility/BlobSize.h +++ b/source/ncconvert/utility/BlobSize.h @@ -21,6 +21,9 @@ auto GetBlobSize(const asset::HullCollider& asset) -> size_t; /** @brief Get the serialized size in bytes for a Mesh. */ auto GetBlobSize(const asset::Mesh& asset) -> size_t; +/** @brief Get the serialized size in bytes for a SkeletalAnimation. */ +auto GetBlobSize(const asset::SkeletalAnimation& asset) -> size_t; + /** @brief Get the serialized size in bytes for a Texture. */ auto GetBlobSize(const asset::Texture& asset) -> size_t; } // namespace nc::convert diff --git a/source/ncconvert/utility/EnumExtensions.cpp b/source/ncconvert/utility/EnumExtensions.cpp index 166e172..9379e2b 100644 --- a/source/ncconvert/utility/EnumExtensions.cpp +++ b/source/ncconvert/utility/EnumExtensions.cpp @@ -9,27 +9,7 @@ namespace nc::convert { auto CanOutputMany(asset::AssetType type) -> bool { - switch(type) - { - case asset::AssetType::AudioClip: - return false; - case asset::AssetType::CubeMap: - return false; - case asset::AssetType::ConcaveCollider: - return false; - case asset::AssetType::HullCollider: - return false; - case asset::AssetType::Mesh: - return true; - case asset::AssetType::Texture: - return false; - default: - break; - } - - throw NcError( - fmt::format("Unknown AssetType: {}", static_cast(type)) - ); + return type == asset::AssetType::Mesh || type == asset::AssetType::SkeletalAnimation; } auto ToAssetType(std::string type) -> asset::AssetType @@ -46,6 +26,8 @@ auto ToAssetType(std::string type) -> asset::AssetType return asset::AssetType::HullCollider; else if(type == "mesh") return asset::AssetType::Mesh; + else if(type == "skeletal-animation") + return asset::AssetType::SkeletalAnimation; else if(type == "texture") return asset::AssetType::Texture; @@ -66,6 +48,8 @@ auto ToString(asset::AssetType type) -> std::string return "hull-collider"; case asset::AssetType::Mesh: return "mesh"; + case asset::AssetType::SkeletalAnimation: + return "skeletal-animation"; case asset::AssetType::Texture: return "texture"; default: diff --git a/test/collateral/CollateralGeometry.h b/test/collateral/CollateralGeometry.h index da232d2..e424871 100644 --- a/test/collateral/CollateralGeometry.h +++ b/test/collateral/CollateralGeometry.h @@ -76,6 +76,12 @@ namespace real_world_model_fbx const auto filePath = collateralDirectory / "real_world_model.fbx"; } // namespace real_world_model_fbx +// Describes the collateral file simple_cube_animation.fbx +namespace simple_cube_animation_fbx +{ +const auto filePath = collateralDirectory / "simple_cube_animation.fbx"; +} // namespace simple_cube_animation_fbx + // Describes the collateral file multicube.fbx namespace multicube_fbx { diff --git a/test/collateral/manifest.json b/test/collateral/manifest.json index baae7a5..e6d4850 100644 --- a/test/collateral/manifest.json +++ b/test/collateral/manifest.json @@ -62,6 +62,17 @@ ] } ], + "skeletal-animation": [ + { + "sourcePath": "simple_cube_animation.fbx", + "assetNames": [ + { + "subResourceName" : "Armature|Wiggle", + "assetName" : "wiggle" + } + ] + } + ], "texture": [ { "sourcePath": "rgb_corners_4x8.png", diff --git a/test/collateral/simple_cube_animation.fbx b/test/collateral/simple_cube_animation.fbx new file mode 100644 index 0000000000000000000000000000000000000000..8bf78259aed0a74abd986b953a2959b8bf1c2bb5 GIT binary patch literal 47740 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 literal 0 HcmV?d00001 diff --git a/test/integration/BuildAndImport_integration_tests.cpp b/test/integration/BuildAndImport_integration_tests.cpp index e4eb584..8f25671 100644 --- a/test/integration/BuildAndImport_integration_tests.cpp +++ b/test/integration/BuildAndImport_integration_tests.cpp @@ -138,6 +138,39 @@ TEST_F(BuildAndImportTest, Mesh_from_fbx) EXPECT_TRUE(std::ranges::all_of(asset.indices, [&nVertices](auto i){ return i < nVertices; })); } +TEST_F(BuildAndImportTest, SkeletalAnimation_from_fbx) +{ + namespace test_data = collateral::simple_cube_animation_fbx; + const auto inFile = test_data::filePath; + const auto outFile = ncaTestOutDirectory / "simple_cube_animation.nca"; + const auto target = nc::convert::Target(inFile, outFile, std::string{"Armature|Wiggle"}); + auto builder = nc::convert::Builder{}; + ASSERT_TRUE(builder.Build(nc::asset::AssetType::SkeletalAnimation, target)); + + auto asset = nc::asset::ImportSkeletalAnimation(outFile); + + EXPECT_EQ(asset.name, "Armature|Wiggle"); + EXPECT_EQ(asset.durationInTicks, 60); + EXPECT_EQ(asset.ticksPerSecond, 24); + EXPECT_EQ(asset.framesPerBone.size(), 4); + + EXPECT_EQ(asset.framesPerBone["Armature"].positionFrames.size(), 2); + EXPECT_EQ(asset.framesPerBone["Armature"].rotationFrames.size(), 2); + EXPECT_EQ(asset.framesPerBone["Armature"].rotationFrames.size(), 2); + + EXPECT_EQ(asset.framesPerBone["Root"].positionFrames.size(), 60); + EXPECT_EQ(asset.framesPerBone["Root"].rotationFrames.size(), 60); + EXPECT_EQ(asset.framesPerBone["Root"].rotationFrames.size(), 60); + + EXPECT_EQ(asset.framesPerBone["Tip"].positionFrames.size(), 2); + EXPECT_EQ(asset.framesPerBone["Tip"].rotationFrames.size(), 2); + EXPECT_EQ(asset.framesPerBone["Tip"].rotationFrames.size(), 2); + + EXPECT_EQ(asset.framesPerBone["Tip_end"].positionFrames.size(), 61); + EXPECT_EQ(asset.framesPerBone["Tip_end"].rotationFrames.size(), 61); + EXPECT_EQ(asset.framesPerBone["Tip_end"].rotationFrames.size(), 61); +} + TEST_F(BuildAndImportTest, AudioClip_from_wav) { namespace test_data = collateral::sine; diff --git a/test/integration/NcConvert_integration_tests.cpp b/test/integration/NcConvert_integration_tests.cpp index 54fbda6..4d10cd7 100644 --- a/test/integration/NcConvert_integration_tests.cpp +++ b/test/integration/NcConvert_integration_tests.cpp @@ -176,6 +176,7 @@ TEST_F(NcConvertIntegration, Manifest_succeeds) EXPECT_TRUE(std::filesystem::exists(ncaTestOutDirectory / "cube1a.nca")); EXPECT_TRUE(std::filesystem::exists(ncaTestOutDirectory / "cube2.nca")); EXPECT_TRUE(std::filesystem::exists(ncaTestOutDirectory / "cube3.nca")); + EXPECT_TRUE(std::filesystem::exists(ncaTestOutDirectory / "wiggle.nca")); } TEST_F(NcConvertIntegration, Manifest_subResourceMeshNotPresent_manifestFails) diff --git a/test/integration/Serialize_integration_tests.cpp b/test/integration/Serialize_integration_tests.cpp index cde0250..b2f1b58 100644 --- a/test/integration/Serialize_integration_tests.cpp +++ b/test/integration/Serialize_integration_tests.cpp @@ -144,6 +144,7 @@ TEST(SerializationTest, Mesh_hasBones_roundTrip_succeeds) 0, 1, 2, 1, 2, 0, 2, 0, 1 }, .bonesData = nc::asset::BonesData{ + .boneMapping = std::unordered_map{}, .vertexSpaceToBoneSpace = std::vector(0), .boneSpaceToParentSpace = std::vector(0) } @@ -179,6 +180,9 @@ TEST(SerializationTest, Mesh_hasBones_roundTrip_succeeds) .indexOfFirstChild = 0u }); + // Can't initialize above due to internal compiler error in MS. + expectedAsset.bonesData.value().boneMapping.emplace("Bone0Bone0Bone0Bone0Bone0Bone0Bone0Bone0Bone0Bone0Bone0Bone0Bone0Bone0Bone0Bone0Bone0Bone0Bone0Bone0Bone0Bone0Bone0Bone0Bone0Bone0Bone0Bone0Bone0Bone0Bone0Bone0Bone0Bone0Bone0", 0); + auto stream = std::stringstream{std::ios::in | std::ios::out | std::ios::binary}; nc::convert::Serialize(stream, expectedAsset, assetId); const auto [actualHeader, actualAsset] = nc::asset::DeserializeMesh(stream); @@ -367,3 +371,108 @@ TEST(SerializationTest, CubeMap_roundTrip_succeeds) expectedAsset.pixelData.cend(), actualAsset.pixelData.cbegin())); } + +TEST(SerializationTest, SkeletalAnimation_roundTrip_succeeds) +{ + constexpr auto assetId = 1234ull; + + const auto firstBoneFrame = nc::asset::SkeletalAnimationFrames + { + std::vector + { + nc::asset::PositionFrame{0, nc::Vector3{0.0f, 0.0f, 0.0f}}, + nc::asset::PositionFrame{1, nc::Vector3{0.1f, 0.1f, 0.1f}}, + nc::asset::PositionFrame{2, nc::Vector3{0.2f, 0.2f, 0.2f}} + }, + + std::vector + { + nc::asset::RotationFrame{0, nc::Quaternion{1.0f, 1.0f, 1.0f, 1.0f}}, + nc::asset::RotationFrame{1, nc::Quaternion{1.1f, 1.1f, 1.1f, 1.0f}}, + nc::asset::RotationFrame{2, nc::Quaternion{1.2f, 1.2f, 1.2f, 1.0f}} + }, + + std::vector + { + nc::asset::ScaleFrame{0, nc::Vector3{2.0f, 2.0f, 2.0f}}, + nc::asset::ScaleFrame{1, nc::Vector3{2.1f, 2.1f, 2.1f}}, + nc::asset::ScaleFrame{2, nc::Vector3{2.2f, 2.2f, 2.2f}} + } + }; + + const auto secondBoneFrame = nc::asset::SkeletalAnimationFrames + { + std::vector + { + nc::asset::PositionFrame{0, nc::Vector3{3.0f, 3.0f, 3.0f}}, + nc::asset::PositionFrame{1, nc::Vector3{3.1f, 3.1f, 3.1f}}, + nc::asset::PositionFrame{2, nc::Vector3{3.2f, 3.2f, 3.2f}} + }, + + std::vector + { + nc::asset::RotationFrame{0, nc::Quaternion{4.0f, 4.0f, 4.0f, 4.0f}}, + nc::asset::RotationFrame{1, nc::Quaternion{4.1f, 4.1f, 4.1f, 4.0f}}, + nc::asset::RotationFrame{2, nc::Quaternion{4.2f, 4.2f, 4.2f, 4.0f}} + }, + + std::vector + { + nc::asset::ScaleFrame{0, nc::Vector3{5.0f, 5.0f, 5.0f}}, + nc::asset::ScaleFrame{1, nc::Vector3{5.1f, 5.1f, 5.1f}}, + nc::asset::ScaleFrame{2, nc::Vector3{5.2f, 5.2f, 5.2f}} + } + }; + + const auto skeletalAnimationFrames = std::unordered_map + { + {{std::string{"Bone0"}}, std::move(firstBoneFrame)}, + {{std::string{"Bone1"}}, std::move(secondBoneFrame)} + }; + + const auto expectedAsset = nc::asset::SkeletalAnimation{ + .name = "Test", + .durationInTicks = 128, + .ticksPerSecond = 64, + .framesPerBone = std::move(skeletalAnimationFrames) + }; + + auto stream = std::stringstream{std::ios::in | std::ios::out | std::ios::binary}; + nc::convert::Serialize(stream, expectedAsset, assetId); + const auto [actualHeader, actualAsset] = nc::asset::DeserializeSkeletalAnimation(stream); + + EXPECT_EQ(actualAsset.name, std::string{"Test"}); + EXPECT_EQ(actualAsset.durationInTicks, 128); + EXPECT_EQ(actualAsset.ticksPerSecond, 64); + EXPECT_EQ(actualAsset.framesPerBone.size(), 2); + + const auto& firstBoneFrames = actualAsset.framesPerBone.at("Bone0"); + EXPECT_EQ(firstBoneFrames.positionFrames.at(0).timeInTicks, 0); + EXPECT_EQ(firstBoneFrames.positionFrames.at(1).timeInTicks, 1); + EXPECT_EQ(firstBoneFrames.positionFrames.at(2).timeInTicks, 2); + EXPECT_FLOAT_EQ(firstBoneFrames.positionFrames.at(0).position.x, 0.0f); + EXPECT_FLOAT_EQ(firstBoneFrames.positionFrames.at(0).position.y, 0.0f); + EXPECT_FLOAT_EQ(firstBoneFrames.positionFrames.at(0).position.z, 0.0f); + EXPECT_FLOAT_EQ(firstBoneFrames.rotationFrames.at(1).rotation.x, 1.1f); + EXPECT_FLOAT_EQ(firstBoneFrames.rotationFrames.at(1).rotation.y, 1.1f); + EXPECT_FLOAT_EQ(firstBoneFrames.rotationFrames.at(1).rotation.z, 1.1f); + EXPECT_FLOAT_EQ(firstBoneFrames.rotationFrames.at(1).rotation.w, 1.0f); + EXPECT_FLOAT_EQ(firstBoneFrames.scaleFrames.at(2).scale.x, 2.2f); + EXPECT_FLOAT_EQ(firstBoneFrames.scaleFrames.at(2).scale.y, 2.2f); + EXPECT_FLOAT_EQ(firstBoneFrames.scaleFrames.at(2).scale.z, 2.2f); + + const auto& secondBoneFrames = actualAsset.framesPerBone.at("Bone1"); + EXPECT_EQ(secondBoneFrames.positionFrames.at(0).timeInTicks, 0); + EXPECT_EQ(secondBoneFrames.positionFrames.at(1).timeInTicks, 1); + EXPECT_EQ(secondBoneFrames.positionFrames.at(2).timeInTicks, 2); + EXPECT_FLOAT_EQ(secondBoneFrames.positionFrames.at(0).position.x, 3.0f); + EXPECT_FLOAT_EQ(secondBoneFrames.positionFrames.at(0).position.y, 3.0f); + EXPECT_FLOAT_EQ(secondBoneFrames.positionFrames.at(0).position.z, 3.0f); + EXPECT_FLOAT_EQ(secondBoneFrames.rotationFrames.at(1).rotation.x, 4.1f); + EXPECT_FLOAT_EQ(secondBoneFrames.rotationFrames.at(1).rotation.y, 4.1f); + EXPECT_FLOAT_EQ(secondBoneFrames.rotationFrames.at(1).rotation.z, 4.1f); + EXPECT_FLOAT_EQ(secondBoneFrames.rotationFrames.at(1).rotation.w, 4.0f); + EXPECT_FLOAT_EQ(secondBoneFrames.scaleFrames.at(2).scale.x, 5.2f); + EXPECT_FLOAT_EQ(secondBoneFrames.scaleFrames.at(2).scale.y, 5.2f); + EXPECT_FLOAT_EQ(secondBoneFrames.scaleFrames.at(2).scale.z, 5.2f); +} diff --git a/test/ncconvert/EnumExtensions_unit_tests.cpp b/test/ncconvert/EnumExtensions_unit_tests.cpp index aa19f99..3865ff4 100644 --- a/test/ncconvert/EnumExtensions_unit_tests.cpp +++ b/test/ncconvert/EnumExtensions_unit_tests.cpp @@ -10,6 +10,7 @@ TEST(EnumExtensionsTest, ToAssetType_fromString_succeeds) EXPECT_EQ(nc::convert::ToAssetType("cube-map"), nc::asset::AssetType::CubeMap); EXPECT_EQ(nc::convert::ToAssetType("hull-collider"), nc::asset::AssetType::HullCollider); EXPECT_EQ(nc::convert::ToAssetType("mesh"), nc::asset::AssetType::Mesh); + EXPECT_EQ(nc::convert::ToAssetType("skeletal-animation"), nc::asset::AssetType::SkeletalAnimation); EXPECT_EQ(nc::convert::ToAssetType("texture"), nc::asset::AssetType::Texture); } @@ -25,6 +26,7 @@ TEST(EnumExtensionsTest, ToString_fromAssetType_succeeds) EXPECT_EQ(nc::convert::ToString(nc::asset::AssetType::CubeMap), "cube-map"); EXPECT_EQ(nc::convert::ToString(nc::asset::AssetType::HullCollider), "hull-collider"); EXPECT_EQ(nc::convert::ToString(nc::asset::AssetType::Mesh), "mesh"); + EXPECT_EQ(nc::convert::ToString(nc::asset::AssetType::SkeletalAnimation), "skeletal-animation"); EXPECT_EQ(nc::convert::ToString(nc::asset::AssetType::Texture), "texture"); } diff --git a/test/ncconvert/GeometryConverter_unit_tests.cpp b/test/ncconvert/GeometryConverter_unit_tests.cpp index c7902ae..755fe20 100644 --- a/test/ncconvert/GeometryConverter_unit_tests.cpp +++ b/test/ncconvert/GeometryConverter_unit_tests.cpp @@ -86,7 +86,7 @@ TEST(GeometryConverterTest, ImportedMesh_multipleSubResources_specifiedMeshParse EXPECT_EQ(planeMesh.vertices.size(), 4); } -TEST(GeometryConverterTest, GetBoneWeights_SingleBone_1WeightAllVertices) +TEST(GeometryConverterTest, GetBoneWeights_singleBone_1WeightAllVertices) { namespace test_data = collateral::single_bone_four_vertex_fbx; auto uut = nc::convert::GeometryConverter{}; @@ -105,7 +105,7 @@ TEST(GeometryConverterTest, GetBoneWeights_SingleBone_1WeightAllVertices) } } -TEST(GeometryConverterTest, GetBoneWeights_FourBones_QuarterWeightAllVertices) +TEST(GeometryConverterTest, GetBoneWeights_fourBones_quarterWeightAllVertices) { namespace test_data = collateral::four_bone_four_vertex_fbx; auto uut = nc::convert::GeometryConverter{}; @@ -124,7 +124,7 @@ TEST(GeometryConverterTest, GetBoneWeights_FourBones_QuarterWeightAllVertices) } } -TEST(GeometryConverterTest, GetBoneWeights_FiveBonesPerVertex_ImportFails) +TEST(GeometryConverterTest, GetBoneWeights_fiveBonesPerVertex_importFails) { namespace test_data = collateral::five_bones_per_vertex_fbx; auto uut = nc::convert::GeometryConverter{}; @@ -142,7 +142,7 @@ TEST(GeometryConverterTest, GetBoneWeights_FiveBonesPerVertex_ImportFails) EXPECT_TRUE(threwNcError); } -TEST(GeometryConverterTest, GetBoneWeights_WeightsNotEqual100_ImportFails) +TEST(GeometryConverterTest, GetBoneWeights_weightsNotEqual100_importFails) { namespace test_data = collateral::four_bones_neq100_fbx; auto uut = nc::convert::GeometryConverter{}; @@ -160,7 +160,7 @@ TEST(GeometryConverterTest, GetBoneWeights_WeightsNotEqual100_ImportFails) EXPECT_TRUE(threwNcError); } -TEST(GeometryConverterTest, GetBonesData_RootBoneOffset_EqualsGlobalInverse) +TEST(GeometryConverterTest, GetBonesData_rootBoneOffset_equalsGlobalInverse) { namespace test_data = collateral::single_bone_four_vertex_fbx; auto uut = nc::convert::GeometryConverter{}; @@ -196,11 +196,11 @@ TEST(GeometryConverterTest, GetBonesData_RootBoneOffset_EqualsGlobalInverse) EXPECT_EQ(b1, 0); EXPECT_EQ(b2, 0); - EXPECT_EQ(b3, -1); + EXPECT_EQ(b3, 1); EXPECT_EQ(b4, 0); EXPECT_EQ(c1, 0); - EXPECT_EQ(c2, 1); + EXPECT_EQ(c2, -1); EXPECT_EQ(c3, 0); EXPECT_EQ(c4, 0); @@ -210,7 +210,7 @@ TEST(GeometryConverterTest, GetBonesData_RootBoneOffset_EqualsGlobalInverse) EXPECT_EQ(d4, 1); } -TEST(GeometryConverterTest, GetBonesData_MatrixVectorsPopulated) +TEST(GeometryConverterTest, GetBonesData_matrixVectorsPopulated) { namespace test_data = collateral::single_bone_four_vertex_fbx; auto uut = nc::convert::GeometryConverter{}; @@ -221,7 +221,7 @@ TEST(GeometryConverterTest, GetBonesData_MatrixVectorsPopulated) EXPECT_EQ(bonesData.vertexSpaceToBoneSpace[0].boneName, "Bone"); } -TEST(GeometryConverterTest, GetBonesData_GetBonesWeight_ElementsCorrespond) +TEST(GeometryConverterTest, GetBonesData_getBonesWeight_elementsCorrespond) { namespace test_data = collateral::four_bones_one_bone_70_percent_fbx; auto uut = nc::convert::GeometryConverter{}; @@ -248,7 +248,7 @@ TEST(GeometryConverterTest, GetBonesData_GetBonesWeight_ElementsCorrespond) EXPECT_EQ(bonesData.vertexSpaceToBoneSpace[3].boneName, "Bone3"); } -TEST(GeometryConverterTest, GetBonesData_ComplexMesh_ConvertedCorrectly) +TEST(GeometryConverterTest, GetBonesData_complexMesh_convertedCorrectly) { namespace test_data = collateral::real_world_model_fbx; auto uut = nc::convert::GeometryConverter{}; @@ -267,3 +267,22 @@ TEST(GeometryConverterTest, GetBonesData_ComplexMesh_ConvertedCorrectly) EXPECT_EQ(bonesData.vertexSpaceToBoneSpace[2].boneName, "DEF-spine.005"); EXPECT_EQ(bonesData.vertexSpaceToBoneSpace[3].boneName, "DEF-spine.006"); } + +TEST(GeometryConverterTest, ImportSkeletalAnimation_singleClip_convertedCorrectly) +{ + namespace test_data = collateral::simple_cube_animation_fbx; + auto uut = nc::convert::GeometryConverter{}; + const auto actual = uut.ImportSkeletalAnimation(test_data::filePath, std::string("Armature|Wiggle")); + + EXPECT_EQ(actual.name, std::string("Armature|Wiggle")); + EXPECT_EQ(actual.durationInTicks, 60); + EXPECT_EQ(actual.ticksPerSecond, 24); + EXPECT_EQ(actual.framesPerBone.size(), 4); +} + +TEST(GeometryConverterTest, ImportSkeletalAnimation_incorrectSubResourceName_throws) +{ + namespace test_data = collateral::simple_cube_animation_fbx; + auto uut = nc::convert::GeometryConverter{}; + EXPECT_THROW(uut.ImportSkeletalAnimation(test_data::filePath, std::string("Armature|Wigglde")), nc::NcError); +}