载入中...
搜索中...
未找到
MeshVfxAsset.cpp
浏览该文件的文档.
2
3#include "common/Value.h"
4#include "common/Exception.h"
5
6#include <cmath>
7#include <algorithm>
8#include <limits>
9#include <set>
10
11namespace eve::stylize {
12namespace {
13
14bool finiteNonNegative(float value) { return std::isfinite(value) && value >= 0.f; }
15
16eve::Result<void> rejectUnknown(const eve::Value::Object& object, const std::set<std::string>& allowed,
17 const std::string& path) {
18 for (const auto& [key, value] : object) {
19 (void)value;
20 if (!allowed.contains(key))
22 "unknown mesh VFX field", path + "." + key));
23 }
25}
26
27eve::Result<float> number(const eve::Value& value, const std::string& path) {
28 if (const auto* v = value.getIf<double>()) return eve::Result<float>::success(static_cast<float>(*v));
29 if (const auto* v = value.getIf<std::int64_t>()) return eve::Result<float>::success(static_cast<float>(*v));
32}
33
34eve::Result<MeshEffectPlayback> parsePlayback(const eve::Value* value, const std::string& path) {
35 MeshEffectPlayback result{};
37 const auto* object = value->getIf<eve::Value::Object>();
38 if (!object)
41 auto known = rejectUnknown(*object, {"fadeIn", "hold", "fadeOut", "loop"}, path);
42 if (!known) return eve::Result<MeshEffectPlayback>::failure(known.status());
43 const auto read = [&](const char* key, float& target) -> eve::Result<void> {
44 const auto it = object->find(key);
45 if (it == object->end()) return eve::Result<void>::success();
46 auto parsed = number(it->second, path + "." + key);
47 if (!parsed) return eve::Result<void>::failure(parsed.status());
48 target = std::move(parsed).takeValue();
50 };
51 auto status = read("fadeIn", result.fadeIn);
53 status = read("hold", result.duration);
55 status = read("fadeOut", result.fadeOut);
57 const auto loop = object->find("loop");
58 if (loop != object->end()) {
59 const auto* enabled = loop->second.getIf<bool>();
60 if (!enabled)
63 result.loop = *enabled;
64 }
66}
67
68eve::Result<MeshVfxLayerAsset> parseLayer(const eve::Value& value, std::size_t index) {
69 const std::string path = "layers[" + std::to_string(index) + "]";
70 const auto* object = value.getIf<eve::Value::Object>();
71 if (!object)
74 auto known = rejectUnknown(*object, {"style", "playback", "parameters", "curves"}, path);
75 if (!known) return eve::Result<MeshVfxLayerAsset>::failure(known.status());
76 const auto style = object->find("style");
77 if (style == object->end() || !style->second.isString())
79 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "style must be a string", path + ".style"));
80 MeshVfxLayerAsset layer;
81 layer.style = style->second.asString();
82 const auto playback = object->find("playback");
83 auto parsedPlayback = parsePlayback(playback == object->end() ? nullptr : &playback->second, path + ".playback");
84 if (!parsedPlayback) return eve::Result<MeshVfxLayerAsset>::failure(parsedPlayback.status());
85 layer.playback = std::move(parsedPlayback).takeValue();
86 const auto parameters = object->find("parameters");
87 if (parameters != object->end()) {
88 const auto* values = parameters->second.getIf<eve::Value::Object>();
89 if (!values)
91 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "expected object", path + ".parameters"));
92 for (const auto& [name, raw] : *values) {
93 auto parsed = number(raw, path + ".parameters." + name);
94 if (!parsed) return eve::Result<MeshVfxLayerAsset>::failure(parsed.status());
95 layer.floatParameters.emplace(name, std::move(parsed).takeValue());
96 }
97 }
98 const auto curves = object->find("curves");
99 if (curves != object->end()) {
100 const auto* values = curves->second.getIf<eve::Value::Object>();
101 if (!values)
103 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "expected object", path + ".curves"));
104 for (const auto& [name, rawCurve] : *values) {
105 const auto* rawKeys = rawCurve.getIf<eve::Value::Array>();
106 if (!rawKeys)
108 eve::DiagnosticCode::InvalidArgument, "curve must be an array", path + ".curves." + name));
109 MeshVfxFloatCurve curve;
110 for (std::size_t keyIndex = 0; keyIndex < rawKeys->size(); ++keyIndex) {
111 const auto* pair = (*rawKeys)[keyIndex].getIf<eve::Value::Array>();
112 const std::string keyPath = path + ".curves." + name + "[" + std::to_string(keyIndex) + "]";
113 if (!pair || pair->size() != 2)
115 eve::DiagnosticCode::InvalidArgument, "curve key must be [time, value]", keyPath));
116 auto time = number((*pair)[0], keyPath + "[0]");
118 auto keyValue = number((*pair)[1], keyPath + "[1]");
119 if (!keyValue) return eve::Result<MeshVfxLayerAsset>::failure(keyValue.status());
120 curve.keys.push_back({std::move(time).takeValue(), std::move(keyValue).takeValue()});
121 }
122 layer.floatCurves.emplace(name, std::move(curve));
123 }
124 }
126}
127
129 const auto* object = value.getIf<eve::Value::Object>();
130 if (!object)
133 auto known = rejectUnknown(*object, {"maxSamples", "lifetime", "minDistance", "teleportDistance",
134 "rootAttachment", "tipAttachment", "rootOffset", "tipOffset"}, "trail");
135 if (!known) return eve::Result<TrailSettings>::failure(known.status());
136 TrailSettings trail{};
137 const auto read = [&](const char* key, float& target) -> eve::Result<void> {
138 const auto it = object->find(key);
139 if (it == object->end()) return eve::Result<void>::success();
140 auto parsed = number(it->second, std::string("trail.") + key);
141 if (!parsed) return eve::Result<void>::failure(parsed.status());
142 target = std::move(parsed).takeValue();
144 };
145 const auto maxSamples = object->find("maxSamples");
146 if (maxSamples != object->end()) {
147 const auto* count = maxSamples->second.getIf<std::int64_t>();
148 if (!count || *count <= 0)
150 eve::DiagnosticCode::InvalidArgument, "maxSamples must be a positive integer", "trail.maxSamples"));
151 trail.maxSamples = static_cast<std::size_t>(*count);
152 }
153 auto status = read("lifetime", trail.lifetime);
155 status = read("minDistance", trail.minSampleDistance);
157 status = read("teleportDistance", trail.teleportDistance);
160}
161
162eve::Result<eve::AttachmentPoint> parseAttachmentOffset(const eve::Value* value, const std::string& path) {
164 const auto* array = value->getIf<eve::Value::Array>();
165 if (!array || array->size() != 3)
167 eve::DiagnosticCode::InvalidArgument, "attachment offset must be a three-number array", path));
169 float* components[] = {&point.x, &point.y, &point.z};
170 for (std::size_t index = 0; index < 3; ++index) {
171 auto parsed = number((*array)[index], path + "[" + std::to_string(index) + "]");
172 if (!parsed) return eve::Result<eve::AttachmentPoint>::failure(parsed.status());
173 *components[index] = std::move(parsed).takeValue();
174 }
176}
177
179 const auto* object = value.getIf<eve::Value::Object>();
180 if (!object)
183 const auto root = object->find("rootAttachment");
184 const auto tip = object->find("tipAttachment");
185 if (root == object->end() && tip == object->end())
186 return eve::Result<std::optional<MeshVfxTrailBinding>>::success(std::nullopt);
187 if (root == object->end() || tip == object->end() || !root->second.isString() || !tip->second.isString())
190 "trail rootAttachment and tipAttachment must both be strings", "trail"));
191 const auto rootOffsetIt = object->find("rootOffset");
192 const auto tipOffsetIt = object->find("tipOffset");
193 auto rootOffset = parseAttachmentOffset(rootOffsetIt == object->end() ? nullptr : &rootOffsetIt->second,
194 "trail.rootOffset");
195 if (!rootOffset) return eve::Result<std::optional<MeshVfxTrailBinding>>::failure(rootOffset.status());
196 auto tipOffset = parseAttachmentOffset(tipOffsetIt == object->end() ? nullptr : &tipOffsetIt->second,
197 "trail.tipOffset");
198 if (!tipOffset) return eve::Result<std::optional<MeshVfxTrailBinding>>::failure(tipOffset.status());
199 return eve::Result<std::optional<MeshVfxTrailBinding>>::success(MeshVfxTrailBinding{
200 root->second.asString(), tip->second.asString(), std::move(rootOffset).takeValue(),
201 std::move(tipOffset).takeValue()});
202}
203
205 const auto* array = value.getIf<eve::Value::Array>();
206 if (!array)
208 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "events must be an array", "events"));
209 std::vector<MeshVfxEventAsset> events;
210 events.reserve(array->size());
211 for (std::size_t index = 0; index < array->size(); ++index) {
212 const std::string path = "events[" + std::to_string(index) + "]";
213 const auto* object = (*array)[index].getIf<eve::Value::Object>();
214 if (!object)
217 auto known = rejectUnknown(*object, {"time", "name"}, path);
218 if (!known) return eve::Result<std::vector<MeshVfxEventAsset>>::failure(known.status());
219 const auto timeIt = object->find("time");
220 const auto nameIt = object->find("name");
221 if (timeIt == object->end())
223 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "event time is required", path + ".time"));
224 if (nameIt == object->end() || !nameIt->second.isString())
226 eve::DiagnosticCode::InvalidArgument, "event name must be a string", path + ".name"));
227 auto time = number(timeIt->second, path + ".time");
228 if (!time) return eve::Result<std::vector<MeshVfxEventAsset>>::failure(time.status());
229 events.push_back({std::move(time).takeValue(), nameIt->second.asString()});
230 }
231 return eve::Result<std::vector<MeshVfxEventAsset>>::success(std::move(events));
232}
233
234eve::Result<std::vector<std::string>> parseEventNames(const eve::Value* value, const std::string& path) {
235 if (!value) return eve::Result<std::vector<std::string>>::success({});
236 const auto* array = value->getIf<eve::Value::Array>();
237 if (!array)
239 eve::DiagnosticCode::InvalidArgument, "animation trigger list must be an array", path));
240 std::vector<std::string> names;
241 names.reserve(array->size());
242 for (std::size_t index = 0; index < array->size(); ++index) {
243 if (!(*array)[index].isString())
245 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "animation trigger name must be a string",
246 path + "[" + std::to_string(index) + "]"));
247 names.push_back((*array)[index].asString());
248 }
249 return eve::Result<std::vector<std::string>>::success(std::move(names));
250}
251
252eve::Result<MeshVfxAnimationTriggers> parseAnimationTriggers(const eve::Value& value) {
253 const auto* object = value.getIf<eve::Value::Object>();
254 if (!object)
256 eve::DiagnosticCode::InvalidArgument, "animationTriggers must be an object", "animationTriggers"));
257 auto known = rejectUnknown(*object, {"play", "stop", "trailBreak"}, "animationTriggers");
258 if (!known) return eve::Result<MeshVfxAnimationTriggers>::failure(known.status());
259 MeshVfxAnimationTriggers triggers;
260 for (const auto& [key, destination] : std::initializer_list<std::pair<const char*, std::vector<std::string>*>>{
261 {"play", &triggers.play}, {"stop", &triggers.stop}, {"trailBreak", &triggers.trailBreak}}) {
262 const auto found = object->find(key);
263 auto parsed = parseEventNames(found == object->end() ? nullptr : &found->second,
264 std::string("animationTriggers.") + key);
265 if (!parsed) return eve::Result<MeshVfxAnimationTriggers>::failure(parsed.status());
266 *destination = std::move(parsed).takeValue();
267 }
268 return eve::Result<MeshVfxAnimationTriggers>::success(std::move(triggers));
269}
270
271} // namespace
272
274 auto parsed = eve::Value::fromJson(json);
275 if (!parsed) return eve::Result<MeshVfxAsset>::failure(parsed.status());
276 const auto* root = parsed.value().getIf<eve::Value::Object>();
277 if (!root)
279 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "mesh VFX root must be an object", {}));
280 const auto versionIt = root->find("schemaVersion");
281 if (versionIt == root->end() || !versionIt->second.getIf<std::int64_t>())
283 eve::DiagnosticCode::InvalidArgument, "schemaVersion must be an integer", "schemaVersion"));
284 const auto version = *versionIt->second.getIf<std::int64_t>();
285 MeshVfxAsset asset;
286 if (version == 0) {
287 auto known = rejectUnknown(*root, {"schemaVersion", "style", "playback", "parameters", "curves", "trail", "events", "animationTriggers"}, "$root");
288 if (!known) return eve::Result<MeshVfxAsset>::failure(known.status());
289 eve::Value::Object layerObject;
290 for (const char* key : {"style", "playback", "parameters", "curves"}) {
291 const auto it = root->find(key);
292 if (it != root->end()) layerObject.emplace(key, it->second);
293 }
294 auto layer = parseLayer(eve::Value(std::move(layerObject)), 0);
295 if (!layer) return eve::Result<MeshVfxAsset>::failure(layer.status());
296 asset.layers.push_back(std::move(layer).takeValue());
297 } else if (version == schemaVersion) {
298 auto known = rejectUnknown(*root, {"schema", "schemaVersion", "layers", "trail", "events", "animationTriggers"}, "$root");
299 if (!known) return eve::Result<MeshVfxAsset>::failure(known.status());
300 const auto schema = root->find("schema");
301 if (schema == root->end() || !schema->second.isString() || schema->second.asString() != schemaId)
303 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "unsupported mesh VFX schema", "schema"));
304 const auto layers = root->find("layers");
305 const auto* array = layers == root->end() ? nullptr : layers->second.getIf<eve::Value::Array>();
306 if (!array)
308 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "layers must be an array", "layers"));
309 for (std::size_t index = 0; index < array->size(); ++index) {
310 auto layer = parseLayer((*array)[index], index);
311 if (!layer) return eve::Result<MeshVfxAsset>::failure(layer.status());
312 asset.layers.push_back(std::move(layer).takeValue());
313 }
314 } else {
316 eve::DiagnosticCode::UnknownVersion, "unsupported mesh VFX schema version", "schemaVersion"));
317 }
318 const auto trail = root->find("trail");
319 if (trail != root->end()) {
320 auto parsedTrail = parseTrail(trail->second);
321 if (!parsedTrail) return eve::Result<MeshVfxAsset>::failure(parsedTrail.status());
322 asset.trail = std::move(parsedTrail).takeValue();
323 auto binding = parseTrailBinding(trail->second);
325 asset.trailBinding = std::move(binding).takeValue();
326 }
327 const auto events = root->find("events");
328 if (events != root->end()) {
329 auto parsedEvents = parseEvents(events->second);
330 if (!parsedEvents) return eve::Result<MeshVfxAsset>::failure(parsedEvents.status());
331 asset.events = std::move(parsedEvents).takeValue();
332 }
333 const auto animationTriggers = root->find("animationTriggers");
334 if (animationTriggers != root->end()) {
335 auto parsedTriggers = parseAnimationTriggers(animationTriggers->second);
336 if (!parsedTriggers) return eve::Result<MeshVfxAsset>::failure(parsedTriggers.status());
337 asset.animationTriggers = std::move(parsedTriggers).takeValue();
338 }
339 auto valid = asset.validate();
340 if (!valid) return eve::Result<MeshVfxAsset>::failure(valid.status());
341 return eve::Result<MeshVfxAsset>::success(std::move(asset));
342}
343
345 auto valid = validate();
346 if (!valid) return eve::Result<std::string>::failure(valid.status());
348 root.emplace("schema", eve::Value(std::string(schemaId)));
349 root.emplace("schemaVersion", eve::Value(static_cast<std::int64_t>(schemaVersion)));
350 eve::Value::Array layerValues;
351 layerValues.reserve(layers.size());
352 for (const auto& layer : layers) {
354 object.emplace("style", eve::Value(layer.style));
355 eve::Value::Object playback;
356 playback.emplace("fadeIn", eve::Value(static_cast<double>(layer.playback.fadeIn)));
357 playback.emplace("hold", eve::Value(static_cast<double>(layer.playback.duration)));
358 playback.emplace("fadeOut", eve::Value(static_cast<double>(layer.playback.fadeOut)));
359 playback.emplace("loop", eve::Value(layer.playback.loop));
360 object.emplace("playback", eve::Value(std::move(playback)));
361 if (!layer.floatParameters.empty()) {
362 eve::Value::Object parameters;
363 for (const auto& [name, value] : layer.floatParameters)
364 parameters.emplace(name, eve::Value(static_cast<double>(value)));
365 object.emplace("parameters", eve::Value(std::move(parameters)));
366 }
367 if (!layer.floatCurves.empty()) {
368 eve::Value::Object curves;
369 for (const auto& [name, curve] : layer.floatCurves) {
371 for (const auto& key : curve.keys) {
373 pair.emplace_back(static_cast<double>(key.time));
374 pair.emplace_back(static_cast<double>(key.value));
375 keys.emplace_back(std::move(pair));
376 }
377 curves.emplace(name, eve::Value(std::move(keys)));
378 }
379 object.emplace("curves", eve::Value(std::move(curves)));
380 }
381 layerValues.emplace_back(std::move(object));
382 }
383 root.emplace("layers", eve::Value(std::move(layerValues)));
384 if (trail) {
386 value.emplace("maxSamples", eve::Value(static_cast<std::int64_t>(trail->maxSamples)));
387 value.emplace("lifetime", eve::Value(static_cast<double>(trail->lifetime)));
388 value.emplace("minDistance", eve::Value(static_cast<double>(trail->minSampleDistance)));
389 value.emplace("teleportDistance", eve::Value(static_cast<double>(trail->teleportDistance)));
390 if (trailBinding) {
391 value.emplace("rootAttachment", eve::Value(trailBinding->rootAttachment));
392 value.emplace("tipAttachment", eve::Value(trailBinding->tipAttachment));
393 value.emplace("rootOffset", eve::Value::Array{static_cast<double>(trailBinding->rootOffset.x),
394 static_cast<double>(trailBinding->rootOffset.y),
395 static_cast<double>(trailBinding->rootOffset.z)});
396 value.emplace("tipOffset", eve::Value::Array{static_cast<double>(trailBinding->tipOffset.x),
397 static_cast<double>(trailBinding->tipOffset.y),
398 static_cast<double>(trailBinding->tipOffset.z)});
399 }
400 root.emplace("trail", eve::Value(std::move(value)));
401 }
402 if (!events.empty()) {
404 for (const auto& event : events) {
406 value.emplace("time", eve::Value(static_cast<double>(event.time)));
407 value.emplace("name", eve::Value(event.name));
408 values.emplace_back(std::move(value));
409 }
410 root.emplace("events", eve::Value(std::move(values)));
411 }
412 if (!animationTriggers.play.empty() || !animationTriggers.stop.empty() || !animationTriggers.trailBreak.empty()) {
414 const auto append = [&](const char* key, const std::vector<std::string>& names) {
415 if (names.empty()) return;
416 eve::Value::Array array;
417 for (const auto& name : names) array.emplace_back(name);
418 value.emplace(key, eve::Value(std::move(array)));
419 };
420 append("play", animationTriggers.play);
421 append("stop", animationTriggers.stop);
422 append("trailBreak", animationTriggers.trailBreak);
423 root.emplace("animationTriggers", eve::Value(std::move(value)));
424 }
425 return eve::Value(std::move(root)).toJson();
426}
427
429 if (layers.empty())
431 "mesh VFX requires at least one layer", "layers"));
432 for (std::size_t index = 0; index < layers.size(); ++index) {
433 const auto& layer = layers[index];
434 const std::string path = "layers[" + std::to_string(index) + "]";
435 if (layer.style.empty())
437 "style must not be empty", path + ".style"));
438 if (!finiteNonNegative(layer.playback.fadeIn) || !finiteNonNegative(layer.playback.duration) ||
439 !finiteNonNegative(layer.playback.fadeOut))
442 "playback durations must be finite and non-negative", path + ".playback"));
443 for (const auto& [name, value] : layer.floatParameters)
444 if (name.empty() || !std::isfinite(value))
446 eve::DiagnosticCode::InvalidArgument, "parameter names must be non-empty and values finite",
447 path + ".parameters"));
448 for (const auto& [name, curve] : layer.floatCurves) {
449 if (name.empty() || curve.keys.empty())
451 "curve names and key arrays must not be empty",
452 path + ".curves"));
453 float previous = -1.f;
454 for (const auto& key : curve.keys) {
455 if (!std::isfinite(key.time) || !std::isfinite(key.value) || key.time < 0.f || key.time > 1.f || key.time <= previous)
458 "curve times must be finite, normalized, and strictly increasing", path + ".curves." + name));
459 previous = key.time;
460 }
461 }
462 }
463 if (trail && (trail->maxSamples == 0 || !finiteNonNegative(trail->lifetime) ||
464 !finiteNonNegative(trail->minSampleDistance) || !finiteNonNegative(trail->teleportDistance)))
466 eve::Diagnostic::error(eve::DiagnosticCode::InvalidArgument, "invalid trail settings", "trail"));
467 if (trailBinding && (!trail || trailBinding->rootAttachment.empty() || trailBinding->tipAttachment.empty()))
470 "trail attachment binding requires a trail and two non-empty attachment names", "trail"));
471 float previousEventTime = -1.f;
472 for (const auto& event : events) {
473 if (!std::isfinite(event.time) || event.time < 0.f || event.time > 1.f || event.time < previousEventTime || event.name.empty())
476 "events require non-empty names and non-decreasing normalized times", "events"));
477 previousEventTime = event.time;
478 }
479 std::set<std::string> triggerNames;
480 const auto validateTriggers = [&](const std::vector<std::string>& names) -> eve::Result<void> {
481 for (const auto& name : names)
482 if (name.empty() || !triggerNames.insert(name).second)
485 "animation trigger names must be non-empty and map to exactly one action", "animationTriggers"));
487 };
488 auto triggerStatus = validateTriggers(animationTriggers.play);
489 if (!triggerStatus) return triggerStatus;
490 triggerStatus = validateTriggers(animationTriggers.stop);
491 if (!triggerStatus) return triggerStatus;
492 triggerStatus = validateTriggers(animationTriggers.trailBreak);
493 if (!triggerStatus) return triggerStatus;
495}
496
497float MeshVfxFloatCurve::evaluate(float normalizedTime) const noexcept {
498 if (keys.empty()) return 0.f;
499 const float time = std::clamp(normalizedTime, 0.f, 1.f);
500 if (time <= keys.front().time) return keys.front().value;
501 if (time >= keys.back().time) return keys.back().value;
502 const auto upper = std::upper_bound(keys.begin(), keys.end(), time,
503 [](float value, const MeshVfxFloatKey& key) { return value < key.time; });
504 const auto& right = *upper;
505 const auto& left = *(upper - 1);
506 const float alpha = (time - left.time) / (right.time - left.time);
507 return left.value + (right.value - left.value) * alpha;
508}
509
513
515 const MeshVfxAsset& asset, const std::map<std::string, std::map<std::string, float>>& externalStyles) {
516 auto valid = asset.validate();
517 if (!valid) return eve::Result<std::unique_ptr<MeshVfxAssetInstance>>::failure(valid.status());
518 try {
520 std::unique_ptr<MeshVfxAssetInstance>(new MeshVfxAssetInstance(asset, externalStyles)));
521 } catch (const eve::Exception& error) {
524 } catch (const std::exception& error) {
527 }
528}
529
530MeshVfxAssetInstance::MeshVfxAssetInstance(const MeshVfxAsset& asset,
531 const std::map<std::string, std::map<std::string, float>>& externalStyles) {
532 for (const auto& definition : asset.layers) {
533 const auto external = externalStyles.find(definition.style);
534 auto runtime = external == externalStyles.end()
535 ? std::make_unique<MeshEffectInstance>(definition.style)
536 : std::make_unique<MeshEffectInstance>(definition.style, external->second);
537 runtime->setPlayback(definition.playback);
538 for (const auto& [name, value] : definition.floatParameters) runtime->style().setFloat(name, value);
539 for (const auto& [name, curve] : definition.floatCurves)
540 runtime->style().setFloat(name, curve.evaluate(0.f));
541 layers_.push_back(std::move(runtime));
542 curves_.push_back(definition.floatCurves);
543 }
544 if (asset.trail) trail_ = std::make_unique<TrailEmitter>(*asset.trail);
545 trailBinding_ = asset.trailBinding;
546 events_ = asset.events;
547 animationTriggers_ = asset.animationTriggers;
548}
549
551 for (auto& layer : layers_) layer->bindTarget(target);
552}
553
555 if (trail_) trail_->clear();
556 pendingEvents_.clear();
557 previousNormalizedTime_ = 0.f;
558 for (const auto& event : events_)
559 if (event.time == 0.f) pendingEvents_.push_back(event.name);
560 for (auto& layer : layers_) layer->play();
561}
562
563void MeshVfxAssetInstance::stop(float fadeOutSeconds) {
564 for (auto& layer : layers_)
565 layer->stop(fadeOutSeconds < 0.f ? layer->playback().fadeOut : fadeOutSeconds);
566 if (trail_) trail_->breakTrail();
567}
568
569void MeshVfxAssetInstance::update(float dtSeconds) {
570 for (std::size_t index = 0; index < layers_.size(); ++index) {
571 auto& layer = *layers_[index];
572 layer.update(dtSeconds);
573 const auto playback = layer.playback();
574 const float cycle = playback.fadeIn + playback.duration + playback.fadeOut;
575 const float normalized = cycle > 0.f ? std::clamp(layer.elapsed() / cycle, 0.f, 1.f) : 1.f;
576 for (const auto& [name, curve] : curves_[index]) layer.style().setFloat(name, curve.evaluate(normalized));
577 }
578 if (trail_) trail_->update(dtSeconds);
579 if (layers_.empty()) return;
580 const auto playback = layers_.front()->playback();
581 const float cycle = playback.fadeIn + playback.duration + playback.fadeOut;
582 const float current = cycle > 0.f ? std::clamp(layers_.front()->elapsed() / cycle, 0.f, 1.f) : 1.f;
583 const bool wrapped = playback.loop && current < previousNormalizedTime_;
584 for (const auto& event : events_) {
585 const bool crossed = wrapped ? (event.time > previousNormalizedTime_ || event.time <= current)
586 : (event.time > previousNormalizedTime_ && event.time <= current);
587 if (crossed) pendingEvents_.push_back(event.name);
588 }
589 previousNormalizedTime_ = current;
590}
591
592std::vector<std::string> MeshVfxAssetInstance::drainEvents() {
593 std::vector<std::string> result;
594 result.swap(pendingEvents_);
595 return result;
596}
597
599 if (!trail_ || !trailBinding_)
601 eve::DiagnosticCode::PreconditionViolation, "mesh VFX has no authored trail attachment binding", "trail"));
602 auto root = source.sampleAttachmentPoint(trailBinding_->rootAttachment, trailBinding_->rootOffset);
604 auto tip = source.sampleAttachmentPoint(trailBinding_->tipAttachment, trailBinding_->tipOffset);
605 if (!tip) return eve::Result<TrailAppendResult>::failure(tip.status());
606 const auto rootPoint = std::move(root).takeValue();
607 const auto tipPoint = std::move(tip).takeValue();
609 trail_->append({rootPoint.x, rootPoint.y, rootPoint.z}, {tipPoint.x, tipPoint.y, tipPoint.z}));
610}
611
614 const auto contains = [](const std::vector<std::string>& values, const std::string& name) {
615 return std::find(values.begin(), values.end(), name) != values.end();
616 };
617 for (std::size_t index = 0; index < source.animationEventCount(); ++index) {
618 const std::string name = source.animationEventName(index);
619 if (contains(animationTriggers_.play, name)) {
620 play();
621 ++dispatch.played;
622 } else if (contains(animationTriggers_.stop, name)) {
623 stop();
624 ++dispatch.stopped;
625 } else if (contains(animationTriggers_.trailBreak, name)) {
626 if (trail_) trail_->breakTrail();
627 ++dispatch.trailBreaks;
628 } else {
629 continue;
630 }
631 ++dispatch.handled;
632 }
633 return dispatch;
634}
635
636bool MeshVfxAssetInstance::isFinished() const noexcept {
637 if (layers_.empty()) return true;
638 for (const auto& layer : layers_) {
639 if (!layer) continue;
640 const auto state = layer->state();
642 }
643 return true;
644}
645
647 if (!trail_) throw eve::Exception("MeshVfxAssetInstance.trail: asset has no trail");
648 return *trail_;
649}
650
652 auto valid = asset_.validate();
653 if (!valid) throw eve::Exception("%s", valid.status().describe().c_str());
654}
655
657 auto candidate = MeshVfxAsset::fromJson(json);
658 if (!candidate) return eve::Result<std::uint64_t>::failure(candidate.status());
659 if (revision_ == UINT64_MAX)
661 eve::Diagnostic::error(eve::DiagnosticCode::InvariantViolation, "mesh VFX asset revision exhausted", {}));
662 asset_ = std::move(candidate).takeValue();
663 return eve::Result<std::uint64_t>::success(++revision_);
664}
665
666} // namespace eve::stylize
LogicalId target
double value
int tip
Definition AnimSmr.cpp:121
int root
Definition AnimSmr.cpp:119
std::map< std::string, Var > values
std::uint32_t key
wgpu::PopErrorScopeStatus status
float v
HexVec3 left
HexVec3 right
std::int32_t second
std::string name
bool valid
graphics::Canvas * previous
std::string error
Definition Package.cpp:60
eve::action::ActionVfxBinding binding
TileLayer * layer
std::string path
Definition PlayHost.cpp:110
double number
bool found
double current
std::string string
std::uint32_t count
Battle::Events events
Heightmap curve
Json object
uint32_t index
const UnitySourceAsset & source
Owning, renderer-independent dynamic values.
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
EVENGINE_API_FOUNDATION public API.
Definition Exception.h:13
Read-only cross-module view of animation events emitted by the latest simulation update....
Read-only cross-module contract for sampling named animated attachment points. @ownership Implementat...
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
The canonical owning dynamic value used by data-facing protocols.
Definition Value.h:31
Result< std::string > toJson() const
Serialize this value as deterministic compact JSON.
Definition Value.cpp:67
static Result< Value > fromJson(std::string_view json)
Parse one strict JSON value into an owning Value.
Definition Value.cpp:57
std::map< std::string, Value > Object
Definition Value.h:34
std::vector< Value > Array
Definition Value.h:33
Runtime state for a shader effect attached to a renderable mesh.
Definition MeshEffect.h:40
Owning runtime instantiated from one immutable MeshVfxAsset snapshot. @ownership Owns all layer runti...
bool isFinished() const noexcept
True when every layer is Stopped or Finished.
void stop(float fadeOutSeconds=-1.f)
Stop every layer using an optional fade override.
void play() noexcept
Begin every layer and clear prior trail samples.
TrailEmitter & trail()
Return the owned trail; throws when this asset has no trail.
void update(float dtSeconds)
Advance every layer and the optional trail with caller-supplied simulation time.
void bindTarget(MeshEffectTargetHandle target)
Bind every layer to the same authoritative target handle.
MeshVfxAnimationDispatch processAnimationEvents(const eve::IAnimationEventSource &source)
Apply authored lifecycle mappings for events emitted by the latest animation update.
std::vector< std::string > drainEvents()
Move all timeline events crossed since the previous drain.
static eve::Result< std::unique_ptr< MeshVfxAssetInstance > > create(const MeshVfxAsset &asset)
Build independent runtime state for every asset layer.
eve::Result< TrailAppendResult > sampleTrail(const eve::IAttachmentPointSource &source)
Sample authored root/tip attachments and append one trail segment.
eve::Result< std::uint64_t > reload(std::string_view json)
Parse and atomically replace the asset on success.
MeshVfxAssetSlot(MeshVfxAsset asset)
Construct a slot from an already validated asset.
Deterministic CPU sampler and ribbon mesh generator for weapon trails.
Definition TrailEffect.h:55
T read(const RuntimeTensor &v, size_t i)
Reads read.
bool enabled
Plain world-space point shared by animation consumers without a math-library dependency.
Summary of lifecycle actions dispatched from one animation event batch.
std::vector< std::string > play
std::vector< std::string > trailBreak
std::vector< std::string > stop
Versioned, backend-neutral description of a real-time mesh effect.
MeshVfxAnimationTriggers animationTriggers
std::optional< MeshVfxTrailBinding > trailBinding
static eve::Result< MeshVfxAsset > fromJson(std::string_view json)
Parse, migrate, and validate a JSON asset without mutating live state.
eve::Result< void > validate() const
Validate all layer, playback, parameter, and trail invariants.
static constexpr std::uint32_t schemaVersion
std::vector< MeshVfxEventAsset > events
std::vector< MeshVfxLayerAsset > layers
static constexpr std::string_view schemaId
eve::Result< std::string > toJson() const
Serialize this asset in canonical current-schema JSON form.
std::optional< TrailSettings > trail
float evaluate(float normalizedTime) const noexcept
Evaluate the curve at normalized time, clamped to its first and last keys.
One normalized key in a deterministic float parameter curve.