summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLexi Hale <5723574+velartrill@users.noreply.github.com>2022-07-13 11:57:12 +0200
committerGitHub <noreply@github.com>2022-07-13 11:57:12 +0200
commit20bd6bdb685af11548c35d3a48e5aa33f4222397 (patch)
tree6f60bad900cdb0ea28606bfe3f860b4948eeb277
parent8724fe6e3fc2b6c0b78123f1f95fd8c6c6817dd5 (diff)
downloadminetest-20bd6bdb685af11548c35d3a48e5aa33f4222397.tar.gz
minetest-20bd6bdb685af11548c35d3a48e5aa33f4222397.tar.bz2
minetest-20bd6bdb685af11548c35d3a48e5aa33f4222397.zip
Animated particlespawners and more (#11545)
Co-authored-by: Lars Mueller <appgurulars@gmx.de> Co-authored-by: sfan5 <sfan5@live.de> Co-authored-by: Dmitry Kostenko <codeforsmile@gmail.com>
-rw-r--r--builtin/game/features.lua1
-rw-r--r--doc/lua_api.txt346
-rw-r--r--src/client/particles.cpp442
-rw-r--r--src/client/particles.h70
-rw-r--r--src/network/clientpackethandler.cpp89
-rw-r--r--src/network/networkprotocol.h95
-rw-r--r--src/particles.cpp127
-rw-r--r--src/particles.h375
-rw-r--r--src/script/common/c_content.cpp2
-rw-r--r--src/script/common/c_converter.h1
-rw-r--r--src/script/lua_api/l_particleparams.h279
-rw-r--r--src/script/lua_api/l_particles.cpp214
-rw-r--r--src/script/lua_api/l_particles_local.cpp114
-rw-r--r--src/server.cpp75
-rw-r--r--src/util/numeric.cpp11
-rw-r--r--src/util/numeric.h23
-rw-r--r--src/util/pointer.h13
17 files changed, 1992 insertions, 285 deletions
diff --git a/builtin/game/features.lua b/builtin/game/features.lua
index 0d55bb01f..73b16361e 100644
--- a/builtin/game/features.lua
+++ b/builtin/game/features.lua
@@ -21,6 +21,7 @@ core.features = {
use_texture_alpha_string_modes = true,
degrotate_240_steps = true,
abm_min_max_y = true,
+ particlespawner_tweenable = true,
dynamic_add_media_table = true,
get_sky_as_table = true,
}
diff --git a/doc/lua_api.txt b/doc/lua_api.txt
index 2bf1e2171..f7fdad56e 100644
--- a/doc/lua_api.txt
+++ b/doc/lua_api.txt
@@ -4856,6 +4856,11 @@ Utilities
abm_min_max_y = true,
-- dynamic_add_media supports passing a table with options (5.5.0)
dynamic_add_media_table = true,
+ -- particlespawners support texpools and animation of properties,
+ -- particle textures support smooth fade and scale animations, and
+ -- sprite-sheet particle animations can by synced to the lifetime
+ -- of individual particles (5.6.0)
+ particlespawner_tweenable = true,
-- allows get_sky to return a table instead of separate values (5.6.0)
get_sky_as_table = true,
}
@@ -8984,6 +8989,8 @@ Used by `minetest.add_particle`.
texture = "image.png",
-- The texture of the particle
+ -- v5.6.0 and later: also supports the table format described in the
+ -- following section
playername = "singleplayer",
-- Optional, if specified spawns particle only on the player's client
@@ -9005,6 +9012,12 @@ Used by `minetest.add_particle`.
-- If set to a valid number 1-6, specifies the tile from which the
-- particle texture is picked.
-- Otherwise, the default behavior is used. (currently: any random tile)
+
+ drag = {x=0, y=0, z=0},
+ -- v5.6.0 and later: Optional drag value, consult the following section
+
+ bounce = {min = ..., max = ..., bias = 0},
+ -- v5.6.0 and later: Optional bounce range, consult the following section
}
@@ -9013,7 +9026,20 @@ Used by `minetest.add_particle`.
Used by `minetest.add_particlespawner`.
+Before v5.6.0, particlespawners used a different syntax and had a more limited set
+of features. Definition fields that are the same in both legacy and modern versions
+are shown in the next listing, and the fields that are used by legacy versions are
+shown separated by a comment; the modern fields are too complex to compactly
+describe in this manner and are documented after the listing.
+
+The older syntax can be used in combination with the newer syntax (e.g. having
+`minpos`, `maxpos`, and `pos` all set) to support older servers. On newer servers,
+the new syntax will override the older syntax; on older servers, the newer syntax
+will be ignored.
+
{
+ -- Common fields (same name and meaning in both new and legacy syntax)
+
amount = 1,
-- Number of particles spawned over the time period `time`.
@@ -9022,22 +9048,6 @@ Used by `minetest.add_particlespawner`.
-- If time is 0 spawner has infinite lifespan and spawns the `amount` on
-- a per-second basis.
- minpos = {x=0, y=0, z=0},
- maxpos = {x=0, y=0, z=0},
- minvel = {x=0, y=0, z=0},
- maxvel = {x=0, y=0, z=0},
- minacc = {x=0, y=0, z=0},
- maxacc = {x=0, y=0, z=0},
- minexptime = 1,
- maxexptime = 1,
- minsize = 1,
- maxsize = 1,
- -- The particles' properties are random values between the min and max
- -- values.
- -- applies to: pos, velocity, acceleration, expirationtime, size
- -- If `node` is set, min and maxsize can be set to 0 to spawn
- -- randomly-sized particles (just like actual node dig particles).
-
collisiondetection = false,
-- If true collide with `walkable` nodes and, depending on the
-- `object_collision` field, objects too.
@@ -9066,8 +9076,11 @@ Used by `minetest.add_particlespawner`.
animation = {Tile Animation definition},
-- Optional, specifies how to animate the particles' texture
+ -- v5.6.0 and later: set length to -1 to sychronize the length
+ -- of the animation with the expiration time of individual particles.
+ -- (-2 causes the animation to be played twice, and so on)
- glow = 0
+ glow = 0,
-- Optional, specify particle self-luminescence in darkness.
-- Values 0-14.
@@ -9081,8 +9094,307 @@ Used by `minetest.add_particlespawner`.
-- If set to a valid number 1-6, specifies the tile from which the
-- particle texture is picked.
-- Otherwise, the default behavior is used. (currently: any random tile)
+
+ -- Legacy definition fields
+
+ minpos = {x=0, y=0, z=0},
+ maxpos = {x=0, y=0, z=0},
+ minvel = {x=0, y=0, z=0},
+ maxvel = {x=0, y=0, z=0},
+ minacc = {x=0, y=0, z=0},
+ maxacc = {x=0, y=0, z=0},
+ minexptime = 1,
+ maxexptime = 1,
+ minsize = 1,
+ maxsize = 1,
+ -- The particles' properties are random values between the min and max
+ -- values.
+ -- applies to: pos, velocity, acceleration, expirationtime, size
+ -- If `node` is set, min and maxsize can be set to 0 to spawn
+ -- randomly-sized particles (just like actual node dig particles).
+ }
+
+### Modern definition fields
+
+After v5.6.0, spawner properties can be defined in several different ways depending
+on the level of control you need. `pos` for instance can be set as a single vector,
+in which case all particles will appear at that exact point throughout the lifetime
+of the spawner. Alternately, it can be specified as a min-max pair, specifying a
+cubic range the particles can appear randomly within. Finally, some properties can
+be animated by suffixing their key with `_tween` (e.g. `pos_tween`) and supplying
+a tween table.
+
+The following definitions are all equivalent, listed in order of precedence from
+lowest (the legacy syntax) to highest (tween tables). If multiple forms of a
+property definition are present, the highest-precidence form will be selected
+and all lower-precedence fields will be ignored, allowing for graceful
+degradation in older clients).
+
+ {
+ -- old syntax
+ maxpos = {x = 0, y = 0, z = 0},
+ minpos = {x = 0, y = 0, z = 0},
+
+ -- absolute value
+ pos = 0,
+ -- all components of every particle's position vector will be set to this
+ -- value
+
+ -- vec3
+ pos = vector.new(0,0,0),
+ -- all particles will appear at this exact position throughout the lifetime
+ -- of the particlespawner
+
+ -- vec3 range
+ pos = {
+ -- the particle will appear at a position that is picked at random from
+ -- within a cubic range
+
+ min = vector.new(0,0,0),
+ -- `min` is the minimum value this property will be set to in particles
+ -- spawned by the generator
+
+ max = vector.new(0,0,0),
+ -- `max` is the minimum value this property will be set to in particles
+ -- spawned by the generator
+
+ bias = 0,
+ -- when `bias` is 0, all random values are exactly as likely as any
+ -- other. when it is positive, the higher it is, the more likely values
+ -- will appear towards the minimum end of the allowed spectrum. when
+ -- it is negative, the lower it is, the more likely values will appear
+ -- towards the maximum end of the allowed spectrum. the curve is
+ -- exponential and there is no particular maximum or minimum value
+ },
+
+ -- tween table
+ pos_tween = {...},
+ -- a tween table should consist of a list of frames in the same form as the
+ -- untweened pos property above, which the engine will interpolate between,
+ -- and optionally a number of properties that control how the interpolation
+ -- takes place. currently **only two frames**, the first and the last, are
+ -- used, but extra frames are accepted for the sake of forward compatibility.
+ -- any of the above definition styles can be used here as well in any combination
+ -- supported by the property type
+
+ pos_tween = {
+ style = "fwd",
+ -- linear animation from first to last frame (default)
+ style = "rev",
+ -- linear animation from last to first frame
+ style = "pulse",
+ -- linear animation from first to last then back to first again
+ style = "flicker",
+ -- like "pulse", but slightly randomized to add a bit of stutter
+
+ reps = 1,
+ -- number of times the animation is played over the particle's lifespan
+
+ start = 0.0,
+ -- point in the spawner's lifespan at which the animation begins. 0 is
+ -- the very beginning, 1 is the very end
+
+ -- frames can be defined in a number of different ways, depending on the
+ -- underlying type of the property. for now, all but the first and last
+ -- frame are ignored
+
+ -- frames
+
+ -- floats
+ 0, 0,
+
+ -- vec3s
+ vector.new(0,0,0),
+ vector.new(0,0,0),
+
+ -- vec3 ranges
+ { min = vector.new(0,0,0), max = vector.new(0,0,0), bias = 0 },
+ { min = vector.new(0,0,0), max = vector.new(0,0,0), bias = 0 },
+
+ -- mixed
+ 0, { min = vector.new(0,0,0), max = vector.new(0,0,0), bias = 0 },
+ },
}
+All of the properties that can be defined in this way are listed in the next
+section, along with the datatypes they accept.
+
+#### List of particlespawner properties
+All of the properties in this list can be animated with `*_tween` tables
+unless otherwise specified. For example, `jitter` can be tweened by setting
+a `jitter_tween` table instead of (or in addition to) a `jitter` table/value.
+Types used are defined in the previous section.
+
+* vec3 range `pos`: the position at which particles can appear
+* vec3 range `vel`: the initial velocity of the particle
+* vec3 range `acc`: the direction and speed with which the particle
+ accelerates
+* vec3 range `jitter`: offsets the velocity of each particle by a random
+ amount within the specified range each frame. used to create Brownian motion.
+* vec3 range `drag`: the amount by which absolute particle velocity along
+ each axis is decreased per second. a value of 1.0 means that the particle
+ will be slowed to a stop over the space of a second; a value of -1.0 means
+ that the particle speed will be doubled every second. to avoid interfering
+ with gravity provided by `acc`, a drag vector like `vector.new(1,0,1)` can
+ be used instead of a uniform value.
+* float range `bounce`: how bouncy the particles are when `collisiondetection`
+ is turned on. values less than or equal to `0` turn off particle bounce;
+ `1` makes the particles bounce without losing any velocity, and `2` makes
+ them double their velocity with every bounce. `bounce` is not bounded but
+ values much larger than `1.0` probably aren't very useful.
+* float range `exptime`: the number of seconds after which the particle
+ disappears.
+* table `attract`: sets the birth orientation of particles relative to various
+ shapes defined in world coordinate space. this is an alternative means of
+ setting the velocity which allows particles to emerge from or enter into
+ some entity or node on the map, rather than simply being assigned random
+ velocity values within a range. the velocity calculated by this method will
+ be **added** to that specified by `vel` if `vel` is also set, so in most
+ cases **`vel` should be set to 0**. `attract` has the fields:
+ * string `kind`: selects the kind of shape towards which the particles will
+ be oriented. it must have one of the following values:
+ * `"none"`: no attractor is set and the `attractor` table is ignored
+ * `"point"`: the particles are attracted to a specific point in space.
+ use this also if you want a sphere-like effect, in combination with
+ the `radius` property.
+ * `"line"`: the particles are attracted to an (infinite) line passing
+ through the points `origin` and `angle`. use this for e.g. beacon
+ effects, energy beam effects, etc.
+ * `"plane"`: the particles are attracted to an (infinite) plane on whose
+ surface `origin` designates a point in world coordinate space. use this
+ for e.g. particles entering or emerging from a portal.
+ * float range `strength`: the speed with which particles will move towards
+ `attractor`. If negative, the particles will instead move away from that
+ point.
+ * vec3 `origin`: the origin point of the shape towards which particles will
+ initially be oriented. functions as an offset if `origin_attached` is also
+ set.
+ * vec3 `direction`: sets the direction in which the attractor shape faces. for
+ lines, this sets the angle of the line; e.g. a vector of (0,1,0) will
+ create a vertical line that passes through `origin`. for planes, `direction`
+ is the surface normal of an infinite plane on whose surface `origin` is
+ a point. functions as an offset if `direction_attached` is also set.
+ * entity `origin_attached`: allows the origin to be specified as an offset
+ from the position of an entity rather than a coordinate in world space.
+ * entity `direction_attached`: allows the direction to be specified as an offset
+ from the position of an entity rather than a coordinate in world space.
+ * bool `die_on_contact`: if true, the particles' lifetimes are adjusted so
+ that they will die as they cross the attractor threshold. this behavior
+ is the default but is undesirable for some kinds of animations; set it to
+ false to allow particles to live out their natural lives.
+* vec3 range `radius`: if set, particles will be arranged in a sphere around
+ `pos`. A constant can be used to create a spherical shell of particles, a
+ vector to create an ovoid shell, and a range to create a volume; e.g.
+ `{min = 0.5, max = 1, bias = 1}` will allow particles to appear between 0.5
+ and 1 nodes away from `pos` but will cluster them towards the center of the
+ sphere. Usually if `radius` is used, `pos` should be a single point, but it
+ can still be a range if you really know what you're doing (e.g. to create a
+ "roundcube" emitter volume).
+
+### Textures
+
+In versions before v5.6.0, particlespawner textures could only be specified as a single
+texture string. After v5.6.0, textures can now be specified as a table as well. This
+table contains options that allow simple animations to be applied to the texture.
+
+ texture = {
+ name = "mymod_particle_texture.png",
+ -- the texture specification string
+
+ alpha = 1.0,
+ -- controls how visible the particle is; at 1.0 the particle is fully
+ -- visible, at 0, it is completely invisible.
+
+ alpha_tween = {1, 0},
+ -- can be used instead of `alpha` to animate the alpha value over the
+ -- particle's lifetime. these tween tables work identically to the tween
+ -- tables used in particlespawner properties, except that time references
+ -- are understood with respect to the particle's lifetime, not the
+ -- spawner's. {1,0} fades the particle out over its lifetime.
+
+ scale = 1,
+ scale = {x = 1, y = 1},
+ -- scales the texture onscreen
+
+ scale_tween = {
+ {x = 1, y = 1},
+ {x = 0, y = 1},
+ },
+ -- animates the scale over the particle's lifetime. works like the
+ -- alpha_tween table, but can accept two-dimensional vectors as well as
+ -- integer values. the example value would cause the particle to shrink
+ -- in one dimension over the course of its life until it disappears
+
+ blend = "alpha",
+ -- (default) blends transparent pixels with those they are drawn atop
+ -- according to the alpha channel of the source texture. useful for
+ -- e.g. material objects like rocks, dirt, smoke, or node chunks
+ blend = "add",
+ -- adds the value of pixels to those underneath them, modulo the sources
+ -- alpha channel. useful for e.g. bright light effects like sparks or fire
+ blend = "screen",
+ -- like "add" but less bright. useful for subtler light effecs. note that
+ -- this is NOT formally equivalent to the "screen" effect used in image
+ -- editors and compositors, as it does not respect the alpha channel of
+ -- of the image being blended
+ blend = "sub",
+ -- the inverse of "add"; the value of the source pixel is subtracted from
+ -- the pixel underneath it. a white pixel will turn whatever is underneath
+ -- it black; a black pixel will be "transparent". useful for creating
+ -- darkening effects
+
+ animation = {Tile Animation definition},
+ -- overrides the particlespawner's global animation property for a single
+ -- specific texture
+ }
+
+Instead of setting a single texture definition, it is also possible to set a
+`texpool` property. A `texpool` consists of a list of possible particle textures.
+Every time a particle is spawned, the engine will pick a texture at random from
+the `texpool` and assign it as that particle's texture. You can also specify a
+`texture` in addition to a `texpool`; the `texture` value will be ignored on newer
+clients but will be sent to older (pre-v5.6.0) clients that do not implement
+texpools.
+
+ texpool = {
+ "mymod_particle_texture.png";
+ { name = "mymod_spark.png", fade = "out" },
+ {
+ name = "mymod_dust.png",
+ alpha = 0.3,
+ scale = 1.5,
+ animation = {
+ type = "vertical_frames",
+ aspect_w = 16, aspect_h = 16,
+
+ length = 3,
+ -- the animation lasts for 3s and then repeats
+ length = -3,
+ -- repeat the animation three times over the particle's lifetime
+ -- (post-v5.6.0 clients only)
+ },
+ },
+ }
+
+#### List of animatable texture properties
+
+While animated particlespawner values vary over the course of the particlespawner's
+lifetime, animated texture properties vary over the lifespans of the individual
+particles spawned with that texture. So a particle with the texture property
+
+ alpha_tween = {
+ 0.0, 1.0,
+ style = "pulse",
+ reps = 4,
+ }
+
+would be invisible at its spawning, pulse visible four times throughout its
+lifespan, and then vanish again before expiring.
+
+* float `alpha` (0.0 - 1.0): controls the visibility of the texture
+* vec2 `scale`: controls the size of the displayed billboard onscreen. Its units
+ are multiples of the parent particle's assigned size (see the `size` property above)
+
`HTTPRequest` definition
------------------------
diff --git a/src/client/particles.cpp b/src/client/particles.cpp
index 288826a5f..a1de1bb98 100644
--- a/src/client/particles.cpp
+++ b/src/client/particles.cpp
@@ -34,23 +34,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "settings.h"
/*
- Utility
-*/
-
-static f32 random_f32(f32 min, f32 max)
-{
- return rand() / (float)RAND_MAX * (max - min) + min;
-}
-
-static v3f random_v3f(v3f min, v3f max)
-{
- return v3f(
- random_f32(min.X, max.X),
- random_f32(min.Y, max.Y),
- random_f32(min.Z, max.Z));
-}
-
-/*
Particle
*/
@@ -59,25 +42,71 @@ Particle::Particle(
LocalPlayer *player,
ClientEnvironment *env,
const ParticleParameters &p,
- video::ITexture *texture,
+ const ClientTexRef& texture,
v2f texpos,
v2f texsize,
video::SColor color
):
scene::ISceneNode(((Client *)gamedef)->getSceneManager()->getRootSceneNode(),
- ((Client *)gamedef)->getSceneManager())
+ ((Client *)gamedef)->getSceneManager()),
+ m_texture(texture)
{
// Misc
m_gamedef = gamedef;
m_env = env;
+ // translate blend modes to GL blend functions
+ video::E_BLEND_FACTOR bfsrc, bfdst;
+ video::E_BLEND_OPERATION blendop;
+ const auto blendmode = texture.tex != nullptr
+ ? texture.tex -> blendmode
+ : ParticleParamTypes::BlendMode::alpha;
+
+ switch (blendmode) {
+ case ParticleParamTypes::BlendMode::alpha:
+ bfsrc = video::EBF_SRC_ALPHA;
+ bfdst = video::EBF_ONE_MINUS_SRC_ALPHA;
+ blendop = video::EBO_ADD;
+ break;
+
+ case ParticleParamTypes::BlendMode::add:
+ bfsrc = video::EBF_SRC_ALPHA;
+ bfdst = video::EBF_DST_ALPHA;
+ blendop = video::EBO_ADD;
+ break;
+
+ case ParticleParamTypes::BlendMode::sub:
+ bfsrc = video::EBF_SRC_ALPHA;
+ bfdst = video::EBF_DST_ALPHA;
+ blendop = video::EBO_REVSUBTRACT;
+ break;
+
+ case ParticleParamTypes::BlendMode::screen:
+ bfsrc = video::EBF_ONE;
+ bfdst = video::EBF_ONE_MINUS_SRC_COLOR;
+ blendop = video::EBO_ADD;
+ break;
+
+ default: assert(false);
+ }
+
// Texture
m_material.setFlag(video::EMF_LIGHTING, false);
m_material.setFlag(video::EMF_BACK_FACE_CULLING, false);
m_material.setFlag(video::EMF_BILINEAR_FILTER, false);
m_material.setFlag(video::EMF_FOG_ENABLE, true);
- m_material.MaterialType = video::EMT_TRANSPARENT_ALPHA_CHANNEL;
- m_material.setTexture(0, texture);
+
+ // correctly render layered transparent particles -- see #10398
+ m_material.setFlag(video::EMF_ZWRITE_ENABLE, true);
+
+ // enable alpha blending and set blend mode
+ m_material.MaterialType = video::EMT_ONETEXTURE_BLEND;
+ m_material.MaterialTypeParam = video::pack_textureBlendFunc(
+ bfsrc, bfdst,
+ video::EMFN_MODULATE_1X,
+ video::EAS_TEXTURE | video::EAS_VERTEX_COLOR);
+ m_material.BlendOperation = blendop;
+ m_material.setTexture(0, m_texture.ref);
m_texpos = texpos;
m_texsize = texsize;
m_animation = p.animation;
@@ -90,6 +119,9 @@ Particle::Particle(
m_pos = p.pos;
m_velocity = p.vel;
m_acceleration = p.acc;
+ m_drag = p.drag;
+ m_jitter = p.jitter;
+ m_bounce = p.bounce;
m_expiration = p.expirationtime;
m_player = player;
m_size = p.size;
@@ -98,6 +130,8 @@ Particle::Particle(
m_object_collision = p.object_collision;
m_vertical = p.vertical;
m_glow = p.glow;
+ m_alpha = 0;
+ m_parent = nullptr;
// Irrlicht stuff
const float c = p.size / 2;
@@ -111,6 +145,14 @@ Particle::Particle(
updateVertices();
}
+Particle::~Particle()
+{
+ /* if our textures aren't owned by a particlespawner, we need to clean
+ * them up ourselves when the particle dies */
+ if (m_parent == nullptr)
+ delete m_texture.tex;
+}
+
void Particle::OnRegisterSceneNode()
{
if (IsVisible)
@@ -134,6 +176,12 @@ void Particle::render()
void Particle::step(float dtime)
{
m_time += dtime;
+
+ // apply drag (not handled by collisionMoveSimple) and brownian motion
+ v3f av = vecAbsolute(m_velocity);
+ av -= av * (m_drag * dtime);
+ m_velocity = av*vecSign(m_velocity) + v3f(m_jitter.pickWithin())*dtime;
+
if (m_collisiondetection) {
aabb3f box = m_collisionbox;
v3f p_pos = m_pos * BS;
@@ -141,17 +189,41 @@ void Particle::step(float dtime)
collisionMoveResult r = collisionMoveSimple(m_env, m_gamedef, BS * 0.5f,
box, 0.0f, dtime, &p_pos, &p_velocity, m_acceleration * BS, nullptr,
m_object_collision);
- if (m_collision_removal && r.collides) {
- // force expiration of the particle
- m_expiration = -1.0;
+
+ f32 bounciness = m_bounce.pickWithin();
+ if (r.collides && (m_collision_removal || bounciness > 0)) {
+ if (m_collision_removal) {
+ // force expiration of the particle
+ m_expiration = -1.0f;
+ } else if (bounciness > 0) {
+ /* cheap way to get a decent bounce effect is to only invert the
+ * largest component of the velocity vector, so e.g. you don't
+ * have a rock immediately bounce back in your face when you try
+ * to skip it across the water (as would happen if we simply
+ * downscaled and negated the velocity vector). this means
+ * bounciness will work properly for cubic objects, but meshes
+ * with diagonal angles and entities will not yield the correct
+ * visual. this is probably unavoidable */
+ if (av.Y > av.X && av.Y > av.Z) {
+ m_velocity.Y = -(m_velocity.Y * bounciness);
+ } else if (av.X > av.Y && av.X > av.Z) {
+ m_velocity.X = -(m_velocity.X * bounciness);
+ } else if (av.Z > av.Y && av.Z > av.X) {
+ m_velocity.Z = -(m_velocity.Z * bounciness);
+ } else { // well now we're in a bit of a pickle
+ m_velocity = -(m_velocity * bounciness);
+ }
+ }
} else {
- m_pos = p_pos / BS;
m_velocity = p_velocity / BS;
}
+ m_pos = p_pos / BS;
} else {
+ // apply acceleration
m_velocity += m_acceleration * dtime;
m_pos += m_velocity * dtime;
}
+
if (m_animation.type != TAT_NONE) {
m_animation_time += dtime;
int frame_length_i, frame_count;
@@ -165,11 +237,21 @@ void Particle::step(float dtime)
}
}
+ // animate particle alpha in accordance with settings
+ if (m_texture.tex != nullptr)
+ m_alpha = m_texture.tex -> alpha.blend(m_time / (m_expiration+0.1f));
+ else
+ m_alpha = 1.f;
+
// Update lighting
updateLight();
// Update model
updateVertices();
+
+ // Update position -- see #10398
+ v3s16 camera_offset = m_env->getCameraOffset();
+ setPosition(m_pos*BS - intToFloat(camera_offset, BS));
}
void Particle::updateLight()
@@ -189,7 +271,7 @@ void Particle::updateLight()
light = blend_light(m_env->getDayNightRatio(), LIGHT_SUN, 0);
u8 m_light = decode_light(light + m_glow);
- m_color.set(255,
+ m_color.set(m_alpha*255,
m_light * m_base_color.getRed() / 255,
m_light * m_base_color.getGreen() / 255,
m_light * m_base_color.getBlue() / 255);
@@ -198,6 +280,12 @@ void Particle::updateLight()
void Particle::updateVertices()
{
f32 tx0, tx1, ty0, ty1;
+ v2f scale;
+
+ if (m_texture.tex != nullptr)
+ scale = m_texture.tex -> scale.blend(m_time / (m_expiration+0.1));
+ else
+ scale = v2f(1.f, 1.f);
if (m_animation.type != TAT_NONE) {
const v2u32 texsize = m_material.getTexture(0)->getSize();
@@ -218,16 +306,24 @@ void Particle::updateVertices()
ty1 = m_texpos.Y + m_texsize.Y;
}
- m_vertices[0] = video::S3DVertex(-m_size / 2, -m_size / 2,
+ auto half = m_size * .5f,
+ hx = half * scale.X,
+ hy = half * scale.Y;
+ m_vertices[0] = video::S3DVertex(-hx, -hy,
0, 0, 0, 0, m_color, tx0, ty1);
- m_vertices[1] = video::S3DVertex(m_size / 2, -m_size / 2,
+ m_vertices[1] = video::S3DVertex(hx, -hy,
0, 0, 0, 0, m_color, tx1, ty1);
- m_vertices[2] = video::S3DVertex(m_size / 2, m_size / 2,
+ m_vertices[2] = video::S3DVertex(hx, hy,
0, 0, 0, 0, m_color, tx1, ty0);
- m_vertices[3] = video::S3DVertex(-m_size / 2, m_size / 2,
+ m_vertices[3] = video::S3DVertex(-hx, hy,
0, 0, 0, 0, m_color, tx0, ty0);
- v3s16 camera_offset = m_env->getCameraOffset();
+
+ // see #10398
+ // v3s16 camera_offset = m_env->getCameraOffset();
+ // particle position is now handled by step()
+ m_box.reset(v3f());
+
for (video::S3DVertex &vertex : m_vertices) {
if (m_vertical) {
v3f ppos = m_player->getPosition()/BS;
@@ -238,7 +334,6 @@ void Particle::updateVertices()
vertex.Pos.rotateXZBy(m_player->getYaw());
}
m_box.addInternalPoint(vertex.Pos);
- vertex.Pos += m_pos*BS - intToFloat(camera_offset, BS);
}
}
@@ -251,7 +346,8 @@ ParticleSpawner::ParticleSpawner(
LocalPlayer *player,
const ParticleSpawnerParameters &p,
u16 attached_id,
- video::ITexture *texture,
+ std::unique_ptr<ClientTexture[]>& texpool,
+ size_t texcount,
ParticleManager *p_manager
):
m_particlemanager(p_manager), p(p)
@@ -259,21 +355,66 @@ ParticleSpawner::ParticleSpawner(
m_gamedef = gamedef;
m_player = player;
m_attached_id = attached_id;
- m_texture = texture;
+ m_texpool = std::move(texpool);
+ m_texcount = texcount;
m_time = 0;
+ m_active = 0;
+ m_dying = false;
m_spawntimes.reserve(p.amount + 1);
for (u16 i = 0; i <= p.amount; i++) {
- float spawntime = rand() / (float)RAND_MAX * p.time;
+ float spawntime = myrand_float() * p.time;
m_spawntimes.push_back(spawntime);
}
+
+ size_t max_particles = 0; // maximum number of particles likely to be visible at any given time
+ if (p.time != 0) {
+ auto maxGenerations = p.time / std::min(p.exptime.start.min, p.exptime.end.min);
+ max_particles = p.amount / maxGenerations;
+ } else {
+ auto longestLife = std::max(p.exptime.start.max, p.exptime.end.max);
+ max_particles = p.amount * longestLife;
+ }
+
+ p_manager->reserveParticleSpace(max_particles * 1.2);
+}
+
+namespace {
+ GenericCAO *findObjectByID(ClientEnvironment *env, u16 id) {
+ if (id == 0)
+ return nullptr;
+ return env->getGenericCAO(id);
+ }
}
void ParticleSpawner::spawnParticle(ClientEnvironment *env, float radius,
const core::matrix4 *attached_absolute_pos_rot_matrix)
{
+ float fac = 0;
+ if (p.time != 0) { // ensure safety from divide-by-zeroes
+ fac = m_time / (p.time+0.1f);
+ }
+
+ auto r_pos = p.pos.blend(fac);
+ auto r_vel = p.vel.blend(fac);
+ auto r_acc = p.acc.blend(fac);
+ auto r_drag = p.drag.blend(fac);
+ auto r_radius = p.radius.blend(fac);
+ auto r_jitter = p.jitter.blend(fac);
+ auto r_bounce = p.bounce.blend(fac);
+ v3f attractor_origin = p.attractor_origin.blend(fac);
+ v3f attractor_direction = p.attractor_direction.blend(fac);
+ auto attractor_obj = findObjectByID(env, p.attractor_attachment);
+ auto attractor_direction_obj = findObjectByID(env, p.attractor_direction_attachment);
+
+ auto r_exp = p.exptime.blend(fac);
+ auto r_size = p.size.blend(fac);
+ auto r_attract = p.attract.blend(fac);
+ auto attract = r_attract.pickWithin();
+
v3f ppos = m_player->getPosition() / BS;
- v3f pos = random_v3f(p.minpos, p.maxpos);
+ v3f pos = r_pos.pickWithin();
+ v3f sphere_radius = r_radius.pickWithin();
// Need to apply this first or the following check
// will be wrong for attached spawners
@@ -287,15 +428,18 @@ void ParticleSpawner::spawnParticle(ClientEnvironment *env, float radius,
pos.Z += camera_offset.Z;
}
- if (pos.getDistanceFrom(ppos) > radius)
+ if (pos.getDistanceFromSQ(ppos) > radius*radius)
return;
// Parameters for the single particle we're about to spawn
ParticleParameters pp;
pp.pos = pos;
- pp.vel = random_v3f(p.minvel, p.maxvel);
- pp.acc = random_v3f(p.minacc, p.maxacc);
+ pp.vel = r_vel.pickWithin();
+ pp.acc = r_acc.pickWithin();
+ pp.drag = r_drag.pickWithin();
+ pp.jitter = r_jitter;
+ pp.bounce = r_bounce;
if (attached_absolute_pos_rot_matrix) {
// Apply attachment rotation
@@ -303,30 +447,137 @@ void ParticleSpawner::spawnParticle(ClientEnvironment *env, float radius,
attached_absolute_pos_rot_matrix->rotateVect(pp.acc);
}
- pp.expirationtime = random_f32(p.minexptime, p.maxexptime);
+ if (attractor_obj)
+ attractor_origin += attractor_obj->getPosition() / BS;
+ if (attractor_direction_obj) {
+ auto *attractor_absolute_pos_rot_matrix = attractor_direction_obj->getAbsolutePosRotMatrix();
+ if (attractor_absolute_pos_rot_matrix)
+ attractor_absolute_pos_rot_matrix->rotateVect(attractor_direction);
+ }
+
+ pp.expirationtime = r_exp.pickWithin();
+
+ if (sphere_radius != v3f()) {
+ f32 l = sphere_radius.getLength();
+ v3f mag = sphere_radius;
+ mag.normalize();
+
+ v3f ofs = v3f(l,0,0);
+ ofs.rotateXZBy(myrand_range(0.f,360.f));
+ ofs.rotateYZBy(myrand_range(0.f,360.f));
+ ofs.rotateXYBy(myrand_range(0.f,360.f));
+
+ pp.pos += ofs * mag;
+ }
+
+ if (p.attractor_kind != ParticleParamTypes::AttractorKind::none && attract != 0) {
+ v3f dir;
+ f32 dist = 0; /* =0 necessary to silence warning */
+ switch (p.attractor_kind) {
+ case ParticleParamTypes::AttractorKind::none:
+ break;
+
+ case ParticleParamTypes::AttractorKind::point: {
+ dist = pp.pos.getDistanceFrom(attractor_origin);
+ dir = pp.pos - attractor_origin;
+ dir.normalize();
+ break;
+ }
+
+ case ParticleParamTypes::AttractorKind::line: {
+ // https://github.com/minetest/minetest/issues/11505#issuecomment-915612700
+ const auto& lorigin = attractor_origin;
+ v3f ldir = attractor_direction;
+ ldir.normalize();
+ auto origin_to_point = pp.pos - lorigin;
+ auto scalar_projection = origin_to_point.dotProduct(ldir);
+ auto point_on_line = lorigin + (ldir * scalar_projection);
+
+ dist = pp.pos.getDistanceFrom(point_on_line);
+ dir = (point_on_line - pp.pos);
+ dir.normalize();
+ dir *= -1; // flip it around so strength=1 attracts, not repulses
+ break;
+ }
+
+ case ParticleParamTypes::AttractorKind::plane: {
+ // https://github.com/minetest/minetest/issues/11505#issuecomment-915612700
+ const v3f& porigin = attractor_origin;
+ v3f normal = attractor_direction;
+ normal.normalize();
+ v3f point_to_origin = porigin - pp.pos;
+ f32 factor = normal.dotProduct(point_to_origin);
+ if (numericAbsolute(factor) == 0.0f) {
+ dir = normal;
+ } else {
+ factor = numericSign(factor);
+ dir = normal * factor;
+ }
+ dist = numericAbsolute(normal.dotProduct(pp.pos - porigin));
+ dir *= -1; // flip it around so strength=1 attracts, not repulses
+ break;
+ }
+ }
+
+ f32 speedTowards = numericAbsolute(attract) * dist;
+ v3f avel = dir * speedTowards;
+ if (attract > 0 && speedTowards > 0) {
+ avel *= -1;
+ if (p.attractor_kill) {
+ // make sure the particle dies after crossing the attractor threshold
+ f32 timeToCenter = dist / speedTowards;
+ if (timeToCenter < pp.expirationtime)
+ pp.expirationtime = timeToCenter;
+ }
+ }
+ pp.vel += avel;
+ }
+
p.copyCommon(pp);
- video::ITexture *texture;
+ ClientTexRef texture;
v2f texpos, texsize;
video::SColor color(0xFFFFFFFF);
if (p.node.getContent() != CONTENT_IGNORE) {
const ContentFeatures &f =
m_particlemanager->m_env->getGameDef()->ndef()->get(p.node);
- if (!ParticleManager::getNodeParticleParams(p.node, f, pp, &texture,
+ if (!ParticleManager::getNodeParticleParams(p.node, f, pp, &texture.ref,
texpos, texsize, &color, p.node_tile))
return;
} else {
- texture = m_texture;
+ if (m_texcount == 0)
+ return;
+ texture = decltype(texture)(m_texpool[m_texcount == 1 ? 0 : myrand_range(0,m_texcount-1)]);
texpos = v2f(0.0f, 0.0f);
texsize = v2f(1.0f, 1.0f);
+ if (texture.tex->animated)
+ pp.animation = texture.tex->animation;
+ }
+
+ // synchronize animation length with particle life if desired
+ if (pp.animation.type != TAT_NONE) {
+ if (pp.animation.type == TAT_VERTICAL_FRAMES &&
+ pp.animation.vertical_frames.length < 0) {
+ auto& a = pp.animation.vertical_frames;
+ // we add a tiny extra value to prevent the first frame
+ // from flickering back on just before the particle dies
+ a.length = (pp.expirationtime / -a.length) + 0.1;
+ } else if (pp.animation.type == TAT_SHEET_2D &&
+ pp.animation.sheet_2d.frame_length < 0) {
+ auto& a = pp.animation.sheet_2d;
+ auto frames = a.frames_w * a.frames_h;
+ auto runtime = (pp.expirationtime / -a.frame_length) + 0.1;
+ pp.animation.sheet_2d.frame_length = frames / runtime;
+ }
}
// Allow keeping default random size
- if (p.maxsize > 0.0f)
- pp.size = random_f32(p.minsize, p.maxsize);
+ if (p.size.start.max > 0.0f || p.size.end.max > 0.0f)
+ pp.size = r_size.pickWithin();
- m_particlemanager->addParticle(new Particle(
+ ++m_active;
+ auto pa = new Particle(
m_gamedef,
m_player,
env,
@@ -335,7 +586,9 @@ void ParticleSpawner::spawnParticle(ClientEnvironment *env, float radius,
texpos,
texsize,
color
- ));
+ );
+ pa->m_parent = this;
+ m_particlemanager->addParticle(pa);
}
void ParticleSpawner::step(float dtime, ClientEnvironment *env)
@@ -348,7 +601,7 @@ void ParticleSpawner::step(float dtime, ClientEnvironment *env)
bool unloaded = false;
const core::matrix4 *attached_absolute_pos_rot_matrix = nullptr;
if (m_attached_id) {
- if (GenericCAO *attached = dynamic_cast<GenericCAO *>(env->getActiveObject(m_attached_id))) {
+ if (GenericCAO *attached = env->getGenericCAO(m_attached_id)) {
attached_absolute_pos_rot_matrix = attached->getAbsolutePosRotMatrix();
} else {
unloaded = true;
@@ -379,7 +632,7 @@ void ParticleSpawner::step(float dtime, ClientEnvironment *env)
return;
for (int i = 0; i <= p.amount; i++) {
- if (rand() / (float)RAND_MAX < dtime)
+ if (myrand_float() < dtime)
spawnParticle(env, radius, attached_absolute_pos_rot_matrix);
}
}
@@ -408,9 +661,15 @@ void ParticleManager::stepSpawners(float dtime)
{
MutexAutoLock lock(m_spawner_list_lock);
for (auto i = m_particle_spawners.begin(); i != m_particle_spawners.end();) {
- if (i->second->get_expired()) {
- delete i->second;
- m_particle_spawners.erase(i++);
+ if (i->second->getExpired()) {
+ // the particlespawner owns the textures, so we need to make
+ // sure there are no active particles before we free it
+ if (i->second->m_active == 0) {
+ delete i->second;
+ m_particle_spawners.erase(i++);
+ } else {
+ ++i;
+ }
} else {
i->second->step(dtime, m_env);
++i;
@@ -423,6 +682,10 @@ void ParticleManager::stepParticles(float dtime)
MutexAutoLock lock(m_particle_list_lock);
for (auto i = m_particles.begin(); i != m_particles.end();) {
if ((*i)->get_expired()) {
+ if ((*i)->m_parent) {
+ assert((*i)->m_parent->m_active != 0);
+ --(*i)->m_parent->m_active;
+ }
(*i)->remove();
delete *i;
i = m_particles.erase(i);
@@ -464,13 +727,29 @@ void ParticleManager::handleParticleEvent(ClientEvent *event, Client *client,
const ParticleSpawnerParameters &p = *event->add_particlespawner.p;
- video::ITexture *texture =
- client->tsrc()->getTextureForMesh(p.texture);
+ // texture pool
+ std::unique_ptr<ClientTexture[]> texpool = nullptr;
+ size_t txpsz = 0;
+ if (!p.texpool.empty()) {
+ txpsz = p.texpool.size();
+ texpool = decltype(texpool)(new ClientTexture [txpsz]);
+
+ for (size_t i = 0; i < txpsz; ++i) {
+ texpool[i] = ClientTexture(p.texpool[i], client->tsrc());
+ }
+ } else {
+ // no texpool in use, use fallback texture
+ txpsz = 1;
+ texpool = decltype(texpool)(new ClientTexture[1] {
+ ClientTexture(p.texture, client->tsrc())
+ });
+ }
auto toadd = new ParticleSpawner(client, player,
p,
event->add_particlespawner.attached_id,
- texture,
+ texpool,
+ txpsz,
this);
addParticleSpawner(event->add_particlespawner.id, toadd);
@@ -481,7 +760,7 @@ void ParticleManager::handleParticleEvent(ClientEvent *event, Client *client,
case CE_SPAWN_PARTICLE: {
ParticleParameters &p = *event->spawn_particle;
- video::ITexture *texture;
+ ClientTexRef texture;
v2f texpos, texsize;
video::SColor color(0xFFFFFFFF);
@@ -489,11 +768,15 @@ void ParticleManager::handleParticleEvent(ClientEvent *event, Client *client,
if (p.node.getContent() != CONTENT_IGNORE) {
const ContentFeatures &f = m_env->getGameDef()->ndef()->get(p.node);
- if (!getNodeParticleParams(p.node, f, p, &texture, texpos,
- texsize, &color, p.node_tile))
- texture = nullptr;
+ getNodeParticleParams(p.node, f, p, &texture.ref, texpos,
+ texsize, &color, p.node_tile);
} else {
- texture = client->tsrc()->getTextureForMesh(p.texture);
+ /* with no particlespawner to own the texture, we need
+ * to save it on the heap. it will be freed when the
+ * particle is destroyed */
+ auto texstore = new ClientTexture(p.texture, client->tsrc());
+
+ texture = ClientTexRef(*texstore);
texpos = v2f(0.0f, 0.0f);
texsize = v2f(1.0f, 1.0f);
}
@@ -502,7 +785,7 @@ void ParticleManager::handleParticleEvent(ClientEvent *event, Client *client,
if (oldsize > 0.0f)
p.size = oldsize;
- if (texture) {
+ if (texture.ref) {
Particle *toadd = new Particle(client, player, m_env,
p, texture, texpos, texsize, color);
@@ -529,7 +812,7 @@ bool ParticleManager::getNodeParticleParams(const MapNode &n,
if (tilenum > 0 && tilenum <= 6)
texid = tilenum - 1;
else
- texid = rand() % 6;
+ texid = myrand_range(0,5);
const TileLayer &tile = f.tiles[texid].layers[0];
p.animation.type = TAT_NONE;
@@ -539,13 +822,13 @@ bool ParticleManager::getNodeParticleParams(const MapNode &n,
else
*texture = tile.texture;
- float size = (rand() % 8) / 64.0f;
+ float size = (myrand_range(0,8)) / 64.0f;
p.size = BS * size;
if (tile.scale)
size /= tile.scale;
texsize = v2f(size * 2.0f, size * 2.0f);
- texpos.X = (rand() % 64) / 64.0f - texsize.X;
- texpos.Y = (rand() % 64) / 64.0f - texsize.Y;
+ texpos.X = (myrand_range(0,64)) / 64.0f - texsize.X;
+ texpos.Y = (myrand_range(0,64)) / 64.0f - texsize.Y;
if (tile.has_color)
*color = tile.color;
@@ -577,20 +860,20 @@ void ParticleManager::addNodeParticle(IGameDef *gamedef,
LocalPlayer *player, v3s16 pos, const MapNode &n, const ContentFeatures &f)
{
ParticleParameters p;
- video::ITexture *texture;
+ video::ITexture *ref = nullptr;
v2f texpos, texsize;
video::SColor color;
- if (!getNodeParticleParams(n, f, p, &texture, texpos, texsize, &color))
+ if (!getNodeParticleParams(n, f, p, &ref, texpos, texsize, &color))
return;
- p.expirationtime = (rand() % 100) / 100.0f;
+ p.expirationtime = myrand_range(0, 100) / 100.0f;
// Physics
p.vel = v3f(
- (rand() % 150) / 50.0f - 1.5f,
- (rand() % 150) / 50.0f,
- (rand() % 150) / 50.0f - 1.5f
+ myrand_range(-1.5f,1.5f),
+ myrand_range(0.f,3.f),
+ myrand_range(-1.5f,1.5f)
);
p.acc = v3f(
0.0f,
@@ -598,9 +881,9 @@ void ParticleManager::addNodeParticle(IGameDef *gamedef,
0.0f
);
p.pos = v3f(
- (f32)pos.X + (rand() % 100) / 200.0f - 0.25f,
- (f32)pos.Y + (rand() % 100) / 200.0f - 0.25f,
- (f32)pos.Z + (rand() % 100) / 200.0f - 0.25f
+ (f32)pos.X + myrand_range(0.f, .5f) - .25f,
+ (f32)pos.Y + myrand_range(0.f, .5f) - .25f,
+ (f32)pos.Z + myrand_range(0.f, .5f) - .25f
);
Particle *toadd = new Particle(
@@ -608,7 +891,7 @@ void ParticleManager::addNodeParticle(IGameDef *gamedef,
player,
m_env,
p,
- texture,
+ ClientTexRef(ref),
texpos,
texsize,
color);
@@ -616,6 +899,12 @@ void ParticleManager::addNodeParticle(IGameDef *gamedef,
addParticle(toadd);
}
+void ParticleManager::reserveParticleSpace(size_t max_estimate)
+{
+ MutexAutoLock lock(m_particle_list_lock);
+ m_particles.reserve(m_particles.size() + max_estimate);
+}
+
void ParticleManager::addParticle(Particle *toadd)
{
MutexAutoLock lock(m_particle_list_lock);
@@ -634,7 +923,6 @@ void ParticleManager::deleteParticleSpawner(u64 id)
MutexAutoLock lock(m_spawner_list_lock);
auto it = m_particle_spawners.find(id);
if (it != m_particle_spawners.end()) {
- delete it->second;
- m_particle_spawners.erase(it);
+ it->second->setDying();
}
}
diff --git a/src/client/particles.h b/src/client/particles.h
index 2011f0262..36be903f1 100644
--- a/src/client/particles.h
+++ b/src/client/particles.h
@@ -31,20 +31,53 @@ class ClientEnvironment;
struct MapNode;
struct ContentFeatures;
+struct ClientTexture
+{
+ /* per-spawner structure used to store the ParticleTexture structs
+ * that spawned particles will refer to through ClientTexRef */
+ ParticleTexture tex;
+ video::ITexture *ref = nullptr;
+
+ ClientTexture() = default;
+ ClientTexture(const ClientTexture&) = default;
+ ClientTexture(const ServerParticleTexture& p, ITextureSource *t):
+ tex(p),
+ ref(t->getTextureForMesh(p.string)) {};
+};
+
+struct ClientTexRef
+{
+ /* per-particle structure used to avoid massively duplicating the
+ * fairly large ParticleTexture struct */
+ ParticleTexture* tex = nullptr;
+ video::ITexture* ref = nullptr;
+ ClientTexRef() = default;
+ ClientTexRef(const ClientTexRef&) = default;
+
+ /* constructor used by particles spawned from a spawner */
+ ClientTexRef(ClientTexture& t):
+ tex(&t.tex), ref(t.ref) {};
+
+ /* constructor used for node particles */
+ ClientTexRef(decltype(ref) tp): ref(tp) {};
+};
+
+class ParticleSpawner;
+
class Particle : public scene::ISceneNode
{
- public:
+public:
Particle(
- IGameDef* gamedef,
+ IGameDef *gamedef,
LocalPlayer *player,
ClientEnvironment *env,
const ParticleParameters &p,
- video::ITexture *texture,
+ const ClientTexRef &texture,
v2f texpos,
v2f texsize,
video::SColor color
);
- ~Particle() = default;
+ ~Particle();
virtual const aabb3f &getBoundingBox() const
{
@@ -69,9 +102,12 @@ class Particle : public scene::ISceneNode
bool get_expired ()
{ return m_expiration < m_time; }
+ ParticleSpawner *m_parent;
+
private:
void updateLight();
void updateVertices();
+ void setVertexAlpha(float a);
video::S3DVertex m_vertices[4];
float m_time = 0.0f;
@@ -81,14 +117,19 @@ private:
IGameDef *m_gamedef;
aabb3f m_box;
aabb3f m_collisionbox;
+ ClientTexRef m_texture;
video::SMaterial m_material;
v2f m_texpos;
v2f m_texsize;
v3f m_pos;
v3f m_velocity;
v3f m_acceleration;
+ v3f m_drag;
+ ParticleParamTypes::v3fRange m_jitter;
+ ParticleParamTypes::f32Range m_bounce;
LocalPlayer *m_player;
float m_size;
+
//! Color without lighting
video::SColor m_base_color;
//! Final rendered color
@@ -102,24 +143,27 @@ private:
float m_animation_time = 0.0f;
int m_animation_frame = 0;
u8 m_glow;
+ float m_alpha = 0.0f;
};
class ParticleSpawner
{
public:
- ParticleSpawner(IGameDef* gamedef,
+ ParticleSpawner(IGameDef *gamedef,
LocalPlayer *player,
const ParticleSpawnerParameters &p,
u16 attached_id,
- video::ITexture *texture,
+ std::unique_ptr<ClientTexture[]> &texpool,
+ size_t texcount,
ParticleManager* p_manager);
- ~ParticleSpawner() = default;
-
void step(float dtime, ClientEnvironment *env);
- bool get_expired ()
- { return p.amount <= 0 && p.time != 0; }
+ size_t m_active;
+
+ bool getExpired() const
+ { return m_dying || (p.amount <= 0 && p.time != 0); }
+ void setDying() { m_dying = true; }
private:
void spawnParticle(ClientEnvironment *env, float radius,
@@ -127,10 +171,12 @@ private:
ParticleManager *m_particlemanager;
float m_time;
+ bool m_dying;
IGameDef *m_gamedef;
LocalPlayer *m_player;
ParticleSpawnerParameters p;
- video::ITexture *m_texture;
+ std::unique_ptr<ClientTexture[]> m_texpool;
+ size_t m_texcount;
std::vector<float> m_spawntimes;
u16 m_attached_id;
};
@@ -156,6 +202,8 @@ public:
void addNodeParticle(IGameDef *gamedef, LocalPlayer *player, v3s16 pos,
const MapNode &n, const ContentFeatures &f);
+ void reserveParticleSpace(size_t max_estimate);
+
/**
* This function is only used by client particle spawners
*
diff --git a/src/network/clientpackethandler.cpp b/src/network/clientpackethandler.cpp
index 1901c6675..25c1d2690 100644
--- a/src/network/clientpackethandler.cpp
+++ b/src/network/clientpackethandler.cpp
@@ -994,18 +994,18 @@ void Client::handleCommand_AddParticleSpawner(NetworkPacket* pkt)
p.amount = readU16(is);
p.time = readF32(is);
- p.minpos = readV3F32(is);
- p.maxpos = readV3F32(is);
- p.minvel = readV3F32(is);
- p.maxvel = readV3F32(is);
- p.minacc = readV3F32(is);
- p.maxacc = readV3F32(is);
- p.minexptime = readF32(is);
- p.maxexptime = readF32(is);
- p.minsize = readF32(is);
- p.maxsize = readF32(is);
+
+ // older protocols do not support tweening, and send only
+ // static ranges, so we can't just use the normal serialization
+ // functions for the older values.
+ p.pos.start.legacyDeSerialize(is);
+ p.vel.start.legacyDeSerialize(is);
+ p.acc.start.legacyDeSerialize(is);
+ p.exptime.start.legacyDeSerialize(is);
+ p.size.start.legacyDeSerialize(is);
+
p.collisiondetection = readU8(is);
- p.texture = deSerializeString32(is);
+ p.texture.string = deSerializeString32(is);
server_id = readU32(is);
@@ -1018,6 +1018,8 @@ void Client::handleCommand_AddParticleSpawner(NetworkPacket* pkt)
p.glow = readU8(is);
p.object_collision = readU8(is);
+ bool legacy_format = true;
+
// This is kinda awful
do {
u16 tmp_param0 = readU16(is);
@@ -1026,7 +1028,70 @@ void Client::handleCommand_AddParticleSpawner(NetworkPacket* pkt)
p.node.param0 = tmp_param0;
p.node.param2 = readU8(is);
p.node_tile = readU8(is);
- } while (0);
+
+ // v >= 5.6.0
+ f32 tmp_sbias = readF32(is);
+ if (is.eof())
+ break;
+
+ // initial bias must be stored separately in the stream to preserve
+ // backwards compatibility with older clients, which do not support
+ // a bias field in their range "format"
+ p.pos.start.bias = tmp_sbias;
+ p.vel.start.bias = readF32(is);
+ p.acc.start.bias = readF32(is);
+ p.exptime.start.bias = readF32(is);
+ p.size.start.bias = readF32(is);
+
+ p.pos.end.deSerialize(is);
+ p.vel.end.deSerialize(is);
+ p.acc.end.deSerialize(is);
+ p.exptime.end.deSerialize(is);
+ p.size.end.deSerialize(is);
+
+ // properties for legacy texture field
+ p.texture.deSerialize(is, m_proto_ver, true);
+
+ p.drag.deSerialize(is);
+ p.jitter.deSerialize(is);
+ p.bounce.deSerialize(is);
+ ParticleParamTypes::deSerializeParameterValue(is, p.attractor_kind);
+ using ParticleParamTypes::AttractorKind;
+ if (p.attractor_kind != AttractorKind::none) {
+ p.attract.deSerialize(is);
+ p.attractor_origin.deSerialize(is);
+ p.attractor_attachment = readU16(is);
+ /* we only check the first bit, in order to allow this value
+ * to be turned into a bit flag field later if needed */
+ p.attractor_kill = !!(readU8(is) & 1);
+ if (p.attractor_kind != AttractorKind::point) {
+ p.attractor_direction.deSerialize(is);
+ p.attractor_direction_attachment = readU16(is);
+ }
+ }
+ p.radius.deSerialize(is);
+
+ u16 texpoolsz = readU16(is);
+ p.texpool.reserve(texpoolsz);
+ for (u16 i = 0; i < texpoolsz; ++i) {
+ ServerParticleTexture newtex;
+ newtex.deSerialize(is, m_proto_ver);
+ p.texpool.push_back(newtex);
+ }
+
+ legacy_format = false;
+ } while(0);
+
+ if (legacy_format) {
+ // there's no tweening data to be had, so we need to set the
+ // legacy params to constant values, otherwise everything old
+ // will tween to zero
+ p.pos.end = p.pos.start;
+ p.vel.end = p.vel.start;
+ p.acc.end = p.acc.start;
+ p.exptime.end = p.exptime.start;
+ p.size.end = p.size.start;
+ }
auto event = new ClientEvent();
event->type = CE_ADD_PARTICLESPAWNER;
diff --git a/src/network/networkprotocol.h b/src/network/networkprotocol.h
index 33e49afa4..e3fd32866 100644
--- a/src/network/networkprotocol.h
+++ b/src/network/networkprotocol.h
@@ -207,6 +207,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
Minimap modes
PROTOCOL VERSION 40:
TOCLIENT_MEDIA_PUSH changed, TOSERVER_HAVE_MEDIA added
+ Added new particlespawner parameters (5.6.0)
*/
#define LATEST_PROTOCOL_VERSION 40
@@ -511,11 +512,12 @@ enum ToClientCommand
TOCLIENT_SPAWN_PARTICLE = 0x46,
/*
- v3f1000 pos
- v3f1000 velocity
- v3f1000 acceleration
- f1000 expirationtime
- f1000 size
+ -- struct range<T> { T min, T max, f32 bias };
+ v3f pos
+ v3f velocity
+ v3f acceleration
+ f32 expirationtime
+ f32 size
u8 bool collisiondetection
u32 len
u8[len] texture
@@ -524,22 +526,26 @@ enum ToClientCommand
TileAnimation animation
u8 glow
u8 object_collision
+ v3f drag
+ range<v3f> bounce
*/
TOCLIENT_ADD_PARTICLESPAWNER = 0x47,
/*
+ -- struct range<T> { T min, T max, f32 bias };
+ -- struct tween<T> { T start, T end };
u16 amount
- f1000 spawntime
- v3f1000 minpos
- v3f1000 maxpos
- v3f1000 minvel
- v3f1000 maxvel
- v3f1000 minacc
- v3f1000 maxacc
- f1000 minexptime
- f1000 maxexptime
- f1000 minsize
- f1000 maxsize
+ f32 spawntime
+ v3f minpos
+ v3f maxpos
+ v3f minvel
+ v3f maxvel
+ v3f minacc
+ v3f maxacc
+ f32 minexptime
+ f32 maxexptime
+ f32 minsize
+ f32 maxsize
u8 bool collisiondetection
u32 len
u8[len] texture
@@ -549,6 +555,63 @@ enum ToClientCommand
TileAnimation animation
u8 glow
u8 object_collision
+
+ f32 pos_start_bias
+ f32 vel_start_bias
+ f32 acc_start_bias
+ f32 exptime_start_bias
+ f32 size_start_bias
+
+ range<v3f> pos_end
+ -- i.e v3f pos_end_min
+ -- v3f pos_end_max
+ -- f32 pos_end_bias
+ range<v3f> vel_end
+ range<v3f> acc_end
+
+ tween<range<v3f>> drag
+ -- i.e. v3f drag_start_min
+ -- v3f drag_start_max
+ -- f32 drag_start_bias
+ -- v3f drag_end_min
+ -- v3f drag_end_max
+ -- f32 drag_end_bias
+ tween<range<v3f>> jitter
+ tween<range<f32>> bounce
+
+ u8 attraction_kind
+ none = 0
+ point = 1
+ line = 2
+ plane = 3
+
+ if attraction_kind > none {
+ tween<range<f32>> attract_strength
+ tween<v3f> attractor_origin
+ u16 attractor_origin_attachment_object_id
+ u8 spawner_flags
+ bit 1: attractor_kill (particles dies on contact)
+ if attraction_mode > point {
+ tween<v3f> attractor_angle
+ u16 attractor_origin_attachment_object_id
+ }
+ }
+
+ tween<range<v3f>> radius
+ tween<range<v3f>> drag
+
+ u16 texpool_sz
+ texpool_sz.times {
+ u8 flags
+ -- bit 0: animated
+ -- other bits free & ignored as of proto v40
+ tween<f32> alpha
+ tween<v2f> scale
+ if flags.animated {
+ TileAnimation animation
+ }
+ }
+
*/
TOCLIENT_DELETE_PARTICLESPAWNER_LEGACY = 0x48, // Obsolete
diff --git a/src/particles.cpp b/src/particles.cpp
index 14c987958..19b3418b7 100644
--- a/src/particles.cpp
+++ b/src/particles.cpp
@@ -18,7 +18,103 @@ with this program; if not, write to the Free Software Foundation, Inc.,
*/
#include "particles.h"
-#include "util/serialize.h"
+#include <type_traits>
+using namespace ParticleParamTypes;
+
+#define PARAM_PVFN(n) ParticleParamTypes::n##ParameterValue
+v2f PARAM_PVFN(pick) (float* f, const v2f a, const v2f b) {
+ return v2f(
+ numericalBlend(f[0], a.X, b.X),
+ numericalBlend(f[1], a.Y, b.Y)
+ );
+}
+
+v3f PARAM_PVFN(pick) (float* f, const v3f a, const v3f b) {
+ return v3f(
+ numericalBlend(f[0], a.X, b.X),
+ numericalBlend(f[1], a.Y, b.Y),
+ numericalBlend(f[2], a.Z, b.Z)
+ );
+}
+
+v2f PARAM_PVFN(interpolate) (float fac, const v2f a, const v2f b)
+ { return b.getInterpolated(a, fac); }
+v3f PARAM_PVFN(interpolate) (float fac, const v3f a, const v3f b)
+ { return b.getInterpolated(a, fac); }
+
+#define PARAM_DEF_SRZR(T, wr, rd) \
+ void PARAM_PVFN(serialize) (std::ostream& os, T v) {wr(os,v); } \
+ void PARAM_PVFN(deSerialize)(std::istream& is, T& v) {v = rd(is);}
+
+
+#define PARAM_DEF_NUM(T, wr, rd) PARAM_DEF_SRZR(T, wr, rd) \
+ T PARAM_PVFN(interpolate)(float fac, const T a, const T b) \
+ { return numericalBlend<T>(fac,a,b); } \
+ T PARAM_PVFN(pick) (float* f, const T a, const T b) \
+ { return numericalBlend<T>(f[0],a,b); }
+
+PARAM_DEF_NUM(u8, writeU8, readU8); PARAM_DEF_NUM(s8, writeS8, readS8);
+PARAM_DEF_NUM(u16, writeU16, readU16); PARAM_DEF_NUM(s16, writeS16, readS16);
+PARAM_DEF_NUM(u32, writeU32, readU32); PARAM_DEF_NUM(s32, writeS32, readS32);
+PARAM_DEF_NUM(f32, writeF32, readF32);
+PARAM_DEF_SRZR(v2f, writeV2F32, readV2F32);
+PARAM_DEF_SRZR(v3f, writeV3F32, readV3F32);
+
+enum class ParticleTextureFlags : u8 {
+ /* each value specifies a bit in a bitmask; if the maximum value
+ * goes above 1<<7 the type of the flags field must be changed
+ * from u8, which will necessitate a protocol change! */
+
+ // the first bit indicates whether the texture is animated
+ animated = 1,
+
+ /* the next three bits indicate the blending mode of the texture
+ * blendmode is encoded by (flags |= (u8)blend << 1); retrieve with
+ * (flags & ParticleTextureFlags::blend) >> 1. note that the third
+ * bit is currently reserved for adding more blend modes in the future */
+ blend = 0x7 << 1,
+};
+
+/* define some shorthand so we don't have to repeat ourselves or use
+ * decltype everywhere */
+using FlagT = std::underlying_type_t<ParticleTextureFlags>;
+
+void ServerParticleTexture::serialize(std::ostream &os, u16 protocol_ver, bool newPropertiesOnly) const
+{
+ /* newPropertiesOnly is used to de/serialize parameters of the legacy texture
+ * field, which are encoded separately from the texspec string */
+ FlagT flags = 0;
+ if (animated)
+ flags |= FlagT(ParticleTextureFlags::animated);
+ if (blendmode != BlendMode::alpha)
+ flags |= FlagT(blendmode) << 1;
+ serializeParameterValue(os, flags);
+
+ alpha.serialize(os);
+ scale.serialize(os);
+ if (!newPropertiesOnly)
+ os << serializeString32(string);
+
+ if (animated)
+ animation.serialize(os, protocol_ver);
+}
+
+void ServerParticleTexture::deSerialize(std::istream &is, u16 protocol_ver, bool newPropertiesOnly)
+{
+ FlagT flags = 0;
+ deSerializeParameterValue(is, flags);
+
+ animated = !!(flags & FlagT(ParticleTextureFlags::animated));
+ blendmode = BlendMode((flags & FlagT(ParticleTextureFlags::blend)) >> 1);
+
+ alpha.deSerialize(is);
+ scale.deSerialize(is);
+ if (!newPropertiesOnly)
+ string = deSerializeString32(is);
+
+ if (animated)
+ animation.deSerialize(is, protocol_ver);
+}
void ParticleParameters::serialize(std::ostream &os, u16 protocol_ver) const
{
@@ -28,7 +124,7 @@ void ParticleParameters::serialize(std::ostream &os, u16 protocol_ver) const
writeF32(os, expirationtime);
writeF32(os, size);
writeU8(os, collisiondetection);
- os << serializeString32(texture);
+ os << serializeString32(texture.string);
writeU8(os, vertical);
writeU8(os, collision_removal);
animation.serialize(os, 6); /* NOT the protocol ver */
@@ -37,6 +133,20 @@ void ParticleParameters::serialize(std::ostream &os, u16 protocol_ver) const
writeU16(os, node.param0);
writeU8(os, node.param2);
writeU8(os, node_tile);
+ writeV3F32(os, drag);
+ jitter.serialize(os);
+ bounce.serialize(os);
+}
+
+template <typename T, T (reader)(std::istream& is)>
+inline bool streamEndsBeforeParam(T& val, std::istream& is)
+{
+ // This is kinda awful
+ T tmp = reader(is);
+ if (is.eof())
+ return true;
+ val = tmp;
+ return false;
}
void ParticleParameters::deSerialize(std::istream &is, u16 protocol_ver)
@@ -47,17 +157,20 @@ void ParticleParameters::deSerialize(std::istream &is, u16 protocol_ver)
expirationtime = readF32(is);
size = readF32(is);
collisiondetection = readU8(is);
- texture = deSerializeString32(is);
+ texture.string = deSerializeString32(is);
vertical = readU8(is);
collision_removal = readU8(is);
animation.deSerialize(is, 6); /* NOT the protocol ver */
glow = readU8(is);
object_collision = readU8(is);
- // This is kinda awful
- u16 tmp_param0 = readU16(is);
- if (is.eof())
+
+ if (streamEndsBeforeParam<u16, readU16>(node.param0, is))
return;
- node.param0 = tmp_param0;
node.param2 = readU8(is);
node_tile = readU8(is);
+
+ if (streamEndsBeforeParam<v3f, readV3F32>(drag, is))
+ return;
+ jitter.deSerialize(is);
+ bounce.deSerialize(is);
}
diff --git a/src/particles.h b/src/particles.h
index 6f518b771..622fee099 100644
--- a/src/particles.h
+++ b/src/particles.h
@@ -20,19 +20,352 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#pragma once
#include <string>
+#include <sstream>
+#include <vector>
+#include <ctgmath>
+#include <type_traits>
#include "irrlichttypes_bloated.h"
#include "tileanimation.h"
#include "mapnode.h"
+#include "util/serialize.h"
+#include "util/numeric.h"
// This file defines the particle-related structures that both the server and
// client need. The ParticleManager and rendering is in client/particles.h
-struct CommonParticleParams {
+namespace ParticleParamTypes
+{
+ template <bool cond, typename T>
+ using enableIf = typename std::enable_if<cond, T>::type;
+ // std::enable_if_t does not appear to be present in GCC????
+ // std::is_enum_v also missing. wtf. these are supposed to be
+ // present as of c++14
+
+ template<typename T> using BlendFunction = T(float,T,T);
+ #define DECL_PARAM_SRZRS(type) \
+ void serializeParameterValue (std::ostream& os, type v); \
+ void deSerializeParameterValue(std::istream& is, type& r);
+ #define DECL_PARAM_OVERLOADS(type) DECL_PARAM_SRZRS(type) \
+ type interpolateParameterValue(float fac, const type a, const type b); \
+ type pickParameterValue (float* facs, const type a, const type b);
+
+ DECL_PARAM_OVERLOADS(u8); DECL_PARAM_OVERLOADS(s8);
+ DECL_PARAM_OVERLOADS(u16); DECL_PARAM_OVERLOADS(s16);
+ DECL_PARAM_OVERLOADS(u32); DECL_PARAM_OVERLOADS(s32);
+ DECL_PARAM_OVERLOADS(f32);
+ DECL_PARAM_OVERLOADS(v2f);
+ DECL_PARAM_OVERLOADS(v3f);
+
+ /* C++ is a strongly typed language. this means that enums cannot be implicitly
+ * cast to integers, as they can be in C. while this may sound good in principle,
+ * it means that our normal serialization functions cannot be called on
+ * enumerations unless they are explicitly cast to a particular type first. this
+ * is problematic, because in C++ enums can have any integral type as an underlying
+ * type, and that type would need to be named everywhere an enumeration is
+ * de/serialized.
+ *
+ * this is obviously not cool, both in terms of writing legible, succinct code,
+ * and in terms of robustness: the underlying type might be changed at some point,
+ * e.g. if a bitmask gets too big for its britches. we could use an equivalent of
+ * `std::to_underlying(value)` everywhere we need to deal with enumerations, but
+ * that's hideous and unintuitive. instead, we supply the following functions to
+ * transparently map enumeration types to their underlying values. */
+
+ template <typename E, enableIf<std::is_enum<E>::value, bool> = true>
+ void serializeParameterValue(std::ostream& os, E k) {
+ serializeParameterValue(os, (std::underlying_type_t<E>)k);
+ }
+
+ template <typename E, enableIf<std::is_enum<E>::value, bool> = true>
+ void deSerializeParameterValue(std::istream& is, E& k) {
+ std::underlying_type_t<E> v;
+ deSerializeParameterValue(is, v);
+ k = (E)v;
+ }
+
+ /* this is your brain on C++. */
+
+ template <typename T, size_t PN>
+ struct Parameter
+ {
+ using ValType = T;
+ using pickFactors = float[PN];
+
+ T val;
+ using This = Parameter<T, PN>;
+
+ Parameter() = default;
+ Parameter(const This& a) = default;
+ template <typename... Args>
+ Parameter(Args... args) : val(args...) {}
+
+ virtual void serialize(std::ostream &os) const
+ { serializeParameterValue (os, this->val); }
+ virtual void deSerialize(std::istream &is)
+ { deSerializeParameterValue(is, this->val); }
+
+ virtual T interpolate(float fac, const This& against) const
+ {
+ return interpolateParameterValue(fac, this->val, against.val);
+ }
+
+ static T pick(float* f, const This& a, const This& b)
+ {
+ return pickParameterValue(f, a.val, b.val);
+ }
+
+ operator T() const { return val; }
+ T operator=(T b) { return val = b; }
+
+ };
+
+ template <typename T> T numericalBlend(float fac, T min, T max)
+ { return min + ((max - min) * fac); }
+
+ template <typename T, size_t N>
+ struct VectorParameter : public Parameter<T,N> {
+ using This = VectorParameter<T,N>;
+ template <typename... Args>
+ VectorParameter(Args... args) : Parameter<T,N>(args...) {}
+ };
+
+ template <typename T, size_t PN>
+ inline std::string dump(const Parameter<T,PN>& p)
+ {
+ return std::to_string(p.val);
+ }
+
+ template <typename T, size_t N>
+ inline std::string dump(const VectorParameter<T,N>& v)
+ {
+ std::ostringstream oss;
+ if (N == 3)
+ oss << PP(v.val);
+ else
+ oss << PP2(v.val);
+ return oss.str();
+ }
+
+ using u8Parameter = Parameter<u8, 1>; using s8Parameter = Parameter<s8, 1>;
+ using u16Parameter = Parameter<u16, 1>; using s16Parameter = Parameter<s16, 1>;
+ using u32Parameter = Parameter<u32, 1>; using s32Parameter = Parameter<s32, 1>;
+
+ using f32Parameter = Parameter<f32, 1>;
+
+ using v2fParameter = VectorParameter<v2f, 2>;
+ using v3fParameter = VectorParameter<v3f, 3>;
+
+ template <typename T>
+ struct RangedParameter
+ {
+ using ValType = T;
+ using This = RangedParameter<T>;
+
+ T min, max;
+ f32 bias = 0;
+
+ RangedParameter() = default;
+ RangedParameter(const This& a) = default;
+ RangedParameter(T _min, T _max) : min(_min), max(_max) {}
+ template <typename M> RangedParameter(M b) : min(b), max(b) {}
+
+ // these functions handle the old range serialization "format"; bias must
+ // be manually encoded in a separate part of the stream. NEVER ADD FIELDS
+ // TO THESE FUNCTIONS
+ void legacySerialize(std::ostream& os) const
+ {
+ min.serialize(os);
+ max.serialize(os);
+ }
+ void legacyDeSerialize(std::istream& is)
+ {
+ min.deSerialize(is);
+ max.deSerialize(is);
+ }
+
+ // these functions handle the format used by new fields. new fields go here
+ void serialize(std::ostream &os) const
+ {
+ legacySerialize(os);
+ writeF32(os, bias);
+ }
+ void deSerialize(std::istream &is)
+ {
+ legacyDeSerialize(is);
+ bias = readF32(is);
+ }
+
+ This interpolate(float fac, const This against) const
+ {
+ This r;
+ r.min = min.interpolate(fac, against.min);
+ r.max = max.interpolate(fac, against.max);
+ r.bias = bias;
+ return r;
+ }
+
+ T pickWithin() const
+ {
+ typename T::pickFactors values;
+ auto p = numericAbsolute(bias) + 1;
+ for (size_t i = 0; i < sizeof(values) / sizeof(values[0]); ++i) {
+ if (bias < 0)
+ values[i] = 1.0f - pow(myrand_float(), p);
+ else
+ values[i] = pow(myrand_float(), p);
+ }
+ return T::pick(values, min, max);
+ }
+
+ };
+
+ template <typename T>
+ inline std::string dump(const RangedParameter<T>& r)
+ {
+ std::ostringstream s;
+ s << "range<" << dump(r.min) << " ~ " << dump(r.max);
+ if (r.bias != 0)
+ s << " :: " << r.bias;
+ s << ">";
+ return s.str();
+ }
+
+ enum class TweenStyle : u8 { fwd, rev, pulse, flicker };
+
+ template <typename T>
+ struct TweenedParameter
+ {
+ using ValType = T;
+ using This = TweenedParameter<T>;
+
+ TweenStyle style = TweenStyle::fwd;
+ u16 reps = 1;
+ f32 beginning = 0.0f;
+
+ T start, end;
+
+ TweenedParameter() = default;
+ TweenedParameter(const This& a) = default;
+ TweenedParameter(T _start, T _end) : start(_start), end(_end) {}
+ template <typename M> TweenedParameter(M b) : start(b), end(b) {}
+
+ T blend(float fac) const
+ {
+ // warp time coordinates in accordance w/ settings
+ if (fac > beginning) {
+ // remap for beginning offset
+ auto len = 1 - beginning;
+ fac -= beginning;
+ fac /= len;
+
+ // remap for repetitions
+ fac *= reps;
+ if (fac > 1) // poor man's modulo
+ fac -= (decltype(reps))fac;
+
+ // remap for style
+ switch (style) {
+ case TweenStyle::fwd: /* do nothing */ break;
+ case TweenStyle::rev: fac = 1.0f - fac; break;
+ case TweenStyle::pulse:
+ case TweenStyle::flicker: {
+ if (fac > 0.5f) {
+ fac = 1.f - (fac*2.f - 1.f);
+ } else {
+ fac = fac * 2;
+ }
+ if (style == TweenStyle::flicker) {
+ fac *= myrand_range(0.7f, 1.0f);
+ }
+ }
+ }
+ if (fac>1.f)
+ fac = 1.f;
+ else if (fac<0.f)
+ fac = 0.f;
+ } else {
+ fac = (style == TweenStyle::rev) ? 1.f : 0.f;
+ }
+
+ return start.interpolate(fac, end);
+ }
+
+ void serialize(std::ostream &os) const
+ {
+ writeU8(os, static_cast<u8>(style));
+ writeU16(os, reps);
+ writeF32(os, beginning);
+ start.serialize(os);
+ end.serialize(os);
+ }
+ void deSerialize(std::istream &is)
+ {
+ style = static_cast<TweenStyle>(readU8(is));
+ reps = readU16(is);
+ beginning = readF32(is);
+ start.deSerialize(is);
+ end.deSerialize(is);
+ }
+ };
+
+ template <typename T>
+ inline std::string dump(const TweenedParameter<T>& t)
+ {
+ std::ostringstream s;
+ const char* icon;
+ switch (t.style) {
+ case TweenStyle::fwd: icon = "→"; break;
+ case TweenStyle::rev: icon = "←"; break;
+ case TweenStyle::pulse: icon = "↔"; break;
+ case TweenStyle::flicker: icon = "↯"; break;
+ }
+ s << "tween<";
+ if (t.reps != 1)
+ s << t.reps << "x ";
+ s << dump(t.start) << " "<<icon<<" " << dump(t.end) << ">";
+ return s.str();
+ }
+
+ enum class AttractorKind : u8 { none, point, line, plane };
+ enum class BlendMode : u8 { alpha, add, sub, screen };
+
+ // these are consistently-named convenience aliases to make code more readable without `using ParticleParamTypes` declarations
+ using v3fRange = RangedParameter<v3fParameter>;
+ using f32Range = RangedParameter<f32Parameter>;
+
+ using v2fTween = TweenedParameter<v2fParameter>;
+ using v3fTween = TweenedParameter<v3fParameter>;
+ using f32Tween = TweenedParameter<f32Parameter>;
+ using v3fRangeTween = TweenedParameter<v3fRange>;
+ using f32RangeTween = TweenedParameter<f32Range>;
+
+ #undef DECL_PARAM_SRZRS
+ #undef DECL_PARAM_OVERLOADS
+}
+
+struct ParticleTexture
+{
+ bool animated = false;
+ ParticleParamTypes::BlendMode blendmode = ParticleParamTypes::BlendMode::alpha;
+ TileAnimationParams animation;
+ ParticleParamTypes::f32Tween alpha{1.0f};
+ ParticleParamTypes::v2fTween scale{v2f(1.0f)};
+};
+
+struct ServerParticleTexture : public ParticleTexture
+{
+ std::string string;
+ void serialize(std::ostream &os, u16 protocol_ver, bool newPropertiesOnly = false) const;
+ void deSerialize(std::istream &is, u16 protocol_ver, bool newPropertiesOnly = false);
+};
+
+struct CommonParticleParams
+{
bool collisiondetection = false;
bool collision_removal = false;
bool object_collision = false;
bool vertical = false;
- std::string texture;
+ ServerParticleTexture texture;
struct TileAnimationParams animation;
u8 glow = 0;
MapNode node;
@@ -58,22 +391,42 @@ struct CommonParticleParams {
}
};
-struct ParticleParameters : CommonParticleParams {
- v3f pos;
- v3f vel;
- v3f acc;
- f32 expirationtime = 1;
- f32 size = 1;
+struct ParticleParameters : CommonParticleParams
+{
+ v3f pos, vel, acc, drag;
+ f32 size = 1, expirationtime = 1;
+ ParticleParamTypes::f32Range bounce;
+ ParticleParamTypes::v3fRange jitter;
void serialize(std::ostream &os, u16 protocol_ver) const;
void deSerialize(std::istream &is, u16 protocol_ver);
};
-struct ParticleSpawnerParameters : CommonParticleParams {
+struct ParticleSpawnerParameters : CommonParticleParams
+{
u16 amount = 1;
- v3f minpos, maxpos, minvel, maxvel, minacc, maxacc;
f32 time = 1;
- f32 minexptime = 1, maxexptime = 1, minsize = 1, maxsize = 1;
+
+ std::vector<ServerParticleTexture> texpool;
+
+ ParticleParamTypes::v3fRangeTween
+ pos, vel, acc, drag, radius, jitter;
+
+ ParticleParamTypes::AttractorKind
+ attractor_kind;
+ ParticleParamTypes::v3fTween
+ attractor_origin, attractor_direction;
+ // object IDs
+ u16 attractor_attachment = 0,
+ attractor_direction_attachment = 0;
+ // do particles disappear when they cross the attractor threshold?
+ bool attractor_kill = true;
+
+ ParticleParamTypes::f32RangeTween
+ exptime{1.0f},
+ size {1.0f},
+ attract{0.0f},
+ bounce {0.0f};
// For historical reasons no (de-)serialization methods here
};
diff --git a/src/script/common/c_content.cpp b/src/script/common/c_content.cpp
index 5595f3b14..10670a60a 100644
--- a/src/script/common/c_content.cpp
+++ b/src/script/common/c_content.cpp
@@ -42,7 +42,7 @@ struct EnumString es_TileAnimationType[] =
{TAT_NONE, "none"},
{TAT_VERTICAL_FRAMES, "vertical_frames"},
{TAT_SHEET_2D, "sheet_2d"},
- {0, NULL},
+ {0, nullptr},
};
/******************************************************************************/
diff --git a/src/script/common/c_converter.h b/src/script/common/c_converter.h
index a14eb9186..5fea3c21f 100644
--- a/src/script/common/c_converter.h
+++ b/src/script/common/c_converter.h
@@ -100,6 +100,7 @@ void setboolfield(lua_State *L, int table,
const char *fieldname, bool value);
v3f checkFloatPos (lua_State *L, int index);
+v2f check_v2f (lua_State *L, int index);
v3f check_v3f (lua_State *L, int index);
v3s16 check_v3s16 (lua_State *L, int index);
diff --git a/src/script/lua_api/l_particleparams.h b/src/script/lua_api/l_particleparams.h
new file mode 100644
index 000000000..4fefc5e3a
--- /dev/null
+++ b/src/script/lua_api/l_particleparams.h
@@ -0,0 +1,279 @@
+/*
+Minetest
+Copyright (C) 2021 velartrill, Lexi Hale <lexi@hale.su>
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License along
+with this program; if not, write to the Free Software Foundation, Inc.,
+51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+*/
+
+#pragma once
+#include "lua_api/l_particles.h"
+#include "lua_api/l_object.h"
+#include "lua_api/l_internal.h"
+#include "common/c_converter.h"
+#include "common/c_content.h"
+#include "server.h"
+#include "particles.h"
+
+namespace LuaParticleParams
+{
+ using namespace ParticleParamTypes;
+
+ template<typename T>
+ inline void readNumericLuaValue(lua_State* L, T& ret)
+ {
+ if (lua_isnil(L,-1))
+ return;
+
+ if (std::is_integral<T>())
+ ret = lua_tointeger(L, -1);
+ else
+ ret = lua_tonumber(L, -1);
+ }
+
+ template <typename T, size_t N>
+ inline void readNumericLuaValue(lua_State* L, Parameter<T,N>& ret)
+ {
+ readNumericLuaValue<T>(L, ret.val);
+ }
+
+ // these are unfortunately necessary as C++ intentionally disallows function template
+ // specialization and there's no way to make template overloads reliably resolve correctly
+ inline void readLuaValue(lua_State* L, f32Parameter& ret) { readNumericLuaValue(L, ret); }
+ inline void readLuaValue(lua_State* L, f32& ret) { readNumericLuaValue(L, ret); }
+ inline void readLuaValue(lua_State* L, u16& ret) { readNumericLuaValue(L, ret); }
+ inline void readLuaValue(lua_State* L, u8& ret) { readNumericLuaValue(L, ret); }
+
+ inline void readLuaValue(lua_State* L, v3fParameter& ret)
+ {
+ if (lua_isnil(L, -1))
+ return;
+
+ if (lua_isnumber(L, -1)) { // shortcut for uniform vectors
+ auto n = lua_tonumber(L, -1);
+ ret = v3fParameter(n,n,n);
+ } else {
+ ret = (v3fParameter)check_v3f(L, -1);
+ }
+ }
+
+ inline void readLuaValue(lua_State* L, v2fParameter& ret)
+ {
+ if (lua_isnil(L, -1))
+ return;
+
+ if (lua_isnumber(L, -1)) { // shortcut for uniform vectors
+ auto n = lua_tonumber(L, -1);
+ ret = v2fParameter(n,n);
+ } else {
+ ret = (v2fParameter)check_v2f(L, -1);
+ }
+ }
+
+ inline void readLuaValue(lua_State* L, TweenStyle& ret)
+ {
+ if (lua_isnil(L, -1))
+ return;
+
+ static const EnumString opts[] = {
+ {(int)TweenStyle::fwd, "fwd"},
+ {(int)TweenStyle::rev, "rev"},
+ {(int)TweenStyle::pulse, "pulse"},
+ {(int)TweenStyle::flicker, "flicker"},
+ {0, nullptr},
+ };
+
+ luaL_checktype(L, -1, LUA_TSTRING);
+ int v = (int)TweenStyle::fwd;
+ if (!string_to_enum(opts, v, lua_tostring(L, -1))) {
+ throw LuaError("tween style must be one of ('fwd', 'rev', 'pulse', 'flicker')");
+ }
+ ret = (TweenStyle)v;
+ }
+
+ inline void readLuaValue(lua_State* L, AttractorKind& ret)
+ {
+ if (lua_isnil(L, -1))
+ return;
+
+ static const EnumString opts[] = {
+ {(int)AttractorKind::none, "none"},
+ {(int)AttractorKind::point, "point"},
+ {(int)AttractorKind::line, "line"},
+ {(int)AttractorKind::plane, "plane"},
+ {0, nullptr},
+ };
+
+ luaL_checktype(L, -1, LUA_TSTRING);
+ int v = (int)AttractorKind::none;
+ if (!string_to_enum(opts, v, lua_tostring(L, -1))) {
+ throw LuaError("attractor kind must be one of ('none', 'point', 'line', 'plane')");
+ }
+ ret = (AttractorKind)v;
+ }
+
+ inline void readLuaValue(lua_State* L, BlendMode& ret)
+ {
+ if (lua_isnil(L, -1))
+ return;
+
+ static const EnumString opts[] = {
+ {(int)BlendMode::alpha, "alpha"},
+ {(int)BlendMode::add, "add"},
+ {(int)BlendMode::sub, "sub"},
+ {(int)BlendMode::screen, "screen"},
+ {0, nullptr},
+ };
+
+ luaL_checktype(L, -1, LUA_TSTRING);
+ int v = (int)BlendMode::alpha;
+ if (!string_to_enum(opts, v, lua_tostring(L, -1))) {
+ throw LuaError("blend mode must be one of ('alpha', 'add', 'sub', 'screen')");
+ }
+ ret = (BlendMode)v;
+ }
+
+ template <typename T> void
+ readLuaValue(lua_State* L, RangedParameter<T>& field)
+ {
+ if (lua_isnil(L,-1))
+ return;
+ if (!lua_istable(L,-1)) // is this is just a literal value?
+ goto set_uniform;
+
+ lua_getfield(L, -1, "min");
+ // handle convenience syntax for non-range values
+ if (lua_isnil(L,-1)) {
+ lua_pop(L, 1);
+ goto set_uniform;
+ }
+ readLuaValue(L,field.min);
+ lua_pop(L, 1);
+
+ lua_getfield(L, -1, "max");
+ readLuaValue(L,field.max);
+ lua_pop(L, 1);
+
+ lua_getfield(L, -1, "bias");
+ if (!lua_isnil(L,-1))
+ readLuaValue(L,field.bias);
+ lua_pop(L, 1);
+ return;
+
+ set_uniform:
+ readLuaValue(L, field.min);
+ readLuaValue(L, field.max);
+ }
+
+ template <typename T> void
+ readLegacyValue(lua_State* L, const char* name, T& field) {}
+
+ template <typename T> void
+ readLegacyValue(lua_State* L, const char* name, RangedParameter<T>& field)
+ {
+ int tbl = lua_gettop(L);
+ lua_pushliteral(L, "min");
+ lua_pushstring(L, name);
+ lua_concat(L, 2);
+ lua_gettable(L, tbl);
+ if (!lua_isnil(L, -1)) {
+ readLuaValue(L, field.min);
+ }
+ lua_settop(L, tbl);
+
+ lua_pushliteral(L, "max");
+ lua_pushstring(L, name);
+ lua_concat(L, 2);
+ lua_gettable(L, tbl);
+ if (!lua_isnil(L, -1)) {
+ readLuaValue(L, field.max);
+ }
+ lua_settop(L, tbl);
+ }
+
+ template <typename T> void
+ readTweenTable(lua_State* L, const char* name, TweenedParameter<T>& field)
+ {
+ int tbl = lua_gettop(L);
+
+ lua_pushstring(L, name);
+ lua_pushliteral(L, "_tween");
+ lua_concat(L, 2);
+ lua_gettable(L, tbl);
+ if(lua_istable(L, -1)) {
+ int tween = lua_gettop(L);
+ // get the starting value
+ lua_pushinteger(L, 1), lua_gettable(L, tween);
+ readLuaValue(L, field.start);
+ lua_pop(L, 1);
+
+ // get the final value -- use len instead of 2 so that this
+ // gracefully degrades if keyframe support is later added
+ lua_pushinteger(L, (lua_Integer)lua_objlen(L, -1)), lua_gettable(L, tween);
+ readLuaValue(L, field.end);
+ lua_pop(L, 1);
+
+ // get the effect settings
+ lua_getfield(L, -1, "style");
+ lua_isnil(L,-1) || (readLuaValue(L, field.style), true);
+ lua_pop(L, 1);
+
+ lua_getfield(L, -1, "reps");
+ lua_isnil(L,-1) || (readLuaValue(L, field.reps), true);
+ lua_pop(L, 1);
+
+ lua_getfield(L, -1, "start");
+ lua_isnil(L,-1) || (readLuaValue(L, field.beginning), true);
+ lua_pop(L, 1);
+
+ goto done;
+ } else {
+ lua_pop(L,1);
+ }
+ // the table is not present; check for nonanimated values
+
+ lua_getfield(L, tbl, name);
+ if(!lua_isnil(L, -1)) {
+ readLuaValue(L, field.start);
+ lua_settop(L, tbl);
+ goto set_uniform;
+ } else {
+ lua_pop(L,1);
+ }
+
+ // the goto did not trigger, so this table is not present either
+ // check for pre-5.6.0 legacy values
+ readLegacyValue(L, name, field.start);
+
+ set_uniform:
+ field.end = field.start;
+ done:
+ lua_settop(L, tbl); // clean up after ourselves
+ }
+
+ inline u16 readAttachmentID(lua_State* L, const char* name)
+ {
+ u16 id = 0;
+ lua_getfield(L, -1, name);
+ if (!lua_isnil(L, -1)) {
+ ObjectRef *ref = ObjectRef::checkobject(L, -1);
+ if (auto obj = ObjectRef::getobject(ref))
+ id = obj->getId();
+ }
+ lua_pop(L, 1);
+ return id;
+ }
+
+ void readTexValue(lua_State* L, ServerParticleTexture& tex);
+}
diff --git a/src/script/lua_api/l_particles.cpp b/src/script/lua_api/l_particles.cpp
index a51c4fe20..586c7dc73 100644
--- a/src/script/lua_api/l_particles.cpp
+++ b/src/script/lua_api/l_particles.cpp
@@ -20,30 +20,50 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "lua_api/l_particles.h"
#include "lua_api/l_object.h"
#include "lua_api/l_internal.h"
+#include "lua_api/l_particleparams.h"
#include "common/c_converter.h"
#include "common/c_content.h"
#include "server.h"
#include "particles.h"
-// add_particle({pos=, velocity=, acceleration=, expirationtime=,
-// size=, collisiondetection=, collision_removal=, object_collision=,
-// vertical=, texture=, player=})
-// pos/velocity/acceleration = {x=num, y=num, z=num}
-// expirationtime = num (seconds)
-// size = num
-// collisiondetection = bool
-// collision_removal = bool
-// object_collision = bool
-// vertical = bool
-// texture = e.g."default_wood.png"
-// animation = TileAnimation definition
-// glow = num
+void LuaParticleParams::readTexValue(lua_State* L, ServerParticleTexture& tex)
+{
+ StackUnroller unroll(L);
+
+ tex.animated = false;
+ if (lua_isstring(L, -1)) {
+ tex.string = lua_tostring(L, -1);
+ return;
+ }
+
+ luaL_checktype(L, -1, LUA_TTABLE);
+ lua_getfield(L, -1, "name");
+ tex.string = luaL_checkstring(L, -1);
+ lua_pop(L, 1);
+
+ lua_getfield(L, -1, "animation");
+ if (! lua_isnil(L, -1)) {
+ tex.animated = true;
+ tex.animation = read_animation_definition(L, -1);
+ }
+ lua_pop(L, 1);
+
+ lua_getfield(L, -1, "blend");
+ LuaParticleParams::readLuaValue(L, tex.blendmode);
+ lua_pop(L, 1);
+
+ LuaParticleParams::readTweenTable(L, "alpha", tex.alpha);
+ LuaParticleParams::readTweenTable(L, "scale", tex.scale);
+
+}
+
+// add_particle({...})
int ModApiParticles::l_add_particle(lua_State *L)
{
NO_MAP_LOCK_REQUIRED;
// Get parameters
- struct ParticleParameters p;
+ ParticleParameters p;
std::string playername;
if (lua_gettop(L) > 1) // deprecated
@@ -56,7 +76,7 @@ int ModApiParticles::l_add_particle(lua_State *L)
p.expirationtime = luaL_checknumber(L, 4);
p.size = luaL_checknumber(L, 5);
p.collisiondetection = readParam<bool>(L, 6);
- p.texture = luaL_checkstring(L, 7);
+ p.texture.string = luaL_checkstring(L, 7);
if (lua_gettop(L) == 8) // only spawn for a single player
playername = luaL_checkstring(L, 8);
}
@@ -108,7 +128,12 @@ int ModApiParticles::l_add_particle(lua_State *L)
p.animation = read_animation_definition(L, -1);
lua_pop(L, 1);
- p.texture = getstringfield_default(L, 1, "texture", p.texture);
+ lua_getfield(L, 1, "texture");
+ if (!lua_isnil(L, -1)) {
+ LuaParticleParams::readTexValue(L, p.texture);
+ }
+ lua_pop(L, 1);
+
p.glow = getintfield_default(L, 1, "glow", p.glow);
lua_getfield(L, 1, "node");
@@ -119,34 +144,26 @@ int ModApiParticles::l_add_particle(lua_State *L)
p.node_tile = getintfield_default(L, 1, "node_tile", p.node_tile);
playername = getstringfield_default(L, 1, "playername", "");
+
+ lua_getfield(L, 1, "drag");
+ if (lua_istable(L, -1))
+ p.drag = check_v3f(L, -1);
+ lua_pop(L, 1);
+
+ lua_getfield(L, 1, "jitter");
+ LuaParticleParams::readLuaValue(L, p.jitter);
+ lua_pop(L, 1);
+
+ lua_getfield(L, 1, "bounce");
+ LuaParticleParams::readLuaValue(L, p.bounce);
+ lua_pop(L, 1);
}
getServer(L)->spawnParticle(playername, p);
return 1;
}
-// add_particlespawner({amount=, time=,
-// minpos=, maxpos=,
-// minvel=, maxvel=,
-// minacc=, maxacc=,
-// minexptime=, maxexptime=,
-// minsize=, maxsize=,
-// collisiondetection=,
-// collision_removal=,
-// object_collision=,
-// vertical=,
-// texture=,
-// player=})
-// minpos/maxpos/minvel/maxvel/minacc/maxacc = {x=num, y=num, z=num}
-// minexptime/maxexptime = num (seconds)
-// minsize/maxsize = num
-// collisiondetection = bool
-// collision_removal = bool
-// object_collision = bool
-// vertical = bool
-// texture = e.g."default_wood.png"
-// animation = TileAnimation definition
-// glow = num
+// add_particlespawner({...})
int ModApiParticles::l_add_particlespawner(lua_State *L)
{
NO_MAP_LOCK_REQUIRED;
@@ -156,24 +173,31 @@ int ModApiParticles::l_add_particlespawner(lua_State *L)
ServerActiveObject *attached = NULL;
std::string playername;
+ using namespace ParticleParamTypes;
if (lua_gettop(L) > 1) //deprecated
{
log_deprecated(L, "Deprecated add_particlespawner call with "
"individual parameters instead of definition");
p.amount = luaL_checknumber(L, 1);
p.time = luaL_checknumber(L, 2);
- p.minpos = check_v3f(L, 3);
- p.maxpos = check_v3f(L, 4);
- p.minvel = check_v3f(L, 5);
- p.maxvel = check_v3f(L, 6);
- p.minacc = check_v3f(L, 7);
- p.maxacc = check_v3f(L, 8);
- p.minexptime = luaL_checknumber(L, 9);
- p.maxexptime = luaL_checknumber(L, 10);
- p.minsize = luaL_checknumber(L, 11);
- p.maxsize = luaL_checknumber(L, 12);
+ auto minpos = check_v3f(L, 3);
+ auto maxpos = check_v3f(L, 4);
+ auto minvel = check_v3f(L, 5);
+ auto maxvel = check_v3f(L, 6);
+ auto minacc = check_v3f(L, 7);
+ auto maxacc = check_v3f(L, 8);
+ auto minexptime = luaL_checknumber(L, 9);
+ auto maxexptime = luaL_checknumber(L, 10);
+ auto minsize = luaL_checknumber(L, 11);
+ auto maxsize = luaL_checknumber(L, 12);
+ p.pos = v3fRange(minpos, maxpos);
+ p.vel = v3fRange(minvel, maxvel);
+ p.acc = v3fRange(minacc, maxacc);
+ p.exptime = f32Range(minexptime, maxexptime);
+ p.size = f32Range(minsize, maxsize);
+
p.collisiondetection = readParam<bool>(L, 13);
- p.texture = luaL_checkstring(L, 14);
+ p.texture.string = luaL_checkstring(L, 14);
if (lua_gettop(L) == 15) // only spawn for a single player
playername = luaL_checkstring(L, 15);
}
@@ -182,40 +206,46 @@ int ModApiParticles::l_add_particlespawner(lua_State *L)
p.amount = getintfield_default(L, 1, "amount", p.amount);
p.time = getfloatfield_default(L, 1, "time", p.time);
- lua_getfield(L, 1, "minpos");
- if (lua_istable(L, -1))
- p.minpos = check_v3f(L, -1);
- lua_pop(L, 1);
-
- lua_getfield(L, 1, "maxpos");
- if (lua_istable(L, -1))
- p.maxpos = check_v3f(L, -1);
- lua_pop(L, 1);
-
- lua_getfield(L, 1, "minvel");
- if (lua_istable(L, -1))
- p.minvel = check_v3f(L, -1);
- lua_pop(L, 1);
-
- lua_getfield(L, 1, "maxvel");
- if (lua_istable(L, -1))
- p.maxvel = check_v3f(L, -1);
- lua_pop(L, 1);
-
- lua_getfield(L, 1, "minacc");
- if (lua_istable(L, -1))
- p.minacc = check_v3f(L, -1);
- lua_pop(L, 1);
-
- lua_getfield(L, 1, "maxacc");
- if (lua_istable(L, -1))
- p.maxacc = check_v3f(L, -1);
- lua_pop(L, 1);
+ // set default values
+ p.exptime = 1;
+ p.size = 1;
+
+ // read spawner parameters from the table
+ LuaParticleParams::readTweenTable(L, "pos", p.pos);
+ LuaParticleParams::readTweenTable(L, "vel", p.vel);
+ LuaParticleParams::readTweenTable(L, "acc", p.acc);
+ LuaParticleParams::readTweenTable(L, "size", p.size);
+ LuaParticleParams::readTweenTable(L, "exptime", p.exptime);
+ LuaParticleParams::readTweenTable(L, "drag", p.drag);
+ LuaParticleParams::readTweenTable(L, "jitter", p.jitter);
+ LuaParticleParams::readTweenTable(L, "bounce", p.bounce);
+ lua_getfield(L, 1, "attract");
+ if (!lua_isnil(L, -1)) {
+ luaL_checktype(L, -1, LUA_TTABLE);
+ lua_getfield(L, -1, "kind");
+ LuaParticleParams::readLuaValue(L, p.attractor_kind);
+ lua_pop(L,1);
+
+ lua_getfield(L, -1, "die_on_contact");
+ if (!lua_isnil(L, -1))
+ p.attractor_kill = readParam<bool>(L, -1);
+ lua_pop(L,1);
+
+ if (p.attractor_kind != AttractorKind::none) {
+ LuaParticleParams::readTweenTable(L, "strength", p.attract);
+ LuaParticleParams::readTweenTable(L, "origin", p.attractor_origin);
+ p.attractor_attachment = LuaParticleParams::readAttachmentID(L, "origin_attached");
+ if (p.attractor_kind != AttractorKind::point) {
+ LuaParticleParams::readTweenTable(L, "direction", p.attractor_direction);
+ p.attractor_direction_attachment = LuaParticleParams::readAttachmentID(L, "direction_attached");
+ }
+ }
+ } else {
+ p.attractor_kind = AttractorKind::none;
+ }
+ lua_pop(L,1);
+ LuaParticleParams::readTweenTable(L, "radius", p.radius);
- p.minexptime = getfloatfield_default(L, 1, "minexptime", p.minexptime);
- p.maxexptime = getfloatfield_default(L, 1, "maxexptime", p.maxexptime);
- p.minsize = getfloatfield_default(L, 1, "minsize", p.minsize);
- p.maxsize = getfloatfield_default(L, 1, "maxsize", p.maxsize);
p.collisiondetection = getboolfield_default(L, 1,
"collisiondetection", p.collisiondetection);
p.collision_removal = getboolfield_default(L, 1,
@@ -234,11 +264,29 @@ int ModApiParticles::l_add_particlespawner(lua_State *L)
attached = ObjectRef::getobject(ref);
}
+ lua_getfield(L, 1, "texture");
+ if (!lua_isnil(L, -1)) {
+ LuaParticleParams::readTexValue(L, p.texture);
+ }
+ lua_pop(L, 1);
+
p.vertical = getboolfield_default(L, 1, "vertical", p.vertical);
- p.texture = getstringfield_default(L, 1, "texture", p.texture);
playername = getstringfield_default(L, 1, "playername", "");
p.glow = getintfield_default(L, 1, "glow", p.glow);
+ lua_getfield(L, 1, "texpool");
+ if (lua_istable(L, -1)) {
+ size_t tl = lua_objlen(L, -1);
+ p.texpool.reserve(tl);
+ for (size_t i = 0; i < tl; ++i) {
+ lua_pushinteger(L, i+1), lua_gettable(L, -2);
+ p.texpool.emplace_back();
+ LuaParticleParams::readTexValue(L, p.texpool.back());
+ lua_pop(L,1);
+ }
+ }
+ lua_pop(L, 1);
+
lua_getfield(L, 1, "node");
if (lua_istable(L, -1))
p.node = readnode(L, -1, getGameDef(L)->ndef());
diff --git a/src/script/lua_api/l_particles_local.cpp b/src/script/lua_api/l_particles_local.cpp
index cc68b13a5..62cbab8e9 100644
--- a/src/script/lua_api/l_particles_local.cpp
+++ b/src/script/lua_api/l_particles_local.cpp
@@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "common/c_converter.h"
#include "lua_api/l_internal.h"
#include "lua_api/l_object.h"
+#include "lua_api/l_particleparams.h"
#include "client/particles.h"
#include "client/client.h"
#include "client/clientevent.h"
@@ -49,6 +50,19 @@ int ModApiParticlesLocal::l_add_particle(lua_State *L)
p.acc = check_v3f(L, -1);
lua_pop(L, 1);
+ lua_getfield(L, 1, "drag");
+ if (lua_istable(L, -1))
+ p.drag = check_v3f(L, -1);
+ lua_pop(L, 1);
+
+ lua_getfield(L, 1, "jitter");
+ LuaParticleParams::readLuaValue(L, p.jitter);
+ lua_pop(L, 1);
+
+ lua_getfield(L, 1, "bounce");
+ LuaParticleParams::readLuaValue(L, p.bounce);
+ lua_pop(L, 1);
+
p.expirationtime = getfloatfield_default(L, 1, "expirationtime",
p.expirationtime);
p.size = getfloatfield_default(L, 1, "size", p.size);
@@ -64,7 +78,11 @@ int ModApiParticlesLocal::l_add_particle(lua_State *L)
p.animation = read_animation_definition(L, -1);
lua_pop(L, 1);
- p.texture = getstringfield_default(L, 1, "texture", p.texture);
+ lua_getfield(L, 1, "texture");
+ if (!lua_isnil(L, -1)) {
+ LuaParticleParams::readTexValue(L,p.texture);
+ }
+ lua_pop(L, 1);
p.glow = getintfield_default(L, 1, "glow", p.glow);
lua_getfield(L, 1, "node");
@@ -88,44 +106,50 @@ int ModApiParticlesLocal::l_add_particlespawner(lua_State *L)
// Get parameters
ParticleSpawnerParameters p;
-
p.amount = getintfield_default(L, 1, "amount", p.amount);
p.time = getfloatfield_default(L, 1, "time", p.time);
- lua_getfield(L, 1, "minpos");
- if (lua_istable(L, -1))
- p.minpos = check_v3f(L, -1);
- lua_pop(L, 1);
-
- lua_getfield(L, 1, "maxpos");
- if (lua_istable(L, -1))
- p.maxpos = check_v3f(L, -1);
- lua_pop(L, 1);
-
- lua_getfield(L, 1, "minvel");
- if (lua_istable(L, -1))
- p.minvel = check_v3f(L, -1);
- lua_pop(L, 1);
-
- lua_getfield(L, 1, "maxvel");
- if (lua_istable(L, -1))
- p.maxvel = check_v3f(L, -1);
- lua_pop(L, 1);
-
- lua_getfield(L, 1, "minacc");
- if (lua_istable(L, -1))
- p.minacc = check_v3f(L, -1);
- lua_pop(L, 1);
+ // set default values
+ p.exptime = 1;
+ p.size = 1;
+
+ // read spawner parameters from the table
+ using namespace ParticleParamTypes;
+ LuaParticleParams::readTweenTable(L, "pos", p.pos);
+ LuaParticleParams::readTweenTable(L, "vel", p.vel);
+ LuaParticleParams::readTweenTable(L, "acc", p.acc);
+ LuaParticleParams::readTweenTable(L, "size", p.size);
+ LuaParticleParams::readTweenTable(L, "exptime", p.exptime);
+ LuaParticleParams::readTweenTable(L, "drag", p.drag);
+ LuaParticleParams::readTweenTable(L, "jitter", p.jitter);
+ LuaParticleParams::readTweenTable(L, "bounce", p.bounce);
+ lua_getfield(L, 1, "attract");
+ if (!lua_isnil(L, -1)) {
+ luaL_checktype(L, -1, LUA_TTABLE);
+ lua_getfield(L, -1, "kind");
+ LuaParticleParams::readLuaValue(L, p.attractor_kind);
+ lua_pop(L,1);
+
+ lua_getfield(L, -1, "die_on_contact");
+ if (!lua_isnil(L, -1))
+ p.attractor_kill = readParam<bool>(L, -1);
+ lua_pop(L,1);
+
+ if (p.attractor_kind != AttractorKind::none) {
+ LuaParticleParams::readTweenTable(L, "strength", p.attract);
+ LuaParticleParams::readTweenTable(L, "origin", p.attractor_origin);
+ p.attractor_attachment = LuaParticleParams::readAttachmentID(L, "origin_attached");
+ if (p.attractor_kind != AttractorKind::point) {
+ LuaParticleParams::readTweenTable(L, "direction", p.attractor_direction);
+ p.attractor_direction_attachment = LuaParticleParams::readAttachmentID(L, "direction_attached");
+ }
+ }
+ } else {
+ p.attractor_kind = AttractorKind::none;
+ }
+ lua_pop(L,1);
+ LuaParticleParams::readTweenTable(L, "radius", p.radius);
- lua_getfield(L, 1, "maxacc");
- if (lua_istable(L, -1))
- p.maxacc = check_v3f(L, -1);
- lua_pop(L, 1);
-
- p.minexptime = getfloatfield_default(L, 1, "minexptime", p.minexptime);
- p.maxexptime = getfloatfield_default(L, 1, "maxexptime", p.maxexptime);
- p.minsize = getfloatfield_default(L, 1, "minsize", p.minsize);
- p.maxsize = getfloatfield_default(L, 1, "maxsize", p.maxsize);
p.collisiondetection = getboolfield_default(L, 1,
"collisiondetection", p.collisiondetection);
p.collision_removal = getboolfield_default(L, 1,
@@ -137,10 +161,28 @@ int ModApiParticlesLocal::l_add_particlespawner(lua_State *L)
p.animation = read_animation_definition(L, -1);
lua_pop(L, 1);
+ lua_getfield(L, 1, "texture");
+ if (!lua_isnil(L, -1)) {
+ LuaParticleParams::readTexValue(L, p.texture);
+ }
+ lua_pop(L, 1);
+
p.vertical = getboolfield_default(L, 1, "vertical", p.vertical);
- p.texture = getstringfield_default(L, 1, "texture", p.texture);
p.glow = getintfield_default(L, 1, "glow", p.glow);
+ lua_getfield(L, 1, "texpool");
+ if (lua_istable(L, -1)) {
+ size_t tl = lua_objlen(L, -1);
+ p.texpool.reserve(tl);
+ for (size_t i = 0; i < tl; ++i) {
+ lua_pushinteger(L, i+1), lua_gettable(L, -2);
+ p.texpool.emplace_back();
+ LuaParticleParams::readTexValue(L, p.texpool.back());
+ lua_pop(L,1);
+ }
+ }
+ lua_pop(L, 1);
+
lua_getfield(L, 1, "node");
if (lua_istable(L, -1))
p.node = readnode(L, -1, getGameDef(L)->ndef());
diff --git a/src/server.cpp b/src/server.cpp
index 4eee14a30..6ca45f2d3 100644
--- a/src/server.cpp
+++ b/src/server.cpp
@@ -160,6 +160,7 @@ v3f ServerPlayingSound::getPos(ServerEnvironment *env, bool *pos_exists) const
return sao->getBasePosition();
}
}
+
return v3f(0,0,0);
}
@@ -1599,7 +1600,12 @@ void Server::SendAddParticleSpawner(session_t peer_id, u16 protocol_version,
if (peer_id == PEER_ID_INEXISTENT) {
std::vector<session_t> clients = m_clients.getClientIDs();
- const v3f pos = (p.minpos + p.maxpos) / 2.0f * BS;
+ const v3f pos = (
+ p.pos.start.min.val +
+ p.pos.start.max.val +
+ p.pos.end.min.val +
+ p.pos.end.max.val
+ ) / 4.0f * BS;
const float radius_sq = radius * radius;
/* Don't send short-lived spawners to distant players.
* This could be replaced with proper tracking at some point. */
@@ -1627,11 +1633,19 @@ void Server::SendAddParticleSpawner(session_t peer_id, u16 protocol_version,
NetworkPacket pkt(TOCLIENT_ADD_PARTICLESPAWNER, 100, peer_id);
- pkt << p.amount << p.time << p.minpos << p.maxpos << p.minvel
- << p.maxvel << p.minacc << p.maxacc << p.minexptime << p.maxexptime
- << p.minsize << p.maxsize << p.collisiondetection;
+ pkt << p.amount << p.time;
+ { // serialize legacy fields
+ std::ostringstream os(std::ios_base::binary);
+ p.pos.start.legacySerialize(os);
+ p.vel.start.legacySerialize(os);
+ p.acc.start.legacySerialize(os);
+ p.exptime.start.legacySerialize(os);
+ p.size.start.legacySerialize(os);
+ pkt.putRawString(os.str());
+ }
+ pkt << p.collisiondetection;
- pkt.putLongString(p.texture);
+ pkt.putLongString(p.texture.string);
pkt << id << p.vertical << p.collision_removal << attached_id;
{
@@ -1642,6 +1656,51 @@ void Server::SendAddParticleSpawner(session_t peer_id, u16 protocol_version,
pkt << p.glow << p.object_collision;
pkt << p.node.param0 << p.node.param2 << p.node_tile;
+ { // serialize new fields
+ // initial bias for older properties
+ pkt << p.pos.start.bias
+ << p.vel.start.bias
+ << p.acc.start.bias
+ << p.exptime.start.bias
+ << p.size.start.bias;
+
+ std::ostringstream os(std::ios_base::binary);
+
+ // final tween frames of older properties
+ p.pos.end.serialize(os);
+ p.vel.end.serialize(os);
+ p.acc.end.serialize(os);
+ p.exptime.end.serialize(os);
+ p.size.end.serialize(os);
+
+ // properties for legacy texture field
+ p.texture.serialize(os, protocol_version, true);
+
+ // new properties
+ p.drag.serialize(os);
+ p.jitter.serialize(os);
+ p.bounce.serialize(os);
+ ParticleParamTypes::serializeParameterValue(os, p.attractor_kind);
+ if (p.attractor_kind != ParticleParamTypes::AttractorKind::none) {
+ p.attract.serialize(os);
+ p.attractor_origin.serialize(os);
+ writeU16(os, p.attractor_attachment); /* object ID */
+ writeU8(os, p.attractor_kill);
+ if (p.attractor_kind != ParticleParamTypes::AttractorKind::point) {
+ p.attractor_direction.serialize(os);
+ writeU16(os, p.attractor_direction_attachment);
+ }
+ }
+ p.radius.serialize(os);
+
+ ParticleParamTypes::serializeParameterValue(os, (u16)p.texpool.size());
+ for (const auto& tex : p.texpool) {
+ tex.serialize(os, protocol_version);
+ }
+
+ pkt.putRawString(os.str());
+ }
+
Send(&pkt);
}
@@ -3267,7 +3326,7 @@ bool Server::hudSetFlags(RemotePlayer *player, u32 flags, u32 mask)
u32 new_hud_flags = (player->hud_flags & ~mask) | flags;
if (new_hud_flags == player->hud_flags) // no change
return true;
-
+
SendHUDSetFlags(player->getPeerId(), flags, mask);
player->hud_flags = new_hud_flags;
@@ -3692,8 +3751,8 @@ v3f Server::findSpawnPos()
s32 range = MYMIN(1 + i, range_max);
// We're going to try to throw the player to this position
v2s16 nodepos2d = v2s16(
- -range + (myrand() % (range * 2)),
- -range + (myrand() % (range * 2)));
+ -range + myrand_range(0, range*2),
+ -range + myrand_range(0, range*2));
// Get spawn level at point
s16 spawn_level = m_emerge->getSpawnLevelAtPoint(nodepos2d);
// Continue if MAX_MAP_GENERATION_LIMIT was returned by the mapgen to
diff --git a/src/util/numeric.cpp b/src/util/numeric.cpp
index 702ddce95..aa3bb843d 100644
--- a/src/util/numeric.cpp
+++ b/src/util/numeric.cpp
@@ -46,11 +46,22 @@ void myrand_bytes(void *out, size_t len)
g_pcgrand.bytes(out, len);
}
+float myrand_float()
+{
+ u32 uv = g_pcgrand.next();
+ return (float)uv / (float)U32_MAX;
+}
+
int myrand_range(int min, int max)
{
return g_pcgrand.range(min, max);
}
+float myrand_range(float min, float max)
+{
+ return (max-min) * myrand_float() + min;
+}
+
/*
64-bit unaligned version of MurmurHash
diff --git a/src/util/numeric.h b/src/util/numeric.h
index 32a6f4312..265046a63 100644
--- a/src/util/numeric.h
+++ b/src/util/numeric.h
@@ -223,6 +223,8 @@ u32 myrand();
void mysrand(unsigned int seed);
void myrand_bytes(void *out, size_t len);
int myrand_range(int min, int max);
+float myrand_range(float min, float max);
+float myrand_float();
/*
Miscellaneous functions
@@ -446,3 +448,24 @@ inline irr::video::SColor multiplyColorValue(const irr::video::SColor &color, fl
core::clamp<u32>(color.getGreen() * mod, 0, 255),
core::clamp<u32>(color.getBlue() * mod, 0, 255));
}
+
+template <typename T> inline T numericAbsolute(T v) { return v < 0 ? T(-v) : v; }
+template <typename T> inline T numericSign(T v) { return T(v < 0 ? -1 : (v == 0 ? 0 : 1)); }
+
+inline v3f vecAbsolute(v3f v)
+{
+ return v3f(
+ numericAbsolute(v.X),
+ numericAbsolute(v.Y),
+ numericAbsolute(v.Z)
+ );
+}
+
+inline v3f vecSign(v3f v)
+{
+ return v3f(
+ numericSign(v.X),
+ numericSign(v.Y),
+ numericSign(v.Z)
+ );
+}
diff --git a/src/util/pointer.h b/src/util/pointer.h
index 245ac85bf..b659cea0e 100644
--- a/src/util/pointer.h
+++ b/src/util/pointer.h
@@ -45,7 +45,7 @@ public:
Buffer()
{
m_size = 0;
- data = NULL;
+ data = nullptr;
}
Buffer(unsigned int size)
{
@@ -53,7 +53,7 @@ public:
if(size != 0)
data = new T[size];
else
- data = NULL;
+ data = nullptr;
}
// Disable class copy
@@ -82,7 +82,7 @@ public:
memcpy(data, t, size);
}
else
- data = NULL;
+ data = nullptr;
}
~Buffer()
@@ -166,7 +166,7 @@ public:
if(m_size != 0)
data = new T[m_size];
else
- data = NULL;
+ data = nullptr;
refcount = new unsigned int;
memset(data,0,sizeof(T)*m_size);
(*refcount) = 1;
@@ -201,7 +201,7 @@ public:
memcpy(data, t, m_size);
}
else
- data = NULL;
+ data = nullptr;
refcount = new unsigned int;
(*refcount) = 1;
}
@@ -216,7 +216,7 @@ public:
memcpy(data, *buffer, buffer.getSize());
}
else
- data = NULL;
+ data = nullptr;
refcount = new unsigned int;
(*refcount) = 1;
}
@@ -256,3 +256,4 @@ private:
unsigned int m_size;
unsigned int *refcount;
};
+