Files
watrbx-game-engine/App/v8datamodel/KeyframeSequence.cpp
T
2025-09-18 17:55:52 -04:00

626 lines
16 KiB
C++

/* Copyright 2003-2007 ROBLOX Corporation, All Rights Reserved */
#include "stdafx.h"
#include "V8DataModel/KeyframeSequence.h"
#include "V8DataModel/AnimationTrackState.h"
#include "V8DataModel/PartInstance.h"
#include "V8DataModel/JointInstance.h"
#include "V8DataModel/Workspace.h"
DYNAMIC_FASTFLAGVARIABLE(AnimationEasingStylesEnabled, false)
DYNAMIC_FASTFLAGVARIABLE(CachedPoseInitialized, false)
namespace RBX {
const char* const sKeyframeSequence = "KeyframeSequence";
REFLECTION_BEGIN();
static Reflection::BoundFuncDesc<KeyframeSequence, shared_ptr<const Instances>()> func_getKeyframes(&KeyframeSequence::getKeyframes, "GetKeyframes", Security::None);
static Reflection::BoundFuncDesc<KeyframeSequence, void(shared_ptr<Instance>)> func_addKeyframe(&KeyframeSequence::addKeyframe, "AddKeyframe", "keyframe", Security::None);
static Reflection::BoundFuncDesc<KeyframeSequence, void(shared_ptr<Instance>)> func_removeKeyframe(&KeyframeSequence::removeKeyframe, "RemoveKeyframe", "keyframe", Security::None);
const Reflection::PropDescriptor<KeyframeSequence, bool> prop_Loop("Loop", category_Data, &KeyframeSequence::getLoop, &KeyframeSequence::setLoop);
const Reflection::EnumPropDescriptor<KeyframeSequence, KeyframeSequence::Priority> prop_Priority("Priority", category_Data, &KeyframeSequence::getPriority, &KeyframeSequence::setPriority);
REFLECTION_END();
const std::string IAnimatableJoint::sNULL = std::string();
const std::string IAnimatableJoint::sROOT = std::string("__Root");
namespace Reflection {
template<>
EnumDesc<RBX::KeyframeSequence::Priority>::EnumDesc()
:EnumDescriptor("AnimationPriority")
{
addPair(KeyframeSequence::IDLE, "Idle");
addPair(KeyframeSequence::MOVEMENT, "Movement");
addPair(KeyframeSequence::ACTION, "Action");
addPair(KeyframeSequence::CORE, "Core");
}
}//namespace Reflection
////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
//
// FRONTEND AND BACKEND
KeyframeSequence::KeyframeSequence()
: DescribedCreatable<KeyframeSequence, Instance, sKeyframeSequence>()
, loop(true)
, priority(KeyframeSequence::ACTION)
{
setName(sKeyframeSequence);
}
shared_ptr<const Instances> KeyframeSequence::getKeyframes()
{
return getChildren2();
}
void KeyframeSequence::addKeyframe(shared_ptr<Instance> keyframe)
{
if (keyframe != NULL) {
keyframe->setParent(this);
}
}
void KeyframeSequence::removeKeyframe(shared_ptr<Instance> keyframe)
{
if (keyframe != NULL) {
if(keyframe->getParent() == this){
keyframe->setParent(NULL);
}
}
}
static void CopyChild(boost::shared_ptr<Instance> instance, Instance* newParent)
{
instance->luaClone()->setParent(newParent);
}
void KeyframeSequence::copyKeyframeSequence(KeyframeSequence* other)
{
setLoop(other->getLoop());
setPriority(other->getPriority());
other->visitChildren(boost::bind(&CopyChild, _1, this));
}
void KeyframeSequence::setLoop(bool value)
{
if(loop != value){
loop = value;
raisePropertyChanged(prop_Loop);
}
}
void KeyframeSequence::setPriority(Priority value)
{
if(priority != value){
priority = value;
raisePropertyChanged(prop_Priority);
}
}
const KeyframeSequence::Cache& KeyframeSequence::getCachedData() const
{
if(!cache.isValid)
{
cacheData();
}
return cache;
}
float KeyframeSequence::getDuration() const
{
return getCachedData().duration;
}
void KeyframeSequence::apply(std::vector<PoseAccumulator>& jointposes, double lastkeyframetime, double keyframetime, float trackweight) const
{
if(trackweight <= 0) return;
// find two keyframes to lerp.
CachedKeyframe* before = NULL;
CachedKeyframe* after = NULL;
getCachedData(); // validates cache.
if(cache.keyframes.size() == 0)
{
return; // no pose.
}
if( (loop && cache.duration > 0.0) && (keyframetime > cache.duration || keyframetime < 0) )
{
float duration = cache.duration;
if (keyframetime < 0)
{
int durationsOff = (int)(fabs(keyframetime) / duration) + 1;
keyframetime += durationsOff * duration;
}
else if (keyframetime > duration)
{
int durationsOff = (int)(keyframetime / duration);
keyframetime -= durationsOff * duration;
}
}
//todo: cache the translation step somewhere in the AnimationTrack /AnimationTrackState
// now we have all the joints, and the two keyframes. lets' go!
for(size_t i = 0; i < cache.animatedJoints.size(); ++i)
{
std::string parentName = cache.namedParts[cache.animatedJoints[i].first];
std::string childName = cache.namedParts[cache.animatedJoints[i].second];
for(size_t j = 0; j < jointposes.size(); ++j)
{
if (jointposes[j].joint.first.lock() != NULL)
{
IAnimatableJoint* joint = jointposes[j].joint.second;
if(joint != NULL &&
joint->getParentName() == parentName &&
joint->getPartName() == childName)
{
// find the before/after keyframes.
before = &cache.keyframes[0];
after = &cache.keyframes[cache.keyframes.size()-1];
for(size_t k = 0; k < cache.keyframes.size(); ++k)
{
if(cache.keyframes[k].time <= keyframetime && cache.keyframes[k].poses[i] != NULL)
{
before = &cache.keyframes[k];
}
else if (cache.keyframes[k].poses[i] != NULL)
{
after = &cache.keyframes[k];
break;
}
}
if (after->poses[i] == NULL)
{
after = before;
}
// found pose;
if(!before->poses[i] || !after->poses[i])
continue; // joint masked out of these two keyframes.
double afterw = (keyframetime - before->time);
double beforew = (after->time - keyframetime);
if(afterw + beforew <= 0)
{
afterw = 1.0f; // just take the after pose.
}
CachedPose ipose = CachedPose::interpolatePoses(*before->poses[i], *after->poses[i], (float)beforew, (float)afterw);
if (DFFlag::CachedPoseInitialized)
ipose.initialized = true;
// re-weight based on trackweight. (for fade-in and fade-out)
ipose.weight = G3D::lerp(0.0f, ipose.weight, trackweight);
ipose.maskWeight = G3D::lerp(1.0f, ipose.maskWeight, trackweight);
if (DFFlag::CachedPoseInitialized && !jointposes[j].pose.initialized)
{
jointposes[j].pose = ipose;
} else {
jointposes[j].pose = CachedPose::blendPoses(jointposes[j].pose, ipose);
}
if (DFFlag::CachedPoseInitialized)
jointposes[j].pose.initialized = true;
continue;
}
}
}
}
}
void KeyframeSequence::onChildAdded(Instance* child)
{
Super::onChildAdded(child);
invalidateCache();
}
void KeyframeSequence::onChildRemoved(Instance* child)
{
Super::onChildRemoved(child);
invalidateCache();
}
void KeyframeSequence::invalidateCache()
{
cache.isValid = false;
}
template<class V>
size_t findOrAdd(std::vector<V>& v, const V& s)
{
size_t i;
for(i = 0; i < v.size() && v[i] != s; ++i);
if(i == v.size())
v.push_back(s);
return i;
}
void KeyframeSequence::AppendPosePass0(const shared_ptr<Instance>& child) const
{
Pose* pose = Instance::fastDynamicCast<Pose>(child.get());
if(pose)
{
CachedPose cpose;
// here we are pre-populating cache.animatedJoints, because we need to know how many total
// *unique* animatable joints are in this animation.
bool isRoot = Instance::fastDynamicCast<Pose>(pose->getParent()) == 0;
std::string parentName = isRoot ? IAnimatableJoint::sROOT : pose->getParent()->getName();
std::string childName = pose->getName();
size_t parentPart = findOrAdd(cache.namedParts, parentName);
size_t childPart = findOrAdd(cache.namedParts, childName);
findOrAdd(cache.animatedJoints, std::make_pair(parentPart, childPart));
// here we keep track of the pose count, so we can pre-allocate the whole block.
// (which will allow us to take pointers into the vector, as opposed to indices)
cache.poseCount++;
}
}
void KeyframeSequence::AppendPosePass1(const shared_ptr<Instance>& child, std::vector<CachedPose*>* poses) const
{
Pose* pose = Instance::fastDynamicCast<Pose>(child.get());
if(pose)
{
CachedPose cpose;
bool isRoot = Instance::fastDynamicCast<Pose>(pose->getParent()) == 0;
std::string parentName = isRoot ? "__Root" : pose->getParent()->getName();
std::string childName = pose->getName();
size_t parentPart = findOrAdd(cache.namedParts, parentName);
size_t childPart = findOrAdd(cache.namedParts, childName);
size_t jointindex = findOrAdd(cache.animatedJoints, std::make_pair(parentPart, childPart));
cpose.weight = pose->getWeight();
cpose.setCFrame(pose->getCoordinateFrame());
cpose.maskWeight = pose->getMaskWeight();
if (DFFlag::AnimationEasingStylesEnabled)
{
cpose.easingStyle = pose->getEasingStyle();
cpose.easingDirection = pose->getEasingDirection();
}
cache.allPoses.push_back(cpose);
(*poses)[jointindex] = &cache.allPoses.back();
}
}
KeyframeSequence::CachedKeyframe KeyframeSequence::makeKeyframe(Keyframe* kf) const
{
CachedKeyframe ckf;
ckf.time = kf->getTime();
ckf.poses.resize(cache.animatedJoints.size()); // all initialized to null
kf->visitDescendants(boost::bind(&KeyframeSequence::AppendPosePass1, this, _1, &ckf.poses));
return ckf;
}
void KeyframeSequence::cacheKeyframePass0(const shared_ptr<Instance>& child) const
{
Keyframe* kf = Instance::fastDynamicCast<Keyframe>(child.get());
if(kf)
{
cache.duration = std::max(cache.duration,kf->getTime());
kf->visitDescendants(boost::bind(&KeyframeSequence::AppendPosePass0, this, _1));
}
}
void KeyframeSequence::cacheKeyframePass1(const shared_ptr<Instance>& child) const
{
Keyframe* kf = Instance::fastDynamicCast<Keyframe>(child.get());
if(kf)
{
cache.keyframes.push_back(makeKeyframe(kf));
}
}
void KeyframeSequence::cacheData() const
{
cache.allPoses.clear();
cache.animatedJoints.clear();
cache.namedParts.clear();
cache.keyframes.clear();
cache.duration = 0.0f;
cache.poseCount = 0;
visitChildren(boost::bind(&KeyframeSequence::cacheKeyframePass0, this, _1));
cache.allPoses.reserve(cache.poseCount);
visitChildren(boost::bind(&KeyframeSequence::cacheKeyframePass1, this, _1));
std::sort(cache.keyframes.begin(), cache.keyframes.end());
cache.isValid = true;
}
CoordinateFrame CachedPose::getCFrame() const
{
Vector3 axis = rotaxisangle;
float angle = axis.unitize();
return CoordinateFrame(Matrix3::fromAxisAngleFast(axis, angle), translation);
}
void CachedPose::setCFrame(const CoordinateFrame& cframe)
{
translation = cframe.translation;
Vector3 axis;
float angle;
cframe.rotation.toAxisAngle(axis, angle);
rotaxisangle = axis * angle;
initialized = true;
}
// converts one axisAngle into the corresponding vector in the oposite direction, but that
// corresponds to the same rotation. Give it the length or the vector.
inline Vector3 flipAxisAngle(const Vector3& r, float lr)
{
return r * ((lr - Math::twoPif())/lr);
}
// assuming length of vector < pi, this lerps the shortest rotation distance.
Vector3 lerpAxisAngle(const Vector3& r0, const Vector3& r1, float w0, float w1)
{
float dot = r0.dot(r1);
if(dot < 0) // vectors oposite. shortest path _possibly_ not the linear lerp.
{
float l0 = r0.length();
float l1 = r1.length();
RBXASSERT(l0 <= Math::pif()+0.01 && l1 <= Math::pif()+0.01);
if (!(l0 < l1))
{
float lenofr1onr0 = - dot / l0;
if(l0 + lenofr1onr0 > Math::pif())
{
// flip r0.
Vector3 r = flipAxisAngle(r0, l0) * w0 + r1 * w1;
// normalize result in the 0..pi range.
float lr_sq = r.squaredLength();
if(lr_sq > Math::pif() * Math::pif())
{
return flipAxisAngle(r, sqrt(lr_sq));
}
else
{
return r;
}
}
}
else // l1 >= l0
{
if(G3D::fuzzyEq(l1, 0))
{
return Vector3::zero();
}
else
{
// i'm lazy. should probably expand this so we can re-use length calculation done above.
return lerpAxisAngle(r1, r0, w1, w0);
}
}
}
return r0 * w0 + r1 * w1;
}
float bounceEasingStyle(float t)
{
if (t < 0.36363636)
{
return 7.5625 * t * t;
}
else if(t < 0.72727272)
{
t -= 0.54545454;
return 7.5625 * t * t + 0.75;
}
else if(t < 0.90909090)
{
t -= 0.81818181;
return 7.5625 * t * t + 0.9375;
}
else
{
t -= 0.95454545;
return 7.5625 * t * t + 0.984375;
}
}
CachedPose CachedPose::interpolatePoses(const CachedPose& p0, const CachedPose& p1, float w0, float w1)
{
if(w0 <= 0 || (DFFlag::CachedPoseInitialized && !p0.initialized))
return p1;
if(w1 <= 0 || (DFFlag::CachedPoseInitialized && !p1.initialized))
return p0;
float nw0 = w0 / (w0 + w1);
float nw1 = w1 / (w0 + w1);
if (DFFlag::AnimationEasingStylesEnabled){
switch(p0.easingStyle)
{
case Pose::POSE_EASING_STYLE_LINEAR:
default:
//these cases don't affect the calculated weights
break;
case Pose::POSE_EASING_STYLE_CONSTANT:
switch(p0.easingDirection)
{
default:
case Pose::POSE_EASING_DIRECTION_OUT:
nw0 = 1;
nw1 = 0;
break;
case Pose::POSE_EASING_DIRECTION_IN_OUT:
if (nw0 > 0.5f)
nw0 = 1.0f;
else
nw0 = 0;
nw1 = 1 - nw0;
break;
case Pose::POSE_EASING_DIRECTION_IN:
nw0 = 0;
nw1 = 1;
break;
}
break;
case Pose::POSE_EASING_STYLE_ELASTIC:
//elastic function: p = overshoot factor
//(p * x) - ( (x ^ 3) * (p - 1) )
switch(p0.easingDirection)
{
default:
case Pose::POSE_EASING_DIRECTION_OUT:
{
float totalTime = 1.0f;
float p = totalTime*.3;
float t = nw1;
float s = p/4;
nw1 = 1 + pow(2,-10*t) * sin( (t*totalTime-s)*(Math::twoPi())/p );
nw0 = 1 - nw1;
break;
}
case Pose::POSE_EASING_DIRECTION_IN:
{
float totalTime = 1.0f;
float p = totalTime*.3;
float t = nw0;
float s = p/4;
nw0 = 1 + pow(2,-10*t) * sin( (t*totalTime-s)*(Math::twoPi())/p );
nw1 = 1 - nw0;
break;
}
case Pose::POSE_EASING_DIRECTION_IN_OUT:
{
float t = nw0 / (0.5f);
float p = (.3*1.5);
float s = p/4;
if (t < 1) {
t -= 1;
nw0 = -.5 * pow(2,10*t) * sin( (t-s)*(Math::twoPi())/p );
}
else {
t -= 1;
nw0 = 1 + 0.5 * pow(2,-10*t) * sin( (t-s)*(Math::twoPi())/p );
}
nw1 = 1 - nw0;
break;
}
}
break;
case Pose::POSE_EASING_STYLE_CUBIC:
switch(p0.easingDirection)
{
default:
case Pose::POSE_EASING_DIRECTION_OUT:
nw0 = 1 - pow((1 - nw0), 3);
nw1 = 1 - nw0;
break;
case Pose::POSE_EASING_DIRECTION_IN_OUT:
if (nw0 < 0.5f)
{
nw0 = pow(2*nw0, 3) * 0.5f;
}
else
{
nw0 = (1 - pow((2 - 2 * nw0),3)) * 0.5f + 0.5f;
}
nw1 = 1 - nw0;
break;
case Pose::POSE_EASING_DIRECTION_IN:
nw0 = pow(nw0, 3);
nw1 = 1 - nw0;
}
break;
case Pose::POSE_EASING_STYLE_BOUNCE:
switch(p0.easingDirection)
{
default:
case Pose::POSE_EASING_DIRECTION_IN:
{
nw0 = bounceEasingStyle(nw0);
nw1 = 1 - nw0;
break;
}
case Pose::POSE_EASING_DIRECTION_OUT:
{
nw0 = 1 - bounceEasingStyle(1 - nw0);
nw1 = 1 - nw0;
break;
}
case Pose::POSE_EASING_DIRECTION_IN_OUT:
{
nw0 = nw0 * 2;
if (nw0 < 1)
{
nw0 = bounceEasingStyle(nw0) * 0.5f;
}
else
{
nw0 = 0.5f + bounceEasingStyle(nw0 - 1) * 0.5f;
}
nw1 = 1 - nw0;
break;
}
}
break;
}
}
CachedPose r;
r.weight = nw0 * p0.weight + nw1 * p1.weight;
r.maskWeight = nw0 * p0.maskWeight + nw1 * p1.maskWeight;
// keep maskWeights normalized, seems like the right thing. confirm.
r.translation = p0.translation * nw0 + p1.translation * nw1;
r.rotaxisangle = lerpAxisAngle(p0.rotaxisangle, p1.rotaxisangle, nw0, nw1);
return r;
}
// applies p1's mask to p0.
CachedPose CachedPose::blendPoses(const CachedPose& p0, const CachedPose& p1)
{
CachedPose r;
float maskedw0 = std::min(p0.weight, p1.maskWeight); // don't want to double fade-out a faded animation. used min instead of *.
r.weight = maskedw0 +p1.weight;
r.maskWeight = std::min(p0.maskWeight, p1.maskWeight); // nb: this value will now only be used at the top level priority collapse.
r.translation = maskedw0 * p0.translation + p1.weight * p1.translation;
r.rotaxisangle = lerpAxisAngle(p0.rotaxisangle, p1.rotaxisangle, maskedw0, p1.weight);
if (DFFlag::CachedPoseInitialized)
r.initialized = true;
return r;
}
void KeyframeSequence::verifySetAncestor(const Instance* const newParent, const Instance* const instanceGettingNewParent) const
{
Super::verifySetAncestor(newParent, instanceGettingNewParent);
}
} // RBX