新增根据帧数控制可见性
This commit is contained in:
311
client/core/eval/ProjectEvaluator.cpp
Normal file
311
client/core/eval/ProjectEvaluator.cpp
Normal file
@@ -0,0 +1,311 @@
|
||||
#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);
|
||||
};
|
||||
|
||||
// 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) : 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});
|
||||
}
|
||||
|
||||
// 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) : 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
|
||||
|
||||
31
client/core/eval/ProjectEvaluator.h
Normal file
31
client/core/eval/ProjectEvaluator.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include "domain/Project.h"
|
||||
|
||||
#include <QHash>
|
||||
#include <QPointF>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
namespace core::eval {
|
||||
|
||||
struct ResolvedEntity {
|
||||
core::Project::Entity entity; // 拷贝:originWorld/imageTopLeftWorld 已替换为求值后的值
|
||||
double opacity = 1.0; // 0..1
|
||||
};
|
||||
|
||||
struct ResolvedTool {
|
||||
core::Project::Tool tool; // 拷贝:originWorld 已替换为求值后的值
|
||||
double opacity = 1.0; // 0..1
|
||||
};
|
||||
|
||||
struct ResolvedProjectFrame {
|
||||
QVector<ResolvedEntity> entities;
|
||||
QVector<ResolvedTool> tools;
|
||||
};
|
||||
|
||||
/// 逐帧求值:处理父子跟随与工具可见性淡入淡出。
|
||||
ResolvedProjectFrame evaluateAtFrame(const core::Project& project, int frame, int fadeFrames = 10);
|
||||
|
||||
} // namespace core::eval
|
||||
|
||||
Reference in New Issue
Block a user