22 changed files with 1158 additions and 878 deletions
+8 -3
View File
@@ -45,8 +45,6 @@ set(ThirdPartyIncludeDeps
# Shared code by main executable and tests
add_library(AnimTestbedCode OBJECT
src/SyncTrack.cc
src/SyncTrack.h
src/ozzutils.cc
src/AnimGraph/AnimGraphNodes.cc
src/AnimGraph/AnimGraphNodes.h
@@ -59,7 +57,11 @@ add_library(AnimTestbedCode OBJECT
src/AnimGraph/AnimNode.cc
src/AnimGraph/AnimNode.h
src/AnimGraph/AnimGraphResource.cc
src/AnimGraph/AnimGraphResource.h)
src/AnimGraph/AnimGraphResource.h
src/AnimGraph/SyncTrack.cc
src/AnimGraph/SyncTrack.h
src/AnimGraph/AnimLibrary.h
)
target_include_directories(
AnimTestbedCode
@@ -150,6 +152,9 @@ target_sources(runtests PRIVATE
tests/AnimGraphEvalTests.cc
tests/NodeDescriptorTests.cc
tests/SyncTrackTests.cc
tests/AnimDataTests.cc
tests/TestAnimData.cc
tests/TestAnimData.h
tests/main.cc
${ozz_offline_test_objs}
)
+35
View File
@@ -0,0 +1,35 @@
## Graph
### 1. Support of math nodes (or non-AnimNodes in general)
* Enables animators to add custom math for blend inputs or to adjust other inputs (e.g. LookAt or IK
targets).
**Open Issues**
1. When to do the evaluation? Two types of subgraphs:
a) Instant inputs (needed for blend node inputs) that have to be evaluated before
UpdateConnections
b) Processing nodes, e.g. for extracted bones.
### 2. Support of multiple output sockets
* E.g. extract Bone transform
* Increases Node complexity:
* AnimOutput
* AnimOutput + Data
* Data
(Data = bool, float, vec3, quat, ...)
**Open Issues**
1. Unclear when this is actually needed. Using more specific nodes that perform the desired logic
may be better (
c.f. https://dev.epicgames.com/documentation/en-us/unreal-engine/animation-blueprint-bone-driven-controller-in-unreal-engine).
Likely this is not crucial so should be avoided for now.
### 3. Multi-skeleton evaluation
Use case: riding on a horse, interaction between two characters.
+2 -1
View File
@@ -6,6 +6,7 @@
#define ANIMTESTBED_ANIMGRAPHBLENDTREE_H
#include <algorithm>
#include <iostream>
#include "AnimNode.h"
@@ -129,7 +130,7 @@ struct AnimGraphBlendTree : public AnimNode {
*
* @tparam T Type of the Socket.
* @param name Name of the Socket.
* @param value_ptr Pointer where the graph output output is written to at the end of evaluation.
* @param value_ptr Pointer where the graph output is written to at the end of evaluation.
*/
template <typename T>
void SetOutput(const char* name, T* value_ptr) {
+31 -8
View File
@@ -8,7 +8,6 @@
#include <ozz/base/maths/soa_transform.h>
#include <cstring>
#include <iostream>
#include <list>
#include <map>
#include <string>
@@ -33,6 +32,33 @@ struct AnimDataRef {
Pose* ptr = nullptr;
};
struct AnimationResource {
std::string m_name;
std::string m_filename;
ozz::animation::Animation* m_animation;
SyncTrack m_sync_track;
};
inline void to_json(
nlohmann::json& j,
const AnimationResource& animation_resource) {
j["type"] = "AnimationResource";
j["name"] = animation_resource.m_name;
j["filename"] = animation_resource.m_filename;
j["synctrack"] = animation_resource.m_sync_track;
}
inline void from_json(
const nlohmann::json& j,
AnimationResource& animation_resource) {
assert(j["type"] == "AnimationResource");
animation_resource.m_name = j["name"];
animation_resource.m_filename = j["filename"];
animation_resource.m_sync_track = j["synctrack"];
}
struct AnimDataAllocator {
struct PoseList {
Pose* m_anim_data = nullptr;
@@ -98,18 +124,20 @@ enum class AnimGraphType {
GraphTypeLast
};
/*** Defines data on which an animation graph is executed (i.e. skeleton, animations).
*/
struct AnimGraphContext {
AnimGraph* m_graph = nullptr;
ozz::animation::Skeleton* m_skeleton = nullptr;
typedef std::map<std::string, ozz::animation::Animation*> AnimationFileMap;
typedef std::map<std::string, AnimationResource> AnimationFileMap;
AnimationFileMap m_animation_map;
void freeAnimations() {
AnimationFileMap::iterator animation_map_iter = m_animation_map.begin();
while (animation_map_iter != m_animation_map.end()) {
delete animation_map_iter->second;
delete animation_map_iter->second.m_animation;
animation_map_iter++;
}
}
@@ -373,11 +401,6 @@ struct NodeDescriptorBase {
*socket->m_reference.ptr_ptr = value_ptr;
}
void SetOutputUnchecked(const char* name, void* value_ptr) {
Socket* socket = FindSocket(name, m_outputs);
*socket->m_reference.ptr_ptr = value_ptr;
}
Socket* GetOutputSocket(const char* name) const {
return FindSocket(name, m_outputs);
}
+7 -2
View File
@@ -132,7 +132,8 @@ bool AnimSamplerNode::Init(AnimGraphContext& context) {
AnimGraphContext::AnimationFileMap::const_iterator animation_map_iter;
animation_map_iter = context.m_animation_map.find(m_filename);
if (animation_map_iter != context.m_animation_map.end()) {
m_animation = animation_map_iter->second;
m_animation = animation_map_iter->second.m_animation;
m_sync_track = animation_map_iter->second.m_sync_track;
} else {
m_animation = new ozz::animation::Animation();
ozz::io::File file(m_filename.c_str(), "rb");
@@ -150,7 +151,11 @@ bool AnimSamplerNode::Init(AnimGraphContext& context) {
archive >> *m_animation;
context.m_animation_map[m_filename] = m_animation;
context.m_animation_map[m_filename] = {
m_filename,
m_filename,
m_animation,
SyncTrack()};
}
assert(context.m_skeleton != nullptr);
+2
View File
@@ -5,6 +5,8 @@
#ifndef ANIMTESTBED_ANIMGRAPHRESOURCE_H
#define ANIMTESTBED_ANIMGRAPHRESOURCE_H
#include <iostream>
#include "3rdparty/json/json.hpp"
#include "AnimGraphNodes.h"
+144
View File
@@ -0,0 +1,144 @@
//
// Created by martin on 11.04.25.
//
#ifndef ANIMLIBRARY_H
#define ANIMLIBRARY_H
#include <iostream>
#include "AnimGraph/AnimGraphData.h"
#include "ozz/base/io/archive.h"
#include "ozz/base/io/stream.h"
#include "ozz/base/log.h"
/** Manage a set of animations used for an AnimGraph.
*
* By default, it behaves like a resource that allows to resolve animation names to
* AnimationResources. However, it can also trigger loading of the referenced resources from their
* filenames. This only happens on-demand.
*/
struct AnimLibrary {
typedef std::map<std::string, AnimationResource> AnimationResourceMap;
AnimationResourceMap mAnimations = {};
std::vector<ozz::animation::Animation*> mManagedAnimations = {};
static constexpr const char* EXTERNAL_ANIMATION = "<external>";
AnimLibrary() = default;
~AnimLibrary() { Reset(); }
bool AddAnimation(
const std::string& name,
ozz::animation::Animation* animation) {
AnimationResource animation_resource;
animation_resource.m_name = name;
animation_resource.m_filename = EXTERNAL_ANIMATION;
animation_resource.m_animation = animation;
mAnimations[name] = animation_resource;
return true;
}
bool AddAnimationFile(const std::string& name, const std::string& filename) {
if (mAnimations.find(name) != mAnimations.end()) {
std::cerr << "Cannot add animation '" << name
<< "' to library. Animation already exists." << std::endl;
return false;
}
AnimationResource animation_resource;
animation_resource.m_name = name;
animation_resource.m_animation = nullptr;
animation_resource.m_filename = filename;
mAnimations[name] = animation_resource;
return true;
}
void Reset() {
for (ozz::animation::Animation* animation : mManagedAnimations) {
assert(animation->num_tracks() < 5);
delete animation;
}
mManagedAnimations.clear();
mAnimations.clear();
}
void LoadAnimations() {
for (AnimationResourceMap::iterator iter = mAnimations.begin();
iter != mAnimations.end();
++iter) {
if (iter->second.m_filename == EXTERNAL_ANIMATION) {
continue;
}
if (iter->second.m_animation != nullptr) {
continue;
}
assert(!iter->second.m_filename.empty());
ozz::io::File file(iter->second.m_filename.c_str(), "rb");
if (!file.opened()) {
ozz::log::Err() << "Failed to open animation file "
<< iter->second.m_filename << "." << std::endl;
continue;
}
ozz::io::IArchive archive(&file);
if (!archive.TestTag<ozz::animation::Animation>()) {
ozz::log::Err() << "Failed to load animation instance from file "
<< iter->second.m_filename << "." << std::endl;
continue;
}
iter->second.m_animation = new ozz::animation::Animation;
archive >> *iter->second.m_animation;
mManagedAnimations.push_back(iter->second.m_animation);
}
}
};
inline void to_json(nlohmann::json& j, const AnimLibrary& animation_library) {
j["type"] = "AnimationLibrary";
for (AnimLibrary::AnimationResourceMap::const_iterator iter =
animation_library.mAnimations.cbegin();
iter != animation_library.mAnimations.cend();
++iter) {
j["animations"][iter->first] = iter->second;
}
}
inline void from_json(const nlohmann::json& j, AnimLibrary& animation_library) {
animation_library.Reset();
if (!j.contains("type") || j["type"] != "AnimationLibrary") {
std::cerr << "Invalid type. Expected 'AnimationLibrary'." << std::endl;
}
if (!j.contains("animations")) {
std::cerr << "Invalid AnimationLibrary. Expected 'animations' key."
<< std::endl;
}
for (nlohmann::json::const_iterator iter = j["animations"].begin();
iter != j["animations"].cend();
++iter) {
AnimationResource animation_resource = *iter;
if (!animation_resource.m_filename.empty()) {
animation_library.AddAnimationFile(
iter.key(),
j["animations"][iter.key()]["filename"]);
animation_library.mAnimations[iter.key()].m_sync_track =
animation_resource.m_sync_track;
} else {
animation_library.mAnimations[iter.key()] = *iter;
}
}
}
#endif //ANIMLIBRARY_H
@@ -3,7 +3,3 @@
//
#include "SyncTrack.h"
#include <imgui.h>
#include <sstream>
+206
View File
@@ -0,0 +1,206 @@
//
// Created by martin on 19.11.21.
//
#ifndef ANIMTESTBED_SYNCTRACK_H
#define ANIMTESTBED_SYNCTRACK_H
#include <cassert>
#include "3rdparty/json/json.hpp"
constexpr int cSyncTrackMaxIntervals = 8;
/// Metadata used for synced animation blending.
//
// A SyncTrack consists of multiple SyncInterval that are adjacent to each
// other.
//
// Important definitions:
//
// - Absolute Time: time within an animation duration in seconds.
// - Ratio: time relative to the animations duration, e.g. 0.5 corresponds to
// 50% of the duration.
// - SyncTime is a floating point value where the integer parts defines the
// SyncInterval and the fractional part the fraction within the interval. I.e.
// a SyncTime of 5.332 means it is ~33% through interval 5.
//
// A SyncInterval is defined by a ratio of the starting point and the ratio of
// the interval's duration.
struct SyncTrack {
SyncTrack() : m_duration(0.f), m_num_intervals(1) {
for (int i = 0; i < cSyncTrackMaxIntervals; i++) {
m_interval_start_ratio[i] = 0.f;
m_interval_duration_ratio[i] = 0.f;
}
m_interval_duration_ratio[0] = 1.0f;
}
float m_duration;
int m_num_intervals;
float m_interval_start_ratio
[cSyncTrackMaxIntervals]; //< Starting time of interval in absolute time.
float m_interval_duration_ratio[cSyncTrackMaxIntervals]; //<
float CalcSyncFromAbsTime(float abs_time) {
for (int i = 0; i < m_num_intervals; i++) {
float query_abs_time = abs_time;
float interval_start = m_interval_start_ratio[i] * m_duration;
float interval_end =
interval_start + m_interval_duration_ratio[i] * m_duration;
if (query_abs_time < interval_start) {
query_abs_time += m_duration;
}
if (query_abs_time >= interval_start && query_abs_time < interval_end) {
return float(i)
+ (query_abs_time - interval_start)
/ (interval_end - interval_start);
}
}
assert(false && "Invalid absolute time");
return -1.f;
}
float CalcRatioFromSyncTime(float sync_time) {
float interval_ratio = fmodf(sync_time, 1.0f);
int interval = int(sync_time - interval_ratio);
return fmodf(
m_interval_start_ratio[interval]
+ m_interval_duration_ratio[interval] * interval_ratio,
1.0f);
}
bool operator==(const SyncTrack& other) const {
bool result = m_duration == other.m_duration
&& m_num_intervals == other.m_num_intervals;
if (!result) {
return false;
}
for (int i = 0; i < m_num_intervals; i++) {
if ((fabsf(m_interval_start_ratio[i] - other.m_interval_start_ratio[i])
> 1.0e-5)
|| (fabsf(
m_interval_duration_ratio[i]
- other.m_interval_duration_ratio[i])
> 1.0e-5)) {
return false;
}
}
return true;
}
/** Constructs SyncTrack from markers.
*
* Markers are specified in absolute time and must be >= 0 and <= duration.
* They define the start of the interval. The last marker is implicitly the
* (possibly looped) first marker.
*/
static SyncTrack CreateFromMarkers(
float duration,
const std::vector<float>& markers) {
assert(markers.size() > 0);
assert(markers.size() < cSyncTrackMaxIntervals);
SyncTrack result;
result.m_duration = duration;
result.m_num_intervals = markers.size();
for (int i = 0; i < markers.size(); i++) {
assert(markers[i] >= 0.f && markers[i] <= duration);
int end_index = i == (markers.size() - 1) ? 0 : i + 1;
float interval_start = markers[i];
float interval_end = markers[end_index];
if (interval_end == interval_start) {
interval_end = interval_start + duration;
} else if (interval_end < interval_start) {
interval_end += duration;
}
result.m_interval_start_ratio[i] = interval_start / duration;
result.m_interval_duration_ratio[i] =
(interval_end - interval_start) / duration;
}
return result;
}
static SyncTrack
Blend(float weight, const SyncTrack& track_A, const SyncTrack& track_B) {
assert(track_A.m_num_intervals == track_B.m_num_intervals);
SyncTrack result;
result.m_num_intervals = track_A.m_num_intervals;
result.m_duration =
(1.0f - weight) * track_A.m_duration + weight * track_B.m_duration;
float interval_0_offset =
track_B.m_interval_start_ratio[0] - track_A.m_interval_start_ratio[0];
if (interval_0_offset > 0.5f) {
interval_0_offset = -fmodf(1.f - interval_0_offset, 1.0f);
} else if (interval_0_offset < -0.5) {
interval_0_offset = fmodf(1.f + interval_0_offset, 1.0f);
}
result.m_interval_start_ratio[0] = fmodf(
1.0 + (1.0f - weight) * track_A.m_interval_start_ratio[0]
+ weight * (track_A.m_interval_start_ratio[0] + interval_0_offset),
1.0f);
for (int i = 0; i < result.m_num_intervals; i++) {
float interval_duration_A = track_A.m_interval_duration_ratio[i];
float interval_duration_B = track_B.m_interval_duration_ratio[i];
result.m_interval_duration_ratio[i] =
(1.0f - weight) * interval_duration_A + weight * interval_duration_B;
if (i < cSyncTrackMaxIntervals) {
result.m_interval_start_ratio[i + 1] =
result.m_interval_start_ratio[i]
+ result.m_interval_duration_ratio[i];
if (result.m_interval_start_ratio[i + 1] > 1.0f) {
result.m_interval_start_ratio[i + 1] =
fmodf(result.m_interval_start_ratio[i + 1], 1.0f);
}
}
}
assert(result.m_num_intervals < cSyncTrackMaxIntervals);
return result;
}
};
inline void to_json(nlohmann::json& j, const SyncTrack& sync_track) {
j["type"] = "SyncTrack";
j["duration"] = sync_track.m_duration;
for (int i = 0; i < sync_track.m_num_intervals; i++) {
j["interval"][i]["start"] = sync_track.m_interval_start_ratio[i];
j["interval"][i]["ratio"] = sync_track.m_interval_duration_ratio[i];
}
}
inline void from_json(const nlohmann::json& j, SyncTrack& sync_track) {
assert(j["type"] == "SyncTrack");
sync_track.m_duration = j["duration"];
sync_track.m_num_intervals = j["interval"].size();
assert(sync_track.m_num_intervals < cSyncTrackMaxIntervals);
for (int i = 0; i < sync_track.m_num_intervals; i++) {
sync_track.m_interval_start_ratio[i] = j["interval"][i]["start"];
sync_track.m_interval_duration_ratio[i] = j["interval"][i]["ratio"];
}
}
#endif //ANIMTESTBED_SYNCTRACK_H
+4 -17
View File
@@ -15,6 +15,7 @@
#include "imnodes.h"
#include "misc/cpp/imgui_stdlib.h"
#include "nfd.h"
#include "ozz/base/log.h"
#include "src/AnimGraph/AnimGraphResource.h"
struct EditorState {
@@ -154,21 +155,7 @@ void SyncTrackEditor(SyncTrack* sync_track) {
}
ImGui::Text("Marker:");
for (int i = 0; i < sync_track->m_num_intervals; i++) {
ImGui::Text("%2d:", i);
ImGui::SameLine();
std::ostringstream marker_stream;
marker_stream << i;
ImGui::SliderFloat(
marker_stream.str().c_str(),
&sync_track->m_sync_markers[i],
0.f,
1.f);
}
if (ImGui::Button("Update Intervals")) {
sync_track->CalcIntervals();
}
ImGui::Text("TODO");
}
void SkinnedMeshWidget(SkinnedMesh* skinned_mesh) {
@@ -797,9 +784,9 @@ void BlendTreeEditorDebugWidget() {
ImGui::TableSetColumnIndex(0);
ImGui::Text("Pin");
ImGui::TableNextColumn();
ImGui::Text("%x", sNodeConnectionDebugState.sourceSocket.pin.AsPointer());
ImGui::Text("%p", sNodeConnectionDebugState.sourceSocket.pin.AsPointer());
ImGui::TableNextColumn();
ImGui::Text("%x", sNodeConnectionDebugState.targetSocket.pin.AsPointer());
ImGui::Text("%p", sNodeConnectionDebugState.targetSocket.pin.AsPointer());
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
+5 -4
View File
@@ -42,8 +42,8 @@ inline void Camera_Init(Camera* camera) {
camera->pitch = 10 * M_PI / 180.0f;
memcpy(&camera->mtxView, &mtx_identity, sizeof(camera->mtxView));
Camera_CalcToMatrix(camera, &camera->mtxView);
Camera_CalcFromMatrix(camera, &camera->mtxView);
Camera_CalcToMatrix(camera, &camera->mtxView[0]);
Camera_CalcFromMatrix(camera, &camera->mtxView[0]);
}
void Camera_CalcFromMatrix(Camera* camera, float* mat) {
@@ -87,7 +87,8 @@ void Camera_CalcToMatrix(Camera* camera, float* mat) {
const float d = 10.0f;
simd4f eye = simd4f_create (camera->pos[0], camera->pos[1], camera->pos[2], 1.f);
simd4f eye =
simd4f_create(camera->pos[0], camera->pos[1], camera->pos[2], 1.f);
simd4f forward = simd4f_create(-cp * ch, -sp, cp * sh, 0.f);
simd4f right = simd4f_cross3(forward, simd4f_create(0.f, 1.f, 0.f, 1.f));
simd4f up = simd4f_cross3(right, forward);
@@ -153,6 +154,6 @@ inline void Camera_Update(
camera->vel[i] = camera->vel[i] * 0.1;
}
Camera_CalcToMatrix(camera, &camera->mtxView);
Camera_CalcToMatrix(camera, &camera->mtxView[0]);
}
}
+5 -2
View File
@@ -4,9 +4,13 @@
#include "SkinnedMesh.h"
#include <HandmadeMath.h>
#include <imgui.h>
#include "ozz/animation/runtime/local_to_model_job.h"
#include "ozz/base/io/archive.h"
#include "ozz/base/io/stream.h"
#include "ozz/base/log.h"
SkinnedMesh::~SkinnedMesh() {
while (m_animations.size() > 0) {
ozz::animation::Animation* animation_ptr =
@@ -98,4 +102,3 @@ void SkinnedMesh::CalcModelMatrices() {
}
void SkinnedMesh::DrawSkeleton() {}
+1 -9
View File
@@ -5,21 +5,13 @@
#ifndef ANIMTESTBED_SKINNEDMESH_H
#define ANIMTESTBED_SKINNEDMESH_H
// ozz-animation headers
#include <cmath> // fmodf
#include <memory> // std::unique_ptr, std::make_unique
#include "AnimGraph/SyncTrack.h"
#include "SyncTrack.h"
#include "ozz/animation/runtime/animation.h"
#include "ozz/animation/runtime/local_to_model_job.h"
#include "ozz/animation/runtime/sampling_job.h"
#include "ozz/animation/runtime/skeleton.h"
#include "ozz/base/containers/vector.h"
#include "ozz/base/io/archive.h"
#include "ozz/base/io/stream.h"
#include "ozz/base/log.h"
#include "ozz/base/maths/soa_transform.h"
#include "ozz/base/maths/vec_float.h"
struct SkinnedMesh {
SkinnedMesh() : m_sync_track_override(false), m_override_anim(0.f) {}
+6 -69
View File
@@ -5,72 +5,15 @@
#include "SkinnedMeshResource.h"
#include <fstream>
#include <iostream>
#include "3rdparty/json/json.hpp"
inline void to_json(nlohmann::json& j, const SyncTrack& syncTrack) {
j["type"] = "SyncTrack";
j["duration"] = syncTrack.m_duration;
for (int i = 0; i < syncTrack.m_num_intervals; i++) {
j["markers"][i] = syncTrack.m_sync_markers[i];
}
}
inline void from_json(const nlohmann::json& j, SyncTrack& syncTrack) {
if (!j.contains("type") || j["type"] != "SyncTrack") {
std::cerr << "Unable to parse SyncTrack: wrong json type!" << std::endl;
return;
}
syncTrack.m_duration = j["duration"];
syncTrack.m_num_intervals = j["markers"].size();
if (syncTrack.m_num_intervals > cSyncTrackMaxIntervals) {
std::cerr << "Invalid number of sync intervals: found " << syncTrack.m_num_intervals << " maximum is " << cSyncTrackMaxIntervals << "." << std::endl;
syncTrack = SyncTrack();
}
for (int i = 0; i < syncTrack.m_num_intervals; i++) {
syncTrack.m_sync_markers[i] = j["markers"].at(i);
}
}
inline void to_json(nlohmann::json& j, const SkinnedMeshResource& skinnedMeshResource) {
j["type"] = "SkinnedMeshResource";
j["skeleton"]["file"] = skinnedMeshResource.m_skeleton_file;
for (int i = 0; i < skinnedMeshResource.m_animation_files.size(); i++) {
j["animations"][i]["file"] = skinnedMeshResource.m_animation_files[i];
j["animations"][i]["sync_track"] = skinnedMeshResource.m_sync_tracks[i];
}
}
inline void from_json(const nlohmann::json& j, SkinnedMeshResource& skinnedMeshResource) {
if (!j.contains("type") || j["type"] != "SkinnedMeshResource") {
std::cerr << "Unable to parse SkinnedMeshResource: wrong json type!" << std::endl;
return;
}
if (!j.contains("skeleton") || !j["skeleton"].contains("file")) {
std::cerr << "Unable to parse SkinnedMeshResource: no skeleton file found!" << std::endl;
}
skinnedMeshResource.m_skeleton_file = j["skeleton"]["file"];
if (j.contains("animations")) {
int num_animations = j["animations"].size();
for (int i = 0; i < num_animations; i++) {
if (!j["animations"][i].contains("file") || !j["animations"][i].contains("sync_track")) {
std::cerr << "Unable to parse SkinnedMeshResource: invalid animation definition" << std::endl;
return;
}
skinnedMeshResource.m_animation_files.push_back(j["animations"][i]["file"]);
skinnedMeshResource.m_sync_tracks.push_back(j["animations"][i]["sync_track"].get<SyncTrack>());
}
}
}
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(
SkinnedMeshResource,
m_type,
m_resource_file,
m_skeleton_file);
bool SkinnedMeshResource::saveToFile(const char* filename) const {
nlohmann::json j = *this;
@@ -103,10 +46,4 @@ bool SkinnedMeshResource::loadFromFile(const char* filename) {
void SkinnedMeshResource::createInstance(SkinnedMesh& skinnedMesh) const {
skinnedMesh.LoadSkeleton(m_skeleton_file.c_str());
for (int i = 0; i < m_animation_files.size(); i++) {
skinnedMesh.LoadAnimation(m_animation_files[i].c_str());
skinnedMesh.m_animation_sync_track.back() = m_sync_tracks[i];
}
}
+3 -3
View File
@@ -8,14 +8,14 @@
#include <string>
#include <vector>
#include "SyncTrack.h"
#include "AnimGraph/SyncTrack.h"
#include "SkinnedMesh.h"
struct SkinnedMeshResource {
constexpr static char TypeStr[] = "SkinnedMeshResource";
std::string m_type = TypeStr;
std::string m_resource_file;
std::string m_skeleton_file;
std::vector<std::string> m_animation_files;
std::vector<SyncTrack> m_sync_tracks;
bool saveToFile(const char* filename) const;
bool loadFromFile(const char* filename);
-155
View File
@@ -1,155 +0,0 @@
//
// Created by martin on 19.11.21.
//
#ifndef ANIMTESTBED_SYNCTRACK_H
#define ANIMTESTBED_SYNCTRACK_H
#include <cassert>
#include <cmath>
#include <iostream>
constexpr int cSyncTrackMaxIntervals = 8;
struct SyncTrack {
SyncTrack() : m_duration(0.f), m_num_intervals(0) {
for (int i = 0; i < cSyncTrackMaxIntervals; i++) {
m_sync_markers[i] = 0.f;
m_interval_ratio[i] = 0.f;
m_interval_ratio[i] = 0.f;
}
}
float m_duration;
int m_num_intervals;
float m_sync_markers[cSyncTrackMaxIntervals];
float m_interval_start[cSyncTrackMaxIntervals];
float m_interval_ratio[cSyncTrackMaxIntervals];
void CalcIntervals() {
if (m_num_intervals == 0) {
m_num_intervals = 1;
m_sync_markers[0] = 0.f;
}
for (int i = 0; i < m_num_intervals; i++) {
assert(m_sync_markers[i] >= 0.f && m_sync_markers[i] <= 1.0f);
int end_index = i < m_num_intervals - 1 ? i + 1 : 0;
m_interval_start[i] = m_sync_markers[i];
float interval_end = m_sync_markers[end_index];
if (interval_end < m_interval_start[i]) {
interval_end += 1.0f;
}
m_interval_ratio[i] = interval_end - m_interval_start[i];
}
}
float CalcSyncFromAbsTime(float abs_time) {
float sync_time = fmodf(abs_time, m_duration) / m_duration;
int interval_index = 0;
while (sync_time >= m_interval_ratio[interval_index]) {
sync_time -= m_interval_ratio[interval_index];
interval_index++;
}
return float(interval_index) + sync_time / m_interval_ratio[interval_index];
}
float CalcRatioFromSyncTime(float sync_time) {
float interval_ratio = fmodf(sync_time, 1.0f);
int interval = int(sync_time - interval_ratio);
return fmodf(
m_interval_start[interval]
+ m_interval_ratio[interval] * interval_ratio,
1.0f);
}
bool operator==(const SyncTrack& other) const {
bool result = m_duration == other.m_duration
&& m_num_intervals == other.m_num_intervals;
if (!result) {
return false;
}
for (int i = 0; i < m_num_intervals; i++) {
if ((fabsf(m_interval_start[i] - other.m_interval_start[i]) > 1.0e-5)
|| (fabsf(m_interval_ratio[i] - other.m_interval_ratio[i])
> 1.0e-5)) {
return false;
}
}
return true;
}
static SyncTrack CreateFromMarkers(
float duration,
int n_markers,
float markers[cSyncTrackMaxIntervals]) {
SyncTrack result;
result.m_duration = duration;
result.m_num_intervals = n_markers;
for (int i = 0; i < n_markers; i++) {
result.m_sync_markers[i] = markers[i];
}
result.CalcIntervals();
return result;
}
static SyncTrack
Blend(float weight, const SyncTrack& track_A, const SyncTrack& track_B) {
assert(track_A.m_num_intervals == track_B.m_num_intervals);
SyncTrack result;
result.m_num_intervals = track_A.m_num_intervals;
result.m_duration =
(1.0f - weight) * track_A.m_duration + weight * track_B.m_duration;
float interval_0_offset =
track_B.m_interval_start[0] - track_A.m_interval_start[0];
if (interval_0_offset > 0.5f) {
interval_0_offset = -fmodf(1.f - interval_0_offset, 1.0f);
} else if (interval_0_offset < -0.5) {
interval_0_offset = fmodf(1.f + interval_0_offset, 1.0f);
}
result.m_interval_start[0] = fmodf(
1.0 + (1.0f - weight) * track_A.m_interval_start[0]
+ weight * (track_A.m_interval_start[0] + interval_0_offset),
1.0f);
result.m_sync_markers[0] = result.m_interval_start[0];
for (int i = 0; i < result.m_num_intervals; i++) {
float interval_duration_A = track_A.m_interval_ratio[i];
float interval_duration_B = track_B.m_interval_ratio[i];
result.m_interval_ratio[i] =
(1.0f - weight) * interval_duration_A + weight * interval_duration_B;
if (i < cSyncTrackMaxIntervals) {
result.m_interval_start[i + 1] =
result.m_interval_start[i] + result.m_interval_ratio[i];
if (result.m_interval_start[i + 1] > 1.0f) {
result.m_interval_start[i + 1] =
fmodf(result.m_interval_start[i + 1], 1.0f);
}
result.m_sync_markers[i + 1] = result.m_interval_start[i + 1];
}
}
assert (result.m_num_intervals < cSyncTrackMaxIntervals);
return result;
}
};
#endif //ANIMTESTBED_SYNCTRACK_H
+311 -452
View File
@@ -39,13 +39,206 @@ const int MaxIndices = MaxVertices * 3;
uint64_t last_time = 0;
const int cMSAASampleCount = 8;
typedef struct {
ImVec2 disp_size;
} vs_params_t;
struct SokolImGuiState {
sg_pass_action pass_action;
sg_pipeline pip;
sg_bindings bind;
typedef struct {
ImVec2 disp_size;
} vs_params_t;
void Init(GLFWwindow* window);
void Render(ImDrawData* draw_data, int width, int height);
void Shutdown();
};
void SokolImGuiState::Init(GLFWwindow* window) {
// setup Dear Imgui
ImGui::CreateContext();
ImGui::StyleColorsDark();
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
io.IniFilename = "ATPImgui.ini";
//io.Fonts->AddFontDefault();
ImFontConfig font_config;
font_config.OversampleH = 4;
font_config.OversampleV = 4;
font_config.GlyphExtraSpacing.x = 1.0f;
io.Fonts->AddFontFromMemoryCompressedTTF(
// roboto_medium_ttf_compressed_data,
// roboto_medium_ttf_compressed_size,
droid_sans_ttf_compressed_data,
droid_sans_ttf_compressed_size,
14,
&font_config);
ImGui_ImplGlfw_InitForOpenGL(window, true);
ImGui_ImplOpenGL3_Init("#version 130");
// ImNodes
ImNodes::CreateContext();
// dynamic vertex- and index-buffers for imgui-generated geometry
sg_buffer_desc vbuf_desc = {};
vbuf_desc.usage = SG_USAGE_STREAM;
vbuf_desc.size = MaxVertices * sizeof(ImDrawVert);
bind.vertex_buffers[0] = sg_make_buffer(&vbuf_desc);
sg_buffer_desc ibuf_desc = {};
ibuf_desc.type = SG_BUFFERTYPE_INDEXBUFFER;
ibuf_desc.usage = SG_USAGE_STREAM;
ibuf_desc.size = MaxIndices * sizeof(ImDrawIdx);
bind.index_buffer = sg_make_buffer(&ibuf_desc);
// font texture for imgui's default font
unsigned char* font_pixels;
int font_width, font_height;
io.Fonts->GetTexDataAsRGBA32(&font_pixels, &font_width, &font_height);
sg_image_desc img_desc = {};
img_desc.width = font_width;
img_desc.height = font_height;
img_desc.pixel_format = SG_PIXELFORMAT_RGBA8;
img_desc.wrap_u = SG_WRAP_CLAMP_TO_EDGE;
img_desc.wrap_v = SG_WRAP_CLAMP_TO_EDGE;
img_desc.data.subimage[0][0] =
sg_range{font_pixels, size_t(font_width * font_height * 4)};
bind.fs_images[0] = sg_make_image(&img_desc);
// shader object for imgui rendering
sg_shader_desc shd_desc = {};
auto& ub = shd_desc.vs.uniform_blocks[0];
ub.size = sizeof(vs_params_t);
ub.uniforms[0].name = "disp_size";
ub.uniforms[0].type = SG_UNIFORMTYPE_FLOAT2;
shd_desc.vs.source =
"#version 330\n"
"uniform vec2 disp_size;\n"
"layout(location=0) in vec2 position;\n"
"layout(location=1) in vec2 texcoord0;\n"
"layout(location=2) in vec4 color0;\n"
"out vec2 uv;\n"
"out vec4 color;\n"
"void main() {\n"
" gl_Position = vec4(((position/disp_size)-0.5)*vec2(2.0,-2.0), 0.5, "
"1.0);\n"
" uv = texcoord0;\n"
" color = color0;\n"
"}\n";
shd_desc.fs.images[0].name = "tex";
shd_desc.fs.images[0].image_type = SG_IMAGETYPE_2D;
shd_desc.fs.source =
"#version 330\n"
"uniform sampler2D tex;\n"
"in vec2 uv;\n"
"in vec4 color;\n"
"out vec4 frag_color;\n"
"void main() {\n"
" frag_color = texture(tex, uv) * color;\n"
"}\n";
sg_shader shd = sg_make_shader(&shd_desc);
// pipeline object for imgui rendering
sg_pipeline_desc pip_desc = {};
pip_desc.layout.buffers[0].stride = sizeof(ImDrawVert);
auto& attrs = pip_desc.layout.attrs;
attrs[0].format = SG_VERTEXFORMAT_FLOAT2;
attrs[1].format = SG_VERTEXFORMAT_FLOAT2;
attrs[2].format = SG_VERTEXFORMAT_UBYTE4N;
pip_desc.shader = shd;
pip_desc.index_type = SG_INDEXTYPE_UINT16;
pip_desc.colors[0].blend.enabled = true;
pip_desc.colors[0].blend.src_factor_rgb = SG_BLENDFACTOR_SRC_ALPHA;
pip_desc.colors[0].blend.dst_factor_rgb = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
pip_desc.colors[0].write_mask = SG_COLORMASK_RGB;
pip_desc.sample_count = cMSAASampleCount;
pip_desc.label = "imgui-rendering";
pip = sg_make_pipeline(&pip_desc);
// initial clear color
pass_action.colors[0].action = SG_ACTION_CLEAR;
pass_action.colors[0].value = {0.1f, 0.1f, 0.1f, 1.0f};
}
void SokolImGuiState::Render(ImDrawData* draw_data, int width, int height) {
assert(draw_data);
sg_begin_default_pass(&pass_action, width, height);
if (draw_data->CmdListsCount == 0) {
sg_end_pass();
return;
}
sg_image default_image = bind.fs_images[0];
// render the command list
sg_apply_pipeline(pip);
vs_params_t vs_params;
vs_params.disp_size.x = ImGui::GetIO().DisplaySize.x;
vs_params.disp_size.y = ImGui::GetIO().DisplaySize.y;
sg_apply_uniforms(SG_SHADERSTAGE_VS, 0, SG_RANGE(vs_params));
for (int cl_index = 0; cl_index < draw_data->CmdListsCount; cl_index++) {
const ImDrawList* cl = draw_data->CmdLists[cl_index];
// append vertices and indices to buffers, record start offsets in resource binding struct
const uint32_t vtx_size = cl->VtxBuffer.size() * sizeof(ImDrawVert);
const uint32_t idx_size = cl->IdxBuffer.size() * sizeof(ImDrawIdx);
const uint32_t vb_offset = sg_append_buffer(
bind.vertex_buffers[0],
{&cl->VtxBuffer.front(), vtx_size});
const uint32_t ib_offset =
sg_append_buffer(bind.index_buffer, {&cl->IdxBuffer.front(), idx_size});
/* don't render anything if the buffer is in overflow state (this is also
checked internally in sokol_gfx, draw calls that attempt from
overflowed buffers will be silently dropped)
*/
if (sg_query_buffer_overflow(bind.vertex_buffers[0])
|| sg_query_buffer_overflow(bind.index_buffer)) {
continue;
}
bind.vertex_buffer_offsets[0] = vb_offset;
bind.index_buffer_offset = ib_offset;
sg_apply_bindings(&bind);
int base_element = 0;
for (const ImDrawCmd& pcmd : cl->CmdBuffer) {
if (pcmd.UserCallback) {
pcmd.UserCallback(cl, &pcmd);
} else {
uint32_t prev_fs_image_id = bind.fs_images[0].id;
if (pcmd.TextureId != 0) {
bind.fs_images[0].id = (uint32_t)(uintptr_t)pcmd.TextureId;
sg_apply_bindings(&bind);
}
const int scissor_x = (int)(pcmd.ClipRect.x);
const int scissor_y = (int)(pcmd.ClipRect.y);
const int scissor_w = (int)(pcmd.ClipRect.z - pcmd.ClipRect.x);
const int scissor_h = (int)(pcmd.ClipRect.w - pcmd.ClipRect.y);
sg_apply_scissor_rect(scissor_x, scissor_y, scissor_w, scissor_h, true);
sg_draw(base_element, pcmd.ElemCount, 1);
if (pcmd.TextureId != 0) {
bind.fs_images[0].id = prev_fs_image_id;
sg_apply_bindings(&bind);
}
}
base_element += pcmd.ElemCount;
}
}
sg_end_pass();
}
void SokolImGuiState::Shutdown() {
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
}
SokolImGuiState gSokolImGuiState = {};
static void draw_imgui(ImDrawData*);
@@ -61,7 +254,6 @@ static void draw_imgui(ImDrawData*);
#include "ozz/animation/runtime/skeleton.h"
#include "ozz/base/containers/vector.h"
#include "ozz/base/io/archive.h"
#include "ozz/base/io/stream.h"
#include "ozz/base/log.h"
#include "ozz/base/maths/soa_transform.h"
@@ -72,7 +264,7 @@ static struct {
ozz::vector<ozz::math::SoaTransform> local_matrices;
} ozz;
sg_pass_action pass_action = {};
Camera camera;
Camera camera = {};
struct {
bool skeleton;
bool animation;
@@ -163,201 +355,92 @@ struct Viewport {
this->pass = sg_make_pass(&offscreen_pass_desc);
}
void Render() {
sg_begin_pass(pass, &pass_action);
sgl_load_pipeline(glpip);
sgl_draw();
sg_end_pass();
}
};
struct ApplicationConfig {
int window_position[2] = {100, 30};
int window_size[2] = {1000, 600};
constexpr static char TypeStr[] = "ApplicationConfig";
std::string type = TypeStr;
struct ScreenRect {
int position[2];
int size[2];
};
NLOHMANN_DEFINE_TYPE_INTRUSIVE(ScreenRect, position, size);
ScreenRect window = {.position = {100, 30}, .size = {1000, 600}};
struct GraphEditor {
ScreenRect rect = {.position = {20, 20}, .size = {800, 500}};
bool visible = false;
int position[2] = {20, 20};
int size[2] = {800, 500};
ax::NodeEditor::Config config = {};
ax::NodeEditor::EditorContext* context = nullptr;
};
NLOHMANN_DEFINE_TYPE_INTRUSIVE(GraphEditor, rect, visible);
GraphEditor graph_editor;
struct SkinnedMeshWidget {
struct WidgetRect {
ScreenRect rect;
bool visible = false;
int position[2] = {20, 20};
int size[2] = {800, 500};
};
SkinnedMeshWidget skinned_mesh_widget;
NLOHMANN_DEFINE_TYPE_INTRUSIVE(WidgetRect, rect, visible);
WidgetRect skinned_mesh_widget = {
.rect = {.position = {20, 20}, .size = {800, 500}},
.visible = false};
struct AnimationPlayerWidget {
bool visible = false;
int position[2] = {20, 20};
int size[2] = {800, 500};
};
AnimationPlayerWidget animation_player_widget;
WidgetRect animation_player_widget = {
.rect = {.position = {20, 20}, .size = {800, 500}},
.visible = false};
struct ViewportWidget {
bool visible = true;
int position[2] = {20, 20};
int size[2] = {800, 500};
};
ViewportWidget viewport_widget;
WidgetRect viewport_widget;
bool show_imgui_demo_window = false;
bool show_another_window = false;
bool show_camera_widget = false;
};
void to_json(nlohmann::json& j, const ApplicationConfig& config) {
j["type"] = "AnimTestbedConfig";
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(
ApplicationConfig,
type,
window,
graph_editor,
skinned_mesh_widget,
animation_player_widget,
viewport_widget,
show_imgui_demo_window,
show_another_window,
show_camera_widget);
j["main_window"]["position"][0] = config.window_position[0];
j["main_window"]["position"][1] = config.window_position[1];
struct ProjectConfig {
static constexpr char TypeStr[] = "ProjectConfig";
std::string type = TypeStr;
std::string animation_library_file = "";
std::string skeleton_file = "";
std::string graph_resource_file = "";
struct ViewConfig {
int pos[2];
};
ViewConfig view_config = {.pos = {300, 200}};
};
j["main_window"]["size"][0] = config.window_size[0];
j["main_window"]["size"][1] = config.window_size[1];
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(ProjectConfig::ViewConfig, pos);
j["graph_editor"]["visible"] = config.graph_editor.visible;
j["graph_editor"]["position"][0] = config.graph_editor.position[0];
j["graph_editor"]["position"][1] = config.graph_editor.position[1];
j["graph_editor"]["size"][0] = config.graph_editor.size[0];
j["graph_editor"]["size"][1] = config.graph_editor.size[1];
j["skinned_mesh_widget"]["visible"] = config.skinned_mesh_widget.visible;
j["skinned_mesh_widget"]["position"][0] =
config.skinned_mesh_widget.position[0];
j["skinned_mesh_widget"]["position"][1] =
config.skinned_mesh_widget.position[1];
j["skinned_mesh_widget"]["size"][0] = config.skinned_mesh_widget.size[0];
j["skinned_mesh_widget"]["size"][1] = config.skinned_mesh_widget.size[1];
j["animation_player_widget"]["visible"] =
config.animation_player_widget.visible;
j["animation_player_widget"]["position"][0] =
config.animation_player_widget.position[0];
j["animation_player_widget"]["position"][1] =
config.animation_player_widget.position[1];
j["animation_player_widget"]["size"][0] =
config.animation_player_widget.size[0];
j["animation_player_widget"]["size"][1] =
config.animation_player_widget.size[1];
j["viewport_widget"]["visible"] = config.viewport_widget.visible;
j["viewport_widget"]["position"][0] = config.viewport_widget.position[0];
j["viewport_widget"]["position"][1] = config.viewport_widget.position[1];
j["viewport_widget"]["size"][0] = config.viewport_widget.size[0];
j["viewport_widget"]["size"][1] = config.viewport_widget.size[1];
j["camera_widget"]["visible"] = config.show_camera_widget;
}
void from_json(const nlohmann::json& j, ApplicationConfig& config) {
if (j.contains("main_window")) {
if (j["main_window"].contains("position")
and j["main_window"]["position"].size() == 2) {
config.window_position[0] = j["main_window"]["position"].at(0);
config.window_position[1] = j["main_window"]["position"].at(1);
}
if (j["main_window"].contains("size")
and j["main_window"]["size"].size() == 2) {
config.window_size[0] = j["main_window"]["size"].at(0);
config.window_size[1] = j["main_window"]["size"].at(1);
}
}
if (j.contains("graph_editor")) {
if (j["graph_editor"].contains("visible")) {
config.graph_editor.visible = j["graph_editor"]["visible"];
}
if (j["graph_editor"].contains("position")
and j["graph_editor"]["position"].size() == 2) {
config.graph_editor.position[0] = j["graph_editor"]["position"].at(0);
config.graph_editor.position[1] = j["graph_editor"]["position"].at(1);
}
if (j["graph_editor"].contains("size")
and j["graph_editor"]["size"].size() == 2) {
config.graph_editor.size[0] = j["graph_editor"]["size"].at(0);
config.graph_editor.size[1] = j["graph_editor"]["size"].at(1);
}
}
if (j.contains("skinned_mesh_widget")) {
if (j["skinned_mesh_widget"].contains("visible")) {
config.skinned_mesh_widget.visible = j["skinned_mesh_widget"]["visible"];
}
if (j["skinned_mesh_widget"].contains("position")
and j["skinned_mesh_widget"]["position"].size() == 2) {
config.skinned_mesh_widget.position[0] =
j["skinned_mesh_widget"]["position"].at(0);
config.skinned_mesh_widget.position[1] =
j["skinned_mesh_widget"]["position"].at(1);
}
if (j["skinned_mesh_widget"].contains("size")
and j["skinned_mesh_widget"]["size"].size() == 2) {
config.skinned_mesh_widget.size[0] =
j["skinned_mesh_widget"]["size"].at(0);
config.skinned_mesh_widget.size[1] =
j["skinned_mesh_widget"]["size"].at(1);
}
}
if (j.contains("animation_player_widget")) {
if (j["animation_player_widget"].contains("visible")) {
config.animation_player_widget.visible =
j["animation_player_widget"]["visible"];
}
if (j["animation_player_widget"].contains("position")
and j["animation_player_widget"]["position"].size() == 2) {
config.animation_player_widget.position[0] =
j["animation_player_widget"]["position"].at(0);
config.animation_player_widget.position[1] =
j["animation_player_widget"]["position"].at(1);
}
if (j["animation_player_widget"].contains("size")
and j["animation_player_widget"]["size"].size() == 2) {
config.animation_player_widget.size[0] =
j["animation_player_widget"]["size"].at(0);
config.animation_player_widget.size[1] =
j["animation_player_widget"]["size"].at(1);
}
}
if (j.contains("viewport_widget")) {
if (j["viewport_widget"].contains("visible")) {
config.viewport_widget.visible = j["viewport_widget"]["visible"];
}
if (j["viewport_widget"].contains("position")
and j["viewport_widget"]["position"].size() == 2) {
config.viewport_widget.position[0] =
j["viewport_widget"]["position"].at(0);
config.viewport_widget.position[1] =
j["viewport_widget"]["position"].at(1);
}
if (j["viewport_widget"].contains("size")
and j["viewport_widget"]["size"].size() == 2) {
config.viewport_widget.size[0] = j["viewport_widget"]["size"].at(0);
config.viewport_widget.size[1] = j["viewport_widget"]["size"].at(1);
}
}
if (j.contains("camera_widget") and j["camera_widget"].contains("visible")) {
config.show_camera_widget = j["camera_widget"]["visible"];
}
}
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(
ProjectConfig,
type,
animation_library_file,
skeleton_file,
graph_resource_file,
view_config);
ApplicationConfig gApplicationConfig;
// io buffers for skeleton and animation data files, we know the max file size upfront
static uint8_t skel_data_buffer[4 * 1024];
static uint8_t anim_data_buffer[32 * 1024];
ProjectConfig gProjectConfig;
static void draw_grid();
static void frame();
void handle_mouse(GLFWwindow* w, GuiInputState* io_input_state) {
if (!glfwGetWindowAttrib(w, GLFW_FOCUSED)) {
@@ -374,7 +457,7 @@ void handle_mouse(GLFWwindow* w, GuiInputState* io_input_state) {
io_input_state->mousedX = 0;
io_input_state->mousedY = 0;
}
io_input_state->mouseX = int32_t(mouse_x);
io_input_state->mouseX = static_cast<int32_t>(mouse_x);
io_input_state->mouseY = int32_t(mouse_y);
io_input_state->mouseButton = glfwGetMouseButton(w, 0)
@@ -397,8 +480,8 @@ void load_application_config(const char* filename) {
}
if (application_config.value("type", "undefined") != "AnimTestbedConfig") {
std::cerr
<< "Invalid json object. Expected type 'AnimTestbedConfig' but got '"
std::cerr << "Invalid json object. Expected type '"
<< ApplicationConfig::TypeStr << "' but got '"
<< application_config["type"] << "'." << std::endl;
application_config["type"] = "AnimTestbedConfig";
}
@@ -433,7 +516,6 @@ void sokol_logger(
int main() {
// window and GL context via GLFW and flextGL
glfwInit();
const char* glsl_version = "#version 130";
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
@@ -446,20 +528,20 @@ int main() {
glfwSwapInterval(1);
load_application_config("animtestbed_config.json");
if (gApplicationConfig.window_position[0] != 0
|| gApplicationConfig.window_position[1] != 0) {
if (gApplicationConfig.window.position[0] != 0
|| gApplicationConfig.window.position[1] != 0) {
glfwSetWindowPos(
window,
gApplicationConfig.window_position[0],
gApplicationConfig.window_position[1]);
gApplicationConfig.window.position[0],
gApplicationConfig.window.position[1]);
}
if (gApplicationConfig.window_size[0] != 0
|| gApplicationConfig.window_size[1] != 0) {
if (gApplicationConfig.window.size[0] != 0
|| gApplicationConfig.window.size[1] != 0) {
glfwSetWindowSize(
window,
gApplicationConfig.window_size[0],
gApplicationConfig.window_size[1]);
gApplicationConfig.window.size[0],
gApplicationConfig.window.size[1]);
}
NFD_Init();
@@ -479,15 +561,11 @@ int main() {
sgl_setup(&sgldesc);
sgl_defaults();
// sgl_context_desc_t sgl_context_desc = {};
// sgl_context ctx = sgl_make_context(&sgl_context_desc);
SkinnedMeshResource skinned_mesh_resource;
skinned_mesh_resource.loadFromFile("../media/SampleSkinnedMesh.json");
SkinnedMesh skinned_mesh;
skinned_mesh_resource.createInstance(skinned_mesh);
skinned_mesh.SetCurrentAnimation(0);
AnimGraphBlendTree anim_graph;
AnimGraphContext anim_graph_context;
@@ -500,111 +578,8 @@ int main() {
Camera_Init(&state.camera);
// setup Dear Imgui
ImGui::CreateContext();
ImGui::StyleColorsDark();
gSokolImGuiState.Init(window);
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
io.IniFilename = "ATPImgui.ini";
//io.Fonts->AddFontDefault();
ImFontConfig font_config;
font_config.OversampleH = 4;
font_config.OversampleV = 4;
font_config.GlyphExtraSpacing.x = 1.0f;
io.Fonts->AddFontFromMemoryCompressedTTF(
// roboto_medium_ttf_compressed_data,
// roboto_medium_ttf_compressed_size,
droid_sans_ttf_compressed_data,
droid_sans_ttf_compressed_size,
14,
&font_config);
ImGui_ImplGlfw_InitForOpenGL(window, true);
ImGui_ImplOpenGL3_Init(glsl_version);
// ImNodes
ImNodes::CreateContext();
// dynamic vertex- and index-buffers for imgui-generated geometry
sg_buffer_desc vbuf_desc = {};
vbuf_desc.usage = SG_USAGE_STREAM;
vbuf_desc.size = MaxVertices * sizeof(ImDrawVert);
bind.vertex_buffers[0] = sg_make_buffer(&vbuf_desc);
sg_buffer_desc ibuf_desc = {};
ibuf_desc.type = SG_BUFFERTYPE_INDEXBUFFER;
ibuf_desc.usage = SG_USAGE_STREAM;
ibuf_desc.size = MaxIndices * sizeof(ImDrawIdx);
bind.index_buffer = sg_make_buffer(&ibuf_desc);
// font texture for imgui's default font
unsigned char* font_pixels;
int font_width, font_height;
io.Fonts->GetTexDataAsRGBA32(&font_pixels, &font_width, &font_height);
sg_image_desc img_desc = {};
img_desc.width = font_width;
img_desc.height = font_height;
img_desc.pixel_format = SG_PIXELFORMAT_RGBA8;
img_desc.wrap_u = SG_WRAP_CLAMP_TO_EDGE;
img_desc.wrap_v = SG_WRAP_CLAMP_TO_EDGE;
img_desc.data.subimage[0][0] =
sg_range{font_pixels, size_t(font_width * font_height * 4)};
bind.fs_images[0] = sg_make_image(&img_desc);
// shader object for imgui rendering
sg_shader_desc shd_desc = {};
auto& ub = shd_desc.vs.uniform_blocks[0];
ub.size = sizeof(vs_params_t);
ub.uniforms[0].name = "disp_size";
ub.uniforms[0].type = SG_UNIFORMTYPE_FLOAT2;
shd_desc.vs.source =
"#version 330\n"
"uniform vec2 disp_size;\n"
"layout(location=0) in vec2 position;\n"
"layout(location=1) in vec2 texcoord0;\n"
"layout(location=2) in vec4 color0;\n"
"out vec2 uv;\n"
"out vec4 color;\n"
"void main() {\n"
" gl_Position = vec4(((position/disp_size)-0.5)*vec2(2.0,-2.0), 0.5, "
"1.0);\n"
" uv = texcoord0;\n"
" color = color0;\n"
"}\n";
shd_desc.fs.images[0].name = "tex";
shd_desc.fs.images[0].image_type = SG_IMAGETYPE_2D;
shd_desc.fs.source =
"#version 330\n"
"uniform sampler2D tex;\n"
"in vec2 uv;\n"
"in vec4 color;\n"
"out vec4 frag_color;\n"
"void main() {\n"
" frag_color = texture(tex, uv) * color;\n"
"}\n";
sg_shader shd = sg_make_shader(&shd_desc);
// pipeline object for imgui rendering
sg_pipeline_desc pip_desc = {};
pip_desc.layout.buffers[0].stride = sizeof(ImDrawVert);
auto& attrs = pip_desc.layout.attrs;
attrs[0].format = SG_VERTEXFORMAT_FLOAT2;
attrs[1].format = SG_VERTEXFORMAT_FLOAT2;
attrs[2].format = SG_VERTEXFORMAT_UBYTE4N;
pip_desc.shader = shd;
pip_desc.index_type = SG_INDEXTYPE_UINT16;
pip_desc.colors[0].blend.enabled = true;
pip_desc.colors[0].blend.src_factor_rgb = SG_BLENDFACTOR_SRC_ALPHA;
pip_desc.colors[0].blend.dst_factor_rgb = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
pip_desc.colors[0].write_mask = SG_COLORMASK_RGB;
pip_desc.sample_count = cMSAASampleCount;
pip_desc.label = "imgui-rendering";
pip = sg_make_pipeline(&pip_desc);
// initial clear color
pass_action.colors[0].action = SG_ACTION_CLEAR;
pass_action.colors[0].value = {0.1f, 0.1f, 0.1f, 1.0f};
Viewport offscreen_viewport;
@@ -635,13 +610,13 @@ int main() {
// Update window state
glfwGetWindowPos(
window,
&gApplicationConfig.window_position[0],
&gApplicationConfig.window_position[1]);
&gApplicationConfig.window.position[0],
&gApplicationConfig.window.position[1]);
glfwGetWindowSize(
window,
&gApplicationConfig.window_size[0],
&gApplicationConfig.window_size[1]);
&gApplicationConfig.window.size[0],
&gApplicationConfig.window.size[1]);
int cur_width, cur_height;
glfwGetFramebufferSize(window, &cur_width, &cur_height);
@@ -769,14 +744,16 @@ int main() {
ImGui::SetNextWindowPos(
ImVec2(
static_cast<float>(
gApplicationConfig.viewport_widget.position[0]),
gApplicationConfig.viewport_widget.rect.position[0]),
static_cast<float>(
gApplicationConfig.viewport_widget.position[1])),
gApplicationConfig.viewport_widget.rect.position[1])),
ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(
ImVec2(
static_cast<float>(gApplicationConfig.viewport_widget.size[0]),
static_cast<float>(gApplicationConfig.viewport_widget.size[1])),
static_cast<float>(
gApplicationConfig.viewport_widget.rect.size[0]),
static_cast<float>(
gApplicationConfig.viewport_widget.rect.size[1])),
ImGuiCond_FirstUseEver);
ImGui::Begin("Viewport", &gApplicationConfig.viewport_widget.visible);
@@ -791,9 +768,9 @@ int main() {
}
ImVec2 viewport_widget_size = ImGui::GetWindowSize();
gApplicationConfig.viewport_widget.size[0] =
gApplicationConfig.viewport_widget.rect.size[0] =
static_cast<int>(viewport_widget_size.x);
gApplicationConfig.viewport_widget.size[1] =
gApplicationConfig.viewport_widget.rect.size[1] =
static_cast<int>(viewport_widget_size.y);
ImGui::Text(
@@ -841,16 +818,16 @@ int main() {
ImGui::SetNextWindowPos(
ImVec2(
static_cast<float>(
gApplicationConfig.skinned_mesh_widget.position[0]),
gApplicationConfig.skinned_mesh_widget.rect.position[0]),
static_cast<float>(
gApplicationConfig.skinned_mesh_widget.position[1])),
gApplicationConfig.skinned_mesh_widget.rect.position[1])),
ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(
ImVec2(
static_cast<float>(
gApplicationConfig.skinned_mesh_widget.size[0]),
gApplicationConfig.skinned_mesh_widget.rect.size[0]),
static_cast<float>(
gApplicationConfig.skinned_mesh_widget.size[1])),
gApplicationConfig.skinned_mesh_widget.rect.size[1])),
ImGuiCond_FirstUseEver);
ImGui::Begin(
@@ -858,15 +835,15 @@ int main() {
&gApplicationConfig.skinned_mesh_widget.visible);
ImVec2 skinned_mesh_widget_position = ImGui::GetWindowPos();
gApplicationConfig.skinned_mesh_widget.position[0] =
gApplicationConfig.skinned_mesh_widget.rect.position[0] =
static_cast<int>(skinned_mesh_widget_position.x);
gApplicationConfig.skinned_mesh_widget.position[1] =
gApplicationConfig.skinned_mesh_widget.rect.position[1] =
static_cast<int>(skinned_mesh_widget_position.y);
ImVec2 skinned_mesh_widget_size = ImGui::GetWindowSize();
gApplicationConfig.skinned_mesh_widget.size[0] =
gApplicationConfig.skinned_mesh_widget.rect.size[0] =
static_cast<int>(skinned_mesh_widget_size.x);
gApplicationConfig.skinned_mesh_widget.size[1] =
gApplicationConfig.skinned_mesh_widget.rect.size[1] =
static_cast<int>(skinned_mesh_widget_size.y);
SkinnedMeshWidget(&skinned_mesh);
@@ -877,17 +854,17 @@ int main() {
if (gApplicationConfig.animation_player_widget.visible) {
ImGui::SetNextWindowPos(
ImVec2(
static_cast<float>(
gApplicationConfig.animation_player_widget.position[0]),
static_cast<float>(
gApplicationConfig.animation_player_widget.position[1])),
static_cast<float>(gApplicationConfig.animation_player_widget
.rect.position[0]),
static_cast<float>(gApplicationConfig.animation_player_widget
.rect.position[1])),
ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(
ImVec2(
static_cast<float>(
gApplicationConfig.animation_player_widget.size[0]),
gApplicationConfig.animation_player_widget.rect.size[0]),
static_cast<float>(
gApplicationConfig.animation_player_widget.size[1])),
gApplicationConfig.animation_player_widget.rect.size[1])),
ImGuiCond_FirstUseEver);
ImGui::Begin(
@@ -895,15 +872,15 @@ int main() {
&gApplicationConfig.animation_player_widget.visible);
ImVec2 animation_player_widget_position = ImGui::GetWindowPos();
gApplicationConfig.animation_player_widget.position[0] =
gApplicationConfig.animation_player_widget.rect.position[0] =
static_cast<int>(animation_player_widget_position.x);
gApplicationConfig.animation_player_widget.position[1] =
gApplicationConfig.animation_player_widget.rect.position[1] =
static_cast<int>(animation_player_widget_position.y);
ImVec2 animation_player_widget_size = ImGui::GetWindowSize();
gApplicationConfig.animation_player_widget.size[0] =
gApplicationConfig.animation_player_widget.rect.size[0] =
static_cast<int>(animation_player_widget_size.x);
gApplicationConfig.animation_player_widget.size[1] =
gApplicationConfig.animation_player_widget.rect.size[1] =
static_cast<int>(animation_player_widget_size.y);
if (anim_graph.m_nodes.size() > 0) {
@@ -1005,13 +982,13 @@ int main() {
if (gApplicationConfig.graph_editor.visible) {
ImGui::SetNextWindowPos(
ImVec2(
gApplicationConfig.graph_editor.position[0],
gApplicationConfig.graph_editor.position[1]),
gApplicationConfig.graph_editor.rect.position[0],
gApplicationConfig.graph_editor.rect.position[1]),
ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(
ImVec2(
gApplicationConfig.graph_editor.size[0],
gApplicationConfig.graph_editor.size[1]),
gApplicationConfig.graph_editor.rect.size[0],
gApplicationConfig.graph_editor.rect.size[1]),
ImGuiCond_FirstUseEver);
ImGui::Begin(
@@ -1020,12 +997,14 @@ int main() {
ImGuiWindowFlags_MenuBar);
ImVec2 graph_editor_position = ImGui::GetWindowPos();
gApplicationConfig.graph_editor.position[0] = graph_editor_position.x;
gApplicationConfig.graph_editor.position[1] = graph_editor_position.y;
gApplicationConfig.graph_editor.rect.position[0] =
graph_editor_position.x;
gApplicationConfig.graph_editor.rect.position[1] =
graph_editor_position.y;
ImVec2 graph_editor_size = ImGui::GetWindowSize();
gApplicationConfig.graph_editor.size[0] = graph_editor_size.x;
gApplicationConfig.graph_editor.size[1] = graph_editor_size.y;
gApplicationConfig.graph_editor.rect.size[0] = graph_editor_size.x;
gApplicationConfig.graph_editor.rect.size[1] = graph_editor_size.y;
AnimGraphEditorUpdate(gApplicationConfig.graph_editor.context);
@@ -1038,16 +1017,12 @@ int main() {
ImGui::ShowDemoWindow();
}
sg_begin_pass(offscreen_viewport.pass, &offscreen_viewport.pass_action);
sgl_load_pipeline(offscreen_viewport.glpip);
sgl_draw();
sg_end_pass();
offscreen_viewport.Render();
// Rendering of the main gui
sg_begin_default_pass(&pass_action, cur_width, cur_height);
ImGui::Render();
draw_imgui(ImGui::GetDrawData());
sg_end_pass();
gSokolImGuiState.Render(ImGui::GetDrawData(), cur_width, cur_height);
sg_commit();
glfwSwapBuffers(window);
@@ -1060,8 +1035,7 @@ int main() {
ax::NodeEditor::DestroyEditor(gApplicationConfig.graph_editor.context);
ImNodes::DestroyContext();
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
gSokolImGuiState.Shutdown();
ImGui::DestroyContext();
@@ -1074,54 +1048,6 @@ int main() {
return 0;
}
bool LoadSkeleton(const char* _filename, ozz::animation::Skeleton* _skeleton) {
assert(_filename && _skeleton);
ozz::log::Out() << "Loading skeleton archive " << _filename << "."
<< std::endl;
ozz::io::File file(_filename, "rb");
if (!file.opened()) {
ozz::log::Err() << "Failed to open skeleton file " << _filename << "."
<< std::endl;
return false;
}
ozz::io::IArchive archive(&file);
if (!archive.TestTag<ozz::animation::Skeleton>()) {
ozz::log::Err() << "Failed to load skeleton instance from file "
<< _filename << "." << std::endl;
return false;
}
// Once the tag is validated, reading cannot fail.
archive >> *_skeleton;
return true;
}
bool LoadAnimation(
const char* _filename,
ozz::animation::Animation* _animation) {
assert(_filename && _animation);
ozz::log::Out() << "Loading animation archive: " << _filename << "."
<< std::endl;
ozz::io::File file(_filename, "rb");
if (!file.opened()) {
ozz::log::Err() << "Failed to open animation file " << _filename << "."
<< std::endl;
return false;
}
ozz::io::IArchive archive(&file);
if (!archive.TestTag<ozz::animation::Animation>()) {
ozz::log::Err() << "Failed to load animation instance from file "
<< _filename << "." << std::endl;
return false;
}
// Once the tag is validated, reading cannot fail.
archive >> *_animation;
return true;
}
static void draw_vec(const ozz::math::SimdFloat4& vec) {
sgl_v3f(ozz::math::GetX(vec), ozz::math::GetY(vec), ozz::math::GetZ(vec));
}
@@ -1172,70 +1098,3 @@ static void draw_grid(void) {
sgl_end();
}
// draw ImGui draw lists via sokol-gfx
void draw_imgui(ImDrawData* draw_data) {
assert(draw_data);
if (draw_data->CmdListsCount == 0) {
return;
}
sg_image default_image = bind.fs_images[0];
// render the command list
sg_apply_pipeline(pip);
vs_params_t vs_params;
vs_params.disp_size.x = ImGui::GetIO().DisplaySize.x;
vs_params.disp_size.y = ImGui::GetIO().DisplaySize.y;
sg_apply_uniforms(SG_SHADERSTAGE_VS, 0, SG_RANGE(vs_params));
for (int cl_index = 0; cl_index < draw_data->CmdListsCount; cl_index++) {
const ImDrawList* cl = draw_data->CmdLists[cl_index];
// append vertices and indices to buffers, record start offsets in resource binding struct
const uint32_t vtx_size = cl->VtxBuffer.size() * sizeof(ImDrawVert);
const uint32_t idx_size = cl->IdxBuffer.size() * sizeof(ImDrawIdx);
const uint32_t vb_offset = sg_append_buffer(
bind.vertex_buffers[0],
{&cl->VtxBuffer.front(), vtx_size});
const uint32_t ib_offset =
sg_append_buffer(bind.index_buffer, {&cl->IdxBuffer.front(), idx_size});
/* don't render anything if the buffer is in overflow state (this is also
checked internally in sokol_gfx, draw calls that attempt from
overflowed buffers will be silently dropped)
*/
if (sg_query_buffer_overflow(bind.vertex_buffers[0])
|| sg_query_buffer_overflow(bind.index_buffer)) {
continue;
}
bind.vertex_buffer_offsets[0] = vb_offset;
bind.index_buffer_offset = ib_offset;
sg_apply_bindings(&bind);
int base_element = 0;
for (const ImDrawCmd& pcmd : cl->CmdBuffer) {
if (pcmd.UserCallback) {
pcmd.UserCallback(cl, &pcmd);
} else {
uint32_t prev_fs_image_id = bind.fs_images[0].id;
if (pcmd.TextureId != 0) {
bind.fs_images[0].id = (uint32_t)(uintptr_t)pcmd.TextureId;
sg_apply_bindings(&bind);
}
const int scissor_x = (int)(pcmd.ClipRect.x);
const int scissor_y = (int)(pcmd.ClipRect.y);
const int scissor_w = (int)(pcmd.ClipRect.z - pcmd.ClipRect.x);
const int scissor_h = (int)(pcmd.ClipRect.w - pcmd.ClipRect.y);
sg_apply_scissor_rect(scissor_x, scissor_y, scissor_w, scissor_h, true);
sg_draw(base_element, pcmd.ElemCount, 1);
if (pcmd.TextureId != 0) {
bind.fs_images[0].id = prev_fs_image_id;
sg_apply_bindings(&bind);
}
}
base_element += pcmd.ElemCount;
}
}
}
+45
View File
@@ -0,0 +1,45 @@
//
// Created by martin on 11.04.25.
//
#include "3rdparty/json/json.hpp"
#include "AnimGraph/AnimLibrary.h"
#include "TestAnimData.h"
#include "catch.hpp"
using namespace nlohmann;
TEST_CASE("Serialize AnimLibrary", "[AnimLibrary]") {
AnimLibrary library;
TestAnimData::SingleBoneSkeleton single_bone_testdata;
REQUIRE(library.AddAnimationFile(
"translation_x",
single_bone_testdata.animation_translate_x_resource.m_filename));
REQUIRE(library.AddAnimationFile(
"translation_y",
single_bone_testdata.animation_translate_y_resource.m_filename));
// We're not actually doing anything with the animations, however loading them here ensures that
// when running valgrind a memory leak is detected.
library.LoadAnimations();
// serialize
json library_data;
library_data = library;
// deserialize
AnimLibrary library_deserialized;
library_deserialized = library_data;
CHECK(library_deserialized.mAnimations.size() == library.mAnimations.size());
for (AnimLibrary::AnimationResourceMap::const_iterator iter =
library.mAnimations.cbegin();
iter != library.mAnimations.cend();
++iter) {
CHECK(
library_deserialized.mAnimations.find(iter->first)
!= library_deserialized.mAnimations.end());
}
}
+15 -6
View File
@@ -18,10 +18,15 @@
struct SimpleAnimFixture {
ozz::unique_ptr<ozz::animation::Skeleton> skeleton = nullptr;
ozz::animation::offline::RawAnimation raw_animation_translation_x;
ozz::unique_ptr<ozz::animation::Animation> animation_translate_x = nullptr;
SyncTrack animation_translate_x_sync_track = {};
ozz::animation::offline::RawAnimation raw_animation_translation_y;
ozz::unique_ptr<ozz::animation::Animation> animation_translate_y = nullptr;
SyncTrack animation_translate_y_sync_track = {};
ozz::vector<ozz::math::SoaTransform> animation_output;
ozz::animation::SamplingJob::Context sampling_context;
@@ -170,10 +175,6 @@ TEST_CASE_METHOD(
AnimNodeResource* blend_node = blend_tree_resource->GetNode(blend_node_index);
blend_node->m_name = "BlendWalkRun";
// Setup graph outputs and inputs
AnimNodeResource* graph_output_node =
blend_tree_resource->GetGraphOutputNode();
blend_tree_resource->RegisterBlendTreeInputSocket<float>("GraphFloatInput");
// Wire up nodes
@@ -197,8 +198,16 @@ TEST_CASE_METHOD(
// Prepare animation maps
AnimGraphContext graph_context;
graph_context.m_skeleton = skeleton.get();
graph_context.m_animation_map["trans_x"] = animation_translate_x.get();
graph_context.m_animation_map["trans_y"] = animation_translate_y.get();
graph_context.m_animation_map["trans_x"] = {
"trans_x",
"",
animation_translate_x.get(),
animation_translate_x_sync_track};
graph_context.m_animation_map["trans_y"] = {
"trans_y",
"",
animation_translate_y.get(),
animation_translate_y_sync_track};
// Instantiate graph
AnimGraphBlendTree blend_tree;
+128 -97
View File
@@ -2,201 +2,232 @@
// Created by martin on 16.11.21.
//
#include "SyncTrack.h"
#include "AnimGraph/SyncTrack.h"
#include "catch.hpp"
TEST_CASE("Basic", "[SyncTrack]") {
SyncTrack track_A;
track_A.m_num_intervals = 2;
track_A.m_duration = 2.0;
track_A.m_interval_start[0] = 0.f;
track_A.m_interval_ratio[0] = 0.7;
track_A.m_interval_start[1] = 0.7f;
track_A.m_interval_ratio[1] = 0.3;
SyncTrack track_a;
track_a.m_num_intervals = 2;
track_a.m_duration = 2.0;
track_a.m_interval_start_ratio[0] = 0.f;
track_a.m_interval_duration_ratio[0] = 0.7;
track_a.m_interval_start_ratio[1] = 0.7f;
track_a.m_interval_duration_ratio[1] = 0.3;
SyncTrack track_B;
track_B.m_num_intervals = 2;
track_B.m_duration = 1.5;
track_B.m_interval_start[0] = 0.0f;
track_B.m_interval_ratio[0] = 0.6;
track_B.m_interval_start[1] = 0.6f;
track_B.m_interval_ratio[1] = 0.4;
SyncTrack track_b;
track_b.m_num_intervals = 2;
track_b.m_duration = 1.5;
track_b.m_interval_start_ratio[0] = 0.0f;
track_b.m_interval_duration_ratio[0] = 0.6;
track_b.m_interval_start_ratio[1] = 0.6f;
track_b.m_interval_duration_ratio[1] = 0.4;
WHEN("Calculating sync time of track_B at 0.5 duration") {
float sync_time_at_0_75 =
track_B.CalcSyncFromAbsTime(0.5 * track_B.m_duration);
track_b.CalcSyncFromAbsTime(0.5 * track_b.m_duration);
REQUIRE(sync_time_at_0_75 == Catch::Detail::Approx(0.83333));
}
WHEN("Calculating sync time of track_B at 0.6 duration") {
float sync_time_at_0_6 =
track_B.CalcSyncFromAbsTime(0.6 * track_B.m_duration);
track_b.CalcSyncFromAbsTime(0.6 * track_b.m_duration);
REQUIRE(sync_time_at_0_6 == Catch::Detail::Approx(1.0));
}
WHEN("Calculating sync time of track_B at 0.7 duration") {
float sync_time_at_0_7 =
track_B.CalcSyncFromAbsTime(0.7 * track_B.m_duration);
track_b.CalcSyncFromAbsTime(0.7 * track_b.m_duration);
REQUIRE(sync_time_at_0_7 == Catch::Detail::Approx(1.25));
}
WHEN("Calculating sync time of track_B at 0.0 duration") {
float sync_time_at_1_0 =
track_B.CalcSyncFromAbsTime(0.0 * track_B.m_duration);
track_b.CalcSyncFromAbsTime(0.0 * track_b.m_duration);
REQUIRE(sync_time_at_1_0 == Catch::Detail::Approx(0.0));
}
WHEN("Calculating sync time of track_B at 1.0 duration") {
float sync_time_at_1_0 =
track_B.CalcSyncFromAbsTime(0.9999 * track_B.m_duration);
track_b.CalcSyncFromAbsTime(0.9999 * track_b.m_duration);
REQUIRE(sync_time_at_1_0 == Catch::Detail::Approx(2.0).epsilon(0.001f));
}
WHEN("Calculating ratio from sync time on track_A at 0.83333") {
float ratio = track_A.CalcRatioFromSyncTime(0.83333333);
float ratio = track_a.CalcRatioFromSyncTime(0.83333333);
REQUIRE(ratio == Catch::Detail::Approx(0.5833333));
}
WHEN("Calculating ratio from sync time on track_A at 0.83333") {
float ratio = track_A.CalcRatioFromSyncTime(1.25);
float ratio = track_a.CalcRatioFromSyncTime(1.25);
REQUIRE(ratio == Catch::Detail::Approx(0.775));
}
WHEN("Blending two synctracks with weight 0.") {
SyncTrack blended = SyncTrack::Blend(0.f, track_A, track_B);
SyncTrack blended = SyncTrack::Blend(0.f, track_a, track_b);
THEN("Result must equal track_A") { REQUIRE(track_A == blended); }
THEN("Result must equal track_A") { REQUIRE(track_a == blended); }
}
WHEN("Blending two synctracks with weight 1.") {
SyncTrack blended = SyncTrack::Blend(1.f, track_A, track_B);
SyncTrack blended = SyncTrack::Blend(1.f, track_a, track_b);
THEN("Result must equal track_B") { REQUIRE(track_B == blended); }
THEN("Result must equal track_B") { REQUIRE(track_b == blended); }
}
}
TEST_CASE("Sync Marker Interval Calculation", "[SyncTrack]") {
SyncTrack track_A;
track_A.m_num_intervals = 2;
track_A.m_duration = 2.0;
track_A.m_sync_markers[0] = 0.9;
track_A.m_sync_markers[1] = 0.2;
TEST_CASE("Sync Track From Marker", "[SyncTrack]") {
SyncTrack track = SyncTrack::CreateFromMarkers(2.0f, {0.9f, 0.2f});
WHEN("Calculating intervals") {
track_A.CalcIntervals();
WHEN("Querying Ratios") {
CHECK(track.m_interval_start_ratio[0] == Catch::Detail::Approx(0.45f));
CHECK(track.m_interval_duration_ratio[0] == Catch::Detail::Approx(0.65f));
CHECK(track_A.m_interval_start[0] == Catch::Detail::Approx(0.9f));
CHECK(track_A.m_interval_ratio[0] == Catch::Detail::Approx(0.3f));
CHECK(track.m_interval_start_ratio[1] == Catch::Detail::Approx(0.1f));
CHECK(track.m_interval_duration_ratio[1] == Catch::Detail::Approx(0.35f));
CHECK(track_A.m_interval_start[1] == Catch::Detail::Approx(0.2f));
CHECK(track_A.m_interval_ratio[1] == Catch::Detail::Approx(0.7f));
WHEN("Querying ratio at sync time at 0.001") {
float ratio = track.CalcRatioFromSyncTime(0.0001f);
CHECK(ratio == Catch::Detail::Approx(0.45).epsilon(0.001));
}
WHEN("Querying ratio at sync time at 1.001") {
float ratio = track_A.CalcRatioFromSyncTime(1.0001f);
CHECK(ratio == Catch::Detail::Approx(0.2).epsilon(0.001));
WHEN("Querying ratio at sync time at 0.9999") {
float ratio = track.CalcRatioFromSyncTime(0.9999f);
CHECK(ratio == Catch::Detail::Approx(0.1).epsilon(0.001));
}
WHEN("Querying ratio at sync time at 1.001") {
float ratio = track_A.CalcRatioFromSyncTime(0.0001f);
CHECK(ratio == Catch::Detail::Approx(0.9).epsilon(0.001));
float ratio = track.CalcRatioFromSyncTime(1.0001f);
CHECK(ratio == Catch::Detail::Approx(0.1).epsilon(0.001));
}
WHEN("Querying ratio at sync time at 1.9999") {
float ratio = track_A.CalcRatioFromSyncTime(0.9999f);
CHECK(ratio == Catch::Detail::Approx(0.2).epsilon(0.001));
float ratio = track.CalcRatioFromSyncTime(1.9999f);
CHECK(ratio == Catch::Detail::Approx(0.45).epsilon(0.001));
}
}
WHEN("Blending sync track with 3 events") {
track_A.m_num_intervals = 3;
track_A.m_duration = 2.0;
track_A.m_sync_markers[0] = 0.;
track_A.m_sync_markers[1] = 0.3;
track_A.m_sync_markers[2] = 0.9;
track_A.CalcIntervals();
WHEN("Querying SyncTime from Absolute Time") {
WHEN("Querying absolute time at 0.9001s") {
float sync_time = track.CalcSyncFromAbsTime(0.9001f);
CHECK_THAT(sync_time, Catch::WithinAbs(0.0, 0.001));
}
SyncTrack track_B;
track_B.m_num_intervals = 3;
track_B.m_duration = 1.5;
track_B.m_sync_markers[0] = 0.7;
track_B.m_sync_markers[1] = 0.9;
track_B.m_sync_markers[2] = 0.2;
track_B.CalcIntervals();
WHEN("Querying absolute time at 0.2001s") {
float sync_time = track.CalcSyncFromAbsTime(0.2001f);
CHECK_THAT(sync_time, Catch::WithinAbs(1.0, 0.001));
}
WHEN("Querying absolute time at 0.8999s") {
float sync_time = track.CalcSyncFromAbsTime(0.8999f);
CHECK_THAT(sync_time, Catch::WithinAbs(1.999, 0.001));
}
WHEN("Querying absolute time at 1.9999s") {
float sync_time = track.CalcSyncFromAbsTime(1.9999f);
CHECK_THAT(sync_time, Catch::WithinAbs(0.84615384, 0.001));
}
}
}
TEST_CASE("Sync Track Blending", "[SyncTrack]") {
SyncTrack track_a = SyncTrack::CreateFromMarkers(2.0, {0., 0.6, 1.8});
SyncTrack track_b = SyncTrack::CreateFromMarkers(1.5f, {1.05, 1.35, 0.3});
WHEN("Calculating A's durations") {
CHECK(track_A.m_interval_ratio[0] == Catch::Detail::Approx(0.3));
CHECK(track_A.m_interval_ratio[1] == Catch::Detail::Approx(0.6));
CHECK(track_A.m_interval_ratio[2] == Catch::Detail::Approx(0.1));
CHECK(track_a.m_interval_duration_ratio[0] == Catch::Detail::Approx(0.3));
CHECK(track_a.m_interval_duration_ratio[1] == Catch::Detail::Approx(0.6));
CHECK(track_a.m_interval_duration_ratio[2] == Catch::Detail::Approx(0.1));
}
WHEN("Calculating B's durations") {
CHECK(track_B.m_interval_ratio[0] == Catch::Detail::Approx(0.2));
CHECK(track_B.m_interval_ratio[1] == Catch::Detail::Approx(0.3));
CHECK(track_B.m_interval_ratio[2] == Catch::Detail::Approx(0.5));
CHECK(track_b.m_interval_duration_ratio[0] == Catch::Detail::Approx(0.2));
CHECK(track_b.m_interval_duration_ratio[1] == Catch::Detail::Approx(0.3));
CHECK(track_b.m_interval_duration_ratio[2] == Catch::Detail::Approx(0.5));
}
WHEN("Blending two synctracks with weight 0.") {
SyncTrack blended = SyncTrack::Blend(0.f, track_A, track_B);
SyncTrack blended = SyncTrack::Blend(0.f, track_a, track_b);
THEN("Result must equal track_A") { REQUIRE(track_A == blended); }
THEN("Result must equal track_A") { REQUIRE(track_a == blended); }
}
WHEN("Blending two synctracks with weight 1.") {
SyncTrack blended = SyncTrack::Blend(1.f, track_A, track_B);
SyncTrack blended = SyncTrack::Blend(1.f, track_a, track_b);
THEN("Result must equal track_B") { REQUIRE(track_B == blended); }
THEN("Result must equal track_B") { REQUIRE(track_b == blended); }
}
WHEN("Blending with weight 0.2") {
float weight = 0.2f;
SyncTrack blended = SyncTrack::Blend(weight, track_A, track_B);
SyncTrack blended = SyncTrack::Blend(weight, track_a, track_b);
REQUIRE(
blended.m_duration
== (1.0f - weight) * track_A.m_duration
+ weight * track_B.m_duration);
== (1.0f - weight) * track_a.m_duration + weight * track_b.m_duration);
REQUIRE(
blended.m_interval_start[0]
blended.m_interval_start_ratio[0]
== fmodf(
(1.0f - weight) * (track_A.m_interval_start[0] + 1.0f)
+ weight * (track_B.m_interval_start[0]),
(1.0f - weight) * (track_a.m_interval_start_ratio[0] + 1.0f)
+ weight * (track_b.m_interval_start_ratio[0]),
1.0f));
REQUIRE(
blended.m_interval_ratio[1]
== (1.0f - weight) * (track_A.m_interval_ratio[1])
+ weight * (track_B.m_interval_ratio[1])
);
blended.m_interval_duration_ratio[1]
== (1.0f - weight) * (track_a.m_interval_duration_ratio[1])
+ weight * (track_b.m_interval_duration_ratio[1]));
REQUIRE(
blended.m_interval_ratio[2]
== (1.0f - weight) * (track_A.m_interval_ratio[2])
+ weight * (track_B.m_interval_ratio[2])
);
blended.m_interval_duration_ratio[2]
== (1.0f - weight) * (track_a.m_interval_duration_ratio[2])
+ weight * (track_b.m_interval_duration_ratio[2]));
}
WHEN("Inverted blending with weight 0.2") {
float weight = 0.2f;
SyncTrack blended = SyncTrack::Blend(weight, track_B, track_A);
SyncTrack blended = SyncTrack::Blend(weight, track_b, track_a);
REQUIRE(
blended.m_duration
== (1.0f - weight) * track_B.m_duration
+ weight * track_A.m_duration);
== (1.0f - weight) * track_b.m_duration + weight * track_a.m_duration);
REQUIRE(
blended.m_interval_start[0]
blended.m_interval_start_ratio[0]
== fmodf(
(1.0f - weight) * (track_B.m_interval_start[0])
+ weight * (track_A.m_interval_start[0] + 1.0f),
(1.0f - weight) * (track_b.m_interval_start_ratio[0])
+ weight * (track_a.m_interval_start_ratio[0] + 1.0f),
1.0f));
REQUIRE(
blended.m_interval_ratio[1]
== (1.0f - weight) * (track_B.m_interval_ratio[1])
+ weight * (track_A.m_interval_ratio[1])
);
blended.m_interval_duration_ratio[1]
== (1.0f - weight) * (track_b.m_interval_duration_ratio[1])
+ weight * (track_a.m_interval_duration_ratio[1]));
REQUIRE(
blended.m_interval_ratio[2]
== (1.0f - weight) * (track_B.m_interval_ratio[2])
+ weight * (track_A.m_interval_ratio[2])
);
blended.m_interval_duration_ratio[2]
== (1.0f - weight) * (track_b.m_interval_duration_ratio[2])
+ weight * (track_a.m_interval_duration_ratio[2]));
}
}
TEST_CASE("Serialization", "[SyncTrack]") {
SyncTrack track;
track.m_num_intervals = 3;
track.m_duration = 2.0;
track.m_interval_start_ratio[0] = 0.f;
track.m_interval_duration_ratio[0] = 0.7;
track.m_interval_start_ratio[1] = 0.7f;
track.m_interval_duration_ratio[1] = 0.3;
track.m_interval_start_ratio[2] = 0.7f;
track.m_interval_duration_ratio[2] = 0.3;
nlohmann::json synctrack_json = track;
const SyncTrack synctrack_deserialized = synctrack_json;
CHECK(synctrack_deserialized.m_duration == track.m_duration);
CHECK(synctrack_deserialized.m_num_intervals == track.m_num_intervals);
for (int i = 0; i < track.m_num_intervals; i++) {
CHECK(
synctrack_deserialized.m_interval_start_ratio[i]
== track.m_interval_start_ratio[i]);
CHECK(
synctrack_deserialized.m_interval_duration_ratio[i]
== track.m_interval_duration_ratio[i]);
}
}
+117
View File
@@ -0,0 +1,117 @@
//
// Created by martin on 11.04.25.
//
#include "TestAnimData.h"
#include <iostream>
#include "ozz/animation/offline/animation_builder.h"
#include "ozz/animation/offline/raw_animation.h"
#include "ozz/animation/offline/raw_skeleton.h"
#include "ozz/base/io/archive.h"
#include "ozz/base/log.h"
namespace TestAnimData {
SingleBoneSkeleton::SingleBoneSkeleton() {
using namespace ozz::animation::offline;
RawSkeleton raw_skeleton;
RawSkeleton::Joint raw_joint;
raw_joint.name = "Bone0";
raw_joint.transform.translation.x = 1.f;
raw_joint.transform.translation.y = 2.f;
raw_joint.transform.translation.z = 3.f;
raw_skeleton.roots.push_back(raw_joint);
SkeletonBuilder skeleton_builder;
skeleton = skeleton_builder(raw_skeleton);
// SingleBoneSkeleton Animations
ozz::animation::offline::RawAnimation raw_animation_translation_x;
raw_animation_translation_x.name = "TranslationX";
RawAnimation::JointTrack bone0_track;
RawAnimation::JointTrack::Translations bone0_translations;
// animation_translate_x
RawAnimation::TranslationKey translation_key;
translation_key.time = 0.f;
translation_key.value = ozz::math::Float3(0.f, 0.f, 0.f);
bone0_translations.push_back(translation_key);
translation_key.time = 1.f;
translation_key.value = ozz::math::Float3(1.f, 0.f, 0.f);
bone0_translations.push_back(translation_key);
bone0_track.translations = bone0_translations;
raw_animation_translation_x.tracks.push_back(bone0_track);
raw_animation_translation_x.duration = 1.f;
if (!raw_animation_translation_x.Validate()) {
std::cerr << "Error: could animation raw data invalid!" << std::endl;
}
AnimationBuilder animation_builder;
animation_translate_x = animation_builder(raw_animation_translation_x);
animation_translate_x_resource.m_animation = animation_translate_x.get();
SaveAnimation("single_bone_translation_z.ozz", animation_translate_x.get());
animation_translate_x_resource.m_name = "single_bone_translation_z";
animation_translate_x_resource.m_filename = "single_bone_translation_z.ozz";
// animation_translate_y
ozz::animation::offline::RawAnimation raw_animation_translation_y;
raw_animation_translation_y.name = "TranslationY";
bone0_translations.clear();
translation_key.time = 0.f;
translation_key.value = ozz::math::Float3(0.f, 0.f, 0.f);
bone0_translations.push_back(translation_key);
translation_key.time = 1.f;
translation_key.value = ozz::math::Float3(0.f, 1.f, 0.f);
bone0_translations.push_back(translation_key);
bone0_track.translations = bone0_translations;
raw_animation_translation_y.tracks.push_back(bone0_track);
raw_animation_translation_y.duration = 1.f;
if (!raw_animation_translation_y.Validate()) {
std::cerr << "Error: could animation raw data invalid!" << std::endl;
}
animation_translate_y = animation_builder(raw_animation_translation_y);
animation_translate_y_resource.m_animation = animation_translate_y.get();
SaveAnimation("single_bone_translation_y.ozz", animation_translate_y.get());
animation_translate_y_resource.m_name = "single_bone_translation_y";
animation_translate_y_resource.m_filename = "single_bone_translation_y.ozz";
}
bool SingleBoneSkeleton::SaveSkeleton(
const char* filename,
ozz::animation::Skeleton* skeleton) {
assert(false);
return false;
}
bool SingleBoneSkeleton::SaveAnimation(
const char* filename,
ozz::animation::Animation* animation) {
ozz::io::File file(filename, "wb");
if (!file.opened()) {
ozz::log::Err() << "Failed to create animation file " << filename << "."
<< std::endl;
delete animation;
return false;
}
ozz::io::OArchive archive(&file);
archive << *animation;
return true;
}
} // namespace TestAnimData
+37
View File
@@ -0,0 +1,37 @@
//
// Created by martin on 11.04.25.
//
#ifndef TESTANIMDATA_H
#define TESTANIMDATA_H
#include "AnimGraph/AnimGraphData.h"
#include "AnimGraph/SyncTrack.h"
#include "ozz/animation/offline/skeleton_builder.h"
#include "ozz/animation/runtime/animation.h"
#include "ozz/animation/runtime/skeleton.h"
namespace TestAnimData {
struct SingleBoneSkeleton {
SingleBoneSkeleton();
ozz::unique_ptr<ozz::animation::Skeleton> skeleton = nullptr;
ozz::unique_ptr<ozz::animation::Animation> animation_translate_x = nullptr;
AnimationResource animation_translate_x_resource;
SyncTrack animation_translate_x_sync_track = {};
ozz::unique_ptr<ozz::animation::Animation> animation_translate_y = nullptr;
AnimationResource animation_translate_y_resource;
SyncTrack animation_translate_y_sync_track = {};
bool SaveSkeleton(const char* filename, ozz::animation::Skeleton* skeleton);
bool SaveAnimation(
const char* filename,
ozz::animation::Animation* animation);
};
} // namespace TestAnimData
#endif //TESTANIMDATA_H