feat(editor): author montage layer bone masks
This commit is contained in:
1 parent
76c7b07f4e
commit
45e4aedabb
5 files changed
+68
-1
No files matched your search
@@ -683,6 +683,9 @@ Montage 运行时副本;Undo/Redo、`jumpRuntimeSection`、`evaluateRuntimeSec
|
||||
`syncRuntimeSection`、`syncRuntimeSectionAndJump`、`clearRuntimeSectionSync` 和磁吸目标因此共享一份边界事实。
|
||||
归一化求值把 `[0,1]` 映射到物理 Section;同步倍率可以只配置,也可以配置后立即跳到该段起点,并可明确清除。
|
||||
运行时按资产的 `animationLayer` 选择协调器层,视口读取该层的双槽归一化合成姿态。
|
||||
`setRuntimeBoneMask` 会同步复制一个与预览骨架匹配的 `AnimBoneMask` 到当前层,调用方随后可以安全销毁或修改
|
||||
原 Mask;`clearRuntimeBoneMask` 恢复全骨骼影响。示例的 Montage Inspector 可从当前选中关节生成 Mask,选择是否
|
||||
包含子关节,并提供紧凑的 Apply/Clear 工具栏。
|
||||
运行中的预览还可用 `setRuntimeRate/getRuntimeRate` 设置和读取严格为正的倍率;倍率只缩放调用者注入的
|
||||
delta,不读取墙钟,也不写回资产的 `basePlayRate`。`getRuntimePhysicalSection`、
|
||||
`getRuntimeSectionProgress` 和 `getRuntimeWeight` 可直接驱动运行诊断 HUD。
|
||||
|
||||
@@ -643,6 +643,13 @@ function panelInspector() {
|
||||
ui.checkbox("Horizontal", true, "montage-root-horizontal");
|
||||
ui.checkbox("Vertical", true, "montage-root-vertical");
|
||||
ui.checkbox("Rotation", true, "montage-root-rotation");
|
||||
ui.text("Layer Bone Mask", "montage-mask-title");
|
||||
ui.slider("Selected Joint", 1.0, 0.0, 1.0, "montage-mask-weight");
|
||||
ui.checkbox("Include Children", true, "montage-mask-children");
|
||||
ui.beginToolbar("montage-mask-tools");
|
||||
ui.iconButton("save", "Apply", "apply-montage-mask");
|
||||
ui.iconButton("close", "Clear", "clear-montage-mask");
|
||||
ui.end();
|
||||
ui.text("Physical Sections", "physical-sections-title");
|
||||
ui.combo("Split", sectionSplitChoices(), combatEditor.selectedSplit, "section-split-selector");
|
||||
ui.slider("Split Time", 0.5, 0.0, combatEditor.timeline.getDuration(), "section-split-time");
|
||||
@@ -1090,6 +1097,23 @@ function handleUiEvents() {
|
||||
applyHistory(id == "track-undo" ? "undo" : "redo");
|
||||
} else if (id == "montage-undo" || id == "montage-redo") {
|
||||
applyHistory(id == "montage-undo" ? "undo" : "redo");
|
||||
} else if (id == "apply-montage-mask") {
|
||||
ui.select("action.inspector");
|
||||
local mask = anim.newBoneMask(combatEditor.skeleton);
|
||||
mask.setAll(0.0);
|
||||
local bone = combatEditor.clipEditor.getSelectedBone();
|
||||
local applied = ui.getChecked("montage-mask-children") ?
|
||||
mask.setBoneAndChildren(bone, ui.getValue("montage-mask-weight")) :
|
||||
mask.setBoneWeightByName(bone, ui.getValue("montage-mask-weight"));
|
||||
if (!applied) combatEditor.status = "Bone mask joint was not found · " + bone;
|
||||
else {
|
||||
local result = combatEditor.timeline.setRuntimeBoneMask(mask);
|
||||
combatEditor.status = result.ok ? "Layer mask applied from joint · " + bone
|
||||
: result.status.summary;
|
||||
}
|
||||
} else if (id == "clear-montage-mask") {
|
||||
local result = combatEditor.timeline.clearRuntimeBoneMask();
|
||||
combatEditor.status = result.ok ? "Layer bone mask cleared" : result.status.summary;
|
||||
} else if (id == "add-section-split") {
|
||||
local splitTime = combatEditor.timeline.getPreviewTime();
|
||||
local result = combatEditor.timeline.addSectionSplit(splitTime);
|
||||
@@ -1179,7 +1203,8 @@ function handleUiEvents() {
|
||||
result = combatEditor.timeline.setSectionSplit(combatEditor.selectedSplit, splitTime);
|
||||
if (result.ok) combatEditor.selectedSplit = sectionSplitIndexAt(splitTime);
|
||||
combatEditor.status = result.ok ? "Physical section split moved" : result.status.summary;
|
||||
} else if (id.find("montage-") == 0 && id != "montage-tools") {
|
||||
} else if (id.find("montage-") == 0 && id != "montage-tools" &&
|
||||
id.find("montage-mask-") != 0) {
|
||||
result = commitMontageSettings();
|
||||
combatEditor.status = result.ok ? "Montage settings committed" : result.status.summary;
|
||||
} else if (id == "insert-track") combatEditor.insertTrack = ui.getValue("insert-track").tointeger();
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "action/editor/ActionTimelineWidget.h"
|
||||
#include "animation/AnimClip.h"
|
||||
#include "animation/AnimImporter.h"
|
||||
#include "animation/AnimLayerMixer.h"
|
||||
#include "animation/AnimPose.h"
|
||||
#include "animation/MontageCoordinator.h"
|
||||
#include "animation/MontagePlayer.h"
|
||||
@@ -713,6 +714,20 @@ public:
|
||||
: StatusCode::NoOp));
|
||||
}
|
||||
|
||||
[[nodiscard]] Result<void> setRuntimeBoneMask(const animation::AnimBoneMask& mask) {
|
||||
if (!coordinator_ || !activeMontage())
|
||||
return Result<void>::failure(
|
||||
Diagnostic::error(DiagnosticCode::Conflict, "montage runtime has not been started", "runtime"));
|
||||
return coordinator_->setLayerBoneMask(activeLayer_, mask);
|
||||
}
|
||||
|
||||
[[nodiscard]] Result<void> clearRuntimeBoneMask() {
|
||||
if (!coordinator_ || !activeMontage())
|
||||
return Result<void>::failure(
|
||||
Diagnostic::error(DiagnosticCode::Conflict, "montage runtime has not been started", "runtime"));
|
||||
return coordinator_->clearLayerBoneMask(activeLayer_);
|
||||
}
|
||||
|
||||
[[nodiscard]] Result<animation::MontageAdvance> beginRuntimeBlendOut(Duration duration) {
|
||||
auto* montage = activeMontage();
|
||||
if (!montage)
|
||||
@@ -1459,6 +1474,19 @@ void exposeActionTimelineScriptBindings(ssq::Table& table, ssq::Class& moduleCla
|
||||
return script::projectResult(vm,
|
||||
self->clearRuntimeSectionSync(static_cast<std::size_t>(sectionIndex)));
|
||||
});
|
||||
actionEditor.addFunc("setRuntimeBoneMask",
|
||||
[vm](ScriptActionTimelineEditor* self, animation::AnimBoneMask* mask) {
|
||||
if (!self || !mask)
|
||||
return bindingFailure(vm, DiagnosticCode::InvalidArgument,
|
||||
"action timeline editor and bone mask must not be null", "mask");
|
||||
return script::projectResult(vm, self->setRuntimeBoneMask(*mask));
|
||||
});
|
||||
actionEditor.addFunc("clearRuntimeBoneMask", [vm](ScriptActionTimelineEditor* self) {
|
||||
if (!self)
|
||||
return bindingFailure(vm, DiagnosticCode::InvalidArgument,
|
||||
"action timeline editor must not be null");
|
||||
return script::projectResult(vm, self->clearRuntimeBoneMask());
|
||||
});
|
||||
actionEditor.addFunc("beginRuntimeBlendOut", [vm](ScriptActionTimelineEditor* self, float durationSeconds) {
|
||||
if (!self)
|
||||
return bindingFailure(vm, DiagnosticCode::InvalidArgument, "action timeline editor must not be null");
|
||||
|
||||
@@ -430,6 +430,10 @@ TEST_CASE("actionMontage.coordinatorLayerMaskRestrictsCompositionPerBone") {
|
||||
REQUIRE(configured.ok());
|
||||
REQUIRE_EQ(configured.value().size(), 2U);
|
||||
CHECK_EQ(configured.value()[1], 0.5f);
|
||||
upperBody.setAll(0.0f);
|
||||
configured = coordinator.layerBoneMask(0);
|
||||
REQUIRE(configured.ok());
|
||||
CHECK_EQ(configured.value()[1], 0.5f);
|
||||
|
||||
auto handle = coordinator.play(0, montageTimeline(), maskedMontageClips(),
|
||||
eve::action::ActionExecutionId(21), eve::SimulationTick(1));
|
||||
|
||||
@@ -41,6 +41,11 @@ TEST_CASE("editor.actionTimeline.scriptUsesCanonicalTransactionsAndWorkspace") {
|
||||
sectionProgressBeforeStart <- actionEditor.evaluateRuntimeSectionProgress(0, 0.5);
|
||||
sectionSyncJumpBeforeStart <- actionEditor.syncRuntimeSectionAndJump(0, 0.25);
|
||||
clearSectionSyncBeforeStart <- actionEditor.clearRuntimeSectionSync(0);
|
||||
testAnimation <- eve.Animation();
|
||||
testSkeleton <- testAnimation.newSkeleton();
|
||||
testBoneMask <- testAnimation.newBoneMask(testSkeleton);
|
||||
setBoneMaskBeforeStart <- actionEditor.setRuntimeBoneMask(testBoneMask);
|
||||
clearBoneMaskBeforeStart <- actionEditor.clearRuntimeBoneMask();
|
||||
configured <- actionEditor.configureWorkspace(workspace);
|
||||
workspacePanelCount <- workspace.getPanelCount();
|
||||
viewport <- actionEditor.setViewport(800.0, 36.0, 120.0);
|
||||
@@ -210,6 +215,8 @@ TEST_CASE("editor.actionTimeline.scriptUsesCanonicalTransactionsAndWorkspace") {
|
||||
CHECK(!vm.find("sectionProgressBeforeStart").toTable().get<bool>("ok"));
|
||||
CHECK(!vm.find("sectionSyncJumpBeforeStart").toTable().get<bool>("ok"));
|
||||
CHECK(!vm.find("clearSectionSyncBeforeStart").toTable().get<bool>("ok"));
|
||||
CHECK(!vm.find("setBoneMaskBeforeStart").toTable().get<bool>("ok"));
|
||||
CHECK(!vm.find("clearBoneMaskBeforeStart").toTable().get<bool>("ok"));
|
||||
CHECK(vm.find("configured").toTable().get<bool>("ok"));
|
||||
CHECK_EQ(vm.find("workspacePanelCount").toInt(), 4);
|
||||
CHECK(vm.find("viewport").toTable().get<bool>("ok"));
|
||||
|
||||
Reference in new issue
Block a user