载入中...
搜索中...
未找到
ParticleSystem.cpp
浏览该文件的文档.
5#include "graphics/Graphics.h"
6#include "graphics/Light.h"
8#include "graphics/Canvas.h"
10#include "common/Module.h"
11
12#include <algorithm>
13#include <cmath>
14#include <string>
15#include <unordered_map>
16#include <vector>
17
18namespace eve::particles {
19
20namespace {
21
22constexpr float kRad2Deg = 180.f / 3.14159265358979323846f;
23
24void sampleColor(const ParticleEmitter::Config &cfg, float t, Color &out) {
25 if (!cfg.colorGradient.empty()) {
26 float r, g, b, a;
27 cfg.colorGradient.sample(t, r, g, b, a);
28 out = Color(r, g, b, a);
29 return;
30 }
31 t = t < 0.f ? 0.f : (t > 1.f ? 1.f : t);
32 out.r = cfg.colorStart.r + (cfg.colorEnd.r - cfg.colorStart.r) * t;
33 out.g = cfg.colorStart.g + (cfg.colorEnd.g - cfg.colorStart.g) * t;
34 out.b = cfg.colorStart.b + (cfg.colorEnd.b - cfg.colorStart.b) * t;
35 out.a = cfg.colorStart.a + (cfg.colorEnd.a - cfg.colorStart.a) * t;
36}
37
38void sampleScale(const ParticleEmitter::Config &cfg, float t, float &scale) {
39 if (!cfg.sizeCurve.empty()) {
40 scale = cfg.sizeCurve.sample(t, 1.f);
41 return;
42 }
43 t = t < 0.f ? 0.f : (t > 1.f ? 1.f : t);
44 scale = cfg.sizeStart + (cfg.sizeEnd - cfg.sizeStart) * t;
45}
46
47void flipbookUV(const ParticleEmitter::Config &cfg, float frame, float &u0, float &v0, float &u1,
48 float &v1) {
49 const int total = cfg.hframes * cfg.vframes;
50 if (total <= 1 || cfg.hframes <= 0 || cfg.vframes <= 0) {
51 u0 = 0.f;
52 v0 = 0.f;
53 u1 = 1.f;
54 v1 = 1.f;
55 return;
56 }
57 int fi = int(std::floor(frame));
58 fi = ((fi % total) + total) % total;
59 const int col = fi % cfg.hframes;
60 const int row = fi / cfg.hframes;
61 const float iw = 1.f / float(cfg.hframes);
62 const float ih = 1.f / float(cfg.vframes);
63 u0 = float(col) * iw;
64 v0 = float(row) * ih;
65 u1 = u0 + iw;
66 v1 = v0 + ih;
67}
68
69void appendParticleItem(const ParticleEmitter::Config &cfg, const ParticleEmitter::Draw &draw,
70 const Particle &p, int order, std::vector<graphics::DrawItem2D> &out) {
71 const float t = p.lifetime > 0.f ? 1.f - (p.life / p.lifetime) : 1.f;
72 Color c;
73 sampleColor(cfg, t, c);
74 float scale;
75 sampleScale(cfg, t, scale);
76 scale *= p.size > 0.f ? p.size : 1.f;
77 const float w = cfg.particleW * scale;
78 const float h = cfg.particleH * scale;
79
80 graphics::DrawItem2D item;
81 item.x = p.x - w * 0.5f;
82 item.y = p.y - h * 0.5f;
83 item.w = w;
84 item.h = h;
85 if (cfg.renderMode == "stretched") {
86 // Elongate along the velocity direction (comet / streak style).
87 const float speed = std::sqrt(p.vx * p.vx + p.vy * p.vy);
88 const float len = std::max(w, speed * cfg.stretchFactor);
89 item.w = len;
90 item.rotation = std::atan2(p.vy, p.vx) * kRad2Deg;
91 } else {
92 item.rotation = p.rot * kRad2Deg;
93 if (!cfg.rotationCurve.empty()) item.rotation += cfg.rotationCurve.sample(t, 0.f);
94 }
95 item.order = order;
96 item.hasOrder = true;
97 item.color = c;
98 item.layer = draw.layer;
99 item.blend = draw.blend;
100 item.texture = draw.texture;
101 item.shader = draw.shader;
102 item.canvas = draw.canvas;
103 item.camera = draw.camera;
104 item.receiveLight = false;
105 if (draw.texture && (cfg.hframes > 1 || cfg.vframes > 1)) {
106 flipbookUV(cfg, p.frame, item.u0, item.v0, item.u1, item.v1);
107 item.hasUV = true;
108 }
109 out.push_back(item);
110}
111
113bool emitterOffscreen(const ParticleEmitter::Config &cfg, const ParticleEmitter::Draw &draw) {
114 auto *cam = draw.camera;
115 if (!cam) return false;
116 auto *gfx = eve::ModuleManager::getInstance<eve::graphics::Graphics>("Graphics");
117 if (!gfx) return false;
118 const float viewW = draw.canvas ? float(draw.canvas->getWidth()) : float(gfx->getWidth());
119 const float viewH = draw.canvas ? float(draw.canvas->getHeight()) : float(gfx->getHeight());
120 if (viewW <= 0.f || viewH <= 0.f) return false;
121 const float z = cam->data()->zoom > 0.f ? cam->data()->zoom : 1e-4f;
122 const float sx = (cfg.x - cam->data()->x) * z + viewW * 0.5f;
123 const float sy = (cfg.y - cam->data()->y) * z + viewH * 0.5f;
124 const float maxHalf =
125 std::max(cfg.particleW, cfg.particleH) *
126 std::max(std::abs(cfg.sizeStart), std::abs(cfg.sizeEnd)) * 0.5f +
127 1.f;
128 const float margin =
129 cfg.speedMax * cfg.lifeMax + std::max(cfg.areaX, cfg.areaY) + maxHalf;
130 return sx < -margin || sx > viewW + margin || sy < -margin || sy > viewH + margin;
131}
132
133int64_t fileModtime(const std::string &path) {
134 auto *fs = eve::ModuleManager::getInstance<eve::filesystem::Filesystem>("Filesystem");
135 if (!fs) fs = eve::filesystem::Filesystem::create();
137 if (!fs->getInfo(path, info)) return -1;
138 return info.modtime;
139}
140
141} // namespace
142
144 if (ecs::current()->getManager<ParticleEmitter>() == nullptr) return;
145
149 for (auto it = view.begin(); it != view.end(); ++it) {
150 auto [cfg, sim, draw, attach, skinSrc, gpuSim] = *it;
151 (void)gpuSim;
152 if (sim->alive <= 0 && emitterOffscreen(*cfg, *draw)) continue;
153 syncEmitterSources(*cfg, *sim, *attach, *skinSrc);
154 // gpuSimulation is accepted for config compatibility, but the legacy
155 // upload → dispatch → synchronous-readback path (stepEmitterSimGpu)
156 // stalls the whole GPU queue per emitter per frame (three
157 // executeImmediately round trips) and still runs the CPU-side
158 // collision/death/compaction pass afterwards — a net loss against the
159 // CPU integrator. All emitters integrate on the CPU here; a real GPU
160 // path must keep particle state GPU-resident and render from the SSBO
161 // directly (no readback).
162 stepEmitterSim(*cfg, *sim, dt);
163 }
164}
165
167 if (!gfx) return;
168 if (ecs::current()->getManager<ParticleEmitter>() == nullptr) return;
169
170 std::vector<graphics::DrawItem2D> items;
173 bool anyCanvas = false;
174 int order = 0;
175 for (auto it = view.begin(); it != view.end(); ++it) {
176 auto [cfg, sim, draw] = *it;
177 if (!draw->visible || sim->alive <= 0) continue;
178 if (draw->canvas) anyCanvas = true;
179 for (int i = 0; i < sim->alive; ++i) {
180 appendParticleItem(*cfg, *draw, sim->particles[size_t(i)], order++, items);
181 }
182 }
183 if (items.empty()) return;
184
185 // Unified 2D sprite path: rotation / flipbook UV / blend / layer sorting
186 // and camera handling all come from RenderSystem::drawItems.
187 graphics::RenderSystem::drawItems(*gfx, items, false);
188 if (anyCanvas) gfx->setCanvas();
189}
190
192 if (ecs::current()->getManager<ParticleEmitter>() == nullptr) return;
193
194 // Pass 1: collect emitters. Creating Light2D entities inside a deferred
195 // View would stage them and invalidate stored raw pointers on publish.
196 std::vector<ParticleEmitter *> emitters;
197 {
198 auto view = ecs::View<ParticleEmitter, ParticleEmitter::Config>();
199 for (auto it = view.begin(); it != view.end(); ++it) {
200 auto [cfg] = *it;
201 if (cfg->entity) emitters.push_back(cfg->entity);
202 }
203 }
204
205 // Pass 2: create/sync lights with no View active (stable entity pointers).
206 for (auto *em : emitters) {
207 auto cfg = em->config();
208 auto sim = em->sim();
209 auto draw = em->draw();
210 auto lights = em->lights();
211 if (!cfg->lights.enabled) {
212 for (auto *l : lights->pool)
213 if (l) l->setEnabled(false);
214 continue;
215 }
216 const int maxL = cfg->lights.max > 0 ? (cfg->lights.max > 8 ? 8 : cfg->lights.max) : 0;
217 while (int(lights->pool.size()) < maxL)
218 lights->pool.push_back(graphics::Light2D::createLight());
219 const int n = sim->alive < maxL ? sim->alive : maxL;
220 for (int i = 0; i < maxL; ++i) {
221 graphics::Light2D *l = lights->pool[size_t(i)];
222 if (i < n) {
223 const Particle &p = sim->particles[size_t(i)];
224 l->setPosition(p.x, p.y);
225 l->setRadius(cfg->lights.radius);
226 l->setColor(cfg->lights.r, cfg->lights.g, cfg->lights.b, cfg->lights.intensity);
227 l->setCanvas(draw->canvas);
228 l->setEnabled(true);
229 } else {
230 l->setEnabled(false);
231 }
232 }
233 }
234}
235
237 if (ecs::current()->getManager<ParticleEmitter>() == nullptr) return 0;
238
239 // Watch events are drained by load.nut / HotReload; use modtime as fallback.
240 int reloaded = 0;
241 auto view =
242 ecs::View<ParticleEmitter, ParticleEmitter::Config, ParticleEmitter::Resource>();
243 for (auto it = view.begin(); it != view.end(); ++it) {
244 auto [cfg, res] = *it;
245 if (!res->autoReload || res->path.empty() || !cfg->entity) continue;
246
247 const int64_t mt = fileModtime(res->path);
248 if (mt < 0 || mt == res->modtime) continue;
249 if (reloadConfigFile(cfg->entity, nullptr)) ++reloaded;
250 }
251 return reloaded;
252}
253
254} // namespace eve::particles
int z
Definition Grass.cpp:135
glm::vec3 n
Definition Grass.cpp:64
int h
int w
uint32_t a
uint32_t b
uint32_t c
glm::vec4 p[6]
glm::mat4 view
int margin
float scale
Definition TreeMesh.cpp:122
virtual void setCanvas(Canvas *canvas)=0
nullptr or this → screen. Switching flushes pending draws to the previous target.
Declarative 2D light. Collected by RenderSystem (max 8 per canvas/frame). type: "point" | "dir" (≤15 ...
Definition Light.h:31
void setPosition(float x, float y)
Definition Light.cpp:23
void setColor(float r, float g, float b, float intensity=1.f)
Definition Light.cpp:39
void setCanvas(Canvas *canvas)
Definition Light.cpp:61
void setEnabled(bool enabled)
Definition Light.cpp:50
static Light2D * createLight(const std::string &type="point")
Definition Light.cpp:5
void setRadius(float radius)
Definition Light.cpp:47
static void drawItems(Graphics &gfx, std::vector< DrawItem2D > &items, bool present)
Sort and draw items. If present=true, calls gfx.present() at the end. Map::render uses present=false ...
ECS emitter entity. Script configures components; ParticleSimSystem / ParticleRenderSystem drive per-...
static void render(graphics::Graphics *gfx)
glm::vec4 Color
RGBA color used by every graphics draw call. Lives inside eve::graphics so including a graphics heade...
Definition Color.h:13
bool reloadConfigFile(ParticleEmitter *emitter, std::string *error)
Re-read Resource.path if set; updates modtime.
void stepEmitterSim(ParticleEmitter::Config &cfg, ParticleEmitter::Sim &sim, float dt)
void syncEmitterSources(ParticleEmitter::Config &cfg, ParticleEmitter::Sim &, ParticleEmitter::Attach &attach, ParticleEmitter::SkinSource &skinSrc)
Sync bone attach + refresh skin cache; call before stepEmitterSim when using Attach/SkinSource.
WidgetDesc row(std::vector< WidgetDesc > children, std::string id)
Horizontal elastic layout row.
Definition Widget.cpp:431
Optional bone attachment. When enabled, syncAttach() writes Config.x/y (and optionally direction) fro...
GPU-accelerated simulation state (see ParticleGpuKernel.h for layout).
Optional skinned-mesh surface source. When enabled, newly spawned particles sample random (optionally...
Single live particle (CPU simulation).