mirror of
https://github.com/scp-fs2open/fs2open.github.com.git
synced 2026-11-09 17:52:08 -10:00
407 lines
14 KiB
C++
407 lines
14 KiB
C++
#include "model/modelanimation.h"
|
|
#include "model/modelanimation_segments.h"
|
|
#include "ship/ship.h"
|
|
|
|
extern float flFrametime;
|
|
|
|
namespace animation {
|
|
|
|
std::multimap<ship*, std::shared_ptr<ModelAnimation>> ModelAnimation::s_runningAnimations;
|
|
|
|
ModelAnimationState ModelAnimation::play(float frametime, ship* ship) {
|
|
|
|
switch (m_state) {
|
|
case ModelAnimationState::UNTRIGGERED:
|
|
//We have a new animation starting up in this phase. Put it in the list of running animations to track and step it later
|
|
s_runningAnimations.emplace(ship, shared_from_this());
|
|
m_duration = 0.0f;
|
|
//Stop other running animations on subsystems we care about. Store subsystems initial values as well.
|
|
for (const auto& animation : m_submodelAnimation) {
|
|
//We need to make sure that we have the submodel index cached before we check if other animations have the same index
|
|
animation->findSubmodel(ship);
|
|
|
|
auto animIterRange = s_runningAnimations.equal_range(ship);
|
|
for (auto animIter = animIterRange.first; animIter != animIterRange.second; animIter++) {
|
|
//Don't stop this animation, even though it (obviously) runs on the same submodels
|
|
if (animIter->second == shared_from_this())
|
|
continue;
|
|
|
|
const auto& otherAnims = animIter->second->m_submodelAnimation;
|
|
bool needStop = false;
|
|
|
|
//See if we can find a submodel that must be reset. In case the other submodel has not been cached, it cannot have been running, so it is fine to miss these here
|
|
for (const auto& otherAnim : otherAnims) {
|
|
if (otherAnim->m_submodel == animation->m_submodel) {
|
|
needStop = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (needStop) {
|
|
animIter->second->stop(animIter->first, false);
|
|
}
|
|
}
|
|
|
|
//Store the submodels current data as the base for this animation and calculate this animations parameters
|
|
float duration = animation->saveCurrentAsBase(ship);
|
|
|
|
//Update this animations duration based on all the submodel animations (note that these could differ in multiple executions of the same animation, for example in case of absolute angles)
|
|
if (duration > m_duration)
|
|
m_duration = duration;
|
|
}
|
|
|
|
m_state = ModelAnimationState::RUNNING_FWD;
|
|
|
|
/* fall-thru */
|
|
case ModelAnimationState::RUNNING_FWD:
|
|
m_time += frametime;
|
|
|
|
//Cap time if needed
|
|
if (m_time > m_duration) {
|
|
m_time = m_duration;
|
|
m_state = ModelAnimationState::COMPLETED;
|
|
}
|
|
|
|
for (const auto& animation : m_submodelAnimation) {
|
|
animation->play(m_time, ship);
|
|
}
|
|
break;
|
|
|
|
case ModelAnimationState::COMPLETED:
|
|
//This means someone requested to start once we were complete, so start moving backwards.
|
|
m_state = ModelAnimationState::RUNNING_RWD;
|
|
/* fall-thru */
|
|
case ModelAnimationState::RUNNING_RWD:
|
|
m_time -= frametime;
|
|
|
|
//Cap time at 0, but don't clean up the animations here since this function is called in a loop over the running animations, and cleaning now would invalidate the iterator
|
|
if (m_time < 0) {
|
|
stop(ship, false);
|
|
break;
|
|
}
|
|
|
|
for (const auto& animation : m_submodelAnimation) {
|
|
animation->play(m_time, ship);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
return m_state;
|
|
}
|
|
|
|
void ModelAnimation::cleanRunning() {
|
|
auto removeIt = s_runningAnimations.cbegin();
|
|
while (removeIt != s_runningAnimations.cend()) {
|
|
if (removeIt->second->m_state == ModelAnimationState::UNTRIGGERED) {
|
|
removeIt = s_runningAnimations.erase(removeIt);
|
|
}
|
|
else
|
|
removeIt++;
|
|
}
|
|
}
|
|
|
|
void ModelAnimation::start(ship* ship, bool reverse) {
|
|
if (reverse) {
|
|
switch (m_state) {
|
|
case ModelAnimationState::RUNNING_RWD:
|
|
case ModelAnimationState::UNTRIGGERED:
|
|
//Cannot reverse-start if it's already running rwd or fully untriggered
|
|
return;
|
|
case ModelAnimationState::RUNNING_FWD:
|
|
//Just pretend we were going in the right direction
|
|
m_state = ModelAnimationState::RUNNING_RWD;
|
|
break;
|
|
case ModelAnimationState::COMPLETED:
|
|
//Nothing special to do. Expected case
|
|
break;
|
|
}
|
|
}
|
|
else {
|
|
switch (m_state) {
|
|
case ModelAnimationState::RUNNING_FWD:
|
|
case ModelAnimationState::COMPLETED:
|
|
//Cannot start if it's already running fwd or fully triggered
|
|
return;
|
|
case ModelAnimationState::RUNNING_RWD:
|
|
//Just pretend we were going in the right direction
|
|
m_state = ModelAnimationState::RUNNING_FWD;
|
|
break;
|
|
case ModelAnimationState::UNTRIGGERED:
|
|
//Nothing special to do. Expected case
|
|
break;
|
|
}
|
|
}
|
|
|
|
//Play them once without changing their set time, this will make sure they stop other conflicting animations and set up properly.
|
|
play(0, ship);
|
|
//In case this stopped some other animations, we need to remove them from the playing buffer
|
|
cleanRunning();
|
|
}
|
|
|
|
void ModelAnimation::stop(ship* ship, bool cleanup) {
|
|
for (const auto& animation : m_submodelAnimation) {
|
|
animation->reset(ship);
|
|
}
|
|
|
|
m_time = 0;
|
|
m_state = ModelAnimationState::UNTRIGGERED;
|
|
|
|
if (cleanup)
|
|
cleanRunning();
|
|
}
|
|
|
|
void ModelAnimation::addSubsystemAnimation(std::unique_ptr<ModelAnimationSubmodel> animation) {
|
|
m_submodelAnimation.push_back(std::move(animation));
|
|
}
|
|
|
|
void ModelAnimation::stepAnimations(float frametime) {
|
|
for (const auto& anim : s_runningAnimations) {
|
|
switch (anim.second->m_state) {
|
|
case ModelAnimationState::RUNNING_FWD:
|
|
case ModelAnimationState::RUNNING_RWD:
|
|
anim.second->play(frametime, anim.first);
|
|
break;
|
|
case ModelAnimationState::COMPLETED:
|
|
//Fully triggered. Keep in buffer in case some other animation starts on that submodel, but don't play without manual starting
|
|
break;
|
|
case ModelAnimationState::UNTRIGGERED:
|
|
UNREACHABLE("An untriggered animation should not be in the runningAnimations buffer");
|
|
break;
|
|
}
|
|
|
|
}
|
|
|
|
//Clear Animations that might have completed this frame.
|
|
cleanRunning();
|
|
}
|
|
|
|
void ModelAnimation::clearAnimations() {
|
|
for (const auto& anim : s_runningAnimations) {
|
|
anim.second->stop(anim.first, false);
|
|
}
|
|
|
|
s_runningAnimations.clear();
|
|
}
|
|
|
|
void ModelAnimation::parseLegacyAnimationTable(model_subsystem* sp, ship_info* sip) {
|
|
//the only thing initial animation type needs is the angle,
|
|
//so to save space lets just make everything optional in this case
|
|
|
|
//Most of these properties were read and allowed, but never actually used for anything.
|
|
//Hence, still allow them in tables, but now just skip reading them
|
|
|
|
angles angle;
|
|
bool isRelative;
|
|
|
|
if (optional_string("+delay:"))
|
|
skip_token();
|
|
|
|
if (optional_string("+reverse_delay:"))
|
|
skip_token();
|
|
|
|
if (optional_string("+absolute_angle:")) {
|
|
vec3d anglesDeg;
|
|
stuff_vec3d(&anglesDeg);
|
|
|
|
angle.p = fl_radians(anglesDeg.xyz.x);
|
|
angle.h = fl_radians(anglesDeg.xyz.y);
|
|
angle.b = fl_radians(anglesDeg.xyz.z);
|
|
|
|
isRelative = false;
|
|
}
|
|
else {
|
|
vec3d anglesDeg;
|
|
if (!optional_string("+relative_angle:"))
|
|
required_string("+relative_angle:");
|
|
|
|
stuff_vec3d(&anglesDeg);
|
|
|
|
angle.p = fl_radians(anglesDeg.xyz.x);
|
|
angle.h = fl_radians(anglesDeg.xyz.y);
|
|
angle.b = fl_radians(anglesDeg.xyz.z);
|
|
|
|
isRelative = true;
|
|
}
|
|
|
|
if (optional_string("+velocity:"))
|
|
skip_token();
|
|
|
|
if (optional_string("+acceleration:"))
|
|
skip_token();
|
|
|
|
if (optional_string("+time:"))
|
|
skip_token();
|
|
|
|
std::shared_ptr<ModelAnimation> anim = std::shared_ptr<ModelAnimation>(new ModelAnimation());
|
|
|
|
char namelower[MAX_NAME_LEN];
|
|
strncpy(namelower, sp->subobj_name, MAX_NAME_LEN);
|
|
strlwr(namelower);
|
|
//since sp->type is not set without reading the pof, we need to infer it by subsystem name (which works, since the same name is used to match the submodels name, which is used to match the type in pof parsing)
|
|
//sadly, we also need to check for engine and radar, since these take precedent (as in, an engineturret is an engine before a turret type)
|
|
if (!strstr(namelower, "engine") && !strstr(namelower, "radar") && strstr(namelower, "turret")) {
|
|
auto rotBase = std::unique_ptr<ModelAnimationSegmentSetAngle>(new ModelAnimationSegmentSetAngle(angle.p));
|
|
auto subsysBase = std::unique_ptr<ModelAnimationSubmodel>(new ModelAnimationSubmodel(sp->subobj_name, false, std::move(rotBase)));
|
|
anim->addSubsystemAnimation(std::move(subsysBase));
|
|
|
|
auto rotBarrel = std::unique_ptr<ModelAnimationSegmentSetAngle>(new ModelAnimationSegmentSetAngle(angle.h));
|
|
auto subsysBarrel = std::unique_ptr<ModelAnimationSubmodel>(new ModelAnimationSubmodel(sp->subobj_name, true, std::move(rotBarrel)));
|
|
anim->addSubsystemAnimation(std::move(subsysBarrel));
|
|
}
|
|
else {
|
|
auto rot = std::unique_ptr<ModelAnimationSegmentSetPHB>(new ModelAnimationSegmentSetPHB(angle, isRelative));
|
|
auto subsys = std::unique_ptr<ModelAnimationSubmodel>(new ModelAnimationSubmodel(sp->subobj_name, std::move(rot)));
|
|
anim->addSubsystemAnimation(std::move(subsys));
|
|
}
|
|
|
|
sip->animations.emplace(anim, "", animation::ModelAnimationTriggerType::Initial);
|
|
}
|
|
|
|
/*std::shared_ptr<ModelAnimation> ModelAnimation::parseAnimationTable() {
|
|
//TODO. This is not the function to parse the legacy table, that is still part of modelread.cpp
|
|
//This will eventually be the function to parse the new animation table
|
|
return std::make_shared<ModelAnimation>(nullptr);
|
|
}*/
|
|
|
|
|
|
ModelAnimationSubmodel::ModelAnimationSubmodel(SCP_string submodelName, std::unique_ptr<ModelAnimationSegment> mainSegment) : m_name(std::move(submodelName)), m_mainSegment(std::move(mainSegment)) { }
|
|
|
|
ModelAnimationSubmodel::ModelAnimationSubmodel(SCP_string subsystemName, bool findBarrel, std::unique_ptr<ModelAnimationSegment> mainSegment) : m_name(std::move(subsystemName)), m_findBarrel(findBarrel), m_mainSegment(std::move(mainSegment)) { }
|
|
|
|
void ModelAnimationSubmodel::play(float frametime, ship* ship) {
|
|
auto dataIt = m_initialData.find(ship);
|
|
auto lastDataIt = m_lastFrame.find(ship);
|
|
|
|
//Specified submodel not found. Don't play
|
|
if (dataIt == m_initialData.end() || lastDataIt == m_lastFrame.end())
|
|
return;
|
|
|
|
//Cap the frametime at this submodels duration
|
|
if (frametime > m_mainSegment->getDuration())
|
|
frametime = m_mainSegment->getDuration();
|
|
|
|
ModelAnimationData<> currentFrame = dataIt->second;
|
|
//Calculate the submodels data (or it's delta from the data stored at the animation's start)
|
|
ModelAnimationData<true> delta = m_mainSegment->calculateAnimation(currentFrame, lastDataIt->second, frametime);
|
|
|
|
currentFrame.applyDelta(delta);
|
|
|
|
//Execute stuff of the animation that doesn't modify this delta (stuff like sounds / particles)
|
|
m_mainSegment->executeAnimation(currentFrame, frametime);
|
|
|
|
//Actually apply the result to the submodel
|
|
copyToSubmodel(currentFrame, ship);
|
|
m_lastFrame[ship] = currentFrame;
|
|
}
|
|
|
|
void ModelAnimationSubmodel::reset(ship* ship) {
|
|
auto dataIt = m_initialData.find(ship);
|
|
if (dataIt == m_initialData.end())
|
|
return;
|
|
|
|
//Since resetting the submodel animation also has to happen for the submodel itself, copy it's state at the animation's start back to the submodel
|
|
copyToSubmodel(dataIt->second, ship);
|
|
}
|
|
|
|
void ModelAnimationSubmodel::copyToSubmodel(const ModelAnimationData<>& data, ship* ship) {
|
|
submodel_instance* submodel = findSubmodel(ship).first;
|
|
if (!submodel)
|
|
return;
|
|
|
|
submodel->canonical_orient = data.orientation;
|
|
//TODO: Once translation is a thing
|
|
//m_subsys->submodel_instance_1->offset = data.position;
|
|
}
|
|
|
|
float ModelAnimationSubmodel::saveCurrentAsBase(ship* ship) {
|
|
auto submodel = findSubmodel(ship);
|
|
if (!submodel.first || !submodel.second)
|
|
return 0;
|
|
|
|
ModelAnimationData<>& data = m_initialData[ship];
|
|
data.orientation = submodel.first->canonical_orient;
|
|
//TODO: Once translation is a thing
|
|
//data.position = m_subsys->submodel_instance_1->offset;
|
|
|
|
m_lastFrame[ship] = data;
|
|
m_mainSegment->recalculate(submodel.first, submodel.second, data);
|
|
return m_mainSegment->getDuration();
|
|
}
|
|
|
|
std::pair<submodel_instance*, bsp_info*> ModelAnimationSubmodel::findSubmodel(ship* ship) {
|
|
//Ship was deleted
|
|
if(ship->objnum < 0)
|
|
return { nullptr, nullptr };
|
|
|
|
int submodelNumber = -1;
|
|
|
|
polymodel* pm = model_get(Ship_info[ship->ship_info_index].model_num);
|
|
|
|
//Do we have a submodel number already cached?
|
|
if (m_submodel.has())
|
|
submodelNumber = m_submodel;
|
|
//Do we know if we were told to find the barrel submodel or not? This implies we have a subsystem name, not a submodel name
|
|
else if (m_findBarrel.has()) {
|
|
ship_info* sip = &Ship_info[ship->ship_info_index];
|
|
for (int i = 0; i < sip->n_subsystems; i++) {
|
|
if (!subsystem_stricmp(sip->subsystems[i].subobj_name, m_name.c_str())) {
|
|
if ((bool)m_findBarrel) {
|
|
//Check if the barrel subobj is a dedicated existing subobj or just the base turret.
|
|
if (sip->subsystems[i].turret_gun_sobj != sip->subsystems[i].subobj_num)
|
|
submodelNumber = sip->subsystems[i].turret_gun_sobj;
|
|
else
|
|
submodelNumber = -1;
|
|
}
|
|
else
|
|
submodelNumber = sip->subsystems[i].subobj_num;
|
|
break;
|
|
}
|
|
}
|
|
|
|
m_submodel = submodelNumber;
|
|
}
|
|
//We seem to have a submodel name
|
|
else {
|
|
for (int i = 0; i < pm->n_models; i++) {
|
|
if (!subsystem_stricmp(pm->submodel[i].name, m_name.c_str())) {
|
|
submodelNumber = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
m_submodel = submodelNumber;
|
|
}
|
|
|
|
//If the model does not exist, return null. The system is expected to just silently tolerate this,
|
|
//as this needs to be the case to be able to handle turrets whose barrels may or may not exist, depending on the pof which is not loaded at table parse time
|
|
if (submodelNumber < 0 || submodelNumber >= pm->n_models)
|
|
return { nullptr, nullptr };
|
|
|
|
polymodel_instance* pmi = model_get_instance(ship->model_instance_num);
|
|
return { &pmi->submodel[submodelNumber], &pm->submodel[submodelNumber] };
|
|
}
|
|
|
|
|
|
float ModelAnimationSegment::getDuration() const {
|
|
return m_duration;
|
|
}
|
|
|
|
|
|
int ModelAnimationSet::SUBTYPE_DEFAULT = ANIMATION_SUBTYPE_ALL;
|
|
|
|
void ModelAnimationSet::emplace(const std::shared_ptr<ModelAnimation>& animation, const SCP_string& name, ModelAnimationTriggerType type, int subtype) {
|
|
animationSet[{type, subtype}].emplace(name, animation);
|
|
}
|
|
|
|
|
|
void anim_set_initial_states(ship* shipp) {
|
|
ship_info* sip = &Ship_info[shipp->ship_info_index];
|
|
|
|
const auto& initialAnims = sip->animations.animationSet[{animation::ModelAnimationTriggerType::Initial, animation::ModelAnimationSet::SUBTYPE_DEFAULT}];
|
|
|
|
for (const auto& anim : initialAnims) {
|
|
anim.second->start(shipp, false);
|
|
}
|
|
}
|
|
} |