3#include <glm/geometric.hpp>
11glm::vec3 safeNormal(glm::vec3
value, glm::vec3 fallback) {
17 return {
a.r + (
b.r -
a.r) *
t,
a.g + (
b.g -
a.g) *
t,
18 a.b + (
b.b -
a.b) *
t,
a.a + (
b.a -
a.a) *
t};
44 sampleAccumulator_ = 0.f;
45 hasPreviousInput_ =
false;
48bool ProjectileTrailEffect::append(
const glm::vec3&
position,
float initialAge,
50 if (!samples_.empty() &&
56 samples_.erase(samples_.begin());
59 samples_.push_back({
position, std::max(0.f, initialAge)});
65 const glm::vec3& currentPosition) {
67 if (!std::isfinite(deltaSeconds) || deltaSeconds < 0.f ||
68 !std::isfinite(currentPosition.x) || !std::isfinite(currentPosition.y) ||
69 !std::isfinite(currentPosition.z)) {
70 report.
alive =
static_cast<std::uint32_t
>(samples_.size());
73 for (
auto& sample : samples_) sample.age += deltaSeconds;
74 const auto expired = std::remove_if(samples_.begin(), samples_.end(), [&](
const Sample& sample) {
75 return sample.age >= config_.sampleLifetimeSeconds;
77 report.
expired +=
static_cast<std::uint32_t
>(std::distance(expired, samples_.end()));
78 samples_.erase(expired, samples_.end());
80 if (!hasPreviousInput_) {
81 previousInput_ = currentPosition;
82 hasPreviousInput_ =
true;
83 (void)append(currentPosition, 0.f, report);
85 const float previousAccumulator = sampleAccumulator_;
86 sampleAccumulator_ += deltaSeconds;
87 const std::uint32_t
count =
static_cast<std::uint32_t
>(
89 for (std::uint32_t i = 0; i <
count; ++i) {
92 const float t = deltaSeconds > 1e-6f ? std::clamp(elapsed / deltaSeconds, 0.f, 1.f) : 1.f;
93 (void)append(previousInput_ + (currentPosition - previousInput_) *
t,
94 std::max(0.f, deltaSeconds - elapsed), report);
97 previousInput_ = currentPosition;
99 report.
alive =
static_cast<std::uint32_t
>(samples_.size());
105 if (samples_.size() < 2)
return mesh;
107 mesh.indices.reserve((samples_.size() - 1u) * 6u);
108 const glm::vec3
view = safeNormal(cameraForward, glm::vec3(0.f, 0.f, -1.f));
109 for (std::size_t i = 0; i < samples_.size(); ++i) {
110 const float t =
static_cast<float>(i) /
static_cast<float>(samples_.size() - 1u);
111 const auto&
previous = samples_[i == 0 ? 0 : i - 1u].position;
112 const auto& next = samples_[std::min(i + 1u, samples_.size() - 1u)].position;
113 const glm::vec3
tangent = safeNormal(next -
previous, glm::vec3(0.f, 1.f, 0.f));
114 glm::vec3
side = safeNormal(glm::cross(
view,
tangent), glm::vec3(1.f, 0.f, 0.f));
118 mesh.vertices.push_back({samples_[i].position -
side * (
width * 0.5f), {
t, 0.f}, alpha});
119 mesh.vertices.push_back({samples_[i].position +
side * (
width * 0.5f), {
t, 1.f}, alpha});
120 if (i == 0)
continue;
121 const std::uint32_t left0 =
static_cast<std::uint32_t
>((i - 1u) * 2u);
122 const std::uint32_t right0 = left0 + 1u;
123 const std::uint32_t left1 =
static_cast<std::uint32_t
>(i * 2u);
124 const std::uint32_t right1 = left1 + 1u;
125 mesh.indices.insert(
mesh.indices.end(), {left0, right0, left1, left1, right0, right1});
145 sampleAccumulator_ = 0.f;
150 const glm::mat4& currentModel) {
152 if (!std::isfinite(deltaSeconds) || deltaSeconds < 0.f) {
153 report.
alive =
static_cast<std::uint32_t
>(images_.size());
156 for (
auto&
image : images_)
image.age += deltaSeconds;
157 const auto expired = std::remove_if(images_.begin(), images_.end(), [&](
const Image&
image) {
158 return image.age >= config_.lifetimeSeconds;
160 report.
expired =
static_cast<std::uint32_t
>(std::distance(expired, images_.end()));
161 images_.erase(expired, images_.end());
163 const float previousAccumulator = sampleAccumulator_;
164 sampleAccumulator_ += deltaSeconds;
165 const auto captures =
static_cast<std::uint32_t
>(
168 for (std::uint32_t i = 0; i < captures; ++i) {
170 images_.erase(images_.begin());
175 const float initialAge = std::max(0.f, deltaSeconds - elapsed);
176 const std::uint64_t stableId = nextStableId_++;
182 images_.push_back({stableId, currentModel, initialAge});
184 report.
alive =
static_cast<std::uint32_t
>(images_.size());
189 std::vector<MeshParticleInstance> result;
190 result.reserve(images_.size());
191 for (
const auto&
image : images_) {
193 result.push_back({
image.stableId,
image.model,
std::array< float, 3 > position
graphics::Canvas * previous
MotionEchoAdvanceReport update(float deltaSeconds, const glm::mat4 ¤tModel)
Advance and capture the current model at fixed intervals.
AfterimageEffect(AfterimageEffectConfig config={})
Construct with sanitized timing/capacity limits.
std::vector< MeshParticleInstance > snapshot() const
Return render instances in oldest-to-newest stable order.
void reset() noexcept
Clear images and reset stable identity/timing state.
MotionEchoAdvanceReport update(float deltaSeconds, const glm::vec3 ¤tPosition)
Advance time and resample movement up to the current world position.
ProjectileTrailEffect(ProjectileTrailConfig config={})
Construct with sanitized positive timing/capacity limits.
void reset() noexcept
Clear samples and timing state.
TrailMeshSnapshot build(const glm::vec3 &cameraForward) const
Build a camera-facing ribbon ordered from oldest tail to newest head.
eve::Color Color
RGBA color used by every graphics draw call. Lives inside eve::graphics so including a graphics heade...
Fixed-rate model snapshot and fade controls for character afterimages.
float sampleIntervalSeconds
std::size_t maximumImages
graphics::Color colorStart
Per-call observable sample and expiration counters.
std::uint32_t distanceRejected
Sampling, retention and ribbon appearance for a projectile trail.
std::size_t maximumSamples
float sampleIntervalSeconds
float sampleLifetimeSeconds
Owning CPU snapshot suitable for upload by a graphics adapter.
std::vector< TrailVertex > vertices