载入中...
搜索中...
未找到
ParticlesCapabilities.cpp
浏览该文件的文档.
6#include "common/Capability.h"
11#include "particles/Particles.h"
13
14#include <algorithm>
15#include <cmath>
16#include <list>
17#include <map>
18#include <memory>
19#include <optional>
20#include <string>
21#include <utility>
22#include <vector>
23
24namespace eve::particles {
25namespace {
26
27class ParticlesQueryImpl final : public eve::IParticlesQuery {
28public:
29 int emitterCount() override {
30 auto *p = eve::ModuleManager::getInstance<Particles>("Particles");
31 return p ? p->getEmitterCount() : 0;
32 }
33
34 bool createEmitter(int bufferSize, float x, float y, const std::string &preset, int count,
35 float *outX, float *outY, int *outCount) override {
36 auto *p = eve::ModuleManager::getInstance<Particles>("Particles");
37 if (!p) return false;
38 auto *em = p->newEmitter(bufferSize > 0 ? bufferSize : 1000);
39 if (!em) return false;
40 em->setPosition(x, y);
41 if (!preset.empty()) em->applyPreset(preset);
42 em->start();
43 em->emit(count);
44 if (outX) *outX = em->getX();
45 if (outY) *outY = em->getY();
46 if (outCount) *outCount = em->getCount();
47 return true;
48 }
49};
50
51using ActiveKey = std::pair<eve::action::ActionExecutionId, std::string>;
52
53class ParticleActionHandler final : public eve::action::IActionNotifyHandler {
54public:
57 const ActiveKey key{context.executionId, event.itemId.format()};
59 const auto found = active_.find(key);
60 if (found == active_.end())
62 finish(found->second);
63 active_.erase(found);
65 }
66 const bool instant = event.kind == eve::action::ActionTimelineEventKind::Notify;
70 "VFX state handler requires enter or exit boundary", "event.kind"));
71 if (!instant && active_.contains(key))
73 eve::Diagnostic::error(eve::DiagnosticCode::Conflict, "VFX state is already active", "itemId"));
76 if (!binding) return eve::Result<void>::failure(binding.status());
77 auto pose = resolvePose(binding.value().spatial, context);
78 if (!pose) return eve::Result<void>::failure(pose.status());
79 auto* particles = eve::ModuleManager::getInstance<Particles>("Particles");
80 if (!particles)
82 eve::Diagnostic::error(eve::DiagnosticCode::NotFound, "Particles module is unavailable", "particles"));
83 std::unique_ptr<ParticleEffect> effect(particles->newEffectFromFile(binding.value().uri));
84 if (!effect)
86 particles->getLastEffectError().empty()
87 ? "VFX asset could not be loaded"
88 : particles->getLastEffectError(),
89 "uri"));
90 applyWorldTransform(*effect, binding.value().spatial, pose.value());
91 effect->start();
92 ActiveEffect owned{std::move(effect), binding.value(), std::move(pose).takeValue()};
93 if (instant) {
95 if (!duration) return eve::Result<void>::failure(duration.status());
96 auto end = context.time.tryAdd(duration.value());
97 if (!end) return eve::Result<void>::failure(end.status());
98 transients_.push_back({context.executionId, std::move(end).takeValue(), std::move(owned)});
99 } else {
100 active_.emplace(key, std::move(owned));
101 }
103 }
104
107 const auto found = active_.find({context.executionId, block.itemId.format()});
108 if (found == active_.end())
110 eve::Diagnostic::error(eve::DiagnosticCode::NotFound, "Active VFX state has no instance", "itemId"));
112 auto pose = resolvePose(found->second.binding.spatial, context);
113 if (!pose) return eve::Result<void>::failure(pose.status());
114 if (found->second.binding.spatial.mode == eve::action::ActionSpatialAttachmentMode::FollowPositionOnly) {
115 found->second.pose.positionX = pose.value().positionX;
116 found->second.pose.positionY = pose.value().positionY;
117 found->second.pose.positionZ = pose.value().positionZ;
118 } else {
119 found->second.pose = std::move(pose).takeValue();
120 }
121 }
122 applyWorldTransform(*found->second.effect, found->second.binding.spatial, found->second.pose);
123 const double duration = block.duration.seconds();
124 if (duration > 0.0) {
125 if (!std::isfinite(context.playbackRate) || context.playbackRate <= 0.0)
128 "VFX playback rate must be positive and finite", "playbackRate"));
129 const double externalRate = found->second.binding.playbackRateSynced ? context.playbackRate : 1.0;
130 const float stretch = static_cast<float>(
131 (found->second.binding.clipEndTime - found->second.binding.clipStartTime) /
132 duration * externalRate);
133 for (int i = 0; i < found->second.effect->getEmitterCount(); ++i)
134 if (auto* emitter = found->second.effect->getEmitter(i)) emitter->setPlaybackSpeed(stretch);
135 }
137 }
138
140 const eve::action::ActionNotifyContext&) const override {
143 if (!binding) return eve::Result<void>::failure(binding.status());
145 }
146
148 bool changed = false;
149 for (auto it = transients_.begin(); it != transients_.end();) {
150 if (it->executionId != context.executionId || context.time < it->endTime) {
151 ++it;
152 continue;
153 }
154 finish(it->owned);
155 it = transients_.erase(it);
156 changed = true;
157 }
158 const auto reaped = std::erase_if(residuals_, [](const std::unique_ptr<ParticleEffect>& effect) {
159 for (int i = 0; i < effect->getEmitterCount(); ++i)
160 if (auto* emitter = effect->getEmitter(i); emitter && emitter->getCount() > 0) return false;
161 return true;
162 });
163 changed = changed || reaped > 0;
166 }
167
168private:
169 struct ActiveEffect {
170 std::unique_ptr<ParticleEffect> effect;
173 };
174 struct TransientEffect {
177 ActiveEffect owned;
178 };
179
180 static eve::Result<eve::EntitySpatialPose> resolvePose(
182 std::optional<ecs::EntityHandle> handle;
185 handle = context.source;
186 attachments = context.sourceAttachment;
187 } else {
188 if (spatial.targetIndex >= context.targets.size())
190 eve::DiagnosticCode::NotFound, "VFX target index is unavailable", "targetIndex"));
192 if (spatial.targetIndex < context.targetAttachments.size())
193 attachments = context.targetAttachments[spatial.targetIndex];
194 }
196 if (handle) {
198 if (!root) return root;
199 pose = std::move(root).takeValue();
200 }
201 if (spatial.bone.empty()) return eve::Result<eve::EntitySpatialPose>::success(std::move(pose));
202 if (!attachments)
204 eve::DiagnosticCode::Unsupported, "VFX bone requires an attachment source", "bone"));
205 auto point = attachments->get().sampleAttachmentPoint(
206 spatial.bone, {static_cast<float>(spatial.positionOffset.x),
207 static_cast<float>(spatial.positionOffset.y),
208 static_cast<float>(spatial.positionOffset.z)});
210 pose.positionX = point.value().x;
211 pose.positionY = point.value().y;
212 pose.positionZ = point.value().z;
214 }
215
216 static void applyWorldTransform(ParticleEffect& effect, const eve::action::ActionSpatialBinding& spatial,
218 const double offsetX = spatial.bone.empty() ? spatial.positionOffset.x : 0.0;
219 const double offsetY = spatial.bone.empty() ? spatial.positionOffset.y : 0.0;
220 effect.setPosition(static_cast<float>(pose.positionX + offsetX),
221 static_cast<float>(pose.positionY + offsetY));
222 effect.setRotation(static_cast<float>((pose.rotationZDegrees + spatial.rotationOffsetDegrees.z) *
223 0.017453292519943295));
224 effect.setScale(static_cast<float>(pose.scaleX * spatial.scale.x));
225 }
226
227 void finish(ActiveEffect& active) {
228 active.effect->stop();
230 active.effect->reset();
231 return;
232 }
233 bool hasParticles = false;
234 for (int i = 0; i < active.effect->getEmitterCount(); ++i)
235 if (auto* emitter = active.effect->getEmitter(i); emitter && emitter->getCount() > 0) {
236 hasParticles = true;
237 break;
238 }
239 if (hasParticles) residuals_.push_back(std::move(active.effect));
240 }
241
242 std::map<ActiveKey, ActiveEffect> active_;
243 std::vector<TransientEffect> transients_;
244 std::vector<std::unique_ptr<ParticleEffect>> residuals_;
245};
246
247class ParticleActionPreviewSink final : public eve::action::IActionPreviewSink {
248public:
249 eve::Result<void> prepare(const eve::action::ActionPreviewFrame& frame) override {
250 discardPrepared();
251 preparedReason_ = frame.reason;
252 for (const auto& block : frame.activeBlocks) {
253 if (block.type.format() != "presentation:vfx-state") continue;
254 const std::string key = block.itemId.format();
257 if (!binding) return failPrepared(binding.status());
258 const double target = mediaTime(binding.value(), block.localTime, block.duration);
259 const auto found = current_.find(key);
260 if (frame.reason == eve::action::ActionPreviewReason::Advance && found != current_.end() &&
261 found->second.payload == block.payload && found->second.duration == block.duration &&
262 target + kTimeEpsilon >= found->second.mediaTime) {
263 preparedRetained_.emplace(key, target);
264 continue;
265 }
266 auto effect = makeEffect(binding.value(), target);
267 if (!effect) return failPrepared(effect.status());
268 preparedStates_.emplace(
269 key, PreviewEffect{block.payload, block.duration, std::move(effect).takeValue(), target});
270 }
271
272 for (const auto& cue : frame.cues) {
273 if (cue.kind != eve::action::ActionPreviewCueKind::Vfx || cue.type.format() != "presentation:vfx")
274 continue;
277 if (!binding) return failPrepared(binding.status());
279 if (!lifetime) return failPrepared(lifetime.status());
280 auto end = cue.time.tryAdd(lifetime.value());
281 if (!end) return failPrepared(end.status());
282 if (frame.current >= end.value()) continue;
283 const auto elapsedNs = std::max<std::int64_t>(0, frame.current.nanoseconds() - cue.time.nanoseconds());
284 const auto elapsed = eve::Duration::fromNanoseconds(elapsedNs);
285 const double target = mediaTime(binding.value(), elapsed, lifetime.value());
286 auto effect = makeEffect(binding.value(), target);
287 if (!effect) return failPrepared(effect.status());
288 preparedInstants_.push_back(
289 TransientEffect{std::move(binding).takeValue(), cue.time, end.value(),
290 std::move(effect).takeValue(), target});
291 }
292 preparedCurrent_ = frame.current;
294 }
295
296 void present(const eve::action::ActionPreviewFrame&) noexcept override {
297 for (auto it = current_.begin(); it != current_.end();) {
298 if (preparedRetained_.contains(it->first)) {
299 ++it;
300 continue;
301 }
302 it = current_.erase(it);
303 }
304 for (const auto& [key, target] : preparedRetained_) {
305 auto& current = current_.at(key);
306 simulateRange(*current.effect, current.mediaTime, target);
307 current.mediaTime = target;
308 }
309 current_.merge(preparedStates_);
310 for (auto& [key, value] : current_) {
311 (void)key;
312 value.effect->setVisible(true);
313 }
314
315 if (preparedReason_ != eve::action::ActionPreviewReason::Advance) transients_.clear();
316 for (auto it = transients_.begin(); it != transients_.end();) {
317 if (preparedCurrent_ >= it->endTime) {
318 it = transients_.erase(it);
319 continue;
320 }
321 const auto elapsedNs = std::max<std::int64_t>(
322 0, preparedCurrent_.nanoseconds() - it->startTime.nanoseconds());
324 it->endTime.nanoseconds() - it->startTime.nanoseconds());
325 const double target = mediaTime(
326 it->binding, eve::Duration::fromNanoseconds(elapsedNs), duration);
327 simulateRange(*it->effect, it->mediaTime, target);
328 it->mediaTime = target;
329 ++it;
330 }
331 for (auto& value : preparedInstants_) value.effect->setVisible(true);
332 transients_.splice(transients_.end(), preparedInstants_);
333 preparedRetained_.clear();
334 }
335
336 void discardPrepared() noexcept override {
337 preparedStates_.clear();
338 preparedInstants_.clear();
339 preparedRetained_.clear();
340 }
341
342private:
343 static constexpr double kTimeEpsilon = 1e-9;
344
345 struct PreviewEffect {
348 std::unique_ptr<ParticleEffect> effect;
349 double mediaTime = 0.0;
350 };
351
352 struct TransientEffect {
356 std::unique_ptr<ParticleEffect> effect;
357 double mediaTime = 0.0;
358 };
359
360 eve::Result<void> failPrepared(const eve::Status& status) {
361 discardPrepared();
363 }
364
366 eve::Duration localTime, eve::Duration blockDuration) noexcept {
367 const double duration = blockDuration.seconds();
368 const double progress = duration > 0.0 ? std::clamp(localTime.seconds() / duration, 0.0, 1.0) : 0.0;
370 }
371
372 static void simulateRange(ParticleEffect& effect, double from, double to) noexcept {
373 double remaining = std::max(0.0, to - from);
374 constexpr float fixedStep = 1.0f / 60.0f;
375 while (remaining > kTimeEpsilon) {
376 const float step = static_cast<float>(std::min<double>(remaining, fixedStep));
377 effect.updateTimeline(step);
378 for (int i = 0; i < effect.getEmitterCount(); ++i) {
379 auto* emitter = effect.getEmitter(i);
380 if (!emitter) continue;
381 auto sim = emitter->sim();
382 const bool paused = sim->paused;
383 sim->paused = false;
384 stepEmitterSim(*emitter->config(), *sim, step);
385 sim->paused = paused;
386 }
387 remaining -= step;
388 }
389 }
390
392 const eve::action::ActionVfxBinding& binding, double targetTime) {
393 auto* particles = eve::ModuleManager::getInstance<Particles>("Particles");
394 if (!particles)
396 eve::DiagnosticCode::NotFound, "VFX preview requires the Particles module", "particles"));
397 std::unique_ptr<ParticleEffect> effect(particles->newEffectFromFile(binding.uri));
398 if (!effect)
401 particles->getLastEffectError().empty() ? "VFX preview asset could not be loaded"
402 : particles->getLastEffectError(),
403 "uri"));
404 effect->setVisible(false);
405 effect->setPosition(static_cast<float>(binding.spatial.positionOffset.x),
406 static_cast<float>(binding.spatial.positionOffset.y));
407 effect->setRotation(static_cast<float>(binding.spatial.rotationOffsetDegrees.z * 0.017453292519943295));
408 effect->setScale(static_cast<float>(binding.spatial.scale.x));
409 for (int i = 0; i < effect->getEmitterCount(); ++i) {
410 auto* emitter = effect->getEmitter(i);
411 if (!emitter) continue;
412 emitter->setRandomSeed(emitter->getRandomSeed());
413 emitter->setPlaybackSpeed(1.0f);
414 }
415 effect->start();
416 simulateRange(*effect, 0.0, targetTime);
417 effect->pause();
418 return eve::Result<std::unique_ptr<ParticleEffect>>::success(std::move(effect));
419 }
420
421 std::map<std::string, PreviewEffect> current_;
422 std::list<TransientEffect> transients_;
423 std::map<std::string, PreviewEffect> preparedStates_;
424 std::list<TransientEffect> preparedInstants_;
425 std::map<std::string, double> preparedRetained_;
427 eve::Duration preparedCurrent_ = eve::Duration::zero();
428};
429
430class ParticleActionProvider final : public eve::action::IActionNotifyProvider,
432public:
433 eve::Result<void> install(eve::action::ActionNotifyRegistry& registry) override {
434 auto handler = std::make_shared<ParticleActionHandler>();
435 auto instant = registry.registerHandler("presentation:vfx", handler);
436 if (!instant) return instant;
437 return registry.registerHandler("presentation:vfx-state", std::move(handler));
438 }
439
440 eve::Result<std::unique_ptr<eve::action::IActionPreviewSink>> createActionPreviewSink() override {
442 std::make_unique<ParticleActionPreviewSink>());
443 }
444};
445
446class ActionVfxDurationProvider final : public eve::action::IActionVfxDurationProvider {
447public:
448 eve::Result<eve::Duration> naturalDuration(std::string_view uri) const override {
449 if (uri.empty())
451 eve::DiagnosticCode::InvalidArgument, "VFX duration URI must be non-empty", "uri"));
452 auto* particles = eve::ModuleManager::getInstance<Particles>("Particles");
453 if (!particles)
455 eve::DiagnosticCode::NotFound, "VFX duration requires the Particles module", "particles"));
456 std::unique_ptr<ParticleEffect> effect(particles->newEffectFromFile(std::string(uri)));
457 if (!effect)
460 particles->getLastEffectError().empty() ? "VFX duration resource could not be loaded"
461 : particles->getLastEffectError(),
462 "uri"));
463 return effect->naturalDuration();
464 }
465};
466
467} // namespace
468
470 static ParticlesQueryImpl impl;
471 static ParticleActionProvider actionProvider;
472 static ActionVfxDurationProvider durationProvider;
473 eve::cap::provide<eve::IParticlesQuery>(&impl);
474 eve::cap::provide<eve::action::IActionVfxDurationProvider>(&durationProvider);
475 eve::cap::addListener<eve::action::IActionNotifyProvider>(&actionProvider);
476 eve::cap::addListener<eve::action::IActionPreviewSinkProvider>(&actionProvider);
478}
479
480} // namespace eve::particles
LogicalId target
double value
Extensible notify descriptors, validation and runtime routing.
Presentation-neutral action preview frame contracts.
Typed spatial payload shared by presentation action blocks.
Value::Object payload
Typed VFX action-block payload contract.
Optional VFX resource-duration query for action editors.
bool & active
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float duration
int root
Definition AnimSmr.cpp:119
std::string from
eve::action::ActionExecutionId executionId
ActiveSource owned
eve::Duration endTime
eve::Duration startTime
eve::EntitySpatialPose pose
eve::action::ActionSpatialBinding spatial
void * impl
glm::vec4 p[6]
Optional ECS-entity to world-pose capability.
std::uint32_t key
ShaderImageInput shape
wgpu::PopErrorScopeStatus status
HexCoordinates to
Cell the unit walks towards on this segment.
Definition HexUnits.cpp:64
HSQOBJECT handler
std::unique_ptr< ParticleEffect > effect
eve::action::ActionVfxBinding binding
double mediaTime
std::weak_ptr< const void > lifetime
std::string uri
PrimitiveHandle handle
bool found
double current
std::uint32_t count
std::unique_ptr< TrailEmitter > emitter
float offsetX
float offsetY
float step
Definition TreeMesh.cpp:314
const VegetationPresetContext & context
glm::vec3 point
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
Definition Diagnostic.h:125
Signed, fixed-resolution simulation duration in nanoseconds.
Definition Time.h:43
constexpr std::int64_t nanoseconds() const noexcept
Return the exact signed nanosecond representation.
Definition Time.h:69
static Result< Duration > fromSeconds(double seconds)
Convert finite seconds to the nearest nanosecond.
Definition Time.cpp:16
static constexpr Duration fromNanoseconds(std::int64_t nanoseconds) noexcept
Construct an exact duration from nanoseconds.
Definition Time.h:55
static constexpr Duration zero() noexcept
Return zero duration.
Definition Time.h:60
double seconds() const noexcept
Return this duration as seconds for legacy/presentation APIs.
Definition Time.cpp:28
Particle system query surface (provided by the particles module).
const std::string & format() const noexcept
Returns the canonical namespace:name representation.
Definition Identity.h:436
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
Structured status and zero or more diagnostics for an operation.
Definition Status.h:68
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
std::map< std::string, Value > Object
Definition Value.h:34
Canonical notify descriptor owner and runtime handler router.
Result< void > registerHandler(std::string_view type, std::shared_ptr< IActionNotifyHandler > handler)
Register an owning handler for an existing descriptor.
Consumer-owned handler for one registered notify type.
Optional module provider that installs handlers into a new registry.
Optional owner-thread provider of one independently owned action preview layer.
Host boundary for 3D animation, VFX, audio and camera preview.
Optional synchronous provider of authored VFX resource duration.
ActionPreviewReason
Reason a preview frame is being presented.
@ WorldTransformAtStart
Capture the complete world transform once on Enter.
@ FollowPositionOnly
Re-evaluate position only and retain the spawn rotation.
void registerParticlesAttackVfxExecutor()
Registers particles attack vfx executor.
void stepEmitterSim(ParticleEmitter::Config &cfg, ParticleEmitter::Sim &sim, float dt)
Step emitter sim.
void registerParticlesCapabilities()
Registers particles capabilities.
double sample(const Heightmap &map, double u, double v)
Sample.
WidgetDesc progress(float fraction, std::string id, std::string overlay)
Progress bar; fraction is clamped to [0,1].
Definition Widget.cpp:395
Build metadata (engine git commit, build time, third-party version).
Definition Build.cpp:16
Result< EntitySpatialPose > resolveEntitySpatialPose(ecs::EntityHandle entity, std::string_view bone)
Resolve through registered providers in deterministic registration order.
StatusCode
Stable outcome category for an operation.
Definition Status.h:27
std::optional< std::reference_wrapper< T > > OptionalRef
Optional borrowed reference; it does not extend T's lifetime.
Definition BorrowedRef.h:24
Renderer-neutral owning world pose resolved for an ECS entity.
Owning per-step sample of one currently active timeline state.
Context linking a routed event to its action execution.
Complete frame atomically prepared and then presented by a preview host.
Validated spatial settings decoded from a notify payload.
ActionSpatialVector3 rotationOffsetDegrees
Owning event projection returned by the runtime sampler.
ActionTimelineEventKind kind
Owning validated VFX settings shared by runtime and editor preview.
ActionSpatialBinding spatial
static Result< ActionVfxBinding > fromPayload(const Value::Object &payload, ActionVfxShape shape)
Decode one dynamic VFX payload transactionally.