From 779b837caa5812a61475e296815f885ce94eb017 Mon Sep 17 00:00:00 2001 From: Zack Buhman Date: Tue, 14 Apr 2026 18:43:37 -0500 Subject: [PATCH] collada scene animation --- Makefile | 3 +- include/collada/animate.h | 15 +++ include/collada/scene.h | 6 +- src/collada/animate.cpp | 253 ++++++++++++++++++++++++++++++++++++++ src/collada/scene.cpp | 31 +++-- src/main.cpp | 30 ++++- 6 files changed, 317 insertions(+), 21 deletions(-) create mode 100644 include/collada/animate.h create mode 100644 src/collada/animate.cpp diff --git a/Makefile b/Makefile index 0e972ca..7eb6c29 100644 --- a/Makefile +++ b/Makefile @@ -45,7 +45,8 @@ OBJS = \ src/vulkan_helper.o \ src/collada/scene/vulkan.o \ src/collada/scene.o \ - src/collada/node_state.o + src/collada/node_state.o \ + src/collada/animate.o SCENES = \ data/scenes/shadow_test/shadow_test.o diff --git a/include/collada/animate.h b/include/collada/animate.h new file mode 100644 index 0000000..96a0cf5 --- /dev/null +++ b/include/collada/animate.h @@ -0,0 +1,15 @@ +#include "collada/instance_types.h" + +namespace collada::animate { + static inline float fract(float f) + { + return f - floorf(f); + } + + static inline float loop(float f, float n) + { + return fract(f / n) * n; + } + + void animate_node(instance_types::node& node_instance, float t); +} diff --git a/include/collada/scene.h b/include/collada/scene.h index a013128..ae29849 100644 --- a/include/collada/scene.h +++ b/include/collada/scene.h @@ -16,9 +16,9 @@ namespace collada::scene { void load_scene(types::descriptor const * const descriptor); void draw(); - void update(XMMATRIX const & projection, - XMMATRIX const & view, - float t); + int find_node_index_by_name(const char * name); + + void update(float t); void unload_scene(); }; diff --git a/src/collada/animate.cpp b/src/collada/animate.cpp new file mode 100644 index 0000000..4cdd7d8 --- /dev/null +++ b/src/collada/animate.cpp @@ -0,0 +1,253 @@ +#include + +#include "directxmath/directxmath.h" + +#include "collada/types.h" +#include "collada/instance_types.h" + +namespace collada::animate { + + struct frame_ix { + int f0; + int f1; + }; + + static inline frame_ix find_frame_ix(types::source const& source, float t) + { + for (int i = 0; i < source.count - 1; i++) { + if (source.float_array[i] <= t && source.float_array[i+1] > t) { + return {i, i + 1}; + } + } + return {source.count - 1, 0}; + } + + static inline float linear_interpolate_iv(types::source const& source, frame_ix frame_ix, float t) + { + float prev = source.float_array[(frame_ix.f0) * source.stride]; + float next = source.float_array[(frame_ix.f1) * source.stride]; + return (t - prev) / (next - prev); + } + + static inline float linear_interpolate_value(types::source const& source, frame_ix frame_ix, int parameter_ix, float iv) + { + float prev = source.float_array[(frame_ix.f0) * source.stride + parameter_ix]; + float next = source.float_array[(frame_ix.f1) * source.stride + parameter_ix]; + return prev + iv * (next - prev); + } + + static inline XMMATRIX linear_interpolate_matrix(types::source const& source, frame_ix frame_ix, float iv) + { + XMFLOAT4X4 const * prev = (XMFLOAT4X4 const *)&source.float_array[(frame_ix.f0) * source.stride]; + XMFLOAT4X4 const * next = (XMFLOAT4X4 const *)&source.float_array[(frame_ix.f1) * source.stride]; + + XMVECTOR prev_scale; + XMVECTOR prev_rotate; + XMVECTOR prev_translate; + + bool prev_srt = XMMatrixDecompose(&prev_scale, &prev_rotate, &prev_translate, XMMatrixTranspose(XMLoadFloat4x4(prev))); + assert(prev_srt == true); + + XMVECTOR next_scale; + XMVECTOR next_rotate; + XMVECTOR next_translate; + + bool next_srt = XMMatrixDecompose(&next_scale, &next_rotate, &next_translate, XMMatrixTranspose(XMLoadFloat4x4(next))); + assert(next_srt == true); + + XMVECTOR scale = XMVectorLerp(prev_scale, next_scale, iv); + XMVECTOR rotate = XMQuaternionSlerp(prev_rotate, next_rotate, iv); + XMVECTOR translate = XMVectorLerp(prev_translate, next_translate, iv); + + return XMMatrixAffineTransformation(scale, + XMVectorZero(), + rotate, + translate); + } + + static inline float pow3(float f) + { + return f * f * f; + } + + static inline float pow2(float f) + { + return f * f; + } + + static inline XMVECTOR bezier(XMVECTOR p0, XMVECTOR c0, XMVECTOR c1, XMVECTOR p1, float s) + { + return + p0 * pow3(1 - s) + + 3 * c0 * s * pow2(1 - s) + + 3 * c1 * pow2(s) * (1 - s) + + p1 * pow3(s); + } + + static inline float bezier_binary_search(XMVECTOR p0, XMVECTOR c0, XMVECTOR c1, XMVECTOR p1, float want) + { + float low = 0.0f; + float high = 1.0f; + + int iterations = 0; + while (iterations < 20) { + iterations += 1; + + float s = (high + low) * 0.5f; + XMVECTOR bs = bezier(p0, c0, c1, p1, s); + float t = XMVectorGetX(bs); + + const float epsilon = 0.001f; + if (fabsf(t - want) < epsilon) { + return XMVectorGetY(bs); + } + + if (t > want) { + high = s; + } else { + low = s; + } + } + + fprintf(stderr, "%f %f\n", XMVectorGetX(p0), XMVectorGetY(p0)); + fprintf(stderr, "%f %f\n", XMVectorGetX(c0), XMVectorGetY(c0)); + fprintf(stderr, "%f %f\n", XMVectorGetX(c1), XMVectorGetY(c1)); + fprintf(stderr, "%f %f\n", XMVectorGetX(p1), XMVectorGetY(p1)); + assert(false); + } + + static inline XMFLOAT2 const * tangent_index(types::source const& source, int frame_ix, int parameter_ix) + { + int ix = frame_ix * source.stride + parameter_ix * 2; + return (XMFLOAT2 const *)&source.float_array[ix]; + } + + static float bezier_sampler(types::sampler const * const sampler, frame_ix frame_ix, int parameter_ix, float t) + { + /* + P0 is (INPUT[i] , OUTPUT[i]) + C0 (or T0) is (OUT_TANGENT[i][0] , OUT_TANGENT[i][1]) + C1 (or T1) is (IN_TANGENT[i+1][0], IN_TANGENT[i+1][1]) + P1 is (INPUT[i+1], OUTPUT[i+1]) + */ + + float frame0_input = sampler->input.float_array[frame_ix.f0]; + float frame1_input = sampler->input.float_array[frame_ix.f1]; + + float frame0_output = sampler->output.float_array[(frame_ix.f0) * sampler->output.stride + parameter_ix]; + float frame1_output = sampler->output.float_array[(frame_ix.f1) * sampler->output.stride + parameter_ix]; + + XMVECTOR p0 = XMVectorSet(frame0_input, frame0_output, 0, 0); + XMVECTOR c0 = XMLoadFloat2(tangent_index(sampler->out_tangent, frame_ix.f0, parameter_ix)); + XMVECTOR c1 = XMLoadFloat2(tangent_index(sampler->in_tangent, frame_ix.f1, parameter_ix)); + XMVECTOR p1 = XMVectorSet(frame1_input, frame1_output, 0, 0); + + return bezier_binary_search(p0, c0, c1, p1, t); + } + + static void apply_transform_target(instance_types::transform& transform, + enum types::target_attribute channel_target_attribute, + float value) + { + switch (transform.type) { + case types::transform_type::TRANSLATE: [[fallthrough]]; + case types::transform_type::SCALE: + switch (channel_target_attribute) { + case types::target_attribute::X: transform.vector = XMVectorSetX(transform.vector, value); return; + case types::target_attribute::Y: transform.vector = XMVectorSetY(transform.vector, value); return; + case types::target_attribute::Z: transform.vector = XMVectorSetZ(transform.vector, value); return; + default: assert(false); + } + case types::transform_type::ROTATE: + switch (channel_target_attribute) { + case types::target_attribute::X: transform.vector = XMVectorSetX(transform.vector, value); return; + case types::target_attribute::Y: transform.vector = XMVectorSetY(transform.vector, value); return; + case types::target_attribute::Z: transform.vector = XMVectorSetZ(transform.vector, value); return; + case types::target_attribute::ANGLE: transform.vector = XMVectorSetW(transform.vector, value); return; + default: assert(false); + } + default: + assert(false); + break; + } + } + + static enum types::target_attribute const rotate_target_attributes[] = { + types::target_attribute::X, + types::target_attribute::Y, + types::target_attribute::Z, + types::target_attribute::ANGLE, + }; + + static enum types::target_attribute const translate_scale_target_attributes[] = { + types::target_attribute::X, + types::target_attribute::Y, + types::target_attribute::Z, + }; + + static void animate_channel_segment(types::channel const& channel, + instance_types::transform& transform, + frame_ix frame_ix, float t) + { + enum types::target_attribute const * target_attributes = &channel.target_attribute; + int target_attributes_count = 1; + if (channel.target_attribute == types::target_attribute::ALL) { + switch (transform.type) { + case types::transform_type::TRANSLATE: [[fallthrough]]; + case types::transform_type::SCALE: + target_attributes = translate_scale_target_attributes; + target_attributes_count = 3; + break; + case types::transform_type::ROTATE: + target_attributes = rotate_target_attributes; + target_attributes_count = 4; + break; + case types::transform_type::MATRIX: + break; + default: + assert(false); + break; + } + } + + assert(channel.source_sampler->interpolation.stride == 1); + + if (transform.type == types::transform_type::MATRIX) { + enum types::interpolation interpolation = channel.source_sampler->interpolation.interpolation_array[frame_ix.f0]; + assert(interpolation == types::interpolation::LINEAR); + + float iv = linear_interpolate_iv(channel.source_sampler->input, frame_ix, t); + XMMATRIX matrix = linear_interpolate_matrix(channel.source_sampler->output, frame_ix, iv); + transform.matrix = matrix; + + } else { + for (int parameter_ix = 0; parameter_ix < target_attributes_count; parameter_ix++) { + + enum types::interpolation interpolation = channel.source_sampler->interpolation.interpolation_array[frame_ix.f0]; + + float value; + if (interpolation == types::interpolation::BEZIER) { + value = bezier_sampler(channel.source_sampler, frame_ix, parameter_ix, t); + } else { + float iv = linear_interpolate_iv(channel.source_sampler->input, frame_ix, t); + value = linear_interpolate_value(channel.source_sampler->output, frame_ix, parameter_ix, iv); + } + + apply_transform_target(transform, target_attributes[parameter_ix], value); + } + } + } + + void animate_node(instance_types::node& node_instance, float t) + { + for (int i = 0; i < node_instance.node->channels_count; i++) { + types::channel const& channel = *node_instance.node->channels[i]; + instance_types::transform& transform = node_instance.transforms[channel.target_transform_index]; + + frame_ix frame_ix = find_frame_ix(channel.source_sampler->input, t); + //assert(frame_ix >= 0); // animation is missing a key frame + + animate_channel_segment(channel, transform, frame_ix, t); + } + } +} diff --git a/src/collada/scene.cpp b/src/collada/scene.cpp index b4f4caf..95dd994 100644 --- a/src/collada/scene.cpp +++ b/src/collada/scene.cpp @@ -1,7 +1,10 @@ -#include "collada/scene.h" - +#include +#include #include +#include "collada/scene.h" +#include "collada/animate.h" + namespace collada::scene { void state::load_scene(types::descriptor const * const descriptor) @@ -35,21 +38,25 @@ namespace collada::scene { } } - void state::update(XMMATRIX const & projection, - XMMATRIX const & view, - float t) + int state::find_node_index_by_name(const char * name) { - //t = animate::loop(t / 1.0f, 1.0f); + for (int i = 0; i < descriptor->nodes_count; i++) { + if (strcmp(descriptor->nodes[i]->name, name) == 0) { + return i; + } + } + fprintf(stderr, "node `%s` not found in scene\n", name); + exit(EXIT_FAILURE); + } + + void state::update(float t) + { + t = animate::loop(t, 3.3f); for (int i = 0; i < descriptor->nodes_count; i++) { - //animate::animate_node(node_state.node_instances[i], t); + animate::animate_node(node_state.node_instances[i], t); node_state.update_node_world_transform(node_state.node_instances[i]); } - - vulkan.transfer_transforms(projection, - view, - descriptor->nodes_count, - node_state.node_instances); } void state::unload_scene() diff --git a/src/main.cpp b/src/main.cpp index 4748fe6..7b67649 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -112,9 +112,10 @@ XMMATRIX currentProjection() return projection; } -XMMATRIX currentView() +XMMATRIX currentView(collada::instance_types::node const & camera_node) { - XMVECTOR eye = XMVectorSet(-57, 159, 269, 0); + + XMVECTOR eye = XMVector3Transform(XMVectorZero(), camera_node.world); XMVECTOR at = XMVectorSet(0, 0, 0, 0); XMVECTOR up = XMVectorSet(0, 0, 1, 0); XMMATRIX view = XMMatrixLookAtLH(eye, at, up); @@ -276,6 +277,14 @@ inline static int positive_modulo(int i, unsigned int n) { return (i % n + n) % n; } +inline static double getTime(int64_t start_time) +{ + int64_t current_time; + SDL_GetCurrentTime(¤t_time); + int64_t time = current_time - start_time; + return (double)(time / 1000) * 0.000001; +} + int main() { SDL_CHECK(SDL_Init(SDL_INIT_VIDEO)); @@ -1191,6 +1200,11 @@ int main() uint32_t imageIndex{ 0 }; bool quit{ false }; int32_t samplerIndex{ 0 }; + int64_t start_time; + SDL_GetCurrentTime(&start_time); + + int cameraIndex = collada_state.find_node_index_by_name("Camera001"); + while (quit == false) { SDL_Event event; while (SDL_PollEvent(&event)) { @@ -1364,10 +1378,16 @@ int main() collada_state.vulkan.change_frame(commandBuffer, frameIndex); - XMMATRIX projection = currentProjection(); - XMMATRIX view = currentView(); - collada_state.update(projection, view, 0); + double time = getTime(start_time); + collada_state.update(time / 3.0f); + XMMATRIX projection = currentProjection(); + XMMATRIX view = currentView(collada_state.node_state.node_instances[cameraIndex]); + + collada_state.vulkan.transfer_transforms(projection, + view, + collada_state.descriptor->nodes_count, + collada_state.node_state.node_instances); collada_state.draw(); vkCmdEndRendering(commandBuffer);