diff --git a/include/ncengine/graphics/Mesh.h b/include/ncengine/graphics/Mesh.h index 0f5a8d36f..e3e9a9b84 100644 --- a/include/ncengine/graphics/Mesh.h +++ b/include/ncengine/graphics/Mesh.h @@ -126,7 +126,8 @@ class SkinnedMesh : public MeshBase void SetMesh(const asset::MeshView& meshAsset); /** @name Animation Functions */ - auto GetAnimationController() -> SkeletalAnimationController& { return m_controller; } + auto GetAnimationController() const -> const SkeletalAnimationController& { return m_controller; } + auto GetAnimationController() -> SkeletalAnimationController& { return m_controller; } private: SkeletalAnimationController m_controller; diff --git a/include/ncengine/graphics/SkeletalAnimationController.h b/include/ncengine/graphics/SkeletalAnimationController.h index 41047818e..8e6ed1079 100644 --- a/include/ncengine/graphics/SkeletalAnimationController.h +++ b/include/ncengine/graphics/SkeletalAnimationController.h @@ -106,7 +106,8 @@ class SkeletalAnimationController auto AddState(LoopAnimation&& properties) -> AnimationStateId; auto AddState(PlayOnceAnimation&& properties) -> AnimationStateId; auto AddState(StopAnimation&& properties) -> AnimationStateId; - void SetAnimation(AnimationStateId stateId, uint64_t animationId); + auto GetAnimation(AnimationStateId stateId) const -> asset::AssetId; + void SetAnimation(AnimationStateId stateId, asset::AssetId animationId); auto GetDefaultTransitionDuration() const -> float { return m_defaultTransitionDuration; } void SetDefaultTransitionDuration(float dur) { m_defaultTransitionDuration = dur; } @@ -116,12 +117,12 @@ class SkeletalAnimationController * Immediate transitions interrupt the state machine and move to the specified state. Control is returned to * the state machine upon the exit condition being met, or the immediate animation finishing, for PlayOnce. */ - void LoopImmediate(uint64_t animId, + void LoopImmediate(asset::AssetId animId, TransitionCondition&& exitWhen, AnimationStateId exitTo = RootAnimationState, float transitionDuration = UseDefaultTransitionDuration); - void PlayOnceImmediate(uint64_t animId, + void PlayOnceImmediate(asset::AssetId animId, AnimationStateId exitTo = RootAnimationState, float transitionDuration = UseDefaultTransitionDuration); diff --git a/source/ncengine/engine/registration/GraphicsTypes.cpp b/source/ncengine/engine/registration/GraphicsTypes.cpp index fa8ae7b2e..df93a1078 100644 --- a/source/ncengine/engine/registration/GraphicsTypes.cpp +++ b/source/ncengine/engine/registration/GraphicsTypes.cpp @@ -9,15 +9,15 @@ namespace nc { void RegisterGraphicsTypes(ecs::ComponentRegistry& registry, size_t maxEntities) { - Register( + Register( registry, maxEntities, StaticMeshId, "StaticMesh", ui::editor::StaticMeshUIWidget, CreateStaticMesh, - nullptr, - nullptr + SerializeStaticMesh, + DeserializeStaticMesh ); Register( @@ -27,8 +27,8 @@ void RegisterGraphicsTypes(ecs::ComponentRegistry& registry, size_t maxEntities) "SkinnedMesh", ui::editor::SkinnedMeshUIWidget, CreateSkinnedMesh, - nullptr, - nullptr + SerializeSkinnedMesh, + DeserializeSkinnedMesh ); Register( diff --git a/source/ncengine/graphics2/SkeletalAnimationController.cpp b/source/ncengine/graphics2/SkeletalAnimationController.cpp index ff1587ca4..032c8bcf5 100644 --- a/source/ncengine/graphics2/SkeletalAnimationController.cpp +++ b/source/ncengine/graphics2/SkeletalAnimationController.cpp @@ -6,7 +6,7 @@ namespace nc { -SkeletalAnimationController::SkeletalAnimationController(uint64_t animationId, +SkeletalAnimationController::SkeletalAnimationController(asset::AssetId animationId, float defaultTransitionDuration) : m_states{4ull, MaxAnimationStates}, m_defaultTransitionDuration{defaultTransitionDuration} @@ -20,7 +20,7 @@ SkeletalAnimationController::SkeletalAnimationController(uint64_t animationId, ); } -auto SkeletalAnimationController::GetCurrentAnimationId() const -> uint64_t +auto SkeletalAnimationController::GetCurrentAnimationId() const -> asset::AssetId { // Id is cached here if queued transition overwrote the current id (e.g. immediate -> immediate) if (m_prevAnimId != asset::NullAssetId) @@ -74,7 +74,12 @@ auto SkeletalAnimationController::AddState(StopAnimation&& properties) -> Animat }); } -void SkeletalAnimationController::SetAnimation(AnimationStateId stateId, uint64_t animationId) +auto SkeletalAnimationController::GetAnimation(AnimationStateId stateId) const -> asset::AssetId +{ + return m_states.at(stateId).animId; +} + +void SkeletalAnimationController::SetAnimation(AnimationStateId stateId, asset::AssetId animationId) { const auto oldId = std::exchange(m_states.at(stateId).animId, animationId); if (m_activeState == stateId) @@ -84,7 +89,7 @@ void SkeletalAnimationController::SetAnimation(AnimationStateId stateId, uint64_ } } -void SkeletalAnimationController::LoopImmediate(uint64_t animId, +void SkeletalAnimationController::LoopImmediate(asset::AssetId animId, TransitionCondition&& exitWhen, AnimationStateId exitTo, float transitionDuration) @@ -99,7 +104,7 @@ void SkeletalAnimationController::LoopImmediate(uint64_t animId, }); } -void SkeletalAnimationController::PlayOnceImmediate(uint64_t animId, +void SkeletalAnimationController::PlayOnceImmediate(asset::AssetId animId, AnimationStateId exitTo, float transitionDuration) { diff --git a/source/ncengine/serialize/ComponentSerialization.cpp b/source/ncengine/serialize/ComponentSerialization.cpp index bed21a7e6..c73103e53 100644 --- a/source/ncengine/serialize/ComponentSerialization.cpp +++ b/source/ncengine/serialize/ComponentSerialization.cpp @@ -4,6 +4,7 @@ #include "ncengine/audio/AudioSource.h" #include "ncengine/graphics/Light.h" #include "ncengine/graphics/ParticleEmitter.h" +#include "ncengine/graphics/Mesh.h" #include "ncengine/physics/Constraints.h" #include "ncengine/physics/RigidBody.h" #include "ncengine/serialize/SceneSerialization.h" @@ -28,6 +29,33 @@ void Deserialize(std::istream& stream, TextureView& out) } } // namespace asset +void SerializeMaterialDesc(std::ostream& stream, const MaterialInstance& out) +{ + const auto& properties = out.GetProperties(); + serialize::Serialize(stream, std::string{out.GetName()}); // don't want to serialize as string_view! + serialize::Serialize(stream, out.GetPasses()); + serialize::Serialize(stream, properties.diffuseTexture); // serialize properties individually so we hit the special handling for textures + serialize::Serialize(stream, properties.normalTexture); + serialize::Serialize(stream, properties.gradientStart); + serialize::Serialize(stream, properties.gradientEnd); + serialize::Serialize(stream, properties.outlineColor); + serialize::Serialize(stream, properties.outlineWidth); +} + +auto DeserializeMaterialDesc(std::istream& stream) -> MaterialDesc +{ + auto out = MaterialDesc{}; + serialize::Deserialize(stream, out.name); + serialize::Deserialize(stream, out.passes); + serialize::Deserialize(stream, out.properties.diffuseTexture); + serialize::Deserialize(stream, out.properties.normalTexture); + serialize::Deserialize(stream, out.properties.gradientStart); + serialize::Deserialize(stream, out.properties.gradientEnd); + serialize::Deserialize(stream, out.properties.outlineColor); + serialize::Deserialize(stream, out.properties.outlineWidth); + return out; +} + void SerializeAudioSource(std::ostream& stream, const audio::AudioSource& out, const SerializationContext& ctx, const std::any&) { serialize::Serialize(stream, ctx.entityMap.at(out.ParentEntity())); @@ -58,6 +86,54 @@ auto DeserializeDirectionalLight(std::istream& stream, const DeserializationCont return out; } +void SerializeSkinnedMesh(std::ostream& stream, const SkinnedMesh& out, const SerializationContext& ctx, const std::any&) +{ + serialize::Serialize(stream, ctx.entityMap.at(out.GetEntity())); + serialize::Serialize(stream, out.GetMeshId()); + SerializeMaterialDesc(stream, out.GetMaterial()); + serialize::Serialize(stream, out.GetAnimationController().GetAnimation(RootAnimationState)); +} + +auto DeserializeSkinnedMesh(std::istream& stream, const DeserializationContext& ctx, const std::any&) -> SkinnedMesh +{ + auto entityId = uint32_t{}; + auto meshId = asset::AssetId{}; + auto animId = asset::AssetId{}; + serialize::Deserialize(stream, entityId); + serialize::Deserialize(stream, meshId); + auto materialDesc = DeserializeMaterialDesc(stream); + serialize::Deserialize(stream, animId); + + return SkinnedMesh{ + ctx.entityMap.at(entityId), + asset::AcquireMeshAsset(meshId), + materialDesc, + animId + }; +} + +void SerializeStaticMesh(std::ostream& stream, const StaticMesh& out, const SerializationContext& ctx, const std::any&) +{ + serialize::Serialize(stream, ctx.entityMap.at(out.GetEntity())); + serialize::Serialize(stream, out.GetMeshId()); + SerializeMaterialDesc(stream, out.GetMaterial()); +} + +auto DeserializeStaticMesh(std::istream& stream, const DeserializationContext& ctx, const std::any&) -> StaticMesh +{ + auto entityId = uint32_t{}; + auto meshId = asset::AssetId{}; + serialize::Deserialize(stream, entityId); + serialize::Deserialize(stream, meshId); + auto materialDesc = DeserializeMaterialDesc(stream); + + return StaticMesh{ + ctx.entityMap.at(entityId), + asset::AcquireMeshAsset(meshId), + materialDesc + }; +} + void SerializeParticleEmitter(std::ostream& stream, const ParticleEmitter& out, const SerializationContext& ctx, const std::any&) { serialize::Serialize(stream, ctx.entityMap.at(out.GetEntity())); diff --git a/source/ncengine/serialize/ComponentSerialization.h b/source/ncengine/serialize/ComponentSerialization.h index bebbdf815..884e1fd61 100644 --- a/source/ncengine/serialize/ComponentSerialization.h +++ b/source/ncengine/serialize/ComponentSerialization.h @@ -18,6 +18,10 @@ void SerializeParticleEmitter(std::ostream& stream, const ParticleEmitter& out, auto DeserializeParticleEmitter(std::istream& stream, const DeserializationContext& ctx, const std::any&) -> ParticleEmitter; void SerializePointLight(std::ostream& stream, const PointLight& out, const SerializationContext& ctx, const std::any&); auto DeserializePointLight(std::istream& stream, const DeserializationContext& ctx, const std::any&) -> PointLight; +void SerializeSkinnedMesh(std::ostream& stream, const SkinnedMesh& out, const SerializationContext& ctx, const std::any&); +auto DeserializeSkinnedMesh(std::istream& stream, const DeserializationContext& ctx, const std::any&) -> SkinnedMesh; +void SerializeStaticMesh(std::ostream& stream, const StaticMesh& out, const SerializationContext& ctx, const std::any&); +auto DeserializeStaticMesh(std::istream& stream, const DeserializationContext& ctx, const std::any&) -> StaticMesh; void SerializeSpotLight(std::ostream& stream, const SpotLight& out, const SerializationContext& ctx, const std::any&); auto DeserializeSpotLight(std::istream& stream, const DeserializationContext& ctx, const std::any&) -> SpotLight; void SerializeRigidBody(std::ostream& stream, const RigidBody& out, const SerializationContext&, const std::any&); diff --git a/test/ncengine/serialize/CMakeLists.txt b/test/ncengine/serialize/CMakeLists.txt index 84acc352f..f7327f7e9 100644 --- a/test/ncengine/serialize/CMakeLists.txt +++ b/test/ncengine/serialize/CMakeLists.txt @@ -4,7 +4,14 @@ add_executable(ComponentSerialization_integration_tests ${NC_SOURCE_DIR}/serialize/ComponentSerialization.cpp ${NC_SOURCE_DIR}/audio/AudioSource.cpp ${NC_SOURCE_DIR}/ecs/Transform.cpp + ${NC_SOURCE_DIR}/graphics2/GraphicsUtility.cpp + ${NC_SOURCE_DIR}/graphics2/Material.cpp + ${NC_SOURCE_DIR}/graphics2/Mesh.cpp ${NC_SOURCE_DIR}/graphics2/ParticleEmitter.cpp + ${NC_SOURCE_DIR}/graphics2/SkeletalAnimationController.cpp + ${NC_SOURCE_DIR}/graphics2/frontend/subsystem/MaterialRegistry.cpp + ${NC_SOURCE_DIR}/graphics2/frontend/subsystem/MeshSubsystem.cpp + ${NC_SOURCE_DIR}/graphics2/frontend/subsystem/TransformCache.cpp ) target_include_directories(ComponentSerialization_integration_tests diff --git a/test/ncengine/serialize/ComponentSerialization_integration_tests.cpp b/test/ncengine/serialize/ComponentSerialization_integration_tests.cpp index 6e65c52f3..90cf82cfe 100644 --- a/test/ncengine/serialize/ComponentSerialization_integration_tests.cpp +++ b/test/ncengine/serialize/ComponentSerialization_integration_tests.cpp @@ -2,12 +2,17 @@ #include "serialize/ComponentSerialization.h" #include "../AssetServiceStub.h" +#include "ncengine/Events.h" #include "ncengine/audio/AudioSource.h" +#include "ncengine/graphics/Mesh.h" #include "ncengine/graphics/Light.h" #include "ncengine/graphics/ParticleEmitter.h" #include "ncengine/physics/Constraints.h" #include "ncengine/physics/RigidBody.h" #include "ncengine/serialize/SceneSerialization.h" +#include "graphics2/frontend/subsystem/MaterialRegistry.h" +#include "graphics2/frontend/subsystem/animation/SkeletalAnimationSubsystem.h" +#include "graphics2/frontend/subsystem/MeshSubsystem.h" #include "graphics2/frontend/subsystem/particle/ParticleSubsystem.h" #include "physics/DeferredPhysicsCreateState.h" @@ -21,25 +26,23 @@ DEFINE_ASSET_SERVICE_STUB(hullColliderAssetManager, nc::asset::AssetType::HullCo DEFINE_ASSET_SERVICE_STUB(meshAssetManager, nc::asset::AssetType::Mesh, nc::asset::MeshView, std::string); DEFINE_ASSET_SERVICE_STUB(textureAssetManager, nc::asset::AssetType::Texture, nc::asset::TextureView, std::string); +constexpr auto g_maxEntities = 10u; + namespace nc { +auto g_systemEvents = std::make_unique(); + namespace asset { -auto AcquireAudioClipAsset(const std::string&) -> AudioClipView -{ - static auto view = AudioClipView{}; - return view; -} - +auto g_mockAudioClipView = AudioClipView{}; +auto g_mockMeshView = MeshView{.id = 1}; auto g_mockTextureView = TextureView{.id = 1, .index = 1}; -auto AcquireTextureAsset(AssetId) -> TextureView -{ - return g_mockTextureView; -} +auto AcquireAudioClipAsset(const std::string&) -> AudioClipView { return g_mockAudioClipView; } +auto AcquireMeshAsset(AssetId) -> MeshView { return g_mockMeshView; } +auto AcquireTextureAsset(AssetId) -> TextureView { return g_mockTextureView; } } // namespace asset - namespace graphics { void ParticleSubsystem::AddEmitter(ParticleEmitter&) {} @@ -47,8 +50,22 @@ void ParticleSubsystem::RemoveEmitter(Entity) {} void ParticleSubsystem::UpdateEmitterInfo(Entity, const ParticleInfo&) {} void ParticleSubsystem::UpdateEmitterTexture(Entity, uint32_t) {} void ParticleSubsystem::Emit(Entity, size_t) {} -} // namespace graphics +auto ISkeletalAnimationSubsystem::AllocateBones(asset::AssetId) -> BoneCacheHandle { return 0; } +void ISkeletalAnimationSubsystem::NotifyRemove(Entity, BoneCacheHandle) {} + +struct MockAnimationSubsystem : public ISkeletalAnimationSubsystem +{ + MockAnimationSubsystem(uint32_t maxBones) + : ISkeletalAnimationSubsystem{maxBones} + { + } +}; + +auto g_mockSkeletalAnimationSubsystem = MockAnimationSubsystem{10}; +auto g_mockMeshSubsystem = MeshSubsystem{g_mockSkeletalAnimationSubsystem, *g_systemEvents, g_maxEntities, g_maxEntities, 1}; +auto g_mockMaterialRegistry = MaterialRegistry{g_maxEntities}; +} // namespace graphics auto g_mockConstraints = std::unordered_map>{}; @@ -121,22 +138,110 @@ TEST(ComponentSerializationTests, RoundTrip_audioSource_preservesValues) EXPECT_EQ(expectedFlags, actualProperties.flags); } +TEST(ComponentSerializationTests, RoundTrip_staticMesh_preservesValues) +{ + // Reset mocks + nc::asset::g_mockMeshView = {.id = 1, .firstIndex = 0}; + nc::asset::g_mockTextureView = {.id = 1, .index = 0}; + + const auto expectedMesh = nc::asset::g_mockMeshView; + const auto expectedMaterialDesc = nc::MaterialDesc{ + .name = "mock", + .properties = nc::MaterialProperties{ + .diffuseTexture = nc::asset::g_mockTextureView, + .normalTexture = nc::asset::g_mockTextureView, + .gradientStart = nc::Vector3::Up(), + .gradientEnd = nc::Vector3::Right(), + .outlineWidth = 3.5f + } + }; + + const auto expected = nc::StaticMesh{g_entity, expectedMesh, expectedMaterialDesc}; + auto stream = std::stringstream{}; + nc::SerializeStaticMesh(stream, expected, g_serializationContext, nullptr); + + // Simulate a different load order prior to deserializing - want to verify assets are (de)serialized purely based on id. + nc::asset::g_mockMeshView.firstIndex = 100; + nc::asset::g_mockTextureView.index = 200; + + const auto actual = nc::DeserializeStaticMesh(stream, g_deserializationContext, nullptr); + EXPECT_EQ(expected.GetEntity(), actual.GetEntity()); + EXPECT_EQ(expectedMesh.id, actual.GetMeshId()); + EXPECT_EQ(expectedMaterialDesc.name, actual.GetMaterial().GetName()); + EXPECT_EQ(expectedMaterialDesc.passes, actual.GetMaterial().GetPasses()); + const auto& actualMaterialProperties = actual.GetMaterial().GetProperties(); + EXPECT_EQ(expectedMaterialDesc.properties.diffuseTexture.id, actualMaterialProperties.diffuseTexture.id); + EXPECT_EQ(expectedMaterialDesc.properties.normalTexture.id, actualMaterialProperties.normalTexture.id); + EXPECT_EQ(200, actualMaterialProperties.diffuseTexture.index); + EXPECT_EQ(200, actualMaterialProperties.normalTexture.index); + EXPECT_EQ(expectedMaterialDesc.properties.gradientStart, actualMaterialProperties.gradientStart); + EXPECT_EQ(expectedMaterialDesc.properties.gradientEnd, actualMaterialProperties.gradientEnd); + EXPECT_EQ(expectedMaterialDesc.properties.outlineColor, actualMaterialProperties.outlineColor); + EXPECT_EQ(expectedMaterialDesc.properties.outlineWidth, actualMaterialProperties.outlineWidth); +} + +TEST(ComponentSerializationTests, RoundTrip_skinnedMesh_preservesValues) +{ + // Reset mocks + nc::asset::g_mockMeshView = {.id = 1, .firstIndex = 0}; + nc::asset::g_mockTextureView = {.id = 1, .index = 0}; + + const auto expectedMesh = nc::asset::g_mockMeshView; + const auto expectedAnimId = nc::asset::AssetId{42}; + const auto expectedMaterialDesc = nc::MaterialDesc{ + .name = "mock", + .properties = nc::MaterialProperties{ + .diffuseTexture = nc::asset::g_mockTextureView, + .normalTexture = nc::asset::g_mockTextureView, + .gradientStart = nc::Vector3::Up(), + .gradientEnd = nc::Vector3::Right(), + .outlineWidth = 3.5f + } + }; + + const auto expected = nc::SkinnedMesh{g_entity, expectedMesh, expectedMaterialDesc, expectedAnimId}; + auto stream = std::stringstream{}; + nc::SerializeSkinnedMesh(stream, expected, g_serializationContext, nullptr); + + // Simulate a different load order prior to deserializing - want to verify assets are (de)serialized purely based on id. + nc::asset::g_mockMeshView.firstIndex = 100; + nc::asset::g_mockTextureView.index = 200; + + const auto actual = nc::DeserializeSkinnedMesh(stream, g_deserializationContext, nullptr); + EXPECT_EQ(expected.GetEntity(), actual.GetEntity()); + EXPECT_EQ(expectedMesh.id, actual.GetMeshId()); + EXPECT_EQ(expectedAnimId, actual.GetAnimationController().GetAnimation(nc::RootAnimationState)); + EXPECT_EQ(expectedMaterialDesc.name, actual.GetMaterial().GetName()); + EXPECT_EQ(expectedMaterialDesc.passes, actual.GetMaterial().GetPasses()); + const auto& actualMaterialProperties = actual.GetMaterial().GetProperties(); + EXPECT_EQ(expectedMaterialDesc.properties.diffuseTexture.id, actualMaterialProperties.diffuseTexture.id); + EXPECT_EQ(expectedMaterialDesc.properties.normalTexture.id, actualMaterialProperties.normalTexture.id); + EXPECT_EQ(200, actualMaterialProperties.diffuseTexture.index); + EXPECT_EQ(200, actualMaterialProperties.normalTexture.index); + EXPECT_EQ(expectedMaterialDesc.properties.gradientStart, actualMaterialProperties.gradientStart); + EXPECT_EQ(expectedMaterialDesc.properties.gradientEnd, actualMaterialProperties.gradientEnd); + EXPECT_EQ(expectedMaterialDesc.properties.outlineColor, actualMaterialProperties.outlineColor); + EXPECT_EQ(expectedMaterialDesc.properties.outlineWidth, actualMaterialProperties.outlineWidth); +} + TEST(ComponentSerializationTests, RoundTrip_particleEmitter_preservesValues) { + // Reset mocks + nc::asset::g_mockTextureView = {.id = 1, .index = 0}; + auto stream = std::stringstream{}; - const auto expectedTexture = nc::asset::AcquireTextureAsset(0); + const auto expectedTexture = nc::asset::g_mockTextureView; const auto expectedInfo = nc::ParticleInfo{}; const auto expected = nc::ParticleEmitter{g_staticEntity, expectedTexture, expectedInfo}; nc::SerializeParticleEmitter(stream, expected, g_serializationContext, nullptr); - // Mock a different texture load order prior to deserializing - want to verify textures are (de)serialized purely based on id. - const auto expectedTextureIndex = expectedTexture.index + 10; - nc::asset::g_mockTextureView.index = expectedTextureIndex; + // Simulate a different load order prior to deserializing - want to verify assets are (de)serialized purely based on id. + nc::asset::g_mockTextureView.index = 100; const auto actual = nc::DeserializeParticleEmitter(stream, g_deserializationContext, nullptr); const auto& actualTexture = actual.GetTexture(); EXPECT_EQ(expectedTexture.id, actualTexture.id); - EXPECT_EQ(expectedTextureIndex, actualTexture.index); + EXPECT_EQ(100, actualTexture.index); const auto& actualInfo = actual.GetInfo(); EXPECT_EQ(expectedInfo.emission.maxParticleCount, actualInfo.emission.maxParticleCount);