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 0000000..e97892a Binary files /dev/null and b/docs/image.png differ diff --git a/include/ncasset/AssetType.h b/include/ncasset/AssetType.h index 0642ac5..ab006e5 100644 --- a/include/ncasset/AssetType.h +++ b/include/ncasset/AssetType.h @@ -10,6 +10,7 @@ enum class AssetType HullCollider, Mesh, Shader, + SkeletalAnimation, Texture }; } // namespace nc::asset diff --git a/include/ncasset/Assets.h b/include/ncasset/Assets.h index 45f95cf..e1b3b0c 100644 --- a/include/ncasset/Assets.h +++ b/include/ncasset/Assets.h @@ -6,6 +6,7 @@ #include #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 0000000..8bf7825 Binary files /dev/null and b/test/collateral/simple_cube_animation.fbx differ 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); +}