]> git.lizzy.rs Git - minetest.git/blob - src/client/particles.cpp
Ratelimit MeshUpdateQueue::cleanupCache() runs
[minetest.git] / src / client / particles.cpp
1 /*
2 Minetest
3 Copyright (C) 2013 celeron55, Perttu Ahola <celeron55@gmail.com>
4
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU Lesser General Public License as published by
7 the Free Software Foundation; either version 2.1 of the License, or
8 (at your option) any later version.
9
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 GNU Lesser General Public License for more details.
14
15 You should have received a copy of the GNU Lesser General Public License along
16 with this program; if not, write to the Free Software Foundation, Inc.,
17 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 */
19
20 #include "particles.h"
21 #include <cmath>
22 #include "client.h"
23 #include "collision.h"
24 #include "client/content_cao.h"
25 #include "client/clientevent.h"
26 #include "client/renderingengine.h"
27 #include "util/numeric.h"
28 #include "light.h"
29 #include "environment.h"
30 #include "clientmap.h"
31 #include "mapnode.h"
32 #include "nodedef.h"
33 #include "client.h"
34 #include "settings.h"
35
36 /*
37         Particle
38 */
39
40 Particle::Particle(
41         IGameDef *gamedef,
42         LocalPlayer *player,
43         ClientEnvironment *env,
44         const ParticleParameters &p,
45         const ClientTexRef& texture,
46         v2f texpos,
47         v2f texsize,
48         video::SColor color
49 ):
50         scene::ISceneNode(((Client *)gamedef)->getSceneManager()->getRootSceneNode(),
51                 ((Client *)gamedef)->getSceneManager()),
52         m_texture(texture)
53 {
54         // Misc
55         m_gamedef = gamedef;
56         m_env = env;
57
58         // translate blend modes to GL blend functions
59         video::E_BLEND_FACTOR bfsrc, bfdst;
60         video::E_BLEND_OPERATION blendop;
61         const auto blendmode = texture.tex != nullptr
62                         ? texture.tex -> blendmode
63                         : ParticleParamTypes::BlendMode::alpha;
64
65         switch (blendmode) {
66                 case ParticleParamTypes::BlendMode::add:
67                         bfsrc = video::EBF_SRC_ALPHA;
68                         bfdst = video::EBF_DST_ALPHA;
69                         blendop = video::EBO_ADD;
70                 break;
71
72                 case ParticleParamTypes::BlendMode::sub:
73                         bfsrc = video::EBF_SRC_ALPHA;
74                         bfdst = video::EBF_DST_ALPHA;
75                         blendop = video::EBO_REVSUBTRACT;
76                 break;
77
78                 case ParticleParamTypes::BlendMode::screen:
79                         bfsrc = video::EBF_ONE;
80                         bfdst = video::EBF_ONE_MINUS_SRC_COLOR;
81                         blendop = video::EBO_ADD;
82                 break;
83
84                 default: // includes ParticleParamTypes::BlendMode::alpha
85                         bfsrc = video::EBF_SRC_ALPHA;
86                         bfdst = video::EBF_ONE_MINUS_SRC_ALPHA;
87                         blendop = video::EBO_ADD;
88                 break;
89         }
90
91         // Texture
92         m_material.setFlag(video::EMF_LIGHTING, false);
93         m_material.setFlag(video::EMF_BACK_FACE_CULLING, false);
94         m_material.setFlag(video::EMF_BILINEAR_FILTER, false);
95         m_material.setFlag(video::EMF_FOG_ENABLE, true);
96
97         // correctly render layered transparent particles -- see #10398
98         m_material.setFlag(video::EMF_ZWRITE_ENABLE, true);
99
100         // enable alpha blending and set blend mode
101         m_material.MaterialType = video::EMT_ONETEXTURE_BLEND;
102         m_material.MaterialTypeParam = video::pack_textureBlendFunc(
103                         bfsrc, bfdst,
104                         video::EMFN_MODULATE_1X,
105                         video::EAS_TEXTURE | video::EAS_VERTEX_COLOR);
106         m_material.BlendOperation = blendop;
107         m_material.setTexture(0, m_texture.ref);
108         m_texpos = texpos;
109         m_texsize = texsize;
110         m_animation = p.animation;
111
112         // Color
113         m_base_color = color;
114         m_color = color;
115
116         // Particle related
117         m_pos = p.pos;
118         m_velocity = p.vel;
119         m_acceleration = p.acc;
120         m_drag = p.drag;
121         m_jitter = p.jitter;
122         m_bounce = p.bounce;
123         m_expiration = p.expirationtime;
124         m_player = player;
125         m_size = p.size;
126         m_collisiondetection = p.collisiondetection;
127         m_collision_removal = p.collision_removal;
128         m_object_collision = p.object_collision;
129         m_vertical = p.vertical;
130         m_glow = p.glow;
131         m_alpha = 0;
132         m_parent = nullptr;
133
134         // Irrlicht stuff
135         const float c = p.size / 2;
136         m_collisionbox = aabb3f(-c, -c, -c, c, c, c);
137         this->setAutomaticCulling(scene::EAC_OFF);
138
139         // Init lighting
140         updateLight();
141
142         // Init model
143         updateVertices();
144 }
145
146 Particle::~Particle()
147 {
148         /* if our textures aren't owned by a particlespawner, we need to clean
149          * them up ourselves when the particle dies */
150         if (m_parent == nullptr)
151                 delete m_texture.tex;
152 }
153
154 void Particle::OnRegisterSceneNode()
155 {
156         if (IsVisible)
157                 SceneManager->registerNodeForRendering(this, scene::ESNRP_TRANSPARENT_EFFECT);
158
159         ISceneNode::OnRegisterSceneNode();
160 }
161
162 void Particle::render()
163 {
164         video::IVideoDriver *driver = SceneManager->getVideoDriver();
165         driver->setMaterial(m_material);
166         driver->setTransform(video::ETS_WORLD, AbsoluteTransformation);
167
168         u16 indices[] = {0,1,2, 2,3,0};
169         driver->drawVertexPrimitiveList(m_vertices, 4,
170                         indices, 2, video::EVT_STANDARD,
171                         scene::EPT_TRIANGLES, video::EIT_16BIT);
172 }
173
174 void Particle::step(float dtime)
175 {
176         m_time += dtime;
177
178         // apply drag (not handled by collisionMoveSimple) and brownian motion
179         v3f av = vecAbsolute(m_velocity);
180         av -= av * (m_drag * dtime);
181         m_velocity = av*vecSign(m_velocity) + v3f(m_jitter.pickWithin())*dtime;
182
183         if (m_collisiondetection) {
184                 aabb3f box = m_collisionbox;
185                 v3f p_pos = m_pos * BS;
186                 v3f p_velocity = m_velocity * BS;
187                 collisionMoveResult r = collisionMoveSimple(m_env, m_gamedef, BS * 0.5f,
188                         box, 0.0f, dtime, &p_pos, &p_velocity, m_acceleration * BS, nullptr,
189                         m_object_collision);
190
191                 f32 bounciness = m_bounce.pickWithin();
192                 if (r.collides && (m_collision_removal || bounciness > 0)) {
193                         if (m_collision_removal) {
194                                 // force expiration of the particle
195                                 m_expiration = -1.0f;
196                         } else if (bounciness > 0) {
197                                 /* cheap way to get a decent bounce effect is to only invert the
198                                  * largest component of the velocity vector, so e.g. you don't
199                                  * have a rock immediately bounce back in your face when you try
200                                  * to skip it across the water (as would happen if we simply
201                                  * downscaled and negated the velocity vector). this means
202                                  * bounciness will work properly for cubic objects, but meshes
203                                  * with diagonal angles and entities will not yield the correct
204                                  * visual. this is probably unavoidable */
205                                 if (av.Y > av.X && av.Y > av.Z) {
206                                         m_velocity.Y = -(m_velocity.Y * bounciness);
207                                 } else if (av.X > av.Y && av.X > av.Z) {
208                                         m_velocity.X = -(m_velocity.X * bounciness);
209                                 } else if (av.Z > av.Y && av.Z > av.X) {
210                                         m_velocity.Z = -(m_velocity.Z * bounciness);
211                                 } else { // well now we're in a bit of a pickle
212                                         m_velocity = -(m_velocity * bounciness);
213                                 }
214                         }
215                 } else {
216                         m_velocity = p_velocity / BS;
217                 }
218                 m_pos = p_pos / BS;
219         } else {
220                 // apply acceleration
221                 m_velocity += m_acceleration * dtime;
222                 m_pos += m_velocity * dtime;
223         }
224
225         if (m_animation.type != TAT_NONE) {
226                 m_animation_time += dtime;
227                 int frame_length_i, frame_count;
228                 m_animation.determineParams(
229                                 m_material.getTexture(0)->getSize(),
230                                 &frame_count, &frame_length_i, NULL);
231                 float frame_length = frame_length_i / 1000.0;
232                 while (m_animation_time > frame_length) {
233                         m_animation_frame++;
234                         m_animation_time -= frame_length;
235                 }
236         }
237
238         // animate particle alpha in accordance with settings
239         if (m_texture.tex != nullptr)
240                 m_alpha = m_texture.tex -> alpha.blend(m_time / (m_expiration+0.1f));
241         else
242                 m_alpha = 1.f;
243
244         // Update lighting
245         updateLight();
246
247         // Update model
248         updateVertices();
249
250         // Update position -- see #10398
251         v3s16 camera_offset = m_env->getCameraOffset();
252         setPosition(m_pos*BS - intToFloat(camera_offset, BS));
253 }
254
255 void Particle::updateLight()
256 {
257         u8 light = 0;
258         bool pos_ok;
259
260         v3s16 p = v3s16(
261                 floor(m_pos.X+0.5),
262                 floor(m_pos.Y+0.5),
263                 floor(m_pos.Z+0.5)
264         );
265         MapNode n = m_env->getClientMap().getNode(p, &pos_ok);
266         if (pos_ok)
267                 light = n.getLightBlend(m_env->getDayNightRatio(), m_gamedef->ndef());
268         else
269                 light = blend_light(m_env->getDayNightRatio(), LIGHT_SUN, 0);
270
271         u8 m_light = decode_light(light + m_glow);
272         m_color.set(m_alpha*255,
273                 m_light * m_base_color.getRed() / 255,
274                 m_light * m_base_color.getGreen() / 255,
275                 m_light * m_base_color.getBlue() / 255);
276 }
277
278 void Particle::updateVertices()
279 {
280         f32 tx0, tx1, ty0, ty1;
281         v2f scale;
282
283         if (m_texture.tex != nullptr)
284                 scale = m_texture.tex -> scale.blend(m_time / (m_expiration+0.1));
285         else
286                 scale = v2f(1.f, 1.f);
287
288         if (m_animation.type != TAT_NONE) {
289                 const v2u32 texsize = m_material.getTexture(0)->getSize();
290                 v2f texcoord, framesize_f;
291                 v2u32 framesize;
292                 texcoord = m_animation.getTextureCoords(texsize, m_animation_frame);
293                 m_animation.determineParams(texsize, NULL, NULL, &framesize);
294                 framesize_f = v2f(framesize.X / (float) texsize.X, framesize.Y / (float) texsize.Y);
295
296                 tx0 = m_texpos.X + texcoord.X;
297                 tx1 = m_texpos.X + texcoord.X + framesize_f.X * m_texsize.X;
298                 ty0 = m_texpos.Y + texcoord.Y;
299                 ty1 = m_texpos.Y + texcoord.Y + framesize_f.Y * m_texsize.Y;
300         } else {
301                 tx0 = m_texpos.X;
302                 tx1 = m_texpos.X + m_texsize.X;
303                 ty0 = m_texpos.Y;
304                 ty1 = m_texpos.Y + m_texsize.Y;
305         }
306
307         auto half = m_size * .5f,
308              hx   = half * scale.X,
309              hy   = half * scale.Y;
310         m_vertices[0] = video::S3DVertex(-hx, -hy,
311                 0, 0, 0, 0, m_color, tx0, ty1);
312         m_vertices[1] = video::S3DVertex(hx, -hy,
313                 0, 0, 0, 0, m_color, tx1, ty1);
314         m_vertices[2] = video::S3DVertex(hx, hy,
315                 0, 0, 0, 0, m_color, tx1, ty0);
316         m_vertices[3] = video::S3DVertex(-hx, hy,
317                 0, 0, 0, 0, m_color, tx0, ty0);
318
319
320         // see #10398
321         // v3s16 camera_offset = m_env->getCameraOffset();
322         // particle position is now handled by step()
323         m_box.reset(v3f());
324
325         for (video::S3DVertex &vertex : m_vertices) {
326                 if (m_vertical) {
327                         v3f ppos = m_player->getPosition()/BS;
328                         vertex.Pos.rotateXZBy(std::atan2(ppos.Z - m_pos.Z, ppos.X - m_pos.X) /
329                                 core::DEGTORAD + 90);
330                 } else {
331                         vertex.Pos.rotateYZBy(m_player->getPitch());
332                         vertex.Pos.rotateXZBy(m_player->getYaw());
333                 }
334                 m_box.addInternalPoint(vertex.Pos);
335         }
336 }
337
338 /*
339         ParticleSpawner
340 */
341
342 ParticleSpawner::ParticleSpawner(
343         IGameDef *gamedef,
344         LocalPlayer *player,
345         const ParticleSpawnerParameters &p,
346         u16 attached_id,
347         std::unique_ptr<ClientTexture[]>& texpool,
348         size_t texcount,
349         ParticleManager *p_manager
350 ):
351         m_particlemanager(p_manager), p(p)
352 {
353         m_gamedef = gamedef;
354         m_player = player;
355         m_attached_id = attached_id;
356         m_texpool = std::move(texpool);
357         m_texcount = texcount;
358         m_time = 0;
359         m_active = 0;
360         m_dying = false;
361
362         m_spawntimes.reserve(p.amount + 1);
363         for (u16 i = 0; i <= p.amount; i++) {
364                 float spawntime = myrand_float() * p.time;
365                 m_spawntimes.push_back(spawntime);
366         }
367
368         size_t max_particles = 0; // maximum number of particles likely to be visible at any given time
369         if (p.time != 0) {
370                 auto maxGenerations = p.time / std::min(p.exptime.start.min, p.exptime.end.min);
371                 max_particles = p.amount / maxGenerations;
372         } else {
373                 auto longestLife = std::max(p.exptime.start.max, p.exptime.end.max);
374                 max_particles = p.amount * longestLife;
375         }
376
377         p_manager->reserveParticleSpace(max_particles * 1.2);
378 }
379
380 namespace {
381         GenericCAO *findObjectByID(ClientEnvironment *env, u16 id) {
382                 if (id == 0)
383                         return nullptr;
384                 return env->getGenericCAO(id);
385         }
386 }
387
388 void ParticleSpawner::spawnParticle(ClientEnvironment *env, float radius,
389         const core::matrix4 *attached_absolute_pos_rot_matrix)
390 {
391         float fac = 0;
392         if (p.time != 0) { // ensure safety from divide-by-zeroes
393                 fac = m_time / (p.time+0.1f);
394         }
395
396         auto r_pos = p.pos.blend(fac);
397         auto r_vel = p.vel.blend(fac);
398         auto r_acc = p.acc.blend(fac);
399         auto r_drag = p.drag.blend(fac);
400         auto r_radius = p.radius.blend(fac);
401         auto r_jitter = p.jitter.blend(fac);
402         auto r_bounce = p.bounce.blend(fac);
403         v3f  attractor_origin = p.attractor_origin.blend(fac);
404         v3f  attractor_direction = p.attractor_direction.blend(fac);
405         auto attractor_obj       = findObjectByID(env, p.attractor_attachment);
406         auto attractor_direction_obj = findObjectByID(env, p.attractor_direction_attachment);
407
408         auto r_exp = p.exptime.blend(fac);
409         auto r_size = p.size.blend(fac);
410         auto r_attract = p.attract.blend(fac);
411         auto attract = r_attract.pickWithin();
412
413         v3f ppos = m_player->getPosition() / BS;
414         v3f pos = r_pos.pickWithin();
415         v3f sphere_radius = r_radius.pickWithin();
416
417         // Need to apply this first or the following check
418         // will be wrong for attached spawners
419         if (attached_absolute_pos_rot_matrix) {
420                 pos *= BS;
421                 attached_absolute_pos_rot_matrix->transformVect(pos);
422                 pos /= BS;
423                 v3s16 camera_offset = m_particlemanager->m_env->getCameraOffset();
424                 pos.X += camera_offset.X;
425                 pos.Y += camera_offset.Y;
426                 pos.Z += camera_offset.Z;
427         }
428
429         if (pos.getDistanceFromSQ(ppos) > radius*radius)
430                 return;
431
432         // Parameters for the single particle we're about to spawn
433         ParticleParameters pp;
434         pp.pos = pos;
435
436         pp.vel = r_vel.pickWithin();
437         pp.acc = r_acc.pickWithin();
438         pp.drag = r_drag.pickWithin();
439         pp.jitter = r_jitter;
440         pp.bounce = r_bounce;
441
442         if (attached_absolute_pos_rot_matrix) {
443                 // Apply attachment rotation
444                 attached_absolute_pos_rot_matrix->rotateVect(pp.vel);
445                 attached_absolute_pos_rot_matrix->rotateVect(pp.acc);
446         }
447
448         if (attractor_obj)
449                 attractor_origin += attractor_obj->getPosition() / BS;
450         if (attractor_direction_obj) {
451                 auto *attractor_absolute_pos_rot_matrix = attractor_direction_obj->getAbsolutePosRotMatrix();
452                 if (attractor_absolute_pos_rot_matrix)
453                         attractor_absolute_pos_rot_matrix->rotateVect(attractor_direction);
454         }
455
456         pp.expirationtime = r_exp.pickWithin();
457
458         if (sphere_radius != v3f()) {
459                 f32 l = sphere_radius.getLength();
460                 v3f mag = sphere_radius;
461                 mag.normalize();
462
463                 v3f ofs = v3f(l,0,0);
464                 ofs.rotateXZBy(myrand_range(0.f,360.f));
465                 ofs.rotateYZBy(myrand_range(0.f,360.f));
466                 ofs.rotateXYBy(myrand_range(0.f,360.f));
467
468                 pp.pos += ofs * mag;
469         }
470
471         if (p.attractor_kind != ParticleParamTypes::AttractorKind::none && attract != 0) {
472                 v3f dir;
473                 f32 dist = 0; /* =0 necessary to silence warning */
474                 switch (p.attractor_kind) {
475                         case ParticleParamTypes::AttractorKind::none:
476                                 break;
477
478                         case ParticleParamTypes::AttractorKind::point: {
479                                 dist = pp.pos.getDistanceFrom(attractor_origin);
480                                 dir = pp.pos - attractor_origin;
481                                 dir.normalize();
482                                 break;
483                         }
484
485                         case ParticleParamTypes::AttractorKind::line: {
486                                 // https://github.com/minetest/minetest/issues/11505#issuecomment-915612700
487                                 const auto& lorigin = attractor_origin;
488                                 v3f ldir = attractor_direction;
489                                 ldir.normalize();
490                                 auto origin_to_point = pp.pos - lorigin;
491                                 auto scalar_projection = origin_to_point.dotProduct(ldir);
492                                 auto point_on_line = lorigin + (ldir * scalar_projection);
493
494                                 dist = pp.pos.getDistanceFrom(point_on_line);
495                                 dir = (point_on_line - pp.pos);
496                                 dir.normalize();
497                                 dir *= -1; // flip it around so strength=1 attracts, not repulses
498                                 break;
499                         }
500
501                         case ParticleParamTypes::AttractorKind::plane: {
502                                 // https://github.com/minetest/minetest/issues/11505#issuecomment-915612700
503                                 const v3f& porigin = attractor_origin;
504                                 v3f normal = attractor_direction;
505                                 normal.normalize();
506                                 v3f point_to_origin = porigin - pp.pos;
507                                 f32 factor = normal.dotProduct(point_to_origin);
508                                 if (numericAbsolute(factor) == 0.0f) {
509                                         dir = normal;
510                                 } else {
511                                         factor = numericSign(factor);
512                                         dir = normal * factor;
513                                 }
514                                 dist = numericAbsolute(normal.dotProduct(pp.pos - porigin));
515                                 dir *= -1; // flip it around so strength=1 attracts, not repulses
516                                 break;
517                         }
518                 }
519
520                 f32 speedTowards = numericAbsolute(attract) * dist;
521                 v3f avel = dir * speedTowards;
522                 if (attract > 0 && speedTowards > 0) {
523                         avel *= -1;
524                         if (p.attractor_kill) {
525                                 // make sure the particle dies after crossing the attractor threshold
526                                 f32 timeToCenter = dist / speedTowards;
527                                 if (timeToCenter < pp.expirationtime)
528                                         pp.expirationtime = timeToCenter;
529                         }
530                 }
531                 pp.vel += avel;
532         }
533
534         p.copyCommon(pp);
535
536         ClientTexRef texture;
537         v2f texpos, texsize;
538         video::SColor color(0xFFFFFFFF);
539
540         if (p.node.getContent() != CONTENT_IGNORE) {
541                 const ContentFeatures &f =
542                         m_particlemanager->m_env->getGameDef()->ndef()->get(p.node);
543                 if (!ParticleManager::getNodeParticleParams(p.node, f, pp, &texture.ref,
544                                 texpos, texsize, &color, p.node_tile))
545                         return;
546         } else {
547                 if (m_texcount == 0)
548                         return;
549                 texture = decltype(texture)(m_texpool[m_texcount == 1 ? 0 : myrand_range(0,m_texcount-1)]);
550                 texpos = v2f(0.0f, 0.0f);
551                 texsize = v2f(1.0f, 1.0f);
552                 if (texture.tex->animated)
553                         pp.animation = texture.tex->animation;
554         }
555
556         // synchronize animation length with particle life if desired
557         if (pp.animation.type != TAT_NONE) {
558                 if (pp.animation.type == TAT_VERTICAL_FRAMES &&
559                         pp.animation.vertical_frames.length < 0) {
560                         auto& a = pp.animation.vertical_frames;
561                         // we add a tiny extra value to prevent the first frame
562                         // from flickering back on just before the particle dies
563                         a.length = (pp.expirationtime / -a.length) + 0.1;
564                 } else if (pp.animation.type == TAT_SHEET_2D &&
565                                    pp.animation.sheet_2d.frame_length < 0) {
566                         auto& a = pp.animation.sheet_2d;
567                         auto frames = a.frames_w * a.frames_h;
568                         auto runtime = (pp.expirationtime / -a.frame_length) + 0.1;
569                         pp.animation.sheet_2d.frame_length = frames / runtime;
570                 }
571         }
572
573         // Allow keeping default random size
574         if (p.size.start.max > 0.0f || p.size.end.max > 0.0f)
575                 pp.size = r_size.pickWithin();
576
577         ++m_active;
578         auto pa = new Particle(
579                 m_gamedef,
580                 m_player,
581                 env,
582                 pp,
583                 texture,
584                 texpos,
585                 texsize,
586                 color
587         );
588         pa->m_parent = this;
589         m_particlemanager->addParticle(pa);
590 }
591
592 void ParticleSpawner::step(float dtime, ClientEnvironment *env)
593 {
594         m_time += dtime;
595
596         static thread_local const float radius =
597                         g_settings->getS16("max_block_send_distance") * MAP_BLOCKSIZE;
598
599         bool unloaded = false;
600         const core::matrix4 *attached_absolute_pos_rot_matrix = nullptr;
601         if (m_attached_id) {
602                 if (GenericCAO *attached = env->getGenericCAO(m_attached_id)) {
603                         attached_absolute_pos_rot_matrix = attached->getAbsolutePosRotMatrix();
604                 } else {
605                         unloaded = true;
606                 }
607         }
608
609         if (p.time != 0) {
610                 // Spawner exists for a predefined timespan
611                 for (auto i = m_spawntimes.begin(); i != m_spawntimes.end(); ) {
612                         if ((*i) <= m_time && p.amount > 0) {
613                                 --p.amount;
614
615                                 // Pretend to, but don't actually spawn a particle if it is
616                                 // attached to an unloaded object or distant from player.
617                                 if (!unloaded)
618                                         spawnParticle(env, radius, attached_absolute_pos_rot_matrix);
619
620                                 i = m_spawntimes.erase(i);
621                         } else {
622                                 ++i;
623                         }
624                 }
625         } else {
626                 // Spawner exists for an infinity timespan, spawn on a per-second base
627
628                 // Skip this step if attached to an unloaded object
629                 if (unloaded)
630                         return;
631
632                 for (int i = 0; i <= p.amount; i++) {
633                         if (myrand_float() < dtime)
634                                 spawnParticle(env, radius, attached_absolute_pos_rot_matrix);
635                 }
636         }
637 }
638
639 /*
640         ParticleManager
641 */
642
643 ParticleManager::ParticleManager(ClientEnvironment *env) :
644         m_env(env)
645 {}
646
647 ParticleManager::~ParticleManager()
648 {
649         clearAll();
650 }
651
652 void ParticleManager::step(float dtime)
653 {
654         stepParticles (dtime);
655         stepSpawners (dtime);
656 }
657
658 void ParticleManager::stepSpawners(float dtime)
659 {
660         MutexAutoLock lock(m_spawner_list_lock);
661         for (auto i = m_particle_spawners.begin(); i != m_particle_spawners.end();) {
662                 if (i->second->getExpired()) {
663                         // the particlespawner owns the textures, so we need to make
664                         // sure there are no active particles before we free it
665                         if (i->second->m_active == 0) {
666                                 delete i->second;
667                                 m_particle_spawners.erase(i++);
668                         } else {
669                                 ++i;
670                         }
671                 } else {
672                         i->second->step(dtime, m_env);
673                         ++i;
674                 }
675         }
676 }
677
678 void ParticleManager::stepParticles(float dtime)
679 {
680         MutexAutoLock lock(m_particle_list_lock);
681         for (auto i = m_particles.begin(); i != m_particles.end();) {
682                 if ((*i)->get_expired()) {
683                         if ((*i)->m_parent) {
684                                 assert((*i)->m_parent->m_active != 0);
685                                 --(*i)->m_parent->m_active;
686                         }
687                         (*i)->remove();
688                         delete *i;
689                         i = m_particles.erase(i);
690                 } else {
691                         (*i)->step(dtime);
692                         ++i;
693                 }
694         }
695 }
696
697 void ParticleManager::clearAll()
698 {
699         MutexAutoLock lock(m_spawner_list_lock);
700         MutexAutoLock lock2(m_particle_list_lock);
701         for (auto i = m_particle_spawners.begin(); i != m_particle_spawners.end();) {
702                 delete i->second;
703                 m_particle_spawners.erase(i++);
704         }
705
706         for(auto i = m_particles.begin(); i != m_particles.end();)
707         {
708                 (*i)->remove();
709                 delete *i;
710                 i = m_particles.erase(i);
711         }
712 }
713
714 void ParticleManager::handleParticleEvent(ClientEvent *event, Client *client,
715         LocalPlayer *player)
716 {
717         switch (event->type) {
718                 case CE_DELETE_PARTICLESPAWNER: {
719                         deleteParticleSpawner(event->delete_particlespawner.id);
720                         // no allocated memory in delete event
721                         break;
722                 }
723                 case CE_ADD_PARTICLESPAWNER: {
724                         deleteParticleSpawner(event->add_particlespawner.id);
725
726                         const ParticleSpawnerParameters &p = *event->add_particlespawner.p;
727
728                         // texture pool
729                         std::unique_ptr<ClientTexture[]> texpool = nullptr;
730                         size_t txpsz = 0;
731                         if (!p.texpool.empty()) {
732                                 txpsz = p.texpool.size();
733                                 texpool = decltype(texpool)(new ClientTexture [txpsz]);
734
735                                 for (size_t i = 0; i < txpsz; ++i) {
736                                         texpool[i] = ClientTexture(p.texpool[i], client->tsrc());
737                                 }
738                         } else {
739                                 // no texpool in use, use fallback texture
740                                 txpsz = 1;
741                                 texpool = decltype(texpool)(new ClientTexture[1] {
742                                         ClientTexture(p.texture, client->tsrc())
743                                 });
744                         }
745
746                         auto toadd = new ParticleSpawner(client, player,
747                                         p,
748                                         event->add_particlespawner.attached_id,
749                                         texpool,
750                                         txpsz,
751                                         this);
752
753                         addParticleSpawner(event->add_particlespawner.id, toadd);
754
755                         delete event->add_particlespawner.p;
756                         break;
757                 }
758                 case CE_SPAWN_PARTICLE: {
759                         ParticleParameters &p = *event->spawn_particle;
760
761                         ClientTexRef texture;
762                         v2f texpos, texsize;
763                         video::SColor color(0xFFFFFFFF);
764
765                         f32 oldsize = p.size;
766
767                         if (p.node.getContent() != CONTENT_IGNORE) {
768                                 const ContentFeatures &f = m_env->getGameDef()->ndef()->get(p.node);
769                                 getNodeParticleParams(p.node, f, p, &texture.ref, texpos,
770                                                 texsize, &color, p.node_tile);
771                         } else {
772                                 /* with no particlespawner to own the texture, we need
773                                  * to save it on the heap. it will be freed when the
774                                  * particle is destroyed */
775                                 auto texstore = new ClientTexture(p.texture, client->tsrc());
776
777                                 texture = ClientTexRef(*texstore);
778                                 texpos = v2f(0.0f, 0.0f);
779                                 texsize = v2f(1.0f, 1.0f);
780                         }
781
782                         // Allow keeping default random size
783                         if (oldsize > 0.0f)
784                                 p.size = oldsize;
785
786                         if (texture.ref) {
787                                 Particle *toadd = new Particle(client, player, m_env,
788                                                 p, texture, texpos, texsize, color);
789
790                                 addParticle(toadd);
791                         }
792
793                         delete event->spawn_particle;
794                         break;
795                 }
796                 default: break;
797         }
798 }
799
800 bool ParticleManager::getNodeParticleParams(const MapNode &n,
801         const ContentFeatures &f, ParticleParameters &p, video::ITexture **texture,
802         v2f &texpos, v2f &texsize, video::SColor *color, u8 tilenum)
803 {
804         // No particles for "airlike" nodes
805         if (f.drawtype == NDT_AIRLIKE)
806                 return false;
807
808         // Texture
809         u8 texid;
810         if (tilenum > 0 && tilenum <= 6)
811                 texid = tilenum - 1;
812         else
813                 texid = myrand_range(0,5);
814         const TileLayer &tile = f.tiles[texid].layers[0];
815         p.animation.type = TAT_NONE;
816
817         // Only use first frame of animated texture
818         if (tile.material_flags & MATERIAL_FLAG_ANIMATION)
819                 *texture = (*tile.frames)[0].texture;
820         else
821                 *texture = tile.texture;
822
823         float size = (myrand_range(0,8)) / 64.0f;
824         p.size = BS * size;
825         if (tile.scale)
826                 size /= tile.scale;
827         texsize = v2f(size * 2.0f, size * 2.0f);
828         texpos.X = (myrand_range(0,64)) / 64.0f - texsize.X;
829         texpos.Y = (myrand_range(0,64)) / 64.0f - texsize.Y;
830
831         if (tile.has_color)
832                 *color = tile.color;
833         else
834                 n.getColor(f, color);
835
836         return true;
837 }
838
839 // The final burst of particles when a node is finally dug, *not* particles
840 // spawned during the digging of a node.
841
842 void ParticleManager::addDiggingParticles(IGameDef *gamedef,
843         LocalPlayer *player, v3s16 pos, const MapNode &n, const ContentFeatures &f)
844 {
845         // No particles for "airlike" nodes
846         if (f.drawtype == NDT_AIRLIKE)
847                 return;
848
849         for (u16 j = 0; j < 16; j++) {
850                 addNodeParticle(gamedef, player, pos, n, f);
851         }
852 }
853
854 // During the digging of a node particles are spawned individually by this
855 // function, called from Game::handleDigging() in game.cpp.
856
857 void ParticleManager::addNodeParticle(IGameDef *gamedef,
858         LocalPlayer *player, v3s16 pos, const MapNode &n, const ContentFeatures &f)
859 {
860         ParticleParameters p;
861         video::ITexture *ref = nullptr;
862         v2f texpos, texsize;
863         video::SColor color;
864
865         if (!getNodeParticleParams(n, f, p, &ref, texpos, texsize, &color))
866                 return;
867
868         p.expirationtime = myrand_range(0, 100) / 100.0f;
869
870         // Physics
871         p.vel = v3f(
872                 myrand_range(-1.5f,1.5f),
873                 myrand_range(0.f,3.f),
874                 myrand_range(-1.5f,1.5f)
875         );
876         p.acc = v3f(
877                 0.0f,
878                 -player->movement_gravity * player->physics_override_gravity / BS,
879                 0.0f
880         );
881         p.pos = v3f(
882                 (f32)pos.X + myrand_range(0.f, .5f) - .25f,
883                 (f32)pos.Y + myrand_range(0.f, .5f) - .25f,
884                 (f32)pos.Z + myrand_range(0.f, .5f) - .25f
885         );
886
887         Particle *toadd = new Particle(
888                 gamedef,
889                 player,
890                 m_env,
891                 p,
892                 ClientTexRef(ref),
893                 texpos,
894                 texsize,
895                 color);
896
897         addParticle(toadd);
898 }
899
900 void ParticleManager::reserveParticleSpace(size_t max_estimate)
901 {
902         MutexAutoLock lock(m_particle_list_lock);
903         m_particles.reserve(m_particles.size() + max_estimate);
904 }
905
906 void ParticleManager::addParticle(Particle *toadd)
907 {
908         MutexAutoLock lock(m_particle_list_lock);
909         m_particles.push_back(toadd);
910 }
911
912
913 void ParticleManager::addParticleSpawner(u64 id, ParticleSpawner *toadd)
914 {
915         MutexAutoLock lock(m_spawner_list_lock);
916         m_particle_spawners[id] = toadd;
917 }
918
919 void ParticleManager::deleteParticleSpawner(u64 id)
920 {
921         MutexAutoLock lock(m_spawner_list_lock);
922         auto it = m_particle_spawners.find(id);
923         if (it != m_particle_spawners.end()) {
924                 it->second->setDying();
925         }
926 }