载入中...
搜索中...
未找到
MotionEchoEffect.cpp
浏览该文件的文档.
2
3#include <glm/geometric.hpp>
4
5#include <algorithm>
6#include <cmath>
7
8namespace eve::stylize {
9namespace {
10
11glm::vec3 safeNormal(glm::vec3 value, glm::vec3 fallback) {
12 const float length = glm::length(value);
13 return length > 1e-6f && std::isfinite(length) ? value / length : fallback;
14}
15
16graphics::Color mixColor(const graphics::Color& a, const graphics::Color& b, float t) {
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};
19}
20
21} // namespace
22
24 : config_(config) {
25 config_.sampleIntervalSeconds =
26 std::max(1e-4f, std::isfinite(config_.sampleIntervalSeconds)
27 ? config_.sampleIntervalSeconds
28 : 1.f / 60.f);
29 config_.sampleLifetimeSeconds =
30 std::max(config_.sampleIntervalSeconds,
31 std::isfinite(config_.sampleLifetimeSeconds)
32 ? config_.sampleLifetimeSeconds
33 : config_.sampleIntervalSeconds);
34 config_.minimumDistance =
35 std::max(0.f, std::isfinite(config_.minimumDistance) ? config_.minimumDistance : 0.f);
36 config_.widthHead = std::max(0.f, std::isfinite(config_.widthHead) ? config_.widthHead : 0.f);
37 config_.widthTail = std::max(0.f, std::isfinite(config_.widthTail) ? config_.widthTail : 0.f);
38 config_.maximumSamples = std::max<std::size_t>(2u, config_.maximumSamples);
39 samples_.reserve(config_.maximumSamples);
40}
41
43 samples_.clear();
44 sampleAccumulator_ = 0.f;
45 hasPreviousInput_ = false;
46}
47
48bool ProjectileTrailEffect::append(const glm::vec3& position, float initialAge,
50 if (!samples_.empty() &&
51 glm::length(position - samples_.back().position) < config_.minimumDistance) {
52 ++report.distanceRejected;
53 return false;
54 }
55 if (samples_.size() == config_.maximumSamples) {
56 samples_.erase(samples_.begin());
57 ++report.expired;
58 }
59 samples_.push_back({position, std::max(0.f, initialAge)});
60 ++report.captured;
61 return true;
62}
63
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());
71 return report;
72 }
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;
76 });
77 report.expired += static_cast<std::uint32_t>(std::distance(expired, samples_.end()));
78 samples_.erase(expired, samples_.end());
79
80 if (!hasPreviousInput_) {
81 previousInput_ = currentPosition;
82 hasPreviousInput_ = true;
83 (void)append(currentPosition, 0.f, report);
84 } else {
85 const float previousAccumulator = sampleAccumulator_;
86 sampleAccumulator_ += deltaSeconds;
87 const std::uint32_t count = static_cast<std::uint32_t>(
88 std::floor(sampleAccumulator_ / config_.sampleIntervalSeconds));
89 for (std::uint32_t i = 0; i < count; ++i) {
90 const float elapsed = config_.sampleIntervalSeconds - previousAccumulator +
91 static_cast<float>(i) * config_.sampleIntervalSeconds;
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);
95 }
96 sampleAccumulator_ -= static_cast<float>(count) * config_.sampleIntervalSeconds;
97 previousInput_ = currentPosition;
98 }
99 report.alive = static_cast<std::uint32_t>(samples_.size());
100 return report;
101}
102
103TrailMeshSnapshot ProjectileTrailEffect::build(const glm::vec3& cameraForward) const {
105 if (samples_.size() < 2) return mesh;
106 mesh.vertices.reserve(samples_.size() * 2u);
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));
115 const float width = config_.widthTail + (config_.widthHead - config_.widthTail) * t;
116 const float alpha = std::clamp(1.f - samples_[i].age / config_.sampleLifetimeSeconds,
117 0.f, 1.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});
126 }
127 return mesh;
128}
129
131 config_.sampleIntervalSeconds =
132 std::max(1e-4f, std::isfinite(config_.sampleIntervalSeconds)
133 ? config_.sampleIntervalSeconds
134 : 0.06f);
135 config_.lifetimeSeconds =
136 std::max(config_.sampleIntervalSeconds,
137 std::isfinite(config_.lifetimeSeconds) ? config_.lifetimeSeconds
138 : config_.sampleIntervalSeconds);
139 config_.maximumImages = std::max<std::size_t>(1u, config_.maximumImages);
140 images_.reserve(config_.maximumImages);
141}
142
143void AfterimageEffect::reset() noexcept {
144 images_.clear();
145 sampleAccumulator_ = 0.f;
146 nextStableId_ = 1;
147}
148
150 const glm::mat4& currentModel) {
152 if (!std::isfinite(deltaSeconds) || deltaSeconds < 0.f) {
153 report.alive = static_cast<std::uint32_t>(images_.size());
154 return report;
155 }
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;
159 });
160 report.expired = static_cast<std::uint32_t>(std::distance(expired, images_.end()));
161 images_.erase(expired, images_.end());
162
163 const float previousAccumulator = sampleAccumulator_;
164 sampleAccumulator_ += deltaSeconds;
165 const auto captures = static_cast<std::uint32_t>(
166 std::floor(sampleAccumulator_ / config_.sampleIntervalSeconds));
167 sampleAccumulator_ -= static_cast<float>(captures) * config_.sampleIntervalSeconds;
168 for (std::uint32_t i = 0; i < captures; ++i) {
169 if (images_.size() == config_.maximumImages) {
170 images_.erase(images_.begin());
171 ++report.expired;
172 }
173 const float elapsed = config_.sampleIntervalSeconds - previousAccumulator +
174 static_cast<float>(i) * config_.sampleIntervalSeconds;
175 const float initialAge = std::max(0.f, deltaSeconds - elapsed);
176 const std::uint64_t stableId = nextStableId_++;
177 ++report.captured;
178 if (initialAge >= config_.lifetimeSeconds) {
179 ++report.expired;
180 continue;
181 }
182 images_.push_back({stableId, currentModel, initialAge});
183 }
184 report.alive = static_cast<std::uint32_t>(images_.size());
185 return report;
186}
187
188std::vector<MeshParticleInstance> AfterimageEffect::snapshot() const {
189 std::vector<MeshParticleInstance> result;
190 result.reserve(images_.size());
191 for (const auto& image : images_) {
192 const float t = std::clamp(image.age / config_.lifetimeSeconds, 0.f, 1.f);
193 result.push_back({image.stableId, image.model,
194 mixColor(config_.colorStart, config_.colorEnd, t), t});
195 }
196 return result;
197}
198
199} // namespace eve::stylize
double value
Vec3 tangent
Definition CaveMesh.cpp:80
float length
Definition CaveMesh.cpp:94
vk::UniqueImage image
std::uint32_t width
std::array< float, 3 > position
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
graphics::Canvas * previous
float t
Mesh * mesh
glm::mat4 view
std::uint32_t count
ecs::EntityHandle side
MotionEchoAdvanceReport update(float deltaSeconds, const glm::mat4 &currentModel)
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 &currentPosition)
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...
Definition Color.h:13
Fixed-rate model snapshot and fade controls for character afterimages.
Per-call observable sample and expiration counters.
Sampling, retention and ribbon appearance for a projectile trail.
Owning CPU snapshot suitable for upload by a graphics adapter.
Definition TrailEffect.h:42
std::vector< TrailVertex > vertices
Definition TrailEffect.h:43