#include "eval/ProjectEvaluator.h" #include "animation/AnimationSampling.h" #include #include 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 normalizeVisibilityKeys(const QVector& keys) { QVector 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 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& keysRaw, int frame, int fadeFrames) { const int nFade = std::max(1, fadeFrames); const QVector 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 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 resolvedOrigin; QHash resolving; resolvedOrigin.reserve(index.size()); resolving.reserve(index.size()); std::function 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& 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 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 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