Files
hfut-bishe/client/core/eval/ProjectEvaluator.cpp
T
2026-05-14 13:30:06 +08:00

259 lines
9.1 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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);
};
// Entitiesresolved 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});
}
// Toolsresolved 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