1 /** 2 Inochi2D Animation Primitives 3 4 Copyright: 5 Copyright © 2020-2026, Inochi2D Project 6 7 License: 8 $(LINK2 https://github.com/Inochi2D/inochi2d/blob/main/LICENSE, BSD 2-clause License) 9 10 Authors: 11 Luna Nielsen 12 */ 13 module inochi2d.animation.animation; 14 import inochi2d.puppet; 15 import inochi2d.param; 16 import inochi2d.core; 17 import inochi2d.common; 18 import numath; 19 import numem.sorting; 20 import numem; 21 22 /** 23 An animation 24 */ 25 struct Animation { 26 public: 27 @nogc: 28 29 /** 30 The timestep of each frame 31 */ 32 float timestep = 0.0166; 33 34 /** 35 Whether the animation is additive. 36 37 Additive animations will not replace main animations, but add their data 38 on top of the running main animation 39 */ 40 bool additive; 41 42 /** 43 The weight of the animation 44 45 This is only relevant for additive animations 46 */ 47 float animationWeight; 48 49 /** 50 All of the animation lanes in this animation 51 */ 52 AnimationLane[] lanes; 53 54 /** 55 Length in frames 56 */ 57 int length; 58 59 /** 60 Time where the lead-in ends 61 */ 62 int leadIn = -1; 63 64 /** 65 Time where the lead-out starts 66 */ 67 int leadOut = -1; 68 69 /** 70 Finalizes the animation 71 */ 72 void finalize(Puppet puppet) { 73 foreach (ref lane; lanes) 74 lane.finalize(puppet); 75 } 76 77 /** 78 Serialization function 79 */ 80 void onSerialize(ref DataNode object, bool recursive = true) @nogc { 81 object["timestep"] = timestep; 82 object["additive"] = additive; 83 object["length"] = length; 84 object["leadIn"] = leadIn; 85 object["leadOut"] = leadOut; 86 object["animationWeight"] = animationWeight; 87 88 object["lanes"] = DataNode.createArray(); 89 foreach (ref AnimationLane lane; lanes) { 90 if (lane.paramRef.targetParam) { 91 object["lanes"] ~= lane.serialize(); 92 } 93 } 94 } 95 96 /** 97 Deserialization function 98 */ 99 void onDeserialize(ref DataNode object, ref ModelState state) @nogc { 100 object.tryGetRef(state, timestep, "timestep", timestep.init); 101 object.tryGetRef(state, additive, "additive", additive.init); 102 object.tryGetRef(state, animationWeight, "animationWeight", animationWeight.init); 103 object.tryGetRef(state, length, "length", length.init); 104 object.tryGetRef(state, leadIn, "leadIn", leadIn.init); 105 object.tryGetRef(state, leadOut, "leadOut", leadOut.init); 106 object.tryGetRef(state, lanes, "lanes", lanes.init); 107 } 108 } 109 110 struct AnimationParameterRef { 111 112 /** 113 A parameter to target 114 */ 115 Parameter targetParam; 116 117 /** 118 Target axis of the parameter 119 */ 120 int targetAxis; 121 122 } 123 124 /** 125 Animation Lane 126 */ 127 struct AnimationLane { 128 private: 129 @nogc: 130 GUID refguid; 131 132 public: 133 134 /** 135 Reference to parameter if any 136 */ 137 AnimationParameterRef* paramRef; 138 139 /** 140 Serialization function 141 */ 142 void onSerialize(ref DataNode object, bool recursive = true) @nogc { 143 object["interpolation"] = cast(uint)interpolation; 144 object["keyframes"] = frames.serialize(); 145 object["merge_mode"] = cast(uint)mergeMode; 146 if (paramRef) { 147 auto targetGuid = paramRef.targetParam.guid.toString; 148 object["guid"] = targetGuid[]; 149 object["target"] = paramRef.targetAxis; 150 } 151 } 152 153 /** 154 Deserialization function 155 */ 156 void onDeserialize(ref DataNode object, ref ModelState state) @nogc { 157 this.paramRef = nogc_new!AnimationParameterRef(null, 0); 158 this.refguid = object.tryGetGUID(state, "uuid", "guid"); 159 160 object.tryGetRef(state, interpolation, "interpolation"); 161 object.tryGetRef(state, paramRef.targetAxis, "target"); 162 object.tryGetRef(state, frames, "keyframes"); 163 object.tryGetRef(state, mergeMode, "merge_mode", mergeMode.init); 164 } 165 166 /** 167 List of frames in the lane 168 */ 169 Keyframe[] frames; 170 171 /** 172 The interpolation between each frame in the lane 173 */ 174 InterpolateMode interpolation; 175 176 /** 177 Merging mode of the lane 178 */ 179 ParameterMergeMode mergeMode = ParameterMergeMode.forced; 180 181 /** 182 Gets the interpolated state of a frame of animation 183 for this lane 184 */ 185 float get(float frame, bool snapSubframes = false) { 186 if (frames.length > 0) { 187 188 // If subframe snapping is turned on then we'll only run at the framerate 189 // of the animation, without any smooth interpolation on faster app rates. 190 if (snapSubframes) 191 frame = floor(frame); 192 193 // Fallback if there's only 1 frame 194 if (frames.length == 1) 195 return frames[0].value; 196 197 foreach (i; 0 .. frames.length) { 198 if (frames[i].frame < frame) 199 continue; 200 201 // Fallback to not try to index frame -1 202 if (i == 0) 203 return frames[0].value; 204 205 // Interpolation "time" 0->1 206 // Note we use floats here in case you're running the 207 // update step faster than the timestep of the animation 208 // This way it won't look choppy 209 float tonext = cast(float)frames[i].frame - frame; 210 float ilen = (cast(float)frames[i].frame - cast(float)frames[i - 1].frame); 211 float t = 1 - (tonext / ilen); 212 213 // Interpolation tension 0->1 214 float tension = frames[i].tension; 215 216 switch (interpolation) { 217 218 // Nearest - Snap to the closest frame 219 case InterpolateMode.nearest: 220 return t > 0.5 ? frames[i].value : frames[i - 1].value; 221 222 // Stepped - Snap to the current active keyframe 223 case InterpolateMode.stepped: 224 return frames[i - 1].value; 225 226 // Linear - Linearly interpolate between frame A and B 227 case InterpolateMode.linear: 228 return lerp(frames[i - 1].value, frames[i].value, t); 229 230 // Cubic - Smoothly in a curve between frame A and B 231 case InterpolateMode.cubic: 232 float prev = frames[max(cast(ptrdiff_t)i - 2, 0)].value; 233 float curr = frames[max(cast(ptrdiff_t)i - 1, 0)].value; 234 float next1 = frames[min(cast(ptrdiff_t)i, frames.length - 1)].value; 235 float next2 = frames[min(cast(ptrdiff_t)i + 1, frames.length - 1)].value; 236 237 // TODO: Switch formulae, catmullrom interpolation 238 return cubic(prev, curr, next1, next2, t); 239 240 // Bezier - Allows the user to specify beziér curves. 241 case InterpolateMode.quadratic: 242 // TODO: Switch formulae, Beziér curve 243 return lerp(frames[i - 1].value, frames[i].value, clamp(hermite(0, 2 * tension, 1, 2 * tension, t), 0, 1)); 244 245 default: 246 assert(0); 247 } 248 } 249 return frames[$ - 1].value; 250 } 251 252 // Fallback, no values. 253 // Ideally we won't even call this function 254 // if there's nothing to do. 255 return 0; 256 } 257 258 void finalize(Puppet puppet) { 259 if (paramRef) 260 paramRef.targetParam = puppet.findParameter(refguid); 261 } 262 263 /** 264 Updates the order of the keyframes 265 */ 266 void updateFrames() { 267 nu_sort!((a, b) => a.frame < b.frame)(frames); 268 } 269 } 270 271 /** 272 A keyframe 273 */ 274 struct Keyframe { 275 /** 276 The frame at which this frame occurs 277 */ 278 int frame; 279 280 /** 281 The value of the parameter at the given frame 282 */ 283 float value; 284 285 /** 286 Interpolation tension for cubic/inout 287 */ 288 float tension = 0.5; 289 290 /** 291 Serialization function 292 */ 293 void onSerialize(ref DataNode object, bool recursive = true) @nogc { 294 object["frame"] = frame; 295 object["value"] = value; 296 object["tension"] = tension; 297 } 298 299 /** 300 Deserialization function 301 */ 302 void onDeserialize(ref DataNode object, ref ModelState state) @nogc { 303 object.tryGetRef(state, frame, "frame"); 304 object.tryGetRef(state, value, "value"); 305 object.tryGetRef(state, tension, "tension"); 306 } 307 }