259 lines
9.1 KiB
C++
259 lines
9.1 KiB
C++
#include "eval/ProjectEvaluator.h"
|
||
|
||
#include "animation/AnimationEvaluator.h"
|
||
#include "animation/AnimationSampling.h"
|
||
|
||
#include <algorithm>
|
||
#include <cmath>
|
||
#include <unordered_set>
|
||
|
||
namespace core::eval {
|
||
|
||
namespace {
|
||
|
||
struct NodeRef {
|
||
enum class Kind { Entity, Tool };
|
||
Kind kind = Kind::Entity;
|
||
int index = -1;
|
||
};
|
||
|
||
struct VisKey {
|
||
int frame = 0;
|
||
bool value = true;
|
||
};
|
||
|
||
static QVector<VisKey> normalizeVisibilityKeys(const QVector<core::Project::ToolKeyframeBool>& keys) {
|
||
QVector<VisKey> out;
|
||
out.reserve(keys.size());
|
||
for (const auto& k : keys) {
|
||
out.push_back(VisKey{k.frame, k.value});
|
||
}
|
||
std::sort(out.begin(), out.end(), [](const VisKey& a, const VisKey& b) { return a.frame < b.frame; });
|
||
// 若同帧重复,保留最后一个
|
||
QVector<VisKey> dedup;
|
||
dedup.reserve(out.size());
|
||
for (const auto& k : out) {
|
||
if (!dedup.isEmpty() && dedup.last().frame == k.frame) {
|
||
dedup.last() = k;
|
||
} else {
|
||
dedup.push_back(k);
|
||
}
|
||
}
|
||
return dedup;
|
||
}
|
||
|
||
double opacityFromBoolKeys(const QVector<core::Project::ToolKeyframeBool>& keysRaw, int frame, int fadeFrames) {
|
||
const int nFade = std::max(1, fadeFrames);
|
||
const QVector<VisKey> keys = normalizeVisibilityKeys(keysRaw);
|
||
if (keys.isEmpty()) {
|
||
return 1.0;
|
||
}
|
||
// 规则:在发生状态变化的关键帧 t 附近做对称淡变
|
||
// fadeFrames=10 -> 约 [t-5, t+5] 渐变,符合“60 帧切换则 55~65 过渡”
|
||
const double half = double(nFade) * 0.5;
|
||
|
||
// first key 之前,直接采用 first value(不做凭空反向切换)
|
||
if (frame <= keys.front().frame) {
|
||
return keys.front().value ? 1.0 : 0.0;
|
||
}
|
||
|
||
bool state = keys.front().value;
|
||
for (int i = 1; i < keys.size(); ++i) {
|
||
const bool prev = keys[i - 1].value;
|
||
const bool cur = keys[i].value;
|
||
const int t = keys[i].frame;
|
||
if (prev == cur) {
|
||
// 状态未变:忽略该 key
|
||
continue;
|
||
}
|
||
const double a = double(t) - half;
|
||
const double b = double(t) + half;
|
||
if (double(frame) < a) {
|
||
// 还没进入该次过渡
|
||
return state ? 1.0 : 0.0;
|
||
}
|
||
if (double(frame) <= b) {
|
||
const double u = std::clamp((double(frame) - a) / std::max(1e-9, b - a), 0.0, 1.0);
|
||
const double x = prev ? 1.0 : 0.0;
|
||
const double y = cur ? 1.0 : 0.0;
|
||
return x + (y - x) * u;
|
||
}
|
||
// 该次过渡已结束,进入新状态
|
||
state = cur;
|
||
}
|
||
return state ? 1.0 : 0.0;
|
||
}
|
||
|
||
} // namespace
|
||
|
||
ResolvedProjectFrame evaluateAtFrame(const core::Project& project, int frame, int fadeFrames,
|
||
const QString& animationId) {
|
||
ResolvedProjectFrame out;
|
||
const auto& ents = project.entities();
|
||
const auto& tools = project.tools();
|
||
const auto& cams = project.cameras();
|
||
out.entities.reserve(ents.size());
|
||
out.tools.reserve(tools.size());
|
||
out.cameras.reserve(cams.size());
|
||
|
||
const int localFrame = std::max(0, frame);
|
||
const core::Project::Animation* activeAnimation = nullptr;
|
||
if (animationId.isEmpty()) {
|
||
activeAnimation = project.activeAnimationOrNull();
|
||
} else {
|
||
activeAnimation = project.findAnimationById(animationId);
|
||
}
|
||
const core::AnimationSample animSample =
|
||
activeAnimation ? core::evaluateAnimation(*activeAnimation, frame) : core::AnimationSample{};
|
||
|
||
QHash<QString, NodeRef> index;
|
||
index.reserve(ents.size() + tools.size());
|
||
for (int i = 0; i < ents.size(); ++i) {
|
||
if (!ents[i].id.isEmpty()) {
|
||
index.insert(ents[i].id, NodeRef{NodeRef::Kind::Entity, i});
|
||
}
|
||
}
|
||
for (int i = 0; i < tools.size(); ++i) {
|
||
if (!tools[i].id.isEmpty() && !index.contains(tools[i].id)) {
|
||
index.insert(tools[i].id, NodeRef{NodeRef::Kind::Tool, i});
|
||
}
|
||
}
|
||
|
||
QHash<QString, QPointF> resolvedOrigin;
|
||
QHash<QString, bool> resolving;
|
||
resolvedOrigin.reserve(index.size());
|
||
resolving.reserve(index.size());
|
||
|
||
std::function<QPointF(const QString&)> resolve = [&](const QString& id) -> QPointF {
|
||
if (resolvedOrigin.contains(id)) {
|
||
return resolvedOrigin.value(id);
|
||
}
|
||
if (!index.contains(id)) {
|
||
resolvedOrigin.insert(id, QPointF());
|
||
return QPointF();
|
||
}
|
||
if (resolving.value(id, false)) {
|
||
// cycle:降级为自身采样 origin
|
||
const NodeRef r = index.value(id);
|
||
QPointF o;
|
||
if (r.kind == NodeRef::Kind::Entity) o = ents[r.index].originWorld;
|
||
else o = tools[r.index].originWorld;
|
||
resolvedOrigin.insert(id, o);
|
||
return o;
|
||
}
|
||
resolving.insert(id, true);
|
||
|
||
const NodeRef r = index.value(id);
|
||
QString parentId;
|
||
QPointF selfSampled;
|
||
if (r.kind == NodeRef::Kind::Entity) {
|
||
const auto& e = ents[r.index];
|
||
parentId = e.parentId;
|
||
selfSampled = e.parentId.isEmpty() ? e.originWorld : e.parentOffsetWorld;
|
||
if (animSample.entityPosition.contains(e.id)) {
|
||
selfSampled = animSample.entityPosition.value(e.id);
|
||
}
|
||
} else {
|
||
const auto& t = tools[r.index];
|
||
parentId = t.parentId;
|
||
selfSampled = t.parentId.isEmpty() ? t.originWorld : t.parentOffsetWorld;
|
||
if (animSample.toolPosition.contains(t.id)) {
|
||
selfSampled = animSample.toolPosition.value(t.id);
|
||
}
|
||
}
|
||
|
||
QPointF outO = selfSampled;
|
||
if (!parentId.isEmpty() && index.contains(parentId)) {
|
||
const QPointF po = resolve(parentId);
|
||
outO = po + selfSampled;
|
||
}
|
||
|
||
resolving.insert(id, false);
|
||
resolvedOrigin.insert(id, outO);
|
||
return outO;
|
||
};
|
||
|
||
auto opacityWithDefault = [&](const QVector<core::Project::ToolKeyframeBool>& keys,
|
||
bool defaultVisible) -> double {
|
||
if (keys.isEmpty()) {
|
||
return defaultVisible ? 1.0 : 0.0;
|
||
}
|
||
return opacityFromBoolKeys(keys, localFrame, fadeFrames);
|
||
};
|
||
|
||
// Entities:resolved origin + opacity(可见性轨道)
|
||
for (int i = 0; i < ents.size(); ++i) {
|
||
core::Project::Entity e = ents[i];
|
||
const QPointF base = e.originWorld;
|
||
const QPointF ro = (!e.id.isEmpty()) ? resolve(e.id) : e.originWorld;
|
||
const QPointF delta = ro - base;
|
||
e.originWorld = ro;
|
||
e.imageTopLeftWorld += delta;
|
||
|
||
if (animSample.entityUserScale.contains(e.id)) {
|
||
e.userScale = animSample.entityUserScale.value(e.id);
|
||
}
|
||
if (animSample.entityDepthScale01.contains(e.id)) {
|
||
const double v01 = std::clamp(animSample.entityDepthScale01.value(e.id), 0.0, 1.0);
|
||
e.depth = int(std::lround(v01 * 255.0));
|
||
}
|
||
e.runtimeImagePng.clear();
|
||
if (animSample.entitySpritePng.contains(e.id)) {
|
||
const QByteArray png = animSample.entitySpritePng.value(e.id);
|
||
if (!png.isEmpty()) {
|
||
e.runtimeImagePng = png;
|
||
}
|
||
}
|
||
if (e.runtimeImagePng.isEmpty() && !e.defaultImagePng.isEmpty()) {
|
||
e.runtimeImagePng = e.defaultImagePng;
|
||
}
|
||
|
||
if (animSample.entityPriority.contains(e.id)) {
|
||
const double p = animSample.entityPriority.value(e.id);
|
||
e.priority = std::clamp(int(std::lround(p)), -1000000, 1000000);
|
||
}
|
||
|
||
const double animOp = animSample.entityVisibility.contains(e.id)
|
||
? (animSample.entityVisibility.value(e.id) ? 1.0 : 0.0)
|
||
: (e.visible ? 1.0 : 0.0);
|
||
out.entities.push_back(ResolvedEntity{e, animOp});
|
||
}
|
||
|
||
// Tools:resolved origin + opacity(可见性轨道)
|
||
for (int i = 0; i < tools.size(); ++i) {
|
||
core::Project::Tool t = tools[i];
|
||
const QPointF base = t.originWorld;
|
||
const QPointF ro = (!t.id.isEmpty()) ? resolve(t.id) : t.originWorld;
|
||
const QPointF delta = ro - base;
|
||
t.originWorld = ro;
|
||
// parentOffsetWorld 已包含相对关系,不在这里改
|
||
(void)delta;
|
||
if (animSample.toolPriority.contains(t.id)) {
|
||
const double p = animSample.toolPriority.value(t.id);
|
||
t.priority = std::clamp(int(std::lround(p)), -1000000, 1000000);
|
||
}
|
||
const double animOp = animSample.toolVisibility.contains(t.id)
|
||
? (animSample.toolVisibility.value(t.id) ? 1.0 : 0.0)
|
||
: (t.visible ? 1.0 : 0.0);
|
||
out.tools.push_back(ResolvedTool{t, animOp});
|
||
}
|
||
|
||
for (const auto& c : cams) {
|
||
core::Project::Camera cam = c;
|
||
cam.centerWorld = c.centerWorld;
|
||
cam.viewScale = c.viewScale;
|
||
if (animSample.cameraPosition.contains(c.id)) {
|
||
cam.centerWorld = animSample.cameraPosition.value(c.id);
|
||
}
|
||
if (animSample.cameraScale.contains(c.id)) {
|
||
cam.viewScale = animSample.cameraScale.value(c.id);
|
||
}
|
||
out.cameras.push_back(ResolvedCamera{std::move(cam)});
|
||
}
|
||
|
||
return out;
|
||
}
|
||
|
||
} // namespace core::eval
|
||
|