Files
hfut-bishe/client/core/eval/ProjectEvaluator.cpp

312 lines
11 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/AnimationSampling.h"
#include <algorithm>
#include <unordered_set>
namespace core::eval {
namespace {
struct NodeRef {
enum class Kind { Entity, Tool };
Kind kind = Kind::Entity;
int index = -1;
};
QPointF sampledOriginForEntity(const core::Project::Entity& e,
const core::Project::AnimationClip* clipOrNull,
int localFrame) {
if (clipOrNull && clipOrNull->entityLocationKeys.contains(e.id)) {
const auto& keys = clipOrNull->entityLocationKeys.value(e.id);
return core::sampleLocation(keys, localFrame, e.originWorld, core::KeyInterpolation::Linear);
}
return core::sampleLocation(e.locationKeys, localFrame, e.originWorld, core::KeyInterpolation::Linear);
}
QPointF sampledOriginForTool(const core::Project::Tool& t,
const core::Project::AnimationClip* clipOrNull,
int localFrame) {
if (clipOrNull && clipOrNull->toolLocationKeys.contains(t.id)) {
const auto& keys = clipOrNull->toolLocationKeys.value(t.id);
return core::sampleLocation(keys, localFrame, t.originWorld, core::KeyInterpolation::Linear);
}
return core::sampleLocation(t.locationKeys, localFrame, t.originWorld, core::KeyInterpolation::Linear);
}
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;
}
struct StripEvalCtx {
const core::Project::AnimationScheme* scheme = nullptr;
const core::Project::NlaStrip* strip = nullptr;
const core::Project::AnimationClip* clip = nullptr;
int slot = 0;
int localFrame = 0; // 0..kClipFixedFrames-1
};
static const core::Project::NlaStrip* findStripById(const core::Project::AnimationScheme& scheme, const QString& id) {
if (id.isEmpty()) return nullptr;
for (const auto& tr : scheme.tracks) {
for (const auto& st : tr.strips) {
if (st.id == id) return &st;
}
}
return nullptr;
}
static bool trackIsEffectivelyMuted(const core::Project::AnimationScheme& scheme, const core::Project::NlaTrack& t) {
// 若有任意 solo=true则只有 solo 的 track 生效(且仍受自身 muted 控制)
bool anySolo = false;
for (const auto& tr : scheme.tracks) {
if (tr.solo) {
anySolo = true;
break;
}
}
if (anySolo && !t.solo) {
return true;
}
return t.muted;
}
static const core::Project::NlaStrip* pickStripAtSlot(const core::Project::AnimationScheme& scheme, int slot) {
const core::Project::NlaStrip* chosen = nullptr;
for (const auto& tr : scheme.tracks) {
if (trackIsEffectivelyMuted(scheme, tr)) continue;
for (const auto& st : tr.strips) {
if (!st.enabled || st.muted) continue;
const int a = st.startSlot;
const int b = st.startSlot + std::max(1, st.slotLen);
if (slot >= a && slot < b) {
chosen = &st; // 轨道顺序靠后的覆盖靠前的(更接近“上层”)
}
}
}
return chosen;
}
static StripEvalCtx resolveStripCtx(const core::Project& project, int globalFrame) {
StripEvalCtx ctx;
const auto* scheme = project.activeSchemeOrNull();
if (!scheme) {
ctx.localFrame = std::max(0, globalFrame);
return ctx;
}
ctx.scheme = scheme;
const int g = std::max(0, globalFrame);
ctx.slot = g / core::Project::kClipFixedFrames;
ctx.localFrame = g % core::Project::kClipFixedFrames;
const core::Project::NlaStrip* st = findStripById(*scheme, project.selectedStripId());
// 若选中条带不覆盖当前 slot则退回自动挑选
if (!st || ctx.slot < st->startSlot || ctx.slot >= (st->startSlot + std::max(1, st->slotLen)) || !st->enabled || st->muted) {
st = pickStripAtSlot(*scheme, ctx.slot);
}
ctx.strip = st;
if (st) {
ctx.clip = project.findClipById(st->clipId);
}
return ctx;
}
} // namespace
ResolvedProjectFrame evaluateAtFrame(const core::Project& project, int frame, int fadeFrames) {
ResolvedProjectFrame out;
const auto& ents = project.entities();
const auto& tools = project.tools();
out.entities.reserve(ents.size());
out.tools.reserve(tools.size());
const StripEvalCtx ctx = resolveStripCtx(project, frame);
const int localFrame = ctx.localFrame;
const core::Project::AnimationClip* clip = ctx.clip;
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 = sampledOriginForEntity(ents[r.index], clip, localFrame);
else o = sampledOriginForTool(tools[r.index], clip, localFrame);
resolvedOrigin.insert(id, o);
return o;
}
resolving.insert(id, true);
const NodeRef r = index.value(id);
QString parentId;
QPointF off;
QPointF selfSampled;
if (r.kind == NodeRef::Kind::Entity) {
const auto& e = ents[r.index];
parentId = e.parentId;
off = e.parentOffsetWorld;
selfSampled = sampledOriginForEntity(e, clip, localFrame);
} else {
const auto& t = tools[r.index];
parentId = t.parentId;
off = t.parentOffsetWorld;
selfSampled = sampledOriginForTool(t, clip, localFrame);
}
QPointF outO = selfSampled;
if (!parentId.isEmpty() && index.contains(parentId)) {
const QPointF po = resolve(parentId);
outO = po + off;
}
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) : sampledOriginForEntity(e, clip, localFrame);
const QPointF delta = ro - base;
e.originWorld = ro;
e.imageTopLeftWorld += delta;
// Clip channels: userScale / imagePath迁移后仍能逐帧显示
if (clip && clip->entityUserScaleKeys.contains(e.id)) {
const auto& keys = clip->entityUserScaleKeys.value(e.id);
e.userScale = core::sampleUserScale(keys, localFrame, e.userScale, core::KeyInterpolation::Linear);
}
if (clip && clip->entityImageFrames.contains(e.id)) {
const auto& frames = clip->entityImageFrames.value(e.id);
e.imagePath = core::sampleImagePath(frames, localFrame, e.imagePath);
}
QVector<core::Project::ToolKeyframeBool> visKeys = e.visibilityKeys;
if (clip && clip->entityVisibilityKeys.contains(e.id)) {
visKeys = clip->entityVisibilityKeys.value(e.id);
}
const double op = opacityWithDefault(visKeys, e.visible);
out.entities.push_back(ResolvedEntity{e, op});
}
// 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) : sampledOriginForTool(t, clip, localFrame);
const QPointF delta = ro - base;
t.originWorld = ro;
// parentOffsetWorld 已包含相对关系,不在这里改
QVector<core::Project::ToolKeyframeBool> visKeys = t.visibilityKeys;
if (clip && clip->toolVisibilityKeys.contains(t.id)) {
visKeys = clip->toolVisibilityKeys.value(t.id);
}
const double op = opacityWithDefault(visKeys, t.visible);
(void)delta;
out.tools.push_back(ResolvedTool{t, op});
}
return out;
}
} // namespace core::eval