This commit is contained in:
2026-05-14 13:30:06 +08:00
parent 974946cee4
commit e43171521d
91 changed files with 10485 additions and 3254 deletions
+65 -169
View File
@@ -1,8 +1,10 @@
#include "eval/ProjectEvaluator.h"
#include "animation/AnimationEvaluator.h"
#include "animation/AnimationSampling.h"
#include <algorithm>
#include <cmath>
#include <unordered_set>
namespace core::eval {
@@ -15,68 +17,6 @@ struct NodeRef {
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 sampledRelativeForEntity(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.parentOffsetWorld, core::KeyInterpolation::Linear);
}
return core::sampleLocation(e.locationKeys, localFrame, e.parentOffsetWorld, 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);
}
QPointF sampledCenterForCamera(const core::Project::Camera& c,
const core::Project::AnimationClip* clipOrNull,
int localFrame) {
if (clipOrNull && clipOrNull->cameraLocationKeys.contains(c.id)) {
const auto& keys = clipOrNull->cameraLocationKeys.value(c.id);
return core::sampleLocation(keys, localFrame, c.centerWorld, core::KeyInterpolation::Linear);
}
return core::sampleLocation(c.locationKeys, localFrame, c.centerWorld, core::KeyInterpolation::Linear);
}
double sampledViewScaleForCamera(const core::Project::Camera& c,
const core::Project::AnimationClip* clipOrNull,
int localFrame) {
if (clipOrNull && clipOrNull->cameraScaleKeys.contains(c.id)) {
const auto& keys = clipOrNull->cameraScaleKeys.value(c.id);
if (!keys.isEmpty()) {
return core::sampleUserScale(keys, localFrame, c.viewScale, core::KeyInterpolation::Linear);
}
}
return core::sampleUserScale(c.scaleKeys, localFrame, c.viewScale, core::KeyInterpolation::Linear);
}
QPointF sampledRelativeForTool(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.parentOffsetWorld, core::KeyInterpolation::Linear);
}
return core::sampleLocation(t.locationKeys, localFrame, t.parentOffsetWorld, core::KeyInterpolation::Linear);
}
struct VisKey {
int frame = 0;
bool value = true;
@@ -144,82 +84,10 @@ double opacityFromBoolKeys(const QVector<core::Project::ToolKeyframeBool>& keysR
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 evaluateAtFrame(const core::Project& project, int frame, int fadeFrames,
const QString& animationId) {
ResolvedProjectFrame out;
const auto& ents = project.entities();
const auto& tools = project.tools();
@@ -228,9 +96,15 @@ ResolvedProjectFrame evaluateAtFrame(const core::Project& project, int frame, in
out.tools.reserve(tools.size());
out.cameras.reserve(cams.size());
const StripEvalCtx ctx = resolveStripCtx(project, frame);
const int localFrame = ctx.localFrame;
const core::Project::AnimationClip* clip = ctx.clip;
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());
@@ -262,8 +136,8 @@ ResolvedProjectFrame evaluateAtFrame(const core::Project& project, int frame, in
// 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);
if (r.kind == NodeRef::Kind::Entity) o = ents[r.index].originWorld;
else o = tools[r.index].originWorld;
resolvedOrigin.insert(id, o);
return o;
}
@@ -275,20 +149,23 @@ ResolvedProjectFrame evaluateAtFrame(const core::Project& project, int frame, in
if (r.kind == NodeRef::Kind::Entity) {
const auto& e = ents[r.index];
parentId = e.parentId;
selfSampled = sampledOriginForEntity(e, clip, localFrame);
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 = sampledOriginForTool(t, clip, localFrame);
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);
const QPointF rel = (r.kind == NodeRef::Kind::Entity)
? sampledRelativeForEntity(ents[r.index], clip, localFrame)
: sampledRelativeForTool(tools[r.index], clip, localFrame);
outO = po + rel;
outO = po + selfSampled;
}
resolving.insert(id, false);
@@ -308,50 +185,69 @@ ResolvedProjectFrame evaluateAtFrame(const core::Project& project, int frame, in
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 ro = (!e.id.isEmpty()) ? resolve(e.id) : e.originWorld;
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 (animSample.entityUserScale.contains(e.id)) {
e.userScale = animSample.entityUserScale.value(e.id);
}
if (clip && clip->entityImageFrames.contains(e.id)) {
const auto& frames = clip->entityImageFrames.value(e.id);
e.imagePath = core::sampleImagePath(frames, localFrame, e.imagePath);
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;
}
QVector<core::Project::ToolKeyframeBool> visKeys = e.visibilityKeys;
if (clip && clip->entityVisibilityKeys.contains(e.id)) {
visKeys = clip->entityVisibilityKeys.value(e.id);
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 op = opacityWithDefault(visKeys, e.visible);
out.entities.push_back(ResolvedEntity{e, op});
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) : sampledOriginForTool(t, clip, localFrame);
const QPointF ro = (!t.id.isEmpty()) ? resolve(t.id) : t.originWorld;
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});
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 = sampledCenterForCamera(c, clip, localFrame);
cam.viewScale = sampledViewScaleForCamera(c, clip, localFrame);
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)});
}