diff --git a/src/engine/animation/animation-context.hpp b/src/engine/animation/animation-context.hpp new file mode 100644 index 00000000..580059c4 --- /dev/null +++ b/src/engine/animation/animation-context.hpp @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2023 C. J. Howard +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef ANTKEEPER_ANIMATION_ANIMATION_CONTEXT_HPP +#define ANTKEEPER_ANIMATION_ANIMATION_CONTEXT_HPP + +#include + +/** + * Context for animation track output functions. + */ +struct animation_context +{ + /** Handle to the entity being animated. */ + entt::handle handle; +}; + +#endif // ANTKEEPER_ANIMATION_ANIMATION_CONTEXT_HPP diff --git a/src/engine/animation/animation-curve.cpp b/src/engine/animation/animation-curve.cpp index 13f59cd2..5988945d 100644 --- a/src/engine/animation/animation-curve.cpp +++ b/src/engine/animation/animation-curve.cpp @@ -2,6 +2,8 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include +#include +#include float animation_curve::evaluate(float time) const { @@ -28,3 +30,12 @@ float animation_curve::evaluate(float time) const return m_interpolator(*previous, *next, time); } +float animation_curve::duration() const +{ + if (m_keyframes.empty()) + { + return 0.0f; + } + + return std::max(0.0f, m_keyframes.rbegin()->time); +} diff --git a/src/engine/animation/animation-curve.hpp b/src/engine/animation/animation-curve.hpp index 6b198a32..1334514f 100644 --- a/src/engine/animation/animation-curve.hpp +++ b/src/engine/animation/animation-curve.hpp @@ -81,6 +81,9 @@ class animation_curve return m_extrapolator; } + /** Returns the non-negative duration of the curve, in seconds. */ + [[nodiscard]] float duration() const; + private: keyframe_container m_keyframes; keyframe_interpolator_type m_interpolator{interpolate_keyframes_linear}; diff --git a/src/engine/animation/animation-player-state.hpp b/src/engine/animation/animation-player-state.hpp new file mode 100644 index 00000000..a0d21cbc --- /dev/null +++ b/src/engine/animation/animation-player-state.hpp @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2023 C. J. Howard +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef ANTKEEPER_ANIMATION_ANIMATION_PLAYBACK_STATE_HPP +#define ANTKEEPER_ANIMATION_ANIMATION_PLAYBACK_STATE_HPP + +/** Animation player states. */ +enum class animation_player_state +{ + /** Animation player is stopped. */ + stopped, + + /** Animation player is playing. */ + playing, + + /** Animation player is paused. */ + paused +}; + +#endif // ANTKEEPER_ANIMATION_ANIMATION_PLAYBACK_STATE_HPP diff --git a/src/engine/animation/animation-player.cpp b/src/engine/animation/animation-player.cpp new file mode 100644 index 00000000..203951ae --- /dev/null +++ b/src/engine/animation/animation-player.cpp @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: 2023 C. J. Howard +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +void animation_player::advance(float seconds) +{ + if (!m_sequence) + { + // No active animation sequence, advance position and return + m_position += seconds; + return; + } + + // Remember previous playback position + const auto previous_position = m_position; + + // Advance playback position + m_position += seconds; + + // Loop + std::size_t loop_count = 0; + if (m_looping && m_position >= m_sequence_duration) + { + if (m_sequence_duration > 0.0f) + { + // Calculate looped position + const auto looped_position = std::fmod(m_position, m_sequence_duration); + + // Calculate number of times looped + loop_count = static_cast(m_position / m_sequence_duration); + + // Set current position to looped position + m_position = looped_position; + } + else + { + // Zero-duration looping sequence + m_position = 0.0f; + } + } + + for (const auto& [path, track]: m_sequence->tracks()) + { + if (!track.output()) + { + // Ignore tracks with no output functions + continue; + } + + if (m_sample_buffer.size() < track.channels().size()) + { + // Grow sample buffer to accommodate track channels + m_sample_buffer.resize(track.channels().size()); + } + + // Sample track + track.sample(m_position, m_sample_buffer); + + // Pass sample buffer and animation context to track output function + track.output()(m_sample_buffer, m_context); + } + + if (loop_count) + { + // Trigger cues on [previous position, m_sequence_duration) + m_sequence->trigger_cues(previous_position, m_sequence_duration, m_context); + + // For each additional loop, trigger cues on [0, m_sequence_duration) + for (std::size_t i = 1; i < loop_count; ++i) + { + m_sequence->trigger_cues(0.0f, m_sequence_duration, m_context); + } + + // Trigger cues on [0, m_position) + m_sequence->trigger_cues(0.0f, m_position, m_context); + } + else + { + // Trigger cues on [previous_position, m_position) + m_sequence->trigger_cues(previous_position, m_position, m_context); + } +} + +void animation_player::play(std::shared_ptr sequence) +{ + m_sequence = std::move(sequence); + + // Determine duration of sequence + if (m_sequence) + { + m_sequence_duration = m_sequence->duration(); + } + else + { + m_sequence_duration = 0.0f; + } + + m_state = animation_player_state::playing; +} + +void animation_player::play() +{ + m_state = animation_player_state::playing; +} + +void animation_player::stop() +{ + m_state = animation_player_state::stopped; + m_position = 0.0f; +} + +void animation_player::rewind() +{ + m_position = 0.0f; +} + +void animation_player::pause() +{ + m_state = animation_player_state::paused; +} + +void animation_player::seek(float seconds) +{ + m_position = seconds; +} + +void animation_player::loop(bool enabled) +{ + m_looping = enabled; +} diff --git a/src/engine/animation/animation-player.hpp b/src/engine/animation/animation-player.hpp new file mode 100644 index 00000000..42633fa9 --- /dev/null +++ b/src/engine/animation/animation-player.hpp @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: 2023 C. J. Howard +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef ANTKEEPER_ANIMATION_ANIMATION_PLAYER_HPP +#define ANTKEEPER_ANIMATION_ANIMATION_PLAYER_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/** + * Plays animation sequences. + */ +class animation_player +{ +public: + /** + * Advances the animation sequence by a given timestep. + * + * @param seconds Timestep, in seconds. + */ + void advance(float seconds); + + /** + * Starts playing an animation sequence. + * + * @param sequence Animation sequence to play. + */ + void play(std::shared_ptr sequence); + + /** Starts playing the current animation sequence. */ + void play(); + + /** Stops playing the current animation sequence. */ + void stop(); + + /** Rewinds the current animation sequence. */ + void rewind(); + + /** Pauses the current animation sequence. */ + void pause(); + + /** + * Sets the playback position of the animation player. + * + * @param seconds Offset from the start of an animation sequence, in seconds. + */ + void seek(float seconds); + + /** + * Enables or disables looping of the animation sequence. + * + * @param enabled `true` to enable looping, `false` to disable looping. + */ + void loop(bool enabled); + + /** Returns the state of the animation player. */ + [[nodiscard]] inline constexpr const auto& state() const noexcept + { + return m_state; + } + + /** Returns `true` if the animation player is stopped, `false` otherwise. */ + [[nodiscard]] inline constexpr auto is_stopped() const noexcept + { + return m_state == animation_player_state::stopped; + } + + /** Returns `true` if the animation player is playing, `false` otherwise. */ + [[nodiscard]] inline constexpr auto is_playing() const noexcept + { + return m_state == animation_player_state::playing; + } + + /** Returns `true` if the animation player is paused, `false` otherwise. */ + [[nodiscard]] inline constexpr auto is_paused() const noexcept + { + return m_state == animation_player_state::paused; + } + + /** Returns the playback position of the animation player. */ + [[nodiscard]] inline constexpr auto position() const noexcept + { + return m_position; + } + + /** Returns `true` if sequence looping is enabled, `false` otherwise. */ + [[nodiscard]] inline constexpr auto is_looping() const noexcept + { + return m_looping; + } + + /** Returns a reference to the animation context of the player. */ + [[nodiscard]] inline constexpr auto& context() noexcept + { + return m_context; + } + + /** @copydoc context() */ + [[nodiscard]] inline constexpr const auto& context() const noexcept + { + return m_context; + } + +private: + std::shared_ptr m_sequence; + float m_sequence_duration{}; + animation_player_state m_state{animation_player_state::stopped}; + float m_position{}; + bool m_looping{}; + std::vector m_sample_buffer; + animation_context m_context{}; +}; + +#endif // ANTKEEPER_ANIMATION_PLAYER_HPP diff --git a/src/engine/animation/animation-sequence.cpp b/src/engine/animation/animation-sequence.cpp index da70058a..a5414472 100644 --- a/src/engine/animation/animation-sequence.cpp +++ b/src/engine/animation/animation-sequence.cpp @@ -6,18 +6,11 @@ #include #include #include +#include #include #include -void animation_sequence::sample_tracks(void* context, float time) const -{ - for (const auto& [key, track]: m_tracks) - { - track.sample(context, time); - } -} - -void animation_sequence::trigger_cues(void* context, float start_time, float end_time) const +void animation_sequence::trigger_cues(float start_time, float end_time, animation_context& context) const { const auto start_it = m_cues.lower_bound(start_time); const auto end_it = m_cues.upper_bound(end_time); @@ -34,6 +27,17 @@ void animation_sequence::trigger_cues(void* context, float start_time, float end } } +float animation_sequence::duration() const +{ + float max_duration = 0.0f; + for (const auto& [path, track]: m_tracks) + { + max_duration = std::max(max_duration, track.duration()); + } + + return max_duration; +} + /** * Deserializes an animation sequence. * diff --git a/src/engine/animation/animation-sequence.hpp b/src/engine/animation/animation-sequence.hpp index 6b22a171..06d4bba1 100644 --- a/src/engine/animation/animation-sequence.hpp +++ b/src/engine/animation/animation-sequence.hpp @@ -5,9 +5,11 @@ #define ANTKEEPER_ANIMATION_ANIMATION_SEQUENCE_HPP #include +#include #include #include #include +#include /** * Set of related animation tracks. @@ -15,22 +17,14 @@ class animation_sequence { public: - /** - * Samples all tracks in the animation sequence at a given time. - * - * @param context User-defined animation context. - * @param time Time at which to sample the sequence. - */ - void sample_tracks(void* context, float time) const; - /** * Triggers all cues on the half-open interval [@p start_time, @p end_time). * - * @param context User-defined animation context. * @param start_time Start of the interval (inclusive). * @param end_time End of the interval (exclusive). + * @param context Animation context. */ - void trigger_cues(void* context, float start_time, float end_time) const; + void trigger_cues(float start_time, float end_time, animation_context& context) const; /** Returns a reference to the name of the sequence. */ [[nodiscard]] inline constexpr auto& name() noexcept @@ -72,10 +66,13 @@ class animation_sequence return m_cues; } + /** Returns the non-negative duration of the sequence. */ + [[nodiscard]] float duration() const; + private: std::string m_name; std::map m_tracks; - std::multimap> m_cues; + std::multimap> m_cues; }; #endif // ANTKEEPER_ANIMATION_SEQUENCE_HPP diff --git a/src/engine/animation/animation-track.cpp b/src/engine/animation/animation-track.cpp index 0e38ef2c..16772c81 100644 --- a/src/engine/animation/animation-track.cpp +++ b/src/engine/animation/animation-track.cpp @@ -2,28 +2,40 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include -#include +#include -void animation_track::sample(void* context, float time) const +void animation_track::sample(float time, std::span samples) const { - if (!m_sampler) + const auto min_size = std::min(m_channels.size(), samples.size()); + + for (std::size_t i = 0; i < min_size; ++i) + { + samples[i] = m_channels[i].evaluate(time); + } +} + +void animation_track::sample(float time, std::size_t first_channel, std::span samples) const +{ + if (first_channel >= m_channels.size()) { - // throw std::runtime_error("Animation track sample failed: no sampler."); return; } - if (m_sample_buffer.size() != m_channels.size()) + const auto min_size = std::min(m_channels.size() - first_channel, samples.size()); + + for (std::size_t i = 0; i < min_size; ++i) { - // Resize sample buffer to accomodate number of channels - m_sample_buffer.resize(m_channels.size()); + samples[i] = m_channels[i + first_channel].evaluate(time); } +} - // Sample channels at given time - for (std::size_t i = 0; i < m_channels.size(); ++i) +float animation_track::duration() const +{ + float max_duration = 0.0f; + for (const auto& channel: m_channels) { - m_sample_buffer[i] = m_channels[i].evaluate(time); + max_duration = std::max(max_duration, channel.duration()); } - // Pass sample buffer to sampler function - m_sampler(context, m_sample_buffer); + return max_duration; } diff --git a/src/engine/animation/animation-track.hpp b/src/engine/animation/animation-track.hpp index 64f25637..665fada9 100644 --- a/src/engine/animation/animation-track.hpp +++ b/src/engine/animation/animation-track.hpp @@ -5,6 +5,7 @@ #define ANTKEEPER_ANIMATION_ANIMATION_TRACK_HPP #include +#include #include #include #include @@ -16,12 +17,28 @@ class animation_track { public: /** - * Samples each channel in the track at a given time, passing the evaluated sample data to the sampler function object. + * Output function type. * - * @param context User-defined animation context. - * @param time Time at which to sample the track. + * Output functions take two parameters: the track samples, and a reference to an animation context. */ - void sample(void* context, float time) const; + using output_function_type = std::function, animation_context&)>; + + /** + * Evaluates the channels of the track at a given time, storing the resulting values in a buffer. + * + * @param[in] time Time at which to sample the track. + * @param[in] first_channel Index of the first channel to sample. + * @param[out] samples Buffer to store the evaluated values of the channels. The number of channels sampled is limited by the size of the buffer. + */ + void sample(float time, std::size_t first_channel, std::span samples) const; + + /** + * Evaluates the channels of the track at a given time, storing the resulting values in a buffer. + * + * @param[in] time Time at which to sample the track. + * @param[out] samples Buffer to store the evaluated values of the channels. The number of channels sampled is limited by the size of the buffer. + */ + void sample(float time, std::span samples) const; /** Returns a reference to the channels of the track. */ [[nodiscard]] inline constexpr auto& channels() noexcept @@ -35,26 +52,24 @@ class animation_track return m_channels; } - /** - * Returns a reference to the track sampler function object. - * - * The sampler function object takes two parameters: a void pointer to a user-defined animation context, and a span containing floating-point sample data. - */ - [[nodiscard]] inline constexpr auto& sampler() noexcept + /** Returns a reference to the output function of the track. */ + [[nodiscard]] inline constexpr auto& output() noexcept { - return m_sampler; + return m_output_function; } - /** @copydoc sampler() */ - [[nodiscard]] inline constexpr const auto& sampler() const noexcept + /** @copydoc output() */ + [[nodiscard]] inline constexpr const auto& output() const noexcept { - return m_sampler; + return m_output_function; } + /** Returns the non-negative duration of the track, in seconds. */ + [[nodiscard]] float duration() const; + private: std::vector m_channels; - std::function)> m_sampler; - mutable std::vector m_sample_buffer; + output_function_type m_output_function; }; #endif // ANTKEEPER_ANIMATION_ANIMATION_TRACK_HPP diff --git a/src/engine/animation/skeletal-animation.cpp b/src/engine/animation/skeletal-animation.cpp index 30b23244..a2da0ed8 100644 --- a/src/engine/animation/skeletal-animation.cpp +++ b/src/engine/animation/skeletal-animation.cpp @@ -6,10 +6,13 @@ #include #include #include +#include +#include "game/components/scene-component.hpp" #include #include +#include -void bind_skeletal_animation(animation_sequence& sequence, [[maybe_unused]] const ::skeleton& skeleton) +void bind_skeletal_animation([[maybe_unused]] animation_sequence& sequence, [[maybe_unused]] const ::skeleton& skeleton) { for (auto& [key, track]: sequence.tracks()) { @@ -40,39 +43,53 @@ void bind_skeletal_animation(animation_sequence& sequence, [[maybe_unused]] cons throw std::runtime_error("Failed to bind animation track to bone: invalid data path."); } - // Set track sampler according to bone and property + // Set track output function according to bone and property if (property_name == "translation") { - track.sampler() = [bone_index](void* context, auto sample) + track.output() = [bone_index](auto samples, auto& context) { - const auto translation = math::fvec3{sample[0], sample[1], sample[2]}; - static_cast(context)->set_relative_translation(bone_index, translation); + auto& scene_object = *(context.handle.get().object); + auto& skeletal_mesh = static_cast(scene_object); + + const auto translation = math::fvec3{samples[0], samples[1], samples[2]}; + + skeletal_mesh.get_pose().set_relative_translation(bone_index, translation); }; } else if (property_name == "rotation_quaternion") { - track.sampler() = [bone_index](void* context, auto sample) + track.output() = [bone_index](auto samples, auto& context) { - const auto rotation = math::normalize(math::fquat{sample[0], sample[1], sample[2], sample[3]}); + auto& scene_object = *(context.handle.get().object); + auto& skeletal_mesh = static_cast(scene_object); - debug::log_debug("rotated bone {} to {}", bone_index, rotation); - static_cast(context)->set_relative_rotation(bone_index, rotation); + const auto rotation = math::normalize(math::fquat{samples[0], samples[1], samples[2], samples[3]}); + + skeletal_mesh.get_pose().set_relative_rotation(bone_index, rotation); }; } else if (property_name == "rotation_euler") { - track.sampler() = [bone_index](void* context, auto sample) + track.output() = [bone_index](auto samples, auto& context) { - const auto rotation = math::euler_xyz_to_quat(math::fvec3{sample[0], sample[1], sample[2]}); - static_cast(context)->set_relative_rotation(bone_index, rotation); + auto& scene_object = *(context.handle.get().object); + auto& skeletal_mesh = static_cast(scene_object); + + const auto rotation = math::euler_xyz_to_quat(math::fvec3{samples[0], samples[1], samples[2]}); + + skeletal_mesh.get_pose().set_relative_rotation(bone_index, rotation); }; } else if (property_name == "scale") { - track.sampler() = [bone_index](void* context, auto sample) + track.output() = [bone_index](auto samples, auto& context) { - const auto scale = math::fvec3{sample[0], sample[1], sample[2]}; - static_cast(context)->set_relative_scale(bone_index, scale); + auto& scene_object = *(context.handle.get().object); + auto& skeletal_mesh = static_cast(scene_object); + + const auto scale = math::fvec3{samples[0], samples[1], samples[2]}; + + skeletal_mesh.get_pose().set_relative_scale(bone_index, scale); }; } else diff --git a/src/engine/animation/skeletal-animation.hpp b/src/engine/animation/skeletal-animation.hpp index 91ce6696..3bd1fcf4 100644 --- a/src/engine/animation/skeletal-animation.hpp +++ b/src/engine/animation/skeletal-animation.hpp @@ -10,7 +10,8 @@ class skeleton; /** * Binds an animation sequence to a skeleton. * - * @note Animation context must be a pointer to a skeleton_pose. + * @param sequence Animation sequence to bind. + * @parma skeleton Skeleton to which the sequence should be bound. */ void bind_skeletal_animation(animation_sequence& sequence, const ::skeleton& skeleton); diff --git a/src/game/components/animation-component.hpp b/src/game/components/animation-component.hpp new file mode 100644 index 00000000..a6318c30 --- /dev/null +++ b/src/game/components/animation-component.hpp @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: 2023 C. J. Howard +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef ANTKEEPER_GAME_ANIMATION_COMPONENT_HPP +#define ANTKEEPER_GAME_ANIMATION_COMPONENT_HPP + +#include + +struct animation_component +{ + animation_player player; +}; + +#endif // ANTKEEPER_GAME_ANIMATION_COMPONENT_HPP diff --git a/src/game/states/experiments/test-state.cpp b/src/game/states/experiments/test-state.cpp index 50ad833e..b4602e13 100644 --- a/src/game/states/experiments/test-state.cpp +++ b/src/game/states/experiments/test-state.cpp @@ -26,6 +26,7 @@ #include "game/components/ovary-component.hpp" #include "game/components/spring-arm-component.hpp" #include "game/components/ant-genome-component.hpp" +#include "game/components/animation-component.hpp" #include "game/constraints/child-of-constraint.hpp" #include "game/constraints/copy-rotation-constraint.hpp" #include "game/constraints/copy-scale-constraint.hpp" diff --git a/src/game/systems/animation-system.cpp b/src/game/systems/animation-system.cpp index 36a7ea65..6e6e9862 100644 --- a/src/game/systems/animation-system.cpp +++ b/src/game/systems/animation-system.cpp @@ -4,6 +4,7 @@ #include "game/systems/animation-system.hpp" #include "game/components/pose-component.hpp" #include "game/components/scene-component.hpp" +#include "game/components/animation-component.hpp" #include #include #include @@ -12,11 +13,20 @@ animation_system::animation_system(entity::registry& registry): updatable_system(registry) -{} +{ + m_registry.on_construct().connect<&animation_system::on_animation_construct>(this); +} + +animation_system::~animation_system() +{ + m_registry.on_construct().disconnect<&animation_system::on_animation_construct>(this); +} -void animation_system::update([[maybe_unused]] float t, [[maybe_unused]] float dt) +void animation_system::update(float t, float dt) { - + m_previous_update_time = m_update_time; + m_update_time = t; + m_fixed_timestep = dt; } void animation_system::interpolate(float alpha) @@ -49,5 +59,33 @@ void animation_system::interpolate(float alpha) } } ); + + m_previous_render_time = m_render_time; + m_render_time = m_previous_update_time + m_fixed_timestep * alpha; + + const auto dt = m_render_time - m_previous_render_time; + + auto animation_view = m_registry.view(); + std::for_each + ( + std::execution::seq, + animation_view.begin(), + animation_view.end(), + [&](auto entity) + { + auto& player = animation_view.get(entity).player; + if (player.is_playing()) + { + player.advance(dt); + } + } + ); } +void animation_system::on_animation_construct(entity::registry& registry, entity::id entity) +{ + auto& animation = registry.get(entity); + + // Init animation player context + animation.player.context() = {entt::handle(registry, entity)}; +} diff --git a/src/game/systems/animation-system.hpp b/src/game/systems/animation-system.hpp index 9d56e2a6..c58ff059 100644 --- a/src/game/systems/animation-system.hpp +++ b/src/game/systems/animation-system.hpp @@ -5,6 +5,11 @@ #define ANTKEEPER_GAME_ANIMATION_SYSTEM_HPP #include "game/systems/updatable-system.hpp" +#include +#include +#include + +class animation_player; /** * @@ -14,9 +19,19 @@ class animation_system: { public: explicit animation_system(entity::registry& registry); - ~animation_system() override = default; + ~animation_system() override; void update(float t, float dt) override; void interpolate(float alpha); + +private: + void on_animation_construct(entity::registry& registry, entity::id entity); + + float m_previous_update_time{}; + float m_update_time{}; + float m_fixed_timestep{}; + + float m_previous_render_time{}; + float m_render_time{}; }; #endif // ANTKEEPER_GAME_ANIMATION_SYSTEM_HPP