├── .gitignore ├── CMakeLists.txt ├── LICENSE ├── README.md ├── fips ├── fips.cmd ├── fips.yml └── src ├── Anim ├── Anim.cc ├── Anim.h ├── AnimTypes.h ├── CMakeLists.txt ├── README.md ├── UnitTests │ ├── AnimLibraryTest.cc │ ├── AnimSkeletonTest.cc │ └── animSequencerTest.cc └── private │ ├── animInstance.h │ ├── animMgr.cc │ ├── animMgr.h │ ├── animSequencer.cc │ └── animSequencer.h └── CMakeLists.txt /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | #>fips 3 | # this area is managed by fips, do not edit 4 | .fips-* 5 | *.pyc 6 | #(); 26 | state->animSetup = setup; 27 | state->mgr.setup(setup); 28 | } 29 | 30 | //------------------------------------------------------------------------------ 31 | void 32 | Anim::Discard() { 33 | o_assert_dbg(IsValid()); 34 | state->mgr.discard(); 35 | Memory::Delete(state); 36 | state = nullptr; 37 | } 38 | 39 | //------------------------------------------------------------------------------ 40 | bool 41 | Anim::IsValid() { 42 | return (nullptr != state); 43 | } 44 | 45 | //------------------------------------------------------------------------------ 46 | double 47 | Anim::CurrentTime() { 48 | o_assert_dbg(IsValid()); 49 | return state->mgr.curTime; 50 | } 51 | 52 | //------------------------------------------------------------------------------ 53 | ResourceLabel 54 | Anim::PushLabel() { 55 | o_assert_dbg(IsValid()); 56 | return state->mgr.resContainer.PushLabel(); 57 | } 58 | 59 | //------------------------------------------------------------------------------ 60 | void 61 | Anim::PushLabel(ResourceLabel label) { 62 | o_assert_dbg(IsValid()); 63 | state->mgr.resContainer.PushLabel(label); 64 | } 65 | 66 | //------------------------------------------------------------------------------ 67 | ResourceLabel 68 | Anim::PopLabel() { 69 | o_assert_dbg(IsValid()); 70 | return state->mgr.resContainer.PopLabel(); 71 | } 72 | 73 | //------------------------------------------------------------------------------ 74 | template<> Id 75 | Anim::Create(const AnimLibrarySetup& setup) { 76 | o_assert_dbg(IsValid()); 77 | return state->mgr.createLibrary(setup); 78 | } 79 | 80 | //------------------------------------------------------------------------------ 81 | template<> Id 82 | Anim::Create(const AnimSkeletonSetup& setup) { 83 | o_assert_dbg(IsValid()); 84 | return state->mgr.createSkeleton(setup); 85 | } 86 | 87 | //------------------------------------------------------------------------------ 88 | template<> Id 89 | Anim::Create(const AnimInstanceSetup& setup) { 90 | o_assert_dbg(IsValid()); 91 | return state->mgr.createInstance(setup); 92 | } 93 | 94 | //------------------------------------------------------------------------------ 95 | Id 96 | Anim::Lookup(const Locator& name) { 97 | o_assert_dbg(IsValid()); 98 | return state->mgr.resContainer.Lookup(name); 99 | } 100 | 101 | //------------------------------------------------------------------------------ 102 | void 103 | Anim::Destroy(ResourceLabel label) { 104 | o_assert_dbg(IsValid()); 105 | return state->mgr.destroy(label); 106 | } 107 | 108 | //------------------------------------------------------------------------------ 109 | bool 110 | Anim::HasLibrary(const Id& libId) { 111 | o_assert_dbg(IsValid()); 112 | return nullptr != state->mgr.lookupLibrary(libId); 113 | } 114 | 115 | //------------------------------------------------------------------------------ 116 | const AnimLibrary& 117 | Anim::Library(const Id& libId) { 118 | o_assert_dbg(IsValid()); 119 | const AnimLibrary* lib = state->mgr.lookupLibrary(libId); 120 | if (lib) { 121 | return *lib; 122 | } 123 | else { 124 | static AnimLibrary dummyLib; 125 | return dummyLib; 126 | } 127 | } 128 | 129 | //------------------------------------------------------------------------------ 130 | void 131 | Anim::WriteKeys(const Id& libId, const uint8_t* ptr, int numBytes) { 132 | o_assert_dbg(IsValid()); 133 | AnimLibrary* lib = state->mgr.lookupLibrary(libId); 134 | if (lib) { 135 | state->mgr.writeKeys(lib, ptr, numBytes); 136 | } 137 | else { 138 | o_warn("Anim::WriteKeys: invalid anim lib id\n"); 139 | } 140 | } 141 | 142 | //------------------------------------------------------------------------------ 143 | bool 144 | Anim::HasSkeleton(const Id& skelId) { 145 | o_assert_dbg(IsValid()); 146 | return nullptr != state->mgr.lookupSkeleton(skelId); 147 | } 148 | 149 | //------------------------------------------------------------------------------ 150 | const AnimSkeleton& 151 | Anim::Skeleton(const Id& skelId) { 152 | o_assert_dbg(IsValid()); 153 | const AnimSkeleton* skel = state->mgr.lookupSkeleton(skelId); 154 | if (skel) { 155 | return *skel; 156 | } 157 | else { 158 | static AnimSkeleton dummySkel; 159 | return dummySkel; 160 | } 161 | } 162 | 163 | //------------------------------------------------------------------------------ 164 | void 165 | Anim::NewFrame() { 166 | o_assert_dbg(IsValid()); 167 | state->mgr.newFrame(); 168 | } 169 | 170 | //------------------------------------------------------------------------------ 171 | bool 172 | Anim::AddActiveInstance(const Id& instId) { 173 | o_assert_dbg(IsValid()); 174 | animInstance* inst = state->mgr.lookupInstance(instId); 175 | if (inst) { 176 | return state->mgr.addActiveInstance(inst); 177 | } 178 | else { 179 | return false; 180 | } 181 | } 182 | 183 | //------------------------------------------------------------------------------ 184 | void 185 | Anim::Evaluate(double frameDurationInSeconds) { 186 | o_assert_dbg(IsValid()); 187 | state->mgr.evaluate(frameDurationInSeconds); 188 | } 189 | 190 | //------------------------------------------------------------------------------ 191 | const Slice& 192 | Anim::Samples(const Id& instId) { 193 | o_assert_dbg(IsValid()); 194 | animInstance* inst = state->mgr.lookupInstance(instId); 195 | if (inst) { 196 | return inst->samples; 197 | } 198 | else { 199 | static Slice dummySlice; 200 | return dummySlice; 201 | } 202 | } 203 | 204 | //------------------------------------------------------------------------------ 205 | const AnimSkinMatrixInfo& 206 | Anim::SkinMatrixInfo() { 207 | o_assert_dbg(IsValid()); 208 | return state->mgr.skinMatrixInfo; 209 | } 210 | 211 | //------------------------------------------------------------------------------ 212 | AnimJobId 213 | Anim::Play(const Id& instId, const AnimJob& job) { 214 | o_assert_dbg(IsValid()); 215 | animInstance* inst = state->mgr.lookupInstance(instId); 216 | if (inst) { 217 | return state->mgr.play(inst, job); 218 | } 219 | else { 220 | return InvalidAnimJobId; 221 | } 222 | } 223 | 224 | //------------------------------------------------------------------------------ 225 | void 226 | Anim::Stop(const Id& instId, AnimJobId jobId, bool allowFadeOut) { 227 | o_assert_dbg(IsValid()); 228 | animInstance* inst = state->mgr.lookupInstance(instId); 229 | if (inst) { 230 | state->mgr.stop(inst, jobId, allowFadeOut); 231 | } 232 | } 233 | 234 | //------------------------------------------------------------------------------ 235 | void 236 | Anim::StopTrack(const Id& instId, int trackIndex, bool allowFadeOut) { 237 | o_assert_dbg(IsValid()); 238 | animInstance* inst = state->mgr.lookupInstance(instId); 239 | if (inst) { 240 | state->mgr.stopTrack(inst, trackIndex, allowFadeOut); 241 | } 242 | } 243 | 244 | //------------------------------------------------------------------------------ 245 | void 246 | Anim::StopAll(const Id& instId, bool allowFadeOut) { 247 | o_assert_dbg(IsValid()); 248 | animInstance* inst = state->mgr.lookupInstance(instId); 249 | if (inst) { 250 | state->mgr.stopAll(inst, allowFadeOut); 251 | } 252 | } 253 | 254 | //------------------------------------------------------------------------------ 255 | const animInstance& 256 | Anim::instance(const Id& instId) { 257 | o_assert_dbg(IsValid()); 258 | const animInstance* inst = state->mgr.lookupInstance(instId); 259 | if (inst) { 260 | return *inst; 261 | } 262 | else { 263 | static animInstance dummyInst; 264 | return dummyInst; 265 | } 266 | } 267 | 268 | } // namespace Oryol 269 | -------------------------------------------------------------------------------- /src/Anim/Anim.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | //------------------------------------------------------------------------------ 3 | /** 4 | @class Oryol::Anim 5 | @ingroup Anim 6 | @brief animation system facade 7 | */ 8 | #include "Anim/AnimTypes.h" 9 | #include "Anim/private/animInstance.h" 10 | #include "Resource/ResourceLabel.h" 11 | #include "Resource/Locator.h" 12 | #include "Core/Time/Duration.h" 13 | 14 | namespace Oryol { 15 | 16 | class Anim { 17 | public: 18 | /// setup the animation module 19 | static void Setup(const AnimSetup& setup = AnimSetup()); 20 | /// discard the animation module 21 | static void Discard(); 22 | /// check if animation module is setup 23 | static bool IsValid(); 24 | /// get the original AnimSetup object 25 | static const struct AnimSetup& AnimSetup(); 26 | /// get the animation systems current absolute time 27 | static double CurrentTime(); 28 | 29 | /// generate new resource label and push on label stack 30 | static ResourceLabel PushLabel(); 31 | /// push explicit resource label on label stack 32 | static void PushLabel(ResourceLabel label); 33 | /// pop resource label from label stack 34 | static ResourceLabel PopLabel(); 35 | 36 | /// create an anim resource object 37 | template static Id Create(const SETUP& setup); 38 | /// lookup an resource id by name 39 | static Id Lookup(const Locator& name); 40 | /// destroy one or several anim resources by label 41 | static void Destroy(ResourceLabel label); 42 | 43 | /// return true if a valid anim library exists for id 44 | static bool HasLibrary(const Id& libId); 45 | /// access an animation library 46 | static const AnimLibrary& Library(const Id& libId); 47 | /// lookup a clip index by name 48 | static int ClipIndex(const Id& libId, const StringAtom& clipName); 49 | /// write anim library keys 50 | static void WriteKeys(const Id& libId, const uint8_t* ptr, int numBytes); 51 | 52 | /// return true if a valid anim skeleton exists for id 53 | static bool HasSkeleton(const Id& skelId); 54 | /// access a skeleton 55 | static const AnimSkeleton& Skeleton(const Id& skelId); 56 | 57 | /// begin new frame, clears all active instances 58 | static void NewFrame(); 59 | /// add an active instance for the current frame 60 | static bool AddActiveInstance(const Id& instId); 61 | /// evaluate all active animation instances 62 | static void Evaluate(double frameDurationInSeconds); 63 | /// access to current samples of an active anim instance (valid after Anim::Evaluate()) 64 | static const Slice& Samples(const Id& instId); 65 | /// access to evaluated skeleton skinning matrix info 66 | static const AnimSkinMatrixInfo& SkinMatrixInfo(); 67 | 68 | /// enqueue an animation job, return job id 69 | static AnimJobId Play(const Id& instId, const AnimJob& job); 70 | /// stop a specific animation job 71 | static void Stop(const Id& instId, AnimJobId jobId, bool allowFadeOut=true); 72 | /// stop all jobs on a mixing track 73 | static void StopTrack(const Id& instId, int trackIndex, bool allowFadeOut=true); 74 | /// stop all jobs 75 | static void StopAll(const Id& instId, bool allowFadeOut=true); 76 | 77 | /// access to anim instance 78 | static const _priv::animInstance& instance(const Id& instId); 79 | }; 80 | 81 | } // namespace Oryol 82 | 83 | -------------------------------------------------------------------------------- /src/Anim/AnimTypes.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | //------------------------------------------------------------------------------ 3 | #include "Core/Types.h" 4 | #include "Core/String/StringAtom.h" 5 | #include "Core/Containers/Array.h" 6 | #include "Core/Containers/StaticArray.h" 7 | #include "Core/Containers/InlineArray.h" 8 | #include "Core/Containers/Map.h" 9 | #include "Resource/ResourceBase.h" 10 | #include "Resource/Locator.h" 11 | #include 12 | #include 13 | #include 14 | 15 | namespace Oryol { 16 | 17 | //------------------------------------------------------------------------------ 18 | /** 19 | @class Oryol::AnimConfig 20 | @ingroup Anim 21 | @brief animation system config constants 22 | */ 23 | struct AnimConfig { 24 | /// max number of bones in a skeleton 25 | static const int MaxNumSkeletonBones = 256; 26 | /// max number of curves in a clip 27 | static const int MaxNumCurvesInClip = MaxNumSkeletonBones * 3; 28 | }; 29 | 30 | //------------------------------------------------------------------------------ 31 | /** 32 | @class Oryol::AnimSetup 33 | @ingroup Anim 34 | @brief setup parameters for Anim module 35 | */ 36 | struct AnimSetup { 37 | /// max number of anim libraries 38 | int MaxNumLibs = 16; 39 | /// max number of skeleton 40 | int MaxNumSkeletons = 16; 41 | /// max overall number of anim instances 42 | int MaxNumInstances = 128; 43 | /// max number of active instances per frame 44 | int MaxNumActiveInstances = 128; 45 | /// max overall number of anim clips 46 | int ClipPoolCapacity = MaxNumLibs * 64; 47 | /// max overall number of anim curves 48 | int CurvePoolCapacity = ClipPoolCapacity * 256; 49 | /// max capacity of the (input) key pool in number of floats 50 | int KeyPoolCapacity = 4 * 1024 * 1024; 51 | /// max number of samples for visible instances (in number of floats) 52 | int SamplePoolCapacity = 4 * 1024 * 1024; 53 | /// max number of the skeleton matrix pool (2 matrices per bone) 54 | int MatrixPoolCapacity = 1024; 55 | /// skinning-matrix table width in number of vec4's 56 | int SkinMatrixTableWidth = 1024; 57 | /// skinning-matrix table height 58 | int SkinMatrixTableHeight = 64; 59 | /// initial resource label stack capacity 60 | int ResourceLabelStackCapacity = 256; 61 | /// initial resource registry capacity 62 | int ResourceRegistryCapacity = 256; 63 | }; 64 | 65 | //------------------------------------------------------------------------------ 66 | /** 67 | @class Oryol::AnimCurveFormat 68 | @ingroup Anim 69 | @brief format of anim keys 70 | */ 71 | struct AnimCurveFormat { 72 | enum Enum { 73 | Float, ///< 1 key, linear interpolation 74 | Float2, ///< 2 keys, linear interpolation 75 | Float3, ///< 3 keys, linear interpolation 76 | Float4, ///< 4 keys, linear interpolation 77 | Quaternion, ///< 4 keys, spherical interpolation 78 | Invalid, 79 | }; 80 | 81 | /// return number of floats for a format 82 | static int Stride(AnimCurveFormat::Enum fmt) { 83 | switch (fmt) { 84 | case Float: return 1; 85 | case Float2: return 2; 86 | case Float3: return 3; 87 | case Float4: return 4; 88 | case Quaternion: return 4; 89 | default: return 0; 90 | } 91 | } 92 | }; 93 | 94 | //------------------------------------------------------------------------------ 95 | /** 96 | @class Oryol::AnimCurveSetup 97 | @ingroup Anim 98 | @brief describe a single anim curve in an anim clip 99 | */ 100 | struct AnimCurveSetup { 101 | /// true if the curve is actually a single value 102 | bool Static = false; 103 | /// the default value of the curve 104 | glm::vec4 StaticValue; 105 | /// the max magnitude of keys in the curve (used to unpack key values) 106 | glm::vec4 Magnitude; 107 | 108 | /// default constructor 109 | AnimCurveSetup() { }; 110 | /// construct from values 111 | AnimCurveSetup(bool isStatic, float x, float y, float z, float w): 112 | Static(isStatic), StaticValue(x, y, z, w) { }; 113 | }; 114 | 115 | //------------------------------------------------------------------------------ 116 | /** 117 | @class Oryol::AnimClipSetup 118 | @ingroup Anim 119 | @brief describe an animation clip, part of AnimLibrarySetup 120 | */ 121 | struct AnimClipSetup { 122 | /// name of the anim clip (must be unique in library) 123 | StringAtom Name; 124 | /// the length of the clip in number of keys 125 | int Length = 0; 126 | /// the time duration from one key to next in seconds 127 | double KeyDuration = 1.0 / 25.0; 128 | /// a description of each curve in the clip 129 | Array Curves; 130 | 131 | /// default constructor 132 | AnimClipSetup() { }; 133 | /// construct from values 134 | AnimClipSetup(const StringAtom& name, int len, float dur, const Array& curves): 135 | Name(name), Length(len), KeyDuration(dur), Curves(curves) { }; 136 | }; 137 | 138 | //------------------------------------------------------------------------------ 139 | /** 140 | @class Oryol::AnimLibrarySetup 141 | @ingroup Anim 142 | @brief describe an animation library 143 | 144 | An animation library is a collection of compatible clips (clip 145 | with the same anim curve layout). 146 | */ 147 | struct AnimLibrarySetup { 148 | /// resource locator for sharing 149 | class Locator Locator = Locator::NonShared(); 150 | /// number and format of curves (must be identical for all clips) 151 | Array CurveLayout; 152 | /// the anim clips in the library 153 | Array Clips; 154 | }; 155 | 156 | //------------------------------------------------------------------------------ 157 | /** 158 | @class Oryol::AnimBoneSetup 159 | @ingroup Anim 160 | @brief setup params for an animation bone 161 | */ 162 | struct AnimBoneSetup { 163 | /// the bone's name 164 | StringAtom Name; 165 | /// index of parent joint, InvalidIndex if a root joint 166 | int16_t ParentIndex = InvalidIndex; 167 | /// the normal (non-inverse) bind pose matrix in model space 168 | glm::mat4 BindPose; 169 | /// the inverse bind pose matrix in model space 170 | glm::mat4 InvBindPose; 171 | 172 | /// default constructor 173 | AnimBoneSetup() { }; 174 | /// construct from params 175 | AnimBoneSetup(const StringAtom& name, int parentIndex, const glm::mat4& bindPose, const glm::mat4& invBindPose): 176 | Name(name), ParentIndex(parentIndex), BindPose(bindPose), InvBindPose(invBindPose) { }; 177 | }; 178 | 179 | //------------------------------------------------------------------------------ 180 | /** 181 | @class Oryol::AnimSkeletonSetup 182 | @ingroup Anim 183 | @brief setup params for a character skeleton 184 | */ 185 | struct AnimSkeletonSetup { 186 | /// locator for resource sharing 187 | class Locator Locator = Locator::NonShared(); 188 | /// the skeleton bones 189 | Array Bones; 190 | }; 191 | 192 | //------------------------------------------------------------------------------ 193 | /** 194 | @class Oryol::AnimInstanceSetup 195 | @ingroup Anim 196 | @brief setup params for an animation instance 197 | */ 198 | struct AnimInstanceSetup { 199 | /// create AnimInstanceSetup from AnimLibrary Id 200 | static AnimInstanceSetup FromLibrary(Id libId) { 201 | AnimInstanceSetup setup; 202 | setup.Library = libId; 203 | return setup; 204 | }; 205 | /// create AnimInstanceSetup from AnimLibrary and AnimSkeleton Id 206 | static AnimInstanceSetup FromLibraryAndSkeleton(Id libId, Id skelId) { 207 | AnimInstanceSetup setup; 208 | setup.Library = libId; 209 | setup.Skeleton = skelId; 210 | return setup; 211 | }; 212 | 213 | /// the AnimLibrary of this instance 214 | Id Library; 215 | /// an optional AnimSkeleton if this is an instance 216 | Id Skeleton; 217 | }; 218 | 219 | //------------------------------------------------------------------------------ 220 | /** 221 | @class Oryol::AnimCurve 222 | @ingroup Anim 223 | @brief an animation curve (part of a clip) 224 | */ 225 | struct AnimCurve { 226 | /// the curve format 227 | AnimCurveFormat::Enum Format = AnimCurveFormat::Invalid; 228 | /// number of values (1, 2, 3 or 4) 229 | int NumValues = 0; 230 | /// is the curve static? (no actual keys in key pool) 231 | bool Static = false; 232 | /// the static value if the curve has no keys 233 | float StaticValue[4]; 234 | /// the key magnitude (for unpacking keys) 235 | float Magnitude[4]; 236 | /// stride in key elements (according to format) 237 | int KeyStride = 0; 238 | /// index of the first key in key pool (relative to clip) 239 | int KeyIndex = InvalidIndex; 240 | }; 241 | 242 | //------------------------------------------------------------------------------ 243 | /** 244 | @class Oryol::AnimClip 245 | @ingroup Anim 246 | @brief an animation clip (part of a library) 247 | */ 248 | struct AnimClip { 249 | /// name of the clip 250 | StringAtom Name; 251 | /// the length of the clip in number of keys 252 | int Length = 0; 253 | /// the time duration from one key to next 254 | double KeyDuration = 1.0f / 25.0f; 255 | /// the stride in key elements from one key of a curve to next in key pool 256 | int KeyStride = 0; 257 | /// access to the clip's curves 258 | Slice Curves; 259 | /// access to the clip's 2D key table 260 | Slice Keys; 261 | }; 262 | 263 | //------------------------------------------------------------------------------ 264 | /** 265 | @class Oryol::AnimLibrary 266 | @ingroup Anim 267 | @brief a collection of clips with the same curve layout 268 | */ 269 | struct AnimLibrary : public ResourceBase { 270 | /// resource locator (name + sig) 271 | class Locator Locator; 272 | /// stride of per-instance samples in number of floats 273 | int SampleStride = 0; 274 | /// access to all clips in the library 275 | Slice Clips; 276 | /// array view over all curves of all clips 277 | Slice Curves; 278 | /// array view over all keys of all clips 279 | Slice Keys; 280 | /// map clip names to clip indices 281 | Map ClipIndexMap; 282 | /// the curve layout (all clips in the library have the same layout) 283 | InlineArray CurveLayout; 284 | 285 | /// clear the object 286 | void clear() { 287 | Locator = Locator::NonShared(); 288 | SampleStride = 0; 289 | Clips.Reset(); 290 | Curves.Reset(); 291 | Keys.Reset(); 292 | ClipIndexMap.Clear(); 293 | }; 294 | }; 295 | 296 | //------------------------------------------------------------------------------ 297 | /** 298 | @class Oryol::AnimSkeleton 299 | @ingroup Anim 300 | @brief runtime struct for an animation skeleton 301 | */ 302 | struct AnimSkeleton : public ResourceBase { 303 | /// resource locator (name + sig) 304 | class Locator Locator; 305 | /// number of bones in the skeleton 306 | int NumBones = 0; 307 | /// the bind pose matrices (non-inverse) 308 | Slice BindPose; 309 | /// the inverse bind pose matrices 310 | Slice InvBindPose; 311 | /// this is the range of all matrices (BindPose and InvBindPose) 312 | Slice Matrices; 313 | /// the parent bone indices (-1 if a root bone) 314 | StaticArray ParentIndices; 315 | 316 | /// clear the object 317 | void clear() { 318 | Locator = Locator::NonShared(); 319 | NumBones = 0; 320 | BindPose.Reset(); 321 | InvBindPose.Reset(); 322 | Matrices.Reset(); 323 | }; 324 | }; 325 | 326 | //------------------------------------------------------------------------------ 327 | /** 328 | @typedef Oryol::AnimJobId 329 | @ingroup Anim 330 | @brief identifies a playing anim job 331 | */ 332 | typedef uint32_t AnimJobId; 333 | static const AnimJobId InvalidAnimJobId = 0xFFFFFFFF; 334 | 335 | //------------------------------------------------------------------------------ 336 | /** 337 | @class Oryol::AnimJob 338 | @ingroup Anim 339 | @brief describe play parameters for an animation 340 | */ 341 | struct AnimJob { 342 | /// index of anim clip to play 343 | int ClipIndex = 0; 344 | /// the track index for priority blending (higher tracks have higher priority) 345 | int TrackIndex = 0; 346 | /// overall weight when mixing with lower-priority track 347 | float MixWeight = 1.0f; 348 | /// start time relative to 'now' in seconds 349 | float StartTime = 0.0f; 350 | /// playback duration or loop count (<= 0.0f is infinite) 351 | float Duration = 0.0f; 352 | /// true if Duration is loop count, false if Duration is seconds 353 | bool DurationIsLoopCount = false; 354 | /// fade-in duration in seconds 355 | float FadeIn = 0.0f; 356 | /// fade-out duration in seconds 357 | float FadeOut = 0.0f; 358 | }; 359 | 360 | //------------------------------------------------------------------------------ 361 | /** 362 | @class Oryol::AnimSkinMatrixInfo 363 | @ingroup Anim 364 | @brief the evaluated skinning matrix data 365 | 366 | This contains the evaluated skin-matrix information for all 367 | active AnimInstances in the current frame. The per-instance 368 | items are in the same order how active AnimInstances had 369 | been added, but only contains AnimInstances with skeletons. 370 | */ 371 | struct AnimSkinMatrixInfo { 372 | /// pointer to the skin-matrix table 373 | const float* SkinMatrixTable = nullptr; 374 | /// size of valid information in the skin matrix table in bytes 375 | int SkinMatrixTableByteSize = 0; 376 | /// per-instance information 377 | struct InstanceInfo { 378 | Id Instance; 379 | glm::vec4 ShaderInfo; // x: u texcoord, y: v texcoord, z: 1.0/texwidth 380 | }; 381 | /// one entry per active anim instance 382 | Array InstanceInfos; 383 | }; 384 | 385 | } // namespace Oryol 386 | -------------------------------------------------------------------------------- /src/Anim/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | fips_begin_module(Anim) 2 | fips_vs_warning_level(3) 3 | fips_files( 4 | Anim.h Anim.cc 5 | AnimTypes.h 6 | ) 7 | fips_dir(private) 8 | fips_files( 9 | animMgr.h animMgr.cc 10 | animSequencer.h animSequencer.cc 11 | animInstance.h 12 | ) 13 | fips_deps(Core Resource) 14 | fips_end_module() 15 | 16 | oryol_begin_unittest(Anim) 17 | fips_vs_warning_level(3) 18 | fips_dir(UnitTests) 19 | fips_files( 20 | AnimLibraryTest.cc 21 | AnimSkeletonTest.cc 22 | animSequencerTest.cc 23 | ) 24 | fips_deps(Anim) 25 | oryol_end_unittest() 26 | 27 | -------------------------------------------------------------------------------- /src/Anim/README.md: -------------------------------------------------------------------------------- 1 | ## Animation Module 2 | 3 | ### Planning 4 | 5 | - one big pool for all animation keys 6 | - keys -> curves -> clips -> libraries 7 | - keys are 1D floats, the animation system doesn't know about points, vectors, quaternions, colors, etc... 8 | - this means that quaternions are *not* spherical interpolated, so rotation keys must be near each other 9 | - ...if this turns out to be a problem, rotation keys must be somehow identified (though a ClipLayout?) 10 | - a curve is a 'column of keys' 11 | - a clip is a 'row of curves', or a 2D table of keys 12 | - a library is a collection of clips with the same number of curves: 13 | - a slice is the group of keys at the same point in time 14 | 15 | ``` 16 | an Anim Library with 3 clips and 10 curves: 17 | +===+===+===+===+===+===+===+===+===+===+ Clip 0 18 | | | | | | | | | | | | -> a slice of keys 19 | +---+---+---+---+---+---+---+---+---+---+ at time 0 20 | | | | | | | | | | | | 21 | +---+---+---+---+---+---+---+---+---+---+ 22 | | | | | | | | | | | | 23 | +---+---+---+---+---+---+---+---+---+---+ 24 | | | | | | | | | | | | 25 | +---+---+---+---+---+---+---+---+---+---+ 26 | | | | | | | | | | | | 27 | +===+===+===+===+===+===+===+===+===+===+ Clip 1 28 | | | | | | | | | | | | 29 | +---+---+---+---+---+---+---+---+---+---+ 30 | | | | | | | | | | | | 31 | +---+---+---+---+---+---+---+---+---+---+ 32 | | | | | | | | | | | | 33 | +---+---+---+---+---+---+---+---+---+---+ 34 | | | | | | | | | | | | 35 | +===+===+===+===+===+===+===+===+===+===+ Clip 2 36 | | | | | | | | | | | | 37 | +---+---+---+---+---+---+---+---+---+---+ 38 | | | | | | | | | | | | 39 | +---+---+---+---+---+---+---+---+---+---+ 40 | | | | | | | | | | | | 41 | +---+---+---+---+---+---+---+---+---+---+ 42 | | | | | | | | | | | | 43 | +---+---+---+---+---+---+---+---+---+---+ 44 | | | | | | | | | | | | 45 | +---+---+---+---+---+---+---+---+---+---+ 46 | | | | | | | | | | | | 47 | +---+---+---+---+---+---+---+---+---+---+ 48 | | | | | | | | | | | | 49 | +===+===+===+===+===+===+===+===+===+===+ 50 | ``` 51 | 52 | Hmm, what about curves that are not actually animated... these 53 | are quite common (often only the rotation in a character skeleton 54 | is animated for instance...) 55 | 56 | -------------------------------------------------------------------------------- /src/Anim/UnitTests/AnimLibraryTest.cc: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // animMgrTest.cc 3 | //------------------------------------------------------------------------------ 4 | #include "Pre.h" 5 | #include "UnitTest++/src/UnitTest++.h" 6 | #include "Anim/AnimTypes.h" 7 | #include "Anim/private/animMgr.h" 8 | 9 | using namespace Oryol; 10 | using namespace _priv; 11 | 12 | TEST(AnimLibraryTest) { 13 | 14 | // setup 15 | AnimSetup setup; 16 | setup.MaxNumLibs = 4; 17 | setup.ClipPoolCapacity = 16; 18 | setup.CurvePoolCapacity = 128; 19 | setup.KeyPoolCapacity = 1024; 20 | setup.ResourceLabelStackCapacity = 16; 21 | setup.ResourceRegistryCapacity = 24; 22 | animMgr mgr; 23 | mgr.setup(setup); 24 | CHECK(mgr.isValid); 25 | CHECK(mgr.resContainer.IsValid()); 26 | CHECK(mgr.resContainer.registry.IsValid()); 27 | CHECK(mgr.libPool.IsValid()); 28 | CHECK(mgr.clipPool.Capacity() == 16); 29 | CHECK(mgr.curvePool.Capacity() == 128); 30 | CHECK(mgr.keys.Size() == 1024); 31 | CHECK(mgr.keys.Offset() == 0); 32 | CHECK(mgr.numKeys == 0); 33 | CHECK(mgr.valuePool != nullptr); 34 | 35 | AnimLibrarySetup libSetup; 36 | libSetup.Locator = "human"; 37 | libSetup.CurveLayout = { 38 | AnimCurveFormat::Float2, 39 | AnimCurveFormat::Float3, 40 | AnimCurveFormat::Float4 41 | }; 42 | libSetup.Clips = { 43 | { "clip1", 10, 0.04f, 44 | { 45 | { false, 1.0f, 2.0f, 3.0f, 4.0f }, 46 | { false, 5.0f, 6.0f, 7.0f, 8.0f }, 47 | { true, 9.0f, 10.0f, 11.0f, 12.0f }, 48 | } 49 | }, 50 | { "clip2", 20, 0.04f, 51 | { 52 | { true, 4.0f, 3.0f, 2.0f, 1.0f }, 53 | { false, 8.0f, 7.0f, 6.0f, 5.0f }, 54 | { true, 12.0f, 11.0f, 10.0f, 9.0f } 55 | } 56 | } 57 | }; 58 | 59 | ResourceLabel l1 = mgr.resContainer.PushLabel(); 60 | Id lib1 = mgr.createLibrary(libSetup); 61 | mgr.resContainer.PopLabel(); 62 | CHECK(lib1.IsValid()); 63 | CHECK(mgr.lookupLibrary(lib1) != nullptr); 64 | CHECK(mgr.libPool.QueryPoolInfo().NumUsedSlots == 1); 65 | CHECK(mgr.clipPool.Size() == 2); 66 | CHECK(mgr.curvePool.Size() == 6); 67 | CHECK(mgr.numKeys == 110); 68 | const AnimLibrary* lib1Ptr = mgr.lookupLibrary(lib1); 69 | CHECK(lib1Ptr->Locator.Location() == "human"); 70 | CHECK(lib1Ptr->SampleStride == 9); 71 | CHECK(lib1Ptr->Clips.Size() == 2); 72 | CHECK(lib1Ptr->Clips[0].Name == "clip1"); 73 | CHECK(lib1Ptr->Clips[0].Length == 10); 74 | CHECK(lib1Ptr->Clips[0].KeyStride == 5); 75 | CHECK(lib1Ptr->Clips[0].Keys.Size() == 50); 76 | CHECK(lib1Ptr->Clips[0].Keys.Offset() == 0); 77 | CHECK(lib1Ptr->Clips[0].Curves.Size() == 3); 78 | CHECK(lib1Ptr->Clips[0].Curves.Offset() == 0); 79 | CHECK(lib1Ptr->Clips[0].Curves[0].Format == AnimCurveFormat::Float2); 80 | CHECK(lib1Ptr->Clips[0].Curves[0].KeyStride == 2); 81 | CHECK(!lib1Ptr->Clips[0].Curves[0].Static); 82 | CHECK(lib1Ptr->Clips[0].Curves[0].KeyIndex == 0); 83 | CHECK_CLOSE(lib1Ptr->Clips[0].Curves[0].StaticValue[0], 1.0f, 0.001f); 84 | CHECK_CLOSE(lib1Ptr->Clips[0].Curves[0].StaticValue[1], 2.0f, 0.001f); 85 | CHECK_CLOSE(lib1Ptr->Clips[0].Curves[0].StaticValue[2], 3.0f, 0.001f); 86 | CHECK_CLOSE(lib1Ptr->Clips[0].Curves[0].StaticValue[3], 4.0f, 0.001f); 87 | CHECK(lib1Ptr->Clips[0].Curves[1].Format == AnimCurveFormat::Float3); 88 | CHECK(lib1Ptr->Clips[0].Curves[1].KeyStride == 3); 89 | CHECK(!lib1Ptr->Clips[0].Curves[1].Static); 90 | CHECK(lib1Ptr->Clips[0].Curves[1].KeyIndex == 2); 91 | CHECK_CLOSE(lib1Ptr->Clips[0].Curves[1].StaticValue[0], 5.0f, 0.001f); 92 | CHECK_CLOSE(lib1Ptr->Clips[0].Curves[1].StaticValue[1], 6.0f, 0.001f); 93 | CHECK_CLOSE(lib1Ptr->Clips[0].Curves[1].StaticValue[2], 7.0f, 0.001f); 94 | CHECK_CLOSE(lib1Ptr->Clips[0].Curves[1].StaticValue[3], 8.0f, 0.001f); 95 | CHECK(lib1Ptr->Clips[0].Curves[2].Format == AnimCurveFormat::Float4); 96 | CHECK(lib1Ptr->Clips[0].Curves[2].KeyStride == 0); 97 | CHECK(lib1Ptr->Clips[0].Curves[2].Static); 98 | CHECK(lib1Ptr->Clips[0].Curves[2].KeyIndex == InvalidIndex); 99 | CHECK_CLOSE(lib1Ptr->Clips[0].Curves[2].StaticValue[0], 9.0f, 0.001f); 100 | CHECK_CLOSE(lib1Ptr->Clips[0].Curves[2].StaticValue[1], 10.0f, 0.001f); 101 | CHECK_CLOSE(lib1Ptr->Clips[0].Curves[2].StaticValue[2], 11.0f, 0.001f); 102 | CHECK_CLOSE(lib1Ptr->Clips[0].Curves[2].StaticValue[3], 12.0f, 0.001f); 103 | CHECK(lib1Ptr->Clips[1].Name == "clip2"); 104 | CHECK(lib1Ptr->Clips[1].Length == 20); 105 | CHECK(lib1Ptr->Clips[1].KeyStride == 3); 106 | CHECK(lib1Ptr->Clips[1].Keys.Size() == 60); 107 | CHECK(lib1Ptr->Clips[1].Keys.Offset() == 50); 108 | CHECK(lib1Ptr->Clips[1].Curves.Size() == 3); 109 | CHECK(lib1Ptr->Clips[1].Curves.Offset() == 3); 110 | CHECK(lib1Ptr->Clips[1].Curves[0].Format == AnimCurveFormat::Float2); 111 | CHECK(lib1Ptr->Clips[1].Curves[0].KeyStride == 0); 112 | CHECK(lib1Ptr->Clips[1].Curves[0].Static); 113 | CHECK(lib1Ptr->Clips[1].Curves[0].KeyIndex == InvalidIndex); 114 | CHECK_CLOSE(lib1Ptr->Clips[1].Curves[0].StaticValue[0], 4.0f, 0.001f); 115 | CHECK_CLOSE(lib1Ptr->Clips[1].Curves[0].StaticValue[1], 3.0f, 0.001f); 116 | CHECK_CLOSE(lib1Ptr->Clips[1].Curves[0].StaticValue[2], 2.0f, 0.001f); 117 | CHECK_CLOSE(lib1Ptr->Clips[1].Curves[0].StaticValue[3], 1.0f, 0.001f); 118 | CHECK(lib1Ptr->Clips[1].Curves[1].Format == AnimCurveFormat::Float3); 119 | CHECK(lib1Ptr->Clips[1].Curves[1].KeyStride == 3); 120 | CHECK(!lib1Ptr->Clips[1].Curves[1].Static); 121 | CHECK(lib1Ptr->Clips[1].Curves[1].KeyIndex == 0); 122 | CHECK_CLOSE(lib1Ptr->Clips[1].Curves[1].StaticValue[0], 8.0f, 0.001f); 123 | CHECK_CLOSE(lib1Ptr->Clips[1].Curves[1].StaticValue[1], 7.0f, 0.001f); 124 | CHECK_CLOSE(lib1Ptr->Clips[1].Curves[1].StaticValue[2], 6.0f, 0.001f); 125 | CHECK_CLOSE(lib1Ptr->Clips[1].Curves[1].StaticValue[3], 5.0f, 0.001f); 126 | CHECK(lib1Ptr->Clips[1].Curves[2].Format == AnimCurveFormat::Float4); 127 | CHECK(lib1Ptr->Clips[1].Curves[2].KeyStride == 0); 128 | CHECK(lib1Ptr->Clips[1].Curves[2].Static); 129 | CHECK(lib1Ptr->Clips[1].Curves[2].KeyIndex == InvalidIndex); 130 | CHECK_CLOSE(lib1Ptr->Clips[1].Curves[2].StaticValue[0], 12.0f, 0.001f); 131 | CHECK_CLOSE(lib1Ptr->Clips[1].Curves[2].StaticValue[1], 11.0f, 0.001f); 132 | CHECK_CLOSE(lib1Ptr->Clips[1].Curves[2].StaticValue[2], 10.0f, 0.001f); 133 | CHECK_CLOSE(lib1Ptr->Clips[1].Curves[2].StaticValue[3], 9.0f, 0.001f); 134 | 135 | libSetup.Locator = "Bla"; 136 | Id lib2 = mgr.createLibrary(libSetup); 137 | CHECK(lib2.IsValid()); 138 | CHECK(mgr.lookupLibrary(lib2) != nullptr); 139 | CHECK(mgr.libPool.QueryPoolInfo().NumUsedSlots == 2); 140 | CHECK(mgr.clipPool.Size() == 4); 141 | CHECK(mgr.curvePool.Size() == 12); 142 | CHECK(mgr.numKeys == 220); 143 | const AnimLibrary* lib2Ptr = mgr.lookupLibrary(lib2); 144 | CHECK(lib2Ptr->Locator.Location() == "Bla"); 145 | CHECK(lib2Ptr->SampleStride == 9); 146 | CHECK(lib2Ptr->Clips.Size() == 2); 147 | CHECK(lib2Ptr->Clips[0].Name == "clip1"); 148 | CHECK(lib2Ptr->Clips[0].Length == 10); 149 | CHECK(lib2Ptr->Clips[0].KeyStride == 5); 150 | CHECK(lib2Ptr->Clips[0].Keys.Size() == 50); 151 | CHECK(lib2Ptr->Clips[0].Keys.Offset() == 110); 152 | CHECK(lib2Ptr->Clips[0].Curves.Size() == 3); 153 | CHECK(lib2Ptr->Clips[0].Curves.Offset() == 6); 154 | CHECK(lib2Ptr->Clips[0].Curves[0].Format == AnimCurveFormat::Float2); 155 | CHECK(lib2Ptr->Clips[0].Curves[0].KeyStride == 2); 156 | CHECK(!lib2Ptr->Clips[0].Curves[0].Static); 157 | CHECK(lib2Ptr->Clips[0].Curves[0].KeyIndex == 0); 158 | CHECK_CLOSE(lib2Ptr->Clips[0].Curves[0].StaticValue[0], 1.0f, 0.001f); 159 | CHECK_CLOSE(lib2Ptr->Clips[0].Curves[0].StaticValue[1], 2.0f, 0.001f); 160 | CHECK_CLOSE(lib2Ptr->Clips[0].Curves[0].StaticValue[2], 3.0f, 0.001f); 161 | CHECK_CLOSE(lib2Ptr->Clips[0].Curves[0].StaticValue[3], 4.0f, 0.001f); 162 | CHECK(lib2Ptr->Clips[0].Curves[1].Format == AnimCurveFormat::Float3); 163 | CHECK(lib2Ptr->Clips[0].Curves[1].KeyStride == 3); 164 | CHECK(!lib2Ptr->Clips[0].Curves[1].Static); 165 | CHECK(lib2Ptr->Clips[0].Curves[1].KeyIndex == 2); 166 | CHECK_CLOSE(lib2Ptr->Clips[0].Curves[1].StaticValue[0], 5.0f, 0.001f); 167 | CHECK_CLOSE(lib2Ptr->Clips[0].Curves[1].StaticValue[1], 6.0f, 0.001f); 168 | CHECK_CLOSE(lib2Ptr->Clips[0].Curves[1].StaticValue[2], 7.0f, 0.001f); 169 | CHECK_CLOSE(lib2Ptr->Clips[0].Curves[1].StaticValue[3], 8.0f, 0.001f); 170 | CHECK(lib2Ptr->Clips[0].Curves[2].Format == AnimCurveFormat::Float4); 171 | CHECK(lib2Ptr->Clips[0].Curves[2].KeyStride == 0); 172 | CHECK(lib2Ptr->Clips[0].Curves[2].Static); 173 | CHECK(lib2Ptr->Clips[0].Curves[2].KeyIndex == InvalidIndex); 174 | CHECK_CLOSE(lib2Ptr->Clips[0].Curves[2].StaticValue[0], 9.0f, 0.001f); 175 | CHECK_CLOSE(lib2Ptr->Clips[0].Curves[2].StaticValue[1], 10.0f, 0.001f); 176 | CHECK_CLOSE(lib2Ptr->Clips[0].Curves[2].StaticValue[2], 11.0f, 0.001f); 177 | CHECK_CLOSE(lib2Ptr->Clips[0].Curves[2].StaticValue[3], 12.0f, 0.001f); 178 | CHECK(lib2Ptr->Clips[1].Name == "clip2"); 179 | CHECK(lib2Ptr->Clips[1].Length == 20); 180 | CHECK(lib2Ptr->Clips[1].KeyStride == 3); 181 | CHECK(lib2Ptr->Clips[1].Keys.Size() == 60); 182 | CHECK(lib2Ptr->Clips[1].Keys.Offset() == 160); 183 | CHECK(lib2Ptr->Clips[1].Curves.Size() == 3); 184 | CHECK(lib2Ptr->Clips[1].Curves.Offset() == 9); 185 | CHECK(lib2Ptr->Clips[1].Curves[0].Format == AnimCurveFormat::Float2); 186 | CHECK(lib2Ptr->Clips[1].Curves[0].KeyStride == 0); 187 | CHECK(lib2Ptr->Clips[1].Curves[0].Static); 188 | CHECK(lib2Ptr->Clips[1].Curves[0].KeyIndex == InvalidIndex); 189 | CHECK_CLOSE(lib2Ptr->Clips[1].Curves[0].StaticValue[0], 4.0f, 0.001f); 190 | CHECK_CLOSE(lib2Ptr->Clips[1].Curves[0].StaticValue[1], 3.0f, 0.001f); 191 | CHECK_CLOSE(lib2Ptr->Clips[1].Curves[0].StaticValue[2], 2.0f, 0.001f); 192 | CHECK_CLOSE(lib2Ptr->Clips[1].Curves[0].StaticValue[3], 1.0f, 0.001f); 193 | CHECK(lib2Ptr->Clips[1].Curves[1].Format == AnimCurveFormat::Float3); 194 | CHECK(lib2Ptr->Clips[1].Curves[1].KeyStride == 3); 195 | CHECK(!lib2Ptr->Clips[1].Curves[1].Static); 196 | CHECK(lib2Ptr->Clips[1].Curves[1].KeyIndex == 0); 197 | CHECK_CLOSE(lib2Ptr->Clips[1].Curves[1].StaticValue[0], 8.0f, 0.001f); 198 | CHECK_CLOSE(lib2Ptr->Clips[1].Curves[1].StaticValue[1], 7.0f, 0.001f); 199 | CHECK_CLOSE(lib2Ptr->Clips[1].Curves[1].StaticValue[2], 6.0f, 0.001f); 200 | CHECK_CLOSE(lib2Ptr->Clips[1].Curves[1].StaticValue[3], 5.0f, 0.001f); 201 | CHECK(lib2Ptr->Clips[1].Curves[2].Format == AnimCurveFormat::Float4); 202 | CHECK(lib2Ptr->Clips[1].Curves[2].KeyStride == 0); 203 | CHECK(lib2Ptr->Clips[1].Curves[2].Static); 204 | CHECK(lib2Ptr->Clips[1].Curves[2].KeyIndex == InvalidIndex); 205 | CHECK_CLOSE(lib2Ptr->Clips[1].Curves[2].StaticValue[0], 12.0f, 0.001f); 206 | CHECK_CLOSE(lib2Ptr->Clips[1].Curves[2].StaticValue[1], 11.0f, 0.001f); 207 | CHECK_CLOSE(lib2Ptr->Clips[1].Curves[2].StaticValue[2], 10.0f, 0.001f); 208 | CHECK_CLOSE(lib2Ptr->Clips[1].Curves[2].StaticValue[3], 9.0f, 0.001f); 209 | mgr.destroy(l1); 210 | CHECK(mgr.libPool.QueryPoolInfo().NumUsedSlots == 1); 211 | CHECK(mgr.clipPool.Size() == 2); 212 | CHECK(mgr.curvePool.Size() == 6); 213 | CHECK(mgr.numKeys == 110); 214 | 215 | mgr.discard(); 216 | CHECK(!mgr.isValid); 217 | CHECK(mgr.clipPool.Size() == 0); 218 | CHECK(mgr.curvePool.Size() == 0); 219 | CHECK(mgr.numKeys == 0); 220 | } 221 | -------------------------------------------------------------------------------- /src/Anim/UnitTests/AnimSkeletonTest.cc: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // AnimSkeletonTest.cc 3 | //------------------------------------------------------------------------------ 4 | #include "Pre.h" 5 | #include "UnitTest++/src/UnitTest++.h" 6 | #include "Anim/AnimTypes.h" 7 | #include "Anim/private/animMgr.h" 8 | #include 9 | 10 | using namespace Oryol; 11 | using namespace _priv; 12 | 13 | TEST(AnimSkeletonTest) { 14 | 15 | AnimSetup setup; 16 | setup.MaxNumSkeletons = 4; 17 | setup.MatrixPoolCapacity = 128; 18 | animMgr mgr; 19 | mgr.setup(setup); 20 | CHECK(mgr.isValid); 21 | CHECK(mgr.skelPool.IsValid()); 22 | CHECK(mgr.matrixPool.Capacity() == 128); 23 | 24 | glm::mat4 m0 = glm::translate(glm::mat4(), glm::vec3(1.0f, 2.0f, 3.0f)); 25 | glm::mat4 m1 = glm::translate(glm::mat4(), glm::vec3(4.0f, 5.0f, 6.0f)); 26 | AnimSkeletonSetup skelSetup; 27 | skelSetup.Locator = "test"; 28 | skelSetup.Bones = { 29 | { "root", -1, glm::mat4(), glm::mat4() }, 30 | { "spine0", 0, m0, glm::inverse(m0) }, 31 | { "spine1", 1, m1, glm::inverse(m1) } 32 | }; 33 | ResourceLabel l1 = mgr.resContainer.PushLabel(); 34 | Id skelId = mgr.createSkeleton(skelSetup); 35 | mgr.resContainer.PopLabel(); 36 | CHECK(mgr.matrixPool.Size() == 6); 37 | AnimSkeleton* skel = mgr.lookupSkeleton(skelId); 38 | CHECK(skel); 39 | CHECK(skel->Locator.Location() == "test"); 40 | 41 | mgr.destroy(l1); 42 | 43 | mgr.discard(); 44 | CHECK(!mgr.isValid); 45 | CHECK(mgr.matrixPool.Size() == 0); 46 | } 47 | -------------------------------------------------------------------------------- /src/Anim/UnitTests/animSequencerTest.cc: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // animSequencerTest.cc 3 | //------------------------------------------------------------------------------ 4 | #include "Pre.h" 5 | #include "UnitTest++/src/UnitTest++.h" 6 | #include "Anim/private/animSequencer.h" 7 | #include 8 | 9 | using namespace Oryol; 10 | using namespace _priv; 11 | 12 | TEST(animSequencerTest) { 13 | 14 | const double delta = 0.00001; 15 | 16 | animSequencer sequencer; 17 | CHECK(sequencer.items.Empty()); 18 | sequencer.garbageCollect(1.0); 19 | CHECK(sequencer.items.Empty()); 20 | 21 | AnimJob job0; 22 | job0.ClipIndex = 0; 23 | job0.TrackIndex = 2; 24 | job0.StartTime = job0.Duration = 0.0f; 25 | job0.FadeIn = job0.FadeOut = 0.1f; 26 | bool res0 = sequencer.add(0.0, 23, job0, 10.0f); 27 | CHECK(res0); 28 | CHECK(sequencer.items.Size() == 1); 29 | CHECK(sequencer.items[0].id == 23); 30 | CHECK(sequencer.items[0].valid); 31 | CHECK(sequencer.items[0].clipIndex == 0); 32 | CHECK(sequencer.items[0].trackIndex == 2); 33 | CHECK_CLOSE(sequencer.items[0].mixWeight, 1.0f, delta); 34 | CHECK_CLOSE(sequencer.items[0].absStartTime, 0.0, delta); 35 | CHECK_CLOSE(sequencer.items[0].absFadeInTime, 0.1, delta); 36 | CHECK_CLOSE(sequencer.items[0].absFadeOutTime, DBL_MAX, delta); 37 | CHECK_CLOSE(sequencer.items[0].absEndTime, DBL_MAX, delta); 38 | 39 | // insert a job at a higher track 40 | AnimJob job1; 41 | job1.ClipIndex = 1; 42 | job1.TrackIndex = 5; 43 | job1.StartTime = 0.0f; 44 | job1.MixWeight = 0.5f; 45 | job1.Duration = 1.5f; 46 | job1.DurationIsLoopCount = true; 47 | job1.FadeIn = job1.FadeOut = 0.1f; 48 | bool res1 = sequencer.add(0.0, 25, job1, 20.0f); 49 | CHECK(res1); 50 | CHECK(sequencer.items.Size() == 2); 51 | CHECK(sequencer.items[0].id == 23); 52 | CHECK(sequencer.items[1].id == 25); 53 | CHECK(sequencer.items[1].valid); 54 | CHECK(sequencer.items[1].clipIndex == 1); 55 | CHECK(sequencer.items[1].trackIndex == 5); 56 | CHECK_CLOSE(sequencer.items[1].mixWeight, 0.5f, delta); 57 | CHECK_CLOSE(sequencer.items[1].absStartTime, 0.0, delta); 58 | CHECK_CLOSE(sequencer.items[1].absFadeInTime, 0.1, delta); 59 | CHECK_CLOSE(sequencer.items[1].absFadeOutTime, 29.9f, delta); 60 | CHECK_CLOSE(sequencer.items[1].absEndTime, 30.0f, delta); 61 | 62 | // insert a job at lower track 63 | AnimJob job2; 64 | job2.ClipIndex = 2; 65 | job2.TrackIndex = 0; 66 | job2.StartTime = 0.0f; 67 | job2.MixWeight = 0.1f; 68 | job2.Duration = 20.0f; 69 | job2.FadeIn = job2.FadeOut = 0.2f; 70 | bool res2 = sequencer.add(0.0, 31, job2, 10.0f); 71 | CHECK(res2); 72 | CHECK(sequencer.items.Size() == 3); 73 | CHECK(sequencer.items[0].id == 31); 74 | CHECK(sequencer.items[1].id == 23); 75 | CHECK(sequencer.items[2].id == 25); 76 | CHECK_CLOSE(sequencer.items[0].mixWeight, 0.1f, delta); 77 | CHECK_CLOSE(sequencer.items[0].absStartTime, 0.0, delta); 78 | CHECK_CLOSE(sequencer.items[0].absFadeInTime, 0.2, delta); 79 | CHECK_CLOSE(sequencer.items[0].absFadeOutTime, 19.8, delta); 80 | CHECK_CLOSE(sequencer.items[0].absEndTime, 20.0, delta); 81 | 82 | // insert at track #4 83 | AnimJob job3; 84 | job3.ClipIndex = 3; 85 | job3.TrackIndex = 4; 86 | job3.StartTime = 1.0f; 87 | job3.Duration = 2.0f; 88 | job3.FadeIn = job3.FadeOut = 0.1f; 89 | bool res3 = sequencer.add(0.0, 44, job3, 10.0f); 90 | CHECK(res3); 91 | CHECK(sequencer.items.Size() == 4); 92 | CHECK(sequencer.items[0].id == 31); 93 | CHECK(sequencer.items[1].id == 23); 94 | CHECK(sequencer.items[2].id == 44); 95 | CHECK(sequencer.items[3].id == 25); 96 | 97 | // insert at track 2 at time 10.0, check that existing job on track 2 is clipped 98 | AnimJob job4; 99 | job4.ClipIndex = 4; 100 | job4.TrackIndex = 2; 101 | job4.StartTime = 0.0f; 102 | job4.Duration = 0.0f; 103 | job4.FadeIn = job4.FadeOut = 0.1f; 104 | bool res4 = sequencer.add(10.0, 55, job4, 10.0f); 105 | CHECK(res4); 106 | CHECK(sequencer.items.Size() == 5); 107 | CHECK(sequencer.items[0].id == 31); 108 | CHECK(sequencer.items[1].id == 23); 109 | CHECK(sequencer.items[2].id == 55); 110 | CHECK(sequencer.items[3].id == 44); 111 | CHECK(sequencer.items[4].id == 25); 112 | CHECK_CLOSE(sequencer.items[1].absStartTime, 0.0, delta); 113 | CHECK_CLOSE(sequencer.items[1].absFadeInTime, 0.1, delta); 114 | CHECK_CLOSE(sequencer.items[1].absFadeOutTime, 10.0, delta); 115 | CHECK_CLOSE(sequencer.items[1].absEndTime, 10.1, delta); 116 | CHECK_CLOSE(sequencer.items[2].absStartTime, 10.0, delta); 117 | CHECK_CLOSE(sequencer.items[2].absFadeInTime, 10.1, delta); 118 | CHECK_CLOSE(sequencer.items[2].absFadeOutTime, DBL_MAX, delta); 119 | CHECK_CLOSE(sequencer.items[2].absEndTime, DBL_MAX, delta); 120 | 121 | // insert on track 2 with overlapping start and end 122 | AnimJob job5; 123 | job5.ClipIndex = 5; 124 | job5.TrackIndex = 2; 125 | job5.StartTime = 5.0f; 126 | job5.Duration = 10.0f; 127 | job5.FadeIn = job5.FadeOut = 0.2f; 128 | bool res5 = sequencer.add(0.0f, 66, job5, 10.0f); 129 | CHECK(res5); 130 | CHECK(sequencer.items.Size() == 6); 131 | CHECK(sequencer.items[0].id == 31); 132 | CHECK(sequencer.items[1].id == 23); 133 | CHECK(sequencer.items[2].id == 66); 134 | CHECK(sequencer.items[3].id == 55); 135 | CHECK(sequencer.items[4].id == 44); 136 | CHECK(sequencer.items[5].id == 25); 137 | CHECK(sequencer.items[1].trackIndex == 2); 138 | CHECK(sequencer.items[2].trackIndex == 2); 139 | CHECK(sequencer.items[3].trackIndex == 2); 140 | CHECK_CLOSE(sequencer.items[1].absStartTime, 0.0, delta); 141 | CHECK_CLOSE(sequencer.items[1].absFadeInTime, 0.1, delta); 142 | CHECK_CLOSE(sequencer.items[1].absFadeOutTime, 5.0, delta); 143 | CHECK_CLOSE(sequencer.items[1].absEndTime, 5.2, delta); 144 | CHECK_CLOSE(sequencer.items[2].absStartTime, 5.0, delta); 145 | CHECK_CLOSE(sequencer.items[2].absFadeInTime, 5.2, delta); 146 | CHECK_CLOSE(sequencer.items[2].absFadeOutTime, 14.8, delta); 147 | CHECK_CLOSE(sequencer.items[2].absEndTime, 15.0, delta); 148 | CHECK_CLOSE(sequencer.items[3].absStartTime, 14.8, delta); 149 | CHECK_CLOSE(sequencer.items[3].absFadeInTime, 15.0, delta); 150 | CHECK_CLOSE(sequencer.items[3].absFadeOutTime, DBL_MAX, delta); 151 | CHECK_CLOSE(sequencer.items[3].absEndTime, DBL_MAX, delta); 152 | 153 | // insert a job which completely obscures another job 154 | AnimJob job6; 155 | job6.ClipIndex = 3; 156 | job6.TrackIndex = 4; 157 | job6.StartTime = 0.0f; 158 | job6.Duration = 5.0f; 159 | job6.FadeIn = job3.FadeOut = 0.1f; 160 | bool res6 = sequencer.add(0.0, 77, job6, 10.0f); 161 | CHECK(res6); 162 | CHECK(sequencer.items.Size() == 7); 163 | CHECK(sequencer.items[0].id == 31); 164 | CHECK(sequencer.items[1].id == 23); 165 | CHECK(sequencer.items[2].id == 66); 166 | CHECK(sequencer.items[3].id == 55); 167 | CHECK(sequencer.items[4].id == 77); 168 | CHECK(sequencer.items[5].id == 44); 169 | CHECK(sequencer.items[6].id == 25); 170 | CHECK(!sequencer.items[5].valid); 171 | 172 | // test garbageCollect 173 | sequencer.garbageCollect(0.0); // this should collect only the one invalid item 174 | CHECK(sequencer.items.Size() == 6); 175 | CHECK(sequencer.items[0].id == 31); 176 | CHECK(sequencer.items[1].id == 23); 177 | CHECK(sequencer.items[2].id == 66); 178 | CHECK(sequencer.items[3].id == 55); 179 | CHECK(sequencer.items[4].id == 77); 180 | CHECK(sequencer.items[5].id == 25); 181 | sequencer.garbageCollect(18.0); 182 | CHECK(sequencer.items.Size() == 3); 183 | CHECK(sequencer.items[0].id == 31); 184 | CHECK(sequencer.items[1].id == 55); 185 | CHECK(sequencer.items[2].id == 25); 186 | 187 | // stop methods 188 | sequencer.stop(0.5, 25, true); 189 | CHECK(sequencer.items[2].id == 25); 190 | CHECK(sequencer.items[2].valid); 191 | CHECK_CLOSE(sequencer.items[2].absEndTime, 0.6, delta); 192 | CHECK_CLOSE(sequencer.items[2].absFadeOutTime, 0.5, delta); 193 | sequencer.stop(0.25, 25, false); 194 | CHECK(sequencer.items[2].id == 25); 195 | CHECK(sequencer.items[2].valid); 196 | CHECK_CLOSE(sequencer.items[2].absEndTime, 0.25, delta); 197 | CHECK_CLOSE(sequencer.items[2].absFadeOutTime, 0.25, delta); 198 | sequencer.stopTrack(7.5, 0, true); 199 | CHECK(sequencer.items[0].id == 31); 200 | CHECK(sequencer.items[0].valid); 201 | CHECK_CLOSE(sequencer.items[0].absEndTime, 7.7, delta); 202 | CHECK_CLOSE(sequencer.items[0].absFadeOutTime, 7.5, delta); 203 | sequencer.garbageCollect(20.0); 204 | CHECK(sequencer.items.Size() == 1); 205 | CHECK(sequencer.items[0].id == 55); 206 | sequencer.stopAll(40.0, false); 207 | CHECK(sequencer.items[0].valid); 208 | CHECK(sequencer.items[0].absEndTime == 40.0); 209 | CHECK(sequencer.items[0].absFadeOutTime == 40.0); 210 | 211 | // check that stopping a future item works 212 | AnimJob job7; 213 | job7.ClipIndex = 3; 214 | job7.TrackIndex = 4; 215 | job7.StartTime = 0.0f; 216 | job7.Duration = 5.0f; 217 | job7.FadeIn = job3.FadeOut = 0.1f; 218 | sequencer.add(50.0, 123, job7, 5.0f); 219 | CHECK(sequencer.items.Size() == 2); 220 | CHECK(sequencer.items[0].id == 55); 221 | CHECK(sequencer.items[1].id == 123); 222 | sequencer.stop(5.0, 123, true); 223 | CHECK(!sequencer.items[1].valid); 224 | } 225 | 226 | -------------------------------------------------------------------------------- /src/Anim/private/animInstance.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | //------------------------------------------------------------------------------ 3 | /** 4 | @class Oryol::_priv::animInstance 5 | @ingroup _priv 6 | @brief internal animInstance class 7 | */ 8 | #include "Resource/ResourceBase.h" 9 | #include "Anim/private/animSequencer.h" 10 | #include "Core/Containers/Slice.h" 11 | 12 | namespace Oryol { 13 | 14 | struct AnimLibrary; 15 | struct AnimSkeleton; 16 | 17 | namespace _priv { 18 | 19 | class animInstance : public ResourceBase { 20 | public: 21 | /// the shared animation library 22 | AnimLibrary* library = nullptr; 23 | /// the shared skeleton (optional) 24 | AnimSkeleton* skeleton = nullptr; 25 | /// anim sequencer to keep track to active anim jobs 26 | animSequencer sequencer; 27 | /// anim evaluation result (only valid for active instances) 28 | Slice samples; 29 | /// skeleton evaluation result as 4x3 transposed matrices (only valid for active instances) 30 | Slice skinMatrices; 31 | 32 | /// clear the object 33 | void clear() { 34 | library = nullptr; 35 | skeleton = nullptr; 36 | samples.Reset(); 37 | skinMatrices.Reset(); 38 | } 39 | }; 40 | 41 | } // namespace _priv 42 | } // namespace Oryol 43 | -------------------------------------------------------------------------------- /src/Anim/private/animMgr.cc: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // animMgr.cc 3 | //------------------------------------------------------------------------------ 4 | #include "Pre.h" 5 | #include "animMgr.h" 6 | #include "Core/Memory/Memory.h" 7 | #include 8 | #include 9 | #include 10 | 11 | namespace Oryol { 12 | namespace _priv { 13 | 14 | //------------------------------------------------------------------------------ 15 | animMgr::~animMgr() { 16 | o_assert_dbg(!this->isValid); 17 | } 18 | 19 | //------------------------------------------------------------------------------ 20 | void 21 | animMgr::setup(const AnimSetup& setup) { 22 | o_assert_dbg(!this->isValid); 23 | 24 | this->animSetup = setup; 25 | this->isValid = true; 26 | this->resContainer.Setup(setup.ResourceLabelStackCapacity, setup.ResourceRegistryCapacity); 27 | this->libPool.Setup(resTypeLib, setup.MaxNumLibs); 28 | this->skelPool.Setup(resTypeSkeleton, setup.MaxNumSkeletons); 29 | this->instPool.Setup(resTypeInstance, setup.MaxNumInstances); 30 | this->clipPool.SetFixedCapacity(setup.ClipPoolCapacity); 31 | this->curvePool.SetFixedCapacity(setup.CurvePoolCapacity); 32 | this->matrixPool.SetFixedCapacity(setup.MatrixPoolCapacity); 33 | this->activeInstances.SetFixedCapacity(setup.MaxNumActiveInstances); 34 | this->skinMatrixInfo.InstanceInfos.SetFixedCapacity(setup.MaxNumActiveInstances); 35 | this->keyPool = (int16_t*) Memory::Alloc(setup.KeyPoolCapacity * sizeof(int16_t)); 36 | this->samplePool = (float*) Memory::Alloc(setup.SamplePoolCapacity * sizeof(float)); 37 | this->keys = Slice(this->keyPool, setup.KeyPoolCapacity, 0, setup.KeyPoolCapacity); 38 | this->samples = Slice(this->samplePool, setup.SamplePoolCapacity, 0, setup.SamplePoolCapacity); 39 | this->skinMatrixTableStride = setup.SkinMatrixTableWidth * 4; 40 | const int skinMatrixPoolNumFloats = this->skinMatrixTableStride * setup.SkinMatrixTableHeight; 41 | const int skinMatrixPoolSize = skinMatrixPoolNumFloats * sizeof(float); 42 | this->skinMatrixPool = (float*) Memory::Alloc(skinMatrixPoolSize); 43 | Memory::Clear(this->skinMatrixPool, skinMatrixPoolSize); 44 | this->skinMatrixTable = Slice(this->skinMatrixPool, skinMatrixPoolNumFloats); 45 | this->skinMatrixInfo.SkinMatrixTable = this->skinMatrixTable.begin(); 46 | } 47 | 48 | //------------------------------------------------------------------------------ 49 | void 50 | animMgr::discard() { 51 | o_assert_dbg(this->isValid); 52 | o_assert_dbg(this->keyPool); 53 | o_assert_dbg(this->samplePool); 54 | o_assert_dbg(this->skinMatrixPool); 55 | 56 | this->destroy(ResourceLabel::All); 57 | this->resContainer.Discard(); 58 | this->instPool.Discard(); 59 | this->skelPool.Discard(); 60 | this->libPool.Discard(); 61 | o_assert_dbg(this->clipPool.Empty()); 62 | o_assert_dbg(this->curvePool.Empty()); 63 | o_assert_dbg(this->matrixPool.Empty()); 64 | this->activeInstances.Clear(); 65 | this->keys.Reset(); 66 | this->samples.Reset(); 67 | this->skinMatrixTable.Reset(); 68 | Memory::Free(this->skinMatrixPool); 69 | this->skinMatrixPool = nullptr; 70 | Memory::Free(this->keyPool); 71 | this->keyPool = nullptr; 72 | Memory::Free(this->samplePool); 73 | this->samplePool = nullptr; 74 | this->isValid = false; 75 | } 76 | 77 | //------------------------------------------------------------------------------ 78 | void 79 | animMgr::destroy(const ResourceLabel& label) { 80 | o_assert_dbg(this->isValid); 81 | Array ids = this->resContainer.registry.Remove(label); 82 | for (const Id& id : ids) { 83 | switch (id.Type) { 84 | case resTypeLib: 85 | this->destroyLibrary(id); 86 | break; 87 | case resTypeSkeleton: 88 | this->destroySkeleton(id); 89 | break; 90 | case resTypeInstance: 91 | this->destroyInstance(id); 92 | break; 93 | default: 94 | o_assert2_dbg(false, "animMgr::destroy: unknown resource type\n"); 95 | break; 96 | } 97 | } 98 | } 99 | 100 | //------------------------------------------------------------------------------ 101 | Id 102 | animMgr::createLibrary(const AnimLibrarySetup& libSetup) { 103 | o_assert_dbg(this->isValid); 104 | o_assert_dbg(libSetup.Locator.HasValidLocation()); 105 | o_assert_dbg(!libSetup.CurveLayout.Empty()); 106 | o_assert_dbg(!libSetup.Clips.Empty()); 107 | 108 | // check if lib already exists 109 | Id resId = this->resContainer.registry.Lookup(libSetup.Locator); 110 | if (resId.IsValid()) { 111 | o_assert_dbg(resId.Type == resTypeLib); 112 | return resId; 113 | } 114 | 115 | // before creating new lib, validate setup params and check against pool limits 116 | if ((this->clipPool.Size() + libSetup.Clips.Size()) > this->clipPool.Capacity()) { 117 | o_warn("Anim: clip pool exhausted!\n"); 118 | return Id::InvalidId(); 119 | } 120 | if ((this->curvePool.Size() + libSetup.CurveLayout.Size()) > this->curvePool.Capacity()) { 121 | o_warn("Anim: curve pool exhausted!\n"); 122 | return Id::InvalidId(); 123 | } 124 | int libNumKeys = 0; 125 | for (const auto& clipSetup : libSetup.Clips) { 126 | if (clipSetup.Curves.Size() != libSetup.CurveLayout.Size()) { 127 | o_warn("Anim: curve number mismatch in clip '%s'!\n", clipSetup.Name.AsCStr()); 128 | return Id::InvalidId(); 129 | } 130 | for (int i = 0; i < clipSetup.Curves.Size(); i++) { 131 | if (!clipSetup.Curves[i].Static) { 132 | libNumKeys += clipSetup.Length * AnimCurveFormat::Stride(libSetup.CurveLayout[i]); 133 | } 134 | } 135 | } 136 | if ((this->numKeys + libNumKeys) > this->keys.Size()) { 137 | o_warn("Anim: key pool exhausted!\n"); 138 | return Id::InvalidId(); 139 | } 140 | 141 | // create a new lib 142 | resId = this->libPool.AllocId(); 143 | AnimLibrary& lib = this->libPool.Assign(resId, ResourceState::Setup); 144 | lib.Locator = libSetup.Locator; 145 | lib.SampleStride = 0; 146 | for (AnimCurveFormat::Enum fmt : libSetup.CurveLayout) { 147 | lib.CurveLayout.Add(fmt); 148 | } 149 | for (auto fmt : libSetup.CurveLayout) { 150 | lib.SampleStride += AnimCurveFormat::Stride(fmt); 151 | } 152 | lib.ClipIndexMap.Reserve(libSetup.Clips.Size()); 153 | const int curvePoolIndex = this->curvePool.Size(); 154 | const int clipPoolIndex = this->clipPool.Size(); 155 | int clipKeyIndex = this->numKeys; 156 | for (const auto& clipSetup : libSetup.Clips) { 157 | lib.ClipIndexMap.Add(clipSetup.Name, this->clipPool.Size()); 158 | AnimClip& clip = this->clipPool.Add(); 159 | clip.Name = clipSetup.Name; 160 | clip.Length = clipSetup.Length; 161 | clip.KeyDuration = clipSetup.KeyDuration; 162 | const int curveIndex = this->curvePool.Size(); 163 | for (int curveIndex = 0; curveIndex < clipSetup.Curves.Size(); curveIndex++) { 164 | const auto& curveSetup = clipSetup.Curves[curveIndex]; 165 | AnimCurve& curve = this->curvePool.Add(); 166 | curve.Static = curveSetup.Static; 167 | curve.Format = libSetup.CurveLayout[curveIndex]; 168 | curve.NumValues = AnimCurveFormat::Stride(curve.Format); 169 | for (int i = 0; i < 4; i++) { 170 | curve.StaticValue[i] = curveSetup.StaticValue[i]; 171 | // premultiply magnitude for 16-bit signed unpacking 172 | curve.Magnitude[i] = curveSetup.Magnitude[i] / 32767.0f; 173 | } 174 | if (!curve.Static) { 175 | curve.KeyIndex = clip.KeyStride; 176 | curve.KeyStride = AnimCurveFormat::Stride(curve.Format); 177 | clip.KeyStride += curve.KeyStride; 178 | } 179 | } 180 | clip.Curves = this->curvePool.MakeSlice(curveIndex, clipSetup.Curves.Size()); 181 | const int clipNumKeys = clip.KeyStride * clip.Length; 182 | if (clipNumKeys > 0) { 183 | clip.Keys = this->keys.MakeSlice(clipKeyIndex, clipNumKeys); 184 | clipKeyIndex += clipNumKeys; 185 | } 186 | } 187 | o_assert_dbg(clipKeyIndex == (this->numKeys + libNumKeys)); 188 | lib.Keys = this->keys.MakeSlice(this->numKeys, libNumKeys); 189 | this->numKeys += libNumKeys; 190 | lib.Curves = this->curvePool.MakeSlice(curvePoolIndex, libSetup.Clips.Size() * libSetup.CurveLayout.Size()); 191 | lib.Clips = this->clipPool.MakeSlice(clipPoolIndex, libSetup.Clips.Size()); 192 | 193 | // initialize clips with their default values 194 | /* 195 | FIXME FIXME FIXME 196 | for (auto& clip : lib.Clips) { 197 | for (int row = 0; row < clip.Length; row++) { 198 | int offset = row * clip.KeyStride; 199 | for (const auto& curve : clip.Curves) { 200 | for (int i = 0; i < curve.KeyStride; i++) { 201 | clip.Keys[offset++] = curve.StaticValue[i]; 202 | } 203 | } 204 | } 205 | } 206 | */ 207 | 208 | this->resContainer.registry.Add(libSetup.Locator, resId, this->resContainer.PeekLabel()); 209 | this->libPool.UpdateState(resId, ResourceState::Valid); 210 | return resId; 211 | } 212 | 213 | //------------------------------------------------------------------------------ 214 | AnimLibrary* 215 | animMgr::lookupLibrary(const Id& resId) { 216 | o_assert_dbg(this->isValid); 217 | o_assert_dbg(resId.Type == resTypeLib); 218 | return this->libPool.Lookup(resId); 219 | } 220 | 221 | //------------------------------------------------------------------------------ 222 | void 223 | animMgr::destroyLibrary(const Id& id) { 224 | AnimLibrary* lib = this->libPool.Lookup(id); 225 | if (lib) { 226 | this->removeClips(lib->Clips); 227 | this->removeCurves(lib->Curves); 228 | this->removeKeys(lib->Keys); 229 | lib->clear(); 230 | } 231 | this->libPool.Unassign(id); 232 | } 233 | 234 | //------------------------------------------------------------------------------ 235 | Id 236 | animMgr::createSkeleton(const AnimSkeletonSetup& setup) { 237 | o_assert_dbg(this->isValid); 238 | o_assert_dbg(setup.Locator.HasValidLocation()); 239 | o_assert_dbg(!setup.Bones.Empty()); 240 | 241 | // check if skeleton already exists 242 | Id resId = this->resContainer.registry.Lookup(setup.Locator); 243 | if (resId.IsValid()) { 244 | o_assert_dbg(resId.Type == resTypeSkeleton); 245 | return resId; 246 | } 247 | 248 | // check if resource limits are reached 249 | if ((this->matrixPool.Size() + setup.Bones.Size()*2) > this->matrixPool.Capacity()) { 250 | o_warn("Anim: matrix pool exhausted!\n"); 251 | return Id::InvalidId(); 252 | } 253 | 254 | // create new skeleton 255 | resId = this->skelPool.AllocId(); 256 | AnimSkeleton& skel = this->skelPool.Assign(resId, ResourceState::Setup); 257 | skel.Locator = setup.Locator; 258 | skel.NumBones = setup.Bones.Size(); 259 | const int matrixPoolIndex = this->matrixPool.Size(); 260 | for (const auto& bone : setup.Bones) { 261 | this->matrixPool.Add(glm::mat4x3(bone.BindPose)); 262 | } 263 | for (const auto& bone : setup.Bones) { 264 | this->matrixPool.Add(glm::mat4x3(bone.InvBindPose)); 265 | } 266 | skel.Matrices = this->matrixPool.MakeSlice(matrixPoolIndex, skel.NumBones * 2); 267 | skel.BindPose = skel.Matrices.MakeSlice(0, skel.NumBones); 268 | skel.InvBindPose = skel.Matrices.MakeSlice(skel.NumBones, skel.NumBones); 269 | for (int i = 0; i < skel.NumBones; i++) { 270 | skel.ParentIndices[i] = setup.Bones[i].ParentIndex; 271 | } 272 | 273 | // register the new resource, and done 274 | this->resContainer.registry.Add(setup.Locator, resId, this->resContainer.PeekLabel()); 275 | this->skelPool.UpdateState(resId, ResourceState::Valid); 276 | return resId; 277 | } 278 | 279 | //------------------------------------------------------------------------------ 280 | AnimSkeleton* 281 | animMgr::lookupSkeleton(const Id& resId) { 282 | o_assert_dbg(this->isValid); 283 | o_assert_dbg(resId.Type == resTypeSkeleton); 284 | return this->skelPool.Lookup(resId); 285 | } 286 | 287 | //------------------------------------------------------------------------------ 288 | void 289 | animMgr::destroySkeleton(const Id& id) { 290 | AnimSkeleton* skel = this->skelPool.Lookup(id); 291 | if (skel) { 292 | this->removeMatrices(skel->Matrices); 293 | skel->clear(); 294 | } 295 | this->skelPool.Unassign(id); 296 | } 297 | 298 | //------------------------------------------------------------------------------ 299 | Id 300 | animMgr::createInstance(const AnimInstanceSetup& setup) { 301 | o_assert_dbg(setup.Library.IsValid()); 302 | 303 | Id resId = this->instPool.AllocId(); 304 | animInstance& inst = this->instPool.Assign(resId, ResourceState::Setup); 305 | o_assert_dbg((inst.library == nullptr) && (inst.skeleton == nullptr)); 306 | inst.library = this->lookupLibrary(setup.Library); 307 | o_assert_dbg(inst.library); 308 | if (setup.Skeleton.IsValid()) { 309 | inst.skeleton = this->lookupSkeleton(setup.Skeleton); 310 | o_assert_dbg(inst.skeleton); 311 | } 312 | this->resContainer.registry.Add(Locator::NonShared(), resId, this->resContainer.PeekLabel()); 313 | this->instPool.UpdateState(resId, ResourceState::Valid); 314 | return resId; 315 | } 316 | 317 | //------------------------------------------------------------------------------ 318 | animInstance* 319 | animMgr::lookupInstance(const Id& resId) { 320 | o_assert_dbg(this->isValid); 321 | o_assert_dbg(resId.Type == resTypeInstance); 322 | return this->instPool.Lookup(resId); 323 | } 324 | 325 | //------------------------------------------------------------------------------ 326 | void 327 | animMgr::destroyInstance(const Id& id) { 328 | animInstance* inst = this->instPool.Lookup(id); 329 | if (inst) { 330 | inst->clear(); 331 | } 332 | this->instPool.Unassign(id); 333 | } 334 | 335 | //------------------------------------------------------------------------------ 336 | void 337 | animMgr::removeKeys(Slice range) { 338 | o_assert_dbg(this->keyPool); 339 | if (range.Empty()) { 340 | return; 341 | } 342 | const int numKeysToMove = this->numKeys - (range.Offset() + range.Size()); 343 | if (numKeysToMove > 0) { 344 | Memory::Move(range.end(), (void*)range.begin(), numKeysToMove * sizeof(float)); 345 | } 346 | this->numKeys -= range.Size(); 347 | o_assert_dbg(this->numKeys >= 0); 348 | 349 | // fix the key array views in libs and clips 350 | for (Id::SlotIndexT slotIndex = 0; slotIndex <= this->libPool.LastAllocSlot; slotIndex++) { 351 | AnimLibrary& lib = this->libPool.slots[slotIndex]; 352 | if (lib.Id.IsValid()) { 353 | lib.Keys.FillGap(range.Offset(), range.Size()); 354 | } 355 | } 356 | for (auto& clip : this->clipPool) { 357 | clip.Keys.FillGap(range.Offset(), range.Size()); 358 | } 359 | } 360 | 361 | //------------------------------------------------------------------------------ 362 | void 363 | animMgr::removeCurves(Slice range) { 364 | this->curvePool.EraseRange(range.Offset(), range.Size()); 365 | 366 | // fix the curve array views in libs and clips 367 | for (Id::SlotIndexT slotIndex = 0; slotIndex <= this->libPool.LastAllocSlot; slotIndex++) { 368 | AnimLibrary& lib = this->libPool.slots[slotIndex]; 369 | if (lib.Id.IsValid()) { 370 | lib.Curves.FillGap(range.Offset(), range.Size()); 371 | } 372 | } 373 | for (auto& clip : this->clipPool) { 374 | clip.Curves.FillGap(range.Offset(), range.Size()); 375 | } 376 | } 377 | 378 | //------------------------------------------------------------------------------ 379 | void 380 | animMgr::removeClips(Slice range) { 381 | this->clipPool.EraseRange(range.Offset(), range.Size()); 382 | 383 | // fix the clip array views in libs 384 | for (Id::SlotIndexT slotIndex = 0; slotIndex <= this->libPool.LastAllocSlot; slotIndex++) { 385 | AnimLibrary& lib = this->libPool.slots[slotIndex]; 386 | if (lib.Id.IsValid()) { 387 | lib.Clips.FillGap(range.Offset(), range.Size()); 388 | } 389 | } 390 | } 391 | 392 | //------------------------------------------------------------------------------ 393 | void 394 | animMgr::removeMatrices(Slice range) { 395 | this->matrixPool.EraseRange(range.Offset(), range.Size()); 396 | 397 | // fix the skeleton matrix slices 398 | for (Id::SlotIndexT slotIndex = 0; slotIndex <= this->skelPool.LastAllocSlot; slotIndex++) { 399 | AnimSkeleton& skel = this->skelPool.slots[slotIndex]; 400 | if (skel.Id.IsValid()) { 401 | skel.BindPose.FillGap(range.Offset(), range.Size()); 402 | skel.InvBindPose.FillGap(range.Offset(), range.Size()); 403 | } 404 | } 405 | } 406 | 407 | //------------------------------------------------------------------------------ 408 | void 409 | animMgr::writeKeys(AnimLibrary* lib, const uint8_t* ptr, int numBytes) { 410 | o_assert_dbg(lib && ptr && numBytes > 0); 411 | // if more bytes are incoming that are needed, just silently clamp 412 | // the size, this may happen because of alignment padding 413 | const int keyDataSize = lib->Keys.Size() * sizeof(int16_t); 414 | if (numBytes > keyDataSize) { 415 | numBytes = keyDataSize; 416 | } 417 | Memory::Copy(ptr, lib->Keys.begin(), numBytes); 418 | } 419 | 420 | //------------------------------------------------------------------------------ 421 | void 422 | animMgr::newFrame() { 423 | o_assert_dbg(!this->inFrame); 424 | for (animInstance* inst : this->activeInstances) { 425 | inst->samples.Reset(); 426 | inst->skinMatrices.Reset(); 427 | } 428 | this->activeInstances.Clear(); 429 | this->numSamples = 0; 430 | this->curSkinMatrixTableX = 0; 431 | this->curSkinMatrixTableY = 0; 432 | this->inFrame = true; 433 | this->skinMatrixInfo.SkinMatrixTableByteSize = 0; 434 | this->skinMatrixInfo.InstanceInfos.Clear(); 435 | } 436 | 437 | //------------------------------------------------------------------------------ 438 | bool 439 | animMgr::addActiveInstance(animInstance* inst) { 440 | o_assert_dbg(inst && inst->library); 441 | o_assert_dbg(this->inFrame); 442 | 443 | // check if resource limits are reached for this frame 444 | if (this->activeInstances.Size() == this->activeInstances.Capacity()) { 445 | // MaxNumActiveInstances reached 446 | return false; 447 | } 448 | if ((this->numSamples + inst->library->SampleStride) > this->samples.Size()) { 449 | // no more room in samples pool 450 | return false; 451 | } 452 | if (inst->skeleton) { 453 | if (((this->curSkinMatrixTableX + (inst->skeleton->NumBones*3)) > this->animSetup.SkinMatrixTableWidth) && 454 | ((this->curSkinMatrixTableY + 1) > this->animSetup.SkinMatrixTableHeight)) 455 | { 456 | // not enough room in the skin matrix table 457 | return false; 458 | } 459 | } 460 | this->activeInstances.Add(inst); 461 | 462 | // assign the samples slice 463 | inst->samples = this->samples.MakeSlice(this->numSamples, inst->library->SampleStride); 464 | this->numSamples += inst->library->SampleStride; 465 | 466 | // assign the skin matrix slice 467 | if (inst->skeleton) { 468 | // each skeleton bones in the skin matrix table takes up 4*3 floats for a 469 | // transposed 4x3 matrix: 470 | // 471 | // |x0 x1 x2 x3|y0 y1 y2 y3|z0 z1 z2 z3| 472 | // 473 | // each "pixel" in the skin matrix table is 4 floats 474 | // 475 | if ((this->curSkinMatrixTableX + (inst->skeleton->NumBones*3)) > this->animSetup.SkinMatrixTableWidth) { 476 | // doesn't fit into current skin matrix table row, start a new row 477 | this->curSkinMatrixTableX = 0; 478 | this->curSkinMatrixTableY++; 479 | } 480 | // one 'pixel' in the skin matrix table is a vec4 481 | const int offset = this->curSkinMatrixTableY*this->skinMatrixTableStride + this->curSkinMatrixTableX * 4; 482 | inst->skinMatrices = this->skinMatrixTable.MakeSlice(offset, inst->skeleton->NumBones * 3 * 4); 483 | 484 | // update skinMatrixInfo 485 | this->skinMatrixInfo.SkinMatrixTableByteSize = (this->curSkinMatrixTableY+1)*this->skinMatrixTableStride * 4; 486 | auto& info = this->skinMatrixInfo.InstanceInfos.Add(); 487 | info.Instance = inst->Id; 488 | const float halfPixelX = 0.5f / float(this->animSetup.SkinMatrixTableWidth); 489 | const float halfPixelY = 0.5f / float(this->animSetup.SkinMatrixTableHeight); 490 | info.ShaderInfo.x = (float(this->curSkinMatrixTableX)/float(this->animSetup.SkinMatrixTableWidth)) + halfPixelX; 491 | info.ShaderInfo.y = (float(this->curSkinMatrixTableY)/float(this->animSetup.SkinMatrixTableHeight)) + halfPixelY; 492 | info.ShaderInfo.z = float(this->animSetup.SkinMatrixTableWidth); 493 | 494 | // advance to next skin matrix table position 495 | this->curSkinMatrixTableX += inst->skeleton->NumBones * 3; 496 | } 497 | return true; 498 | } 499 | 500 | //------------------------------------------------------------------------------ 501 | void 502 | animMgr::evaluate(double frameDur) { 503 | o_assert_dbg(this->inFrame); 504 | // garbage-collect anim jobs in all active instances 505 | for (animInstance* inst : this->activeInstances) { 506 | inst->sequencer.garbageCollect(this->curTime); 507 | } 508 | // evaluate animation of all active instances 509 | for (animInstance* inst : this->activeInstances) { 510 | inst->sequencer.eval(inst->library, this->curTime, inst->samples.begin(), inst->samples.Size()); 511 | } 512 | // compute the skinning matrices for all active instances (which have skeletons) 513 | for (animInstance* inst : this->activeInstances) { 514 | if (inst->skeleton) { 515 | this->genSkinMatrices(inst); 516 | } 517 | } 518 | this->curTime += frameDur; 519 | this->inFrame = false; 520 | } 521 | 522 | //------------------------------------------------------------------------------ 523 | static void 524 | mx_mul4x3(const float* m1, const float* m2, float* m) { 525 | m[0] = m1[0]*m2[0] + m1[3]*m2[1] + m1[6] *m2[2]; 526 | m[1] = m1[1]*m2[0] + m1[4]*m2[1] + m1[7] *m2[2]; 527 | m[2] = m1[2]*m2[0] + m1[5]*m2[1] + m1[8] *m2[2]; 528 | m[3] = m1[0]*m2[3] + m1[3]*m2[4] + m1[6] *m2[5]; 529 | m[4] = m1[1]*m2[3] + m1[4]*m2[4] + m1[7] *m2[5]; 530 | m[5] = m1[2]*m2[3] + m1[5]*m2[4] + m1[8] *m2[5]; 531 | m[6] = m1[0]*m2[6] + m1[3]*m2[7] + m1[6] *m2[8]; 532 | m[7] = m1[1]*m2[6] + m1[4]*m2[7] + m1[7] *m2[8]; 533 | m[8] = m1[2]*m2[6] + m1[5]*m2[7] + m1[8] *m2[8]; 534 | m[9] = m1[0]*m2[9] + m1[3]*m2[10] + m1[6] *m2[11] + m1[9]; 535 | m[10] = m1[1]*m2[9] + m1[4]*m2[10] + m1[7] *m2[11] + m1[10]; 536 | m[11] = m1[2]*m2[9] + m1[5]*m2[10] + m1[8] *m2[11] + m1[11]; 537 | } 538 | 539 | //------------------------------------------------------------------------------ 540 | static void 541 | mx_mul4x3_transpose(const float* m1, const float* m2, float* m) { 542 | m[0] = m1[0]*m2[0] + m1[3]*m2[1] + m1[6] *m2[2]; 543 | m[1] = m1[0]*m2[3] + m1[3]*m2[4] + m1[6] *m2[5]; 544 | m[2] = m1[0]*m2[6] + m1[3]*m2[7] + m1[6] *m2[8]; 545 | m[3] = m1[0]*m2[9] + m1[3]*m2[10] + m1[6] *m2[11] + m1[9]; 546 | m[4] = m1[1]*m2[0] + m1[4]*m2[1] + m1[7] *m2[2]; 547 | m[5] = m1[1]*m2[3] + m1[4]*m2[4] + m1[7] *m2[5]; 548 | m[6] = m1[1]*m2[6] + m1[4]*m2[7] + m1[7] *m2[8]; 549 | m[7] = m1[1]*m2[9] + m1[4]*m2[10] + m1[7] *m2[11] + m1[10]; 550 | m[8] = m1[2]*m2[0] + m1[5]*m2[1] + m1[8] *m2[2]; 551 | m[9] = m1[2]*m2[3] + m1[5]*m2[4] + m1[8] *m2[5]; 552 | m[10] = m1[2]*m2[6] + m1[5]*m2[7] + m1[8] *m2[8]; 553 | m[11] = m1[2]*m2[9] + m1[5]*m2[10] + m1[8] *m2[11] + m1[11]; 554 | } 555 | 556 | //------------------------------------------------------------------------------ 557 | static void 558 | mx_copy(const float* src, float* dst) { 559 | for (int i = 0; i < 12; i++) { 560 | dst[i] = src[i]; 561 | } 562 | } 563 | 564 | //------------------------------------------------------------------------------ 565 | void 566 | animMgr::genSkinMatrices(animInstance* inst) { 567 | o_assert_dbg(inst && inst->skeleton); 568 | const int32_t* parentIndices = &inst->skeleton->ParentIndices[0]; 569 | // pointer to skeleton's inverse bind pose matrices 570 | const float* invBindPose = &(inst->skeleton->InvBindPose[0][0][0]); 571 | // output are transposed 4x3 matrices ready for upload to GPU 572 | float* outSkinMatrices = &(inst->skinMatrices[0]); 573 | // input samples (result of animation evaluation) 574 | const float* smp = &(inst->samples[0]); 575 | 576 | float m0[12], m1[12]; 577 | float tmpBoneMatrices[AnimConfig::MaxNumSkeletonBones][12]; 578 | const int numBones = inst->skeleton->NumBones; 579 | for (int boneIndex=0; boneIndexsequencer.garbageCollect(this->curTime); 614 | AnimJobId jobId = ++this->curAnimJobId; 615 | const auto& clip = inst->library->Clips[job.ClipIndex]; 616 | double clipDuration = clip.KeyDuration * clip.Length; 617 | if (inst->sequencer.add(this->curTime, jobId, job, clipDuration)) { 618 | return jobId; 619 | } 620 | else { 621 | return InvalidAnimJobId; 622 | } 623 | } 624 | 625 | //------------------------------------------------------------------------------ 626 | void 627 | animMgr::stop(animInstance* inst, AnimJobId jobId, bool allowFadeOut) { 628 | inst->sequencer.stop(this->curTime, jobId, allowFadeOut); 629 | inst->sequencer.garbageCollect(this->curTime); 630 | } 631 | 632 | //------------------------------------------------------------------------------ 633 | void 634 | animMgr::stopTrack(animInstance* inst, int trackIndex, bool allowFadeOut) { 635 | inst->sequencer.stopTrack(this->curTime, trackIndex, allowFadeOut); 636 | inst->sequencer.garbageCollect(this->curTime); 637 | } 638 | 639 | //------------------------------------------------------------------------------ 640 | void 641 | animMgr::stopAll(animInstance* inst, bool allowFadeOut) { 642 | inst->sequencer.stopAll(this->curTime, allowFadeOut); 643 | inst->sequencer.garbageCollect(this->curTime); 644 | } 645 | 646 | } // namespace _priv 647 | } // namespace Oryol 648 | -------------------------------------------------------------------------------- /src/Anim/private/animMgr.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | //------------------------------------------------------------------------------ 3 | /** 4 | @class Oryol::_priv::animMgr 5 | @ingroup _priv 6 | @brief resource container of the Anim module 7 | */ 8 | #include "Resource/ResourceContainerBase.h" 9 | #include "Resource/ResourcePool.h" 10 | #include "Anim/AnimTypes.h" 11 | #include "Anim/private/animInstance.h" 12 | 13 | namespace Oryol { 14 | namespace _priv { 15 | 16 | class animMgr { 17 | public: 18 | /// destructor 19 | ~animMgr(); 20 | 21 | /// setup the anim mgr 22 | void setup(const AnimSetup& setup); 23 | /// discard the anim mgr 24 | void discard(); 25 | 26 | /// destroy one or more resources by label 27 | void destroy(const ResourceLabel& label); 28 | 29 | /// create an animation library 30 | Id createLibrary(const AnimLibrarySetup& setup); 31 | /// lookup pointer to an animation library 32 | AnimLibrary* lookupLibrary(const Id& resId); 33 | /// destroy an animation library 34 | void destroyLibrary(const Id& resId); 35 | 36 | /// create a skeleton 37 | Id createSkeleton(const AnimSkeletonSetup& setup); 38 | /// lookup pointer to skeleton 39 | AnimSkeleton* lookupSkeleton(const Id& resId); 40 | /// destroy a skeleton 41 | void destroySkeleton(const Id& resId); 42 | 43 | /// create an animation instance 44 | Id createInstance(const AnimInstanceSetup& setup); 45 | /// lookup pointer to an animation instance 46 | animInstance* lookupInstance(const Id& resId); 47 | /// destroy an animation instance 48 | void destroyInstance(const Id& resId); 49 | 50 | /// remove a range of keys from key pool and fixup indices in curves and clips 51 | void removeKeys(Slice keyRange); 52 | /// remove a range of curves from curve pool, and fixup clips 53 | void removeCurves(Slice curveRange); 54 | /// remove a range of clips from clip pool, and fixup libraries 55 | void removeClips(Slice clipRange); 56 | /// remove a range of matrices from the matrix pool 57 | void removeMatrices(Slice matrixRange); 58 | 59 | /// write animition library keys 60 | void writeKeys(AnimLibrary* lib, const uint8_t* ptr, int numBytes); 61 | 62 | /// begin a new frame, resets the active instances 63 | void newFrame(); 64 | /// add an active instance for the current frame 65 | bool addActiveInstance(animInstance* inst); 66 | /// evaluate all active instances, and reset active instance array 67 | void evaluate(double frameDurationInSeconds); 68 | 69 | /// start an animation on an instance (active or inactive) 70 | AnimJobId play(animInstance* inst, const AnimJob& job); 71 | /// stop a specific anim job 72 | void stop(animInstance* inst, AnimJobId jobId, bool allowFadeOut); 73 | /// stop all anim jobs on a track 74 | void stopTrack(animInstance* inst, int trackIndex, bool allowFadeOut); 75 | /// stop all anim jobs 76 | void stopAll(animInstance* inst, bool allowFadeOut); 77 | 78 | /// generate the skinning matrices for animInstance 79 | void genSkinMatrices(animInstance* inst); 80 | 81 | static const Id::TypeT resTypeLib = 1; 82 | static const Id::TypeT resTypeSkeleton = 2; 83 | static const Id::TypeT resTypeInstance = 3; 84 | 85 | AnimSetup animSetup; 86 | bool isValid = false; 87 | bool inFrame = false; 88 | double curTime = 0.0; 89 | uint32_t curAnimJobId = 0; 90 | ResourceContainerBase resContainer; 91 | ResourcePool libPool; 92 | ResourcePool skelPool; 93 | ResourcePool instPool; 94 | Array clipPool; 95 | Array curvePool; 96 | Array matrixPool; 97 | Array activeInstances; 98 | AnimSkinMatrixInfo skinMatrixInfo; 99 | int numKeys = 0; 100 | Slice keys; 101 | int16_t* keyPool; 102 | int numSamples = 0; 103 | Slice samples; 104 | float* samplePool = nullptr; 105 | int curSkinMatrixTableX = 0; 106 | int curSkinMatrixTableY = 0; 107 | int skinMatrixTableStride = 0; // in number of floats 108 | Slice skinMatrixTable; 109 | float* skinMatrixPool = nullptr; 110 | }; 111 | 112 | } // namespace _priv 113 | } // namespace Oryol 114 | -------------------------------------------------------------------------------- /src/Anim/private/animSequencer.cc: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // animSequencer.cc 3 | //------------------------------------------------------------------------------ 4 | #include "Pre.h" 5 | #include "animSequencer.h" 6 | #include 7 | #include 8 | 9 | namespace Oryol { 10 | namespace _priv { 11 | 12 | //------------------------------------------------------------------------------ 13 | static void 14 | checkValidateItem(animSequencer::item& item) { 15 | if (item.absStartTime >= item.absEndTime) { 16 | item.valid = false; 17 | } 18 | } 19 | 20 | //------------------------------------------------------------------------------ 21 | bool 22 | animSequencer::add(double curTime, AnimJobId jobId, const AnimJob& job, double clipDuration) { 23 | if (this->items.Full()) { 24 | // no more free job slots 25 | return false; 26 | } 27 | 28 | // find insertion position 29 | double absStartTime = curTime + job.StartTime; 30 | int insertIndex; 31 | int numItems = this->items.Size(); 32 | for (insertIndex = 0; insertIndex < numItems; insertIndex++) { 33 | const auto& curItem = this->items[insertIndex]; 34 | if (!curItem.valid) { 35 | // skip invalid items 36 | continue; 37 | } 38 | else if (job.TrackIndex > curItem.trackIndex) { 39 | // skip items on smaller track indices 40 | continue; 41 | } 42 | else if ((job.TrackIndex==curItem.trackIndex) && (absStartTime>curItem.absStartTime)) { 43 | // skip items on same track with smaller start time 44 | continue; 45 | } 46 | else { 47 | // current item has higher track index, or is on same track 48 | // with higher or equal start time, insert the new job right before 49 | break; 50 | } 51 | } 52 | item newItem; 53 | newItem.id = jobId; 54 | newItem.valid = true; 55 | newItem.clipIndex = job.ClipIndex; 56 | newItem.trackIndex = job.TrackIndex; 57 | newItem.mixWeight = job.MixWeight; 58 | newItem.absStartTime = absStartTime; 59 | newItem.absFadeInTime = absStartTime + job.FadeIn; 60 | if (job.Duration > 0.0f) { 61 | if (job.DurationIsLoopCount) { 62 | newItem.absEndTime = absStartTime + job.Duration*clipDuration; 63 | } 64 | else { 65 | newItem.absEndTime = absStartTime + job.Duration; 66 | } 67 | newItem.absFadeOutTime = newItem.absEndTime - job.FadeOut; 68 | } 69 | else { 70 | // infinite duration 71 | newItem.absEndTime = DBL_MAX; 72 | newItem.absFadeOutTime = DBL_MAX; 73 | } 74 | this->items.Insert(insertIndex, newItem); 75 | 76 | // fix the start and end times for all jobs on the same 77 | // track that overlap with the new job, this may lead to 78 | // some jobs becoming infinitly short, these will no longer 79 | // take part in mixing, and will eventually be removed 80 | // when the play cursor passes over them 81 | numItems = this->items.Size(); 82 | for (int i = 0; i < numItems; i++) { 83 | if (i == insertIndex) { 84 | // this is the item that was just inserted 85 | continue; 86 | } 87 | else { 88 | auto& curItem = this->items[i]; 89 | if (curItem.valid && (newItem.trackIndex == curItem.trackIndex)) { 90 | // if before the new item, check overlap and trim the end if yes 91 | if ((i < insertIndex) && (curItem.absEndTime >= newItem.absFadeInTime)) { 92 | curItem.absEndTime = newItem.absFadeInTime; 93 | curItem.absFadeOutTime = newItem.absStartTime; 94 | } 95 | // if after the new item, check overlap and trim start if yes 96 | if ((i > insertIndex) && (curItem.absStartTime <= newItem.absFadeOutTime)) { 97 | curItem.absStartTime = newItem.absFadeOutTime; 98 | curItem.absFadeInTime = newItem.absEndTime; 99 | } 100 | // if both sides were clipped, it means the current item 101 | // is completely 'obscured' and needs to be removed 102 | // during the next 'garbage collection' 103 | checkValidateItem(curItem); 104 | } 105 | } 106 | } 107 | return true; 108 | } 109 | 110 | //------------------------------------------------------------------------------ 111 | static void 112 | checkStopItem(double curTime, bool allowFadeOut, animSequencer::item& item) { 113 | if (curTime < item.absStartTime) { 114 | // the item is in the future, can mark it as invalid 115 | item.valid = false; 116 | } 117 | else if (curTime < item.absEndTime) { 118 | // the item overlaps curTime, clamp the end time 119 | if (allowFadeOut) { 120 | double fadeDuration = item.absEndTime - item.absFadeOutTime; 121 | item.absFadeOutTime = curTime; 122 | item.absEndTime = curTime + fadeDuration; 123 | } 124 | else { 125 | item.absFadeOutTime = curTime; 126 | item.absEndTime = curTime; 127 | } 128 | checkValidateItem(item); 129 | } 130 | } 131 | 132 | //------------------------------------------------------------------------------ 133 | void 134 | animSequencer::stop(double curTime, AnimJobId jobId, bool allowFadeOut) { 135 | for (auto& curItem : this->items) { 136 | if (curItem.id == jobId) { 137 | checkStopItem(curTime, allowFadeOut, curItem); 138 | break; 139 | } 140 | } 141 | } 142 | 143 | //------------------------------------------------------------------------------ 144 | void 145 | animSequencer::stopTrack(double curTime, int trackIndex, bool allowFadeOut) { 146 | for (auto& curItem : this->items) { 147 | if (curItem.trackIndex == trackIndex) { 148 | checkStopItem(curTime, allowFadeOut, curItem); 149 | } 150 | } 151 | } 152 | 153 | //------------------------------------------------------------------------------ 154 | void 155 | animSequencer::stopAll(double curTime, bool allowFadeOut) { 156 | for (auto& curItem : this->items) { 157 | checkStopItem(curTime, allowFadeOut, curItem); 158 | } 159 | } 160 | 161 | //------------------------------------------------------------------------------ 162 | void 163 | animSequencer::garbageCollect(double curTime) { 164 | // remove all invalid items, and items where absEndTime is < curTime 165 | for (int i = this->items.Size() - 1; i >= 0; i--) { 166 | const auto& item = this->items[i]; 167 | if (!item.valid || (item.absEndTime < curTime)) { 168 | this->items.Erase(i); 169 | } 170 | } 171 | } 172 | 173 | //------------------------------------------------------------------------------ 174 | static float fadeWeight(float w0, float w1, double t, double t0, double t1) { 175 | // compute a fade-in or fade-out mixing weight 176 | double dt = t1 - t0; 177 | if ((dt > -0.000001) && (dt < 0.000001)) { 178 | return w0; // just make sure we don't divide by zero 179 | } 180 | float rt = (float) ((t - t0) / (t1 - t0)); 181 | if (rt < 0.0f) rt = 0.0f; 182 | else if (rt > 1.0f) rt = 1.0f; 183 | return w0 + rt*(w1-w0); 184 | } 185 | 186 | //------------------------------------------------------------------------------ 187 | static int clampKeyIndex(int keyIndex, int clipNumKeys) { 188 | // FIXME: handle clamp vs loop here 189 | o_assert_dbg(clipNumKeys > 0); 190 | keyIndex %= clipNumKeys; 191 | if (keyIndex < 0) { 192 | keyIndex += clipNumKeys; 193 | } 194 | return keyIndex; 195 | } 196 | 197 | //------------------------------------------------------------------------------ 198 | static float unpack(int16_t p, float m) { 199 | return float(p) * m; 200 | } 201 | 202 | //------------------------------------------------------------------------------ 203 | bool 204 | animSequencer::eval(const AnimLibrary* lib, double curTime, float* sampleBuffer, int numSamples) { 205 | 206 | // for each item which crosses the current play time... 207 | // FIXME: currently items are evaluated even if they are culled 208 | // completely by higher priority items 209 | int numProcessedItems = 0; 210 | for (const auto& item : this->items) { 211 | // skip current item if it isn't valid or doesn't cross the play cursor 212 | if (!item.valid || (item.absStartTime > curTime) || (item.absEndTime <= curTime)) { 213 | continue; 214 | } 215 | const AnimClip& clip = lib->Clips[item.clipIndex]; 216 | 217 | // compute sampling parameters 218 | int key0 = 0; 219 | int key1 = 0; 220 | float keyPos = 0.0f; 221 | if (clip.Length > 0) { 222 | o_assert_dbg(clip.KeyDuration > 0.0f); 223 | const double clipTime = curTime - item.absStartTime; 224 | key0 = int(clipTime / clip.KeyDuration); 225 | keyPos = float((clipTime - (key0 * clip.KeyDuration)) / clip.KeyDuration); 226 | key0 = clampKeyIndex(key0, clip.Length); 227 | key1 = clampKeyIndex(key0 + 1, clip.Length); 228 | } 229 | 230 | // only sample, or sample and mix with previous track? 231 | const int16_t* src0 = clip.Keys.Empty() ? nullptr : &(clip.Keys[key0 * clip.KeyStride]); 232 | const int16_t* src1 = clip.Keys.Empty() ? nullptr : &(clip.Keys[key1 * clip.KeyStride]); 233 | float* dst = sampleBuffer; 234 | #if ORYOL_DEBUG 235 | const float* dstEnd = dst + numSamples; 236 | #endif 237 | float s0, s1, v0, v1; 238 | if (0 == numProcessedItems) { 239 | // first processed track, only need to sample, not mix with previous track 240 | for (const auto& curve : clip.Curves) { 241 | const int num = curve.NumValues; 242 | if (curve.Static) { 243 | if (num >= 1) *dst++ = curve.StaticValue[0]; 244 | if (num >= 2) *dst++ = curve.StaticValue[1]; 245 | if (num >= 3) *dst++ = curve.StaticValue[2]; 246 | if (num >= 4) *dst++ = curve.StaticValue[3]; 247 | } 248 | else { 249 | // NOTE: simply use linear interpolation for quaternions, 250 | // just assume they are close together 251 | const float* m = curve.Magnitude; 252 | if (num >= 1) { v0=unpack(*src0++,m[0]); v1=unpack(*src1++,m[0]); *dst++=v0+(v1-v0)*keyPos; } 253 | if (num >= 2) { v0=unpack(*src0++,m[1]); v1=unpack(*src1++,m[1]); *dst++=v0+(v1-v0)*keyPos; } 254 | if (num >= 3) { v0=unpack(*src0++,m[2]); v1=unpack(*src1++,m[2]); *dst++=v0+(v1-v0)*keyPos; } 255 | if (num >= 4) { v0=unpack(*src0++,m[3]); v1=unpack(*src1++,m[3]); *dst++=v0+(v1-v0)*keyPos; } 256 | } 257 | } 258 | } 259 | else { 260 | // evaluate track and mix with previous sampling+mixing result 261 | // FIXME: may need to do proper quaternion slerp when mixing 262 | // rotation curves 263 | float weight = item.mixWeight; 264 | if (curTime < item.absFadeInTime) { 265 | weight = fadeWeight(0.0f, weight, curTime, item.absStartTime, item.absFadeInTime); 266 | } 267 | else if (curTime > item.absFadeOutTime) { 268 | weight = fadeWeight(weight, 0.0f, curTime, item.absFadeOutTime, item.absEndTime); 269 | } 270 | for (const auto& curve : clip.Curves) { 271 | const int num = curve.NumValues; 272 | if (curve.Static) { 273 | if (num >= 1) { s0=*dst; s1=curve.StaticValue[0]; *dst++=s0+(s1-s0)*weight; } 274 | if (num >= 2) { s0=*dst; s1=curve.StaticValue[1]; *dst++=s0+(s1-s0)*weight; } 275 | if (num >= 3) { s0=*dst; s1=curve.StaticValue[2]; *dst++=s0+(s1-s0)*weight; } 276 | if (num >= 4) { s0=*dst; s1=curve.StaticValue[3]; *dst++=s0+(s1-s0)*weight; } 277 | } 278 | else { 279 | const float* m = curve.Magnitude; 280 | if (num >= 1) { 281 | v0=unpack(*src0++,m[0]); v1=unpack(*src1++,m[0]); 282 | s0=*dst; s1=v0+(v1-v0)*keyPos; 283 | *dst++=s0+(s1-s0)*weight; 284 | } 285 | if (num >= 2) { 286 | v0=unpack(*src0++,m[1]); v1=unpack(*src1++,m[1]); 287 | s0=*dst; s1=v0+(v1-v0)*keyPos; 288 | *dst++=s0+(s1-s0)*weight; 289 | } 290 | if (num >= 3) { 291 | v0=unpack(*src0++,m[2]); v1=unpack(*src1++,m[2]); 292 | s0=*dst; s1=v0+(v1-v0)*keyPos; 293 | *dst++=s0+(s1-s0)*weight; 294 | } 295 | if (num >= 4) { 296 | v0=unpack(*src0++,m[3]); v1=unpack(*src1++,m[3]); 297 | s0=*dst; s1=v0+(v1-v0)*keyPos; 298 | *dst++=s0+(s1-s0)*weight; 299 | } 300 | } 301 | } 302 | } 303 | o_assert_dbg(dst == dstEnd); 304 | #if ORYOL_DEBUG 305 | if (src0 && src1) { 306 | o_assert_dbg(src0 == (&(clip.Keys[key0 * clip.KeyStride]) + clip.KeyStride)); 307 | o_assert_dbg(src1 == (&(clip.Keys[key1 * clip.KeyStride]) + clip.KeyStride)); 308 | } 309 | #endif 310 | numProcessedItems++; 311 | } 312 | return numProcessedItems > 0; 313 | } 314 | 315 | } // namespace _priv 316 | } // namespace Oryol 317 | -------------------------------------------------------------------------------- /src/Anim/private/animSequencer.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | //------------------------------------------------------------------------------ 3 | /** 4 | @class Oryol::_priv::animSequencer 5 | @ingroup _priv 6 | @brief a 'track sequencer' for animation priority blending 7 | */ 8 | #include "Anim/AnimTypes.h" 9 | #include "Resource/Id.h" 10 | #include "Core/Containers/InlineArray.h" 11 | 12 | namespace Oryol { 13 | namespace _priv { 14 | 15 | class animSequencer { 16 | public: 17 | /// a track item for evaluating an anim job 18 | struct item { 19 | /// this item's id 20 | AnimJobId id = 0; 21 | /// a valid flag, invalid items need to be removed during 'gc' 22 | bool valid = true; 23 | /// the anim clip index 24 | int clipIndex = InvalidIndex; 25 | /// the track index (lower number means higher priority) 26 | int trackIndex = 0; 27 | /// overall mixing weight 28 | float mixWeight = 1.0f; 29 | /// the absolute start time (including fade-in) 30 | double absStartTime = 0.0; 31 | /// the absolute end time (including fade-out) 32 | double absEndTime = 0.0; 33 | /// the absolute time when fade-in is ends 34 | double absFadeInTime = 0.0; 35 | /// the absolute time when fade-out starts 36 | double absFadeOutTime = 0.0; 37 | }; 38 | /// max number of items that can be queued 39 | static const int maxItems = 16; 40 | /// room for enqueued items 41 | InlineArray items; 42 | 43 | /// enqueue a new anim job, return false if queue is full, or job was dropped 44 | bool add(double curTime, AnimJobId jobId, const AnimJob& job, double clipDuration); 45 | /// stop a job, this will just set the end time to the current time 46 | void stop(double curTime, AnimJobId jobId, bool allowFadeOut); 47 | /// stop a track, this will set the end time of jobs overlapping curTime, and invalidate future jobs 48 | void stopTrack(double curTime, int trackIndex, bool allowFadeOut); 49 | /// stop all jobs 50 | void stopAll(double curTime, bool allowFadeOut); 51 | /// remove invalid and expired items 52 | void garbageCollect(double curTime); 53 | /// evaluate all active anim jobs into sample buffer, return false if there was nothing to do 54 | bool eval(const AnimLibrary* lib, double curTime, float* sampleBuffer, int numSamples); 55 | }; 56 | 57 | } // namespace _priv 58 | } // namespace Oryol 59 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | fips_add_subdirectory(Anim) 2 | 3 | --------------------------------------------------------------------------------