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>
This commit is contained in:
Lexi Hale 2022-07-13 11:57:12 +02:00 committed by GitHub
parent 8724fe6e3f
commit 20bd6bdb68
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1986 additions and 279 deletions

@ -21,6 +21,7 @@ core.features = {
use_texture_alpha_string_modes = true, use_texture_alpha_string_modes = true,
degrotate_240_steps = true, degrotate_240_steps = true,
abm_min_max_y = true, abm_min_max_y = true,
particlespawner_tweenable = true,
dynamic_add_media_table = true, dynamic_add_media_table = true,
get_sky_as_table = true, get_sky_as_table = true,
} }

@ -4856,6 +4856,11 @@ Utilities
abm_min_max_y = true, abm_min_max_y = true,
-- dynamic_add_media supports passing a table with options (5.5.0) -- dynamic_add_media supports passing a table with options (5.5.0)
dynamic_add_media_table = true, 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) -- allows get_sky to return a table instead of separate values (5.6.0)
get_sky_as_table = true, get_sky_as_table = true,
} }
@ -8984,6 +8989,8 @@ Used by `minetest.add_particle`.
texture = "image.png", texture = "image.png",
-- The texture of the particle -- The texture of the particle
-- v5.6.0 and later: also supports the table format described in the
-- following section
playername = "singleplayer", playername = "singleplayer",
-- Optional, if specified spawns particle only on the player's client -- 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 -- If set to a valid number 1-6, specifies the tile from which the
-- particle texture is picked. -- particle texture is picked.
-- Otherwise, the default behavior is used. (currently: any random tile) -- 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`. 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, amount = 1,
-- Number of particles spawned over the time period `time`. -- 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 -- If time is 0 spawner has infinite lifespan and spawns the `amount` on
-- a per-second basis. -- 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, collisiondetection = false,
-- If true collide with `walkable` nodes and, depending on the -- If true collide with `walkable` nodes and, depending on the
-- `object_collision` field, objects too. -- `object_collision` field, objects too.
@ -9066,8 +9076,11 @@ Used by `minetest.add_particlespawner`.
animation = {Tile Animation definition}, animation = {Tile Animation definition},
-- Optional, specifies how to animate the particles' texture -- 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. -- Optional, specify particle self-luminescence in darkness.
-- Values 0-14. -- 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 -- If set to a valid number 1-6, specifies the tile from which the
-- particle texture is picked. -- particle texture is picked.
-- Otherwise, the default behavior is used. (currently: any random tile) -- 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 `HTTPRequest` definition
------------------------ ------------------------

@ -33,23 +33,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "client.h" #include "client.h"
#include "settings.h" #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 Particle
*/ */
@ -59,25 +42,71 @@ Particle::Particle(
LocalPlayer *player, LocalPlayer *player,
ClientEnvironment *env, ClientEnvironment *env,
const ParticleParameters &p, const ParticleParameters &p,
video::ITexture *texture, const ClientTexRef& texture,
v2f texpos, v2f texpos,
v2f texsize, v2f texsize,
video::SColor color video::SColor color
): ):
scene::ISceneNode(((Client *)gamedef)->getSceneManager()->getRootSceneNode(), scene::ISceneNode(((Client *)gamedef)->getSceneManager()->getRootSceneNode(),
((Client *)gamedef)->getSceneManager()) ((Client *)gamedef)->getSceneManager()),
m_texture(texture)
{ {
// Misc // Misc
m_gamedef = gamedef; m_gamedef = gamedef;
m_env = env; 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 // Texture
m_material.setFlag(video::EMF_LIGHTING, false); m_material.setFlag(video::EMF_LIGHTING, false);
m_material.setFlag(video::EMF_BACK_FACE_CULLING, false); m_material.setFlag(video::EMF_BACK_FACE_CULLING, false);
m_material.setFlag(video::EMF_BILINEAR_FILTER, false); m_material.setFlag(video::EMF_BILINEAR_FILTER, false);
m_material.setFlag(video::EMF_FOG_ENABLE, true); 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_texpos = texpos;
m_texsize = texsize; m_texsize = texsize;
m_animation = p.animation; m_animation = p.animation;
@ -90,6 +119,9 @@ Particle::Particle(
m_pos = p.pos; m_pos = p.pos;
m_velocity = p.vel; m_velocity = p.vel;
m_acceleration = p.acc; m_acceleration = p.acc;
m_drag = p.drag;
m_jitter = p.jitter;
m_bounce = p.bounce;
m_expiration = p.expirationtime; m_expiration = p.expirationtime;
m_player = player; m_player = player;
m_size = p.size; m_size = p.size;
@ -98,6 +130,8 @@ Particle::Particle(
m_object_collision = p.object_collision; m_object_collision = p.object_collision;
m_vertical = p.vertical; m_vertical = p.vertical;
m_glow = p.glow; m_glow = p.glow;
m_alpha = 0;
m_parent = nullptr;
// Irrlicht stuff // Irrlicht stuff
const float c = p.size / 2; const float c = p.size / 2;
@ -111,6 +145,14 @@ Particle::Particle(
updateVertices(); 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() void Particle::OnRegisterSceneNode()
{ {
if (IsVisible) if (IsVisible)
@ -134,6 +176,12 @@ void Particle::render()
void Particle::step(float dtime) void Particle::step(float dtime)
{ {
m_time += 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) { if (m_collisiondetection) {
aabb3f box = m_collisionbox; aabb3f box = m_collisionbox;
v3f p_pos = m_pos * BS; 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, collisionMoveResult r = collisionMoveSimple(m_env, m_gamedef, BS * 0.5f,
box, 0.0f, dtime, &p_pos, &p_velocity, m_acceleration * BS, nullptr, box, 0.0f, dtime, &p_pos, &p_velocity, m_acceleration * BS, nullptr,
m_object_collision); m_object_collision);
if (m_collision_removal && r.collides) {
// force expiration of the particle f32 bounciness = m_bounce.pickWithin();
m_expiration = -1.0; 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 { } else {
m_pos = p_pos / BS;
m_velocity = p_velocity / BS; m_velocity = p_velocity / BS;
} }
m_pos = p_pos / BS;
} else { } else {
// apply acceleration
m_velocity += m_acceleration * dtime; m_velocity += m_acceleration * dtime;
m_pos += m_velocity * dtime; m_pos += m_velocity * dtime;
} }
if (m_animation.type != TAT_NONE) { if (m_animation.type != TAT_NONE) {
m_animation_time += dtime; m_animation_time += dtime;
int frame_length_i, frame_count; 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 // Update lighting
updateLight(); updateLight();
// Update model // Update model
updateVertices(); updateVertices();
// Update position -- see #10398
v3s16 camera_offset = m_env->getCameraOffset();
setPosition(m_pos*BS - intToFloat(camera_offset, BS));
} }
void Particle::updateLight() void Particle::updateLight()
@ -189,7 +271,7 @@ void Particle::updateLight()
light = blend_light(m_env->getDayNightRatio(), LIGHT_SUN, 0); light = blend_light(m_env->getDayNightRatio(), LIGHT_SUN, 0);
u8 m_light = decode_light(light + m_glow); 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.getRed() / 255,
m_light * m_base_color.getGreen() / 255, m_light * m_base_color.getGreen() / 255,
m_light * m_base_color.getBlue() / 255); m_light * m_base_color.getBlue() / 255);
@ -198,6 +280,12 @@ void Particle::updateLight()
void Particle::updateVertices() void Particle::updateVertices()
{ {
f32 tx0, tx1, ty0, ty1; 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) { if (m_animation.type != TAT_NONE) {
const v2u32 texsize = m_material.getTexture(0)->getSize(); const v2u32 texsize = m_material.getTexture(0)->getSize();
@ -218,16 +306,24 @@ void Particle::updateVertices()
ty1 = m_texpos.Y + m_texsize.Y; 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); 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); 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); 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); 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) { for (video::S3DVertex &vertex : m_vertices) {
if (m_vertical) { if (m_vertical) {
v3f ppos = m_player->getPosition()/BS; v3f ppos = m_player->getPosition()/BS;
@ -238,7 +334,6 @@ void Particle::updateVertices()
vertex.Pos.rotateXZBy(m_player->getYaw()); vertex.Pos.rotateXZBy(m_player->getYaw());
} }
m_box.addInternalPoint(vertex.Pos); m_box.addInternalPoint(vertex.Pos);
vertex.Pos += m_pos*BS - intToFloat(camera_offset, BS);
} }
} }
@ -251,7 +346,8 @@ ParticleSpawner::ParticleSpawner(
LocalPlayer *player, LocalPlayer *player,
const ParticleSpawnerParameters &p, const ParticleSpawnerParameters &p,
u16 attached_id, u16 attached_id,
video::ITexture *texture, std::unique_ptr<ClientTexture[]>& texpool,
size_t texcount,
ParticleManager *p_manager ParticleManager *p_manager
): ):
m_particlemanager(p_manager), p(p) m_particlemanager(p_manager), p(p)
@ -259,21 +355,66 @@ ParticleSpawner::ParticleSpawner(
m_gamedef = gamedef; m_gamedef = gamedef;
m_player = player; m_player = player;
m_attached_id = attached_id; m_attached_id = attached_id;
m_texture = texture; m_texpool = std::move(texpool);
m_texcount = texcount;
m_time = 0; m_time = 0;
m_active = 0;
m_dying = false;
m_spawntimes.reserve(p.amount + 1); m_spawntimes.reserve(p.amount + 1);
for (u16 i = 0; i <= p.amount; i++) { 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); 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, void ParticleSpawner::spawnParticle(ClientEnvironment *env, float radius,
const core::matrix4 *attached_absolute_pos_rot_matrix) 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 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 // Need to apply this first or the following check
// will be wrong for attached spawners // will be wrong for attached spawners
@ -287,15 +428,18 @@ void ParticleSpawner::spawnParticle(ClientEnvironment *env, float radius,
pos.Z += camera_offset.Z; pos.Z += camera_offset.Z;
} }
if (pos.getDistanceFrom(ppos) > radius) if (pos.getDistanceFromSQ(ppos) > radius*radius)
return; return;
// Parameters for the single particle we're about to spawn // Parameters for the single particle we're about to spawn
ParticleParameters pp; ParticleParameters pp;
pp.pos = pos; pp.pos = pos;
pp.vel = random_v3f(p.minvel, p.maxvel); pp.vel = r_vel.pickWithin();
pp.acc = random_v3f(p.minacc, p.maxacc); pp.acc = r_acc.pickWithin();
pp.drag = r_drag.pickWithin();
pp.jitter = r_jitter;
pp.bounce = r_bounce;
if (attached_absolute_pos_rot_matrix) { if (attached_absolute_pos_rot_matrix) {
// Apply attachment rotation // Apply attachment rotation
@ -303,30 +447,137 @@ void ParticleSpawner::spawnParticle(ClientEnvironment *env, float radius,
attached_absolute_pos_rot_matrix->rotateVect(pp.acc); 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); p.copyCommon(pp);
video::ITexture *texture; ClientTexRef texture;
v2f texpos, texsize; v2f texpos, texsize;
video::SColor color(0xFFFFFFFF); video::SColor color(0xFFFFFFFF);
if (p.node.getContent() != CONTENT_IGNORE) { if (p.node.getContent() != CONTENT_IGNORE) {
const ContentFeatures &f = const ContentFeatures &f =
m_particlemanager->m_env->getGameDef()->ndef()->get(p.node); 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)) texpos, texsize, &color, p.node_tile))
return; return;
} else { } 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); texpos = v2f(0.0f, 0.0f);
texsize = v2f(1.0f, 1.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 // Allow keeping default random size
if (p.maxsize > 0.0f) if (p.size.start.max > 0.0f || p.size.end.max > 0.0f)
pp.size = random_f32(p.minsize, p.maxsize); pp.size = r_size.pickWithin();
m_particlemanager->addParticle(new Particle( ++m_active;
auto pa = new Particle(
m_gamedef, m_gamedef,
m_player, m_player,
env, env,
@ -335,7 +586,9 @@ void ParticleSpawner::spawnParticle(ClientEnvironment *env, float radius,
texpos, texpos,
texsize, texsize,
color color
)); );
pa->m_parent = this;
m_particlemanager->addParticle(pa);
} }
void ParticleSpawner::step(float dtime, ClientEnvironment *env) void ParticleSpawner::step(float dtime, ClientEnvironment *env)
@ -348,7 +601,7 @@ void ParticleSpawner::step(float dtime, ClientEnvironment *env)
bool unloaded = false; bool unloaded = false;
const core::matrix4 *attached_absolute_pos_rot_matrix = nullptr; const core::matrix4 *attached_absolute_pos_rot_matrix = nullptr;
if (m_attached_id) { 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(); attached_absolute_pos_rot_matrix = attached->getAbsolutePosRotMatrix();
} else { } else {
unloaded = true; unloaded = true;
@ -379,7 +632,7 @@ void ParticleSpawner::step(float dtime, ClientEnvironment *env)
return; return;
for (int i = 0; i <= p.amount; i++) { 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); spawnParticle(env, radius, attached_absolute_pos_rot_matrix);
} }
} }
@ -408,9 +661,15 @@ void ParticleManager::stepSpawners(float dtime)
{ {
MutexAutoLock lock(m_spawner_list_lock); MutexAutoLock lock(m_spawner_list_lock);
for (auto i = m_particle_spawners.begin(); i != m_particle_spawners.end();) { for (auto i = m_particle_spawners.begin(); i != m_particle_spawners.end();) {
if (i->second->get_expired()) { if (i->second->getExpired()) {
delete i->second; // the particlespawner owns the textures, so we need to make
m_particle_spawners.erase(i++); // 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 { } else {
i->second->step(dtime, m_env); i->second->step(dtime, m_env);
++i; ++i;
@ -423,6 +682,10 @@ void ParticleManager::stepParticles(float dtime)
MutexAutoLock lock(m_particle_list_lock); MutexAutoLock lock(m_particle_list_lock);
for (auto i = m_particles.begin(); i != m_particles.end();) { for (auto i = m_particles.begin(); i != m_particles.end();) {
if ((*i)->get_expired()) { if ((*i)->get_expired()) {
if ((*i)->m_parent) {
assert((*i)->m_parent->m_active != 0);
--(*i)->m_parent->m_active;
}
(*i)->remove(); (*i)->remove();
delete *i; delete *i;
i = m_particles.erase(i); i = m_particles.erase(i);
@ -464,13 +727,29 @@ void ParticleManager::handleParticleEvent(ClientEvent *event, Client *client,
const ParticleSpawnerParameters &p = *event->add_particlespawner.p; const ParticleSpawnerParameters &p = *event->add_particlespawner.p;
video::ITexture *texture = // texture pool
client->tsrc()->getTextureForMesh(p.texture); 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, auto toadd = new ParticleSpawner(client, player,
p, p,
event->add_particlespawner.attached_id, event->add_particlespawner.attached_id,
texture, texpool,
txpsz,
this); this);
addParticleSpawner(event->add_particlespawner.id, toadd); addParticleSpawner(event->add_particlespawner.id, toadd);
@ -481,7 +760,7 @@ void ParticleManager::handleParticleEvent(ClientEvent *event, Client *client,
case CE_SPAWN_PARTICLE: { case CE_SPAWN_PARTICLE: {
ParticleParameters &p = *event->spawn_particle; ParticleParameters &p = *event->spawn_particle;
video::ITexture *texture; ClientTexRef texture;
v2f texpos, texsize; v2f texpos, texsize;
video::SColor color(0xFFFFFFFF); video::SColor color(0xFFFFFFFF);
@ -489,11 +768,15 @@ void ParticleManager::handleParticleEvent(ClientEvent *event, Client *client,
if (p.node.getContent() != CONTENT_IGNORE) { if (p.node.getContent() != CONTENT_IGNORE) {
const ContentFeatures &f = m_env->getGameDef()->ndef()->get(p.node); const ContentFeatures &f = m_env->getGameDef()->ndef()->get(p.node);
if (!getNodeParticleParams(p.node, f, p, &texture, texpos, getNodeParticleParams(p.node, f, p, &texture.ref, texpos,
texsize, &color, p.node_tile)) texsize, &color, p.node_tile);
texture = nullptr;
} else { } 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); texpos = v2f(0.0f, 0.0f);
texsize = v2f(1.0f, 1.0f); texsize = v2f(1.0f, 1.0f);
} }
@ -502,7 +785,7 @@ void ParticleManager::handleParticleEvent(ClientEvent *event, Client *client,
if (oldsize > 0.0f) if (oldsize > 0.0f)
p.size = oldsize; p.size = oldsize;
if (texture) { if (texture.ref) {
Particle *toadd = new Particle(client, player, m_env, Particle *toadd = new Particle(client, player, m_env,
p, texture, texpos, texsize, color); p, texture, texpos, texsize, color);
@ -529,7 +812,7 @@ bool ParticleManager::getNodeParticleParams(const MapNode &n,
if (tilenum > 0 && tilenum <= 6) if (tilenum > 0 && tilenum <= 6)
texid = tilenum - 1; texid = tilenum - 1;
else else
texid = rand() % 6; texid = myrand_range(0,5);
const TileLayer &tile = f.tiles[texid].layers[0]; const TileLayer &tile = f.tiles[texid].layers[0];
p.animation.type = TAT_NONE; p.animation.type = TAT_NONE;
@ -539,13 +822,13 @@ bool ParticleManager::getNodeParticleParams(const MapNode &n,
else else
*texture = tile.texture; *texture = tile.texture;
float size = (rand() % 8) / 64.0f; float size = (myrand_range(0,8)) / 64.0f;
p.size = BS * size; p.size = BS * size;
if (tile.scale) if (tile.scale)
size /= tile.scale; size /= tile.scale;
texsize = v2f(size * 2.0f, size * 2.0f); texsize = v2f(size * 2.0f, size * 2.0f);
texpos.X = (rand() % 64) / 64.0f - texsize.X; texpos.X = (myrand_range(0,64)) / 64.0f - texsize.X;
texpos.Y = (rand() % 64) / 64.0f - texsize.Y; texpos.Y = (myrand_range(0,64)) / 64.0f - texsize.Y;
if (tile.has_color) if (tile.has_color)
*color = tile.color; *color = tile.color;
@ -577,20 +860,20 @@ void ParticleManager::addNodeParticle(IGameDef *gamedef,
LocalPlayer *player, v3s16 pos, const MapNode &n, const ContentFeatures &f) LocalPlayer *player, v3s16 pos, const MapNode &n, const ContentFeatures &f)
{ {
ParticleParameters p; ParticleParameters p;
video::ITexture *texture; video::ITexture *ref = nullptr;
v2f texpos, texsize; v2f texpos, texsize;
video::SColor color; video::SColor color;
if (!getNodeParticleParams(n, f, p, &texture, texpos, texsize, &color)) if (!getNodeParticleParams(n, f, p, &ref, texpos, texsize, &color))
return; return;
p.expirationtime = (rand() % 100) / 100.0f; p.expirationtime = myrand_range(0, 100) / 100.0f;
// Physics // Physics
p.vel = v3f( p.vel = v3f(
(rand() % 150) / 50.0f - 1.5f, myrand_range(-1.5f,1.5f),
(rand() % 150) / 50.0f, myrand_range(0.f,3.f),
(rand() % 150) / 50.0f - 1.5f myrand_range(-1.5f,1.5f)
); );
p.acc = v3f( p.acc = v3f(
0.0f, 0.0f,
@ -598,9 +881,9 @@ void ParticleManager::addNodeParticle(IGameDef *gamedef,
0.0f 0.0f
); );
p.pos = v3f( p.pos = v3f(
(f32)pos.X + (rand() % 100) / 200.0f - 0.25f, (f32)pos.X + myrand_range(0.f, .5f) - .25f,
(f32)pos.Y + (rand() % 100) / 200.0f - 0.25f, (f32)pos.Y + myrand_range(0.f, .5f) - .25f,
(f32)pos.Z + (rand() % 100) / 200.0f - 0.25f (f32)pos.Z + myrand_range(0.f, .5f) - .25f
); );
Particle *toadd = new Particle( Particle *toadd = new Particle(
@ -608,7 +891,7 @@ void ParticleManager::addNodeParticle(IGameDef *gamedef,
player, player,
m_env, m_env,
p, p,
texture, ClientTexRef(ref),
texpos, texpos,
texsize, texsize,
color); color);
@ -616,6 +899,12 @@ void ParticleManager::addNodeParticle(IGameDef *gamedef,
addParticle(toadd); 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) void ParticleManager::addParticle(Particle *toadd)
{ {
MutexAutoLock lock(m_particle_list_lock); MutexAutoLock lock(m_particle_list_lock);
@ -634,7 +923,6 @@ void ParticleManager::deleteParticleSpawner(u64 id)
MutexAutoLock lock(m_spawner_list_lock); MutexAutoLock lock(m_spawner_list_lock);
auto it = m_particle_spawners.find(id); auto it = m_particle_spawners.find(id);
if (it != m_particle_spawners.end()) { if (it != m_particle_spawners.end()) {
delete it->second; it->second->setDying();
m_particle_spawners.erase(it);
} }
} }

@ -31,20 +31,53 @@ class ClientEnvironment;
struct MapNode; struct MapNode;
struct ContentFeatures; 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 class Particle : public scene::ISceneNode
{ {
public: public:
Particle( Particle(
IGameDef* gamedef, IGameDef *gamedef,
LocalPlayer *player, LocalPlayer *player,
ClientEnvironment *env, ClientEnvironment *env,
const ParticleParameters &p, const ParticleParameters &p,
video::ITexture *texture, const ClientTexRef &texture,
v2f texpos, v2f texpos,
v2f texsize, v2f texsize,
video::SColor color video::SColor color
); );
~Particle() = default; ~Particle();
virtual const aabb3f &getBoundingBox() const virtual const aabb3f &getBoundingBox() const
{ {
@ -69,9 +102,12 @@ class Particle : public scene::ISceneNode
bool get_expired () bool get_expired ()
{ return m_expiration < m_time; } { return m_expiration < m_time; }
ParticleSpawner *m_parent;
private: private:
void updateLight(); void updateLight();
void updateVertices(); void updateVertices();
void setVertexAlpha(float a);
video::S3DVertex m_vertices[4]; video::S3DVertex m_vertices[4];
float m_time = 0.0f; float m_time = 0.0f;
@ -81,14 +117,19 @@ private:
IGameDef *m_gamedef; IGameDef *m_gamedef;
aabb3f m_box; aabb3f m_box;
aabb3f m_collisionbox; aabb3f m_collisionbox;
ClientTexRef m_texture;
video::SMaterial m_material; video::SMaterial m_material;
v2f m_texpos; v2f m_texpos;
v2f m_texsize; v2f m_texsize;
v3f m_pos; v3f m_pos;
v3f m_velocity; v3f m_velocity;
v3f m_acceleration; v3f m_acceleration;
v3f m_drag;
ParticleParamTypes::v3fRange m_jitter;
ParticleParamTypes::f32Range m_bounce;
LocalPlayer *m_player; LocalPlayer *m_player;
float m_size; float m_size;
//! Color without lighting //! Color without lighting
video::SColor m_base_color; video::SColor m_base_color;
//! Final rendered color //! Final rendered color
@ -102,24 +143,27 @@ private:
float m_animation_time = 0.0f; float m_animation_time = 0.0f;
int m_animation_frame = 0; int m_animation_frame = 0;
u8 m_glow; u8 m_glow;
float m_alpha = 0.0f;
}; };
class ParticleSpawner class ParticleSpawner
{ {
public: public:
ParticleSpawner(IGameDef* gamedef, ParticleSpawner(IGameDef *gamedef,
LocalPlayer *player, LocalPlayer *player,
const ParticleSpawnerParameters &p, const ParticleSpawnerParameters &p,
u16 attached_id, u16 attached_id,
video::ITexture *texture, std::unique_ptr<ClientTexture[]> &texpool,
size_t texcount,
ParticleManager* p_manager); ParticleManager* p_manager);
~ParticleSpawner() = default;
void step(float dtime, ClientEnvironment *env); void step(float dtime, ClientEnvironment *env);
bool get_expired () size_t m_active;
{ return p.amount <= 0 && p.time != 0; }
bool getExpired() const
{ return m_dying || (p.amount <= 0 && p.time != 0); }
void setDying() { m_dying = true; }
private: private:
void spawnParticle(ClientEnvironment *env, float radius, void spawnParticle(ClientEnvironment *env, float radius,
@ -127,10 +171,12 @@ private:
ParticleManager *m_particlemanager; ParticleManager *m_particlemanager;
float m_time; float m_time;
bool m_dying;
IGameDef *m_gamedef; IGameDef *m_gamedef;
LocalPlayer *m_player; LocalPlayer *m_player;
ParticleSpawnerParameters p; ParticleSpawnerParameters p;
video::ITexture *m_texture; std::unique_ptr<ClientTexture[]> m_texpool;
size_t m_texcount;
std::vector<float> m_spawntimes; std::vector<float> m_spawntimes;
u16 m_attached_id; u16 m_attached_id;
}; };
@ -156,6 +202,8 @@ public:
void addNodeParticle(IGameDef *gamedef, LocalPlayer *player, v3s16 pos, void addNodeParticle(IGameDef *gamedef, LocalPlayer *player, v3s16 pos,
const MapNode &n, const ContentFeatures &f); const MapNode &n, const ContentFeatures &f);
void reserveParticleSpace(size_t max_estimate);
/** /**
* This function is only used by client particle spawners * This function is only used by client particle spawners
* *

@ -994,18 +994,18 @@ void Client::handleCommand_AddParticleSpawner(NetworkPacket* pkt)
p.amount = readU16(is); p.amount = readU16(is);
p.time = readF32(is); p.time = readF32(is);
p.minpos = readV3F32(is);
p.maxpos = readV3F32(is); // older protocols do not support tweening, and send only
p.minvel = readV3F32(is); // static ranges, so we can't just use the normal serialization
p.maxvel = readV3F32(is); // functions for the older values.
p.minacc = readV3F32(is); p.pos.start.legacyDeSerialize(is);
p.maxacc = readV3F32(is); p.vel.start.legacyDeSerialize(is);
p.minexptime = readF32(is); p.acc.start.legacyDeSerialize(is);
p.maxexptime = readF32(is); p.exptime.start.legacyDeSerialize(is);
p.minsize = readF32(is); p.size.start.legacyDeSerialize(is);
p.maxsize = readF32(is);
p.collisiondetection = readU8(is); p.collisiondetection = readU8(is);
p.texture = deSerializeString32(is); p.texture.string = deSerializeString32(is);
server_id = readU32(is); server_id = readU32(is);
@ -1018,6 +1018,8 @@ void Client::handleCommand_AddParticleSpawner(NetworkPacket* pkt)
p.glow = readU8(is); p.glow = readU8(is);
p.object_collision = readU8(is); p.object_collision = readU8(is);
bool legacy_format = true;
// This is kinda awful // This is kinda awful
do { do {
u16 tmp_param0 = readU16(is); u16 tmp_param0 = readU16(is);
@ -1026,7 +1028,70 @@ void Client::handleCommand_AddParticleSpawner(NetworkPacket* pkt)
p.node.param0 = tmp_param0; p.node.param0 = tmp_param0;
p.node.param2 = readU8(is); p.node.param2 = readU8(is);
p.node_tile = 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(); auto event = new ClientEvent();
event->type = CE_ADD_PARTICLESPAWNER; event->type = CE_ADD_PARTICLESPAWNER;

@ -207,6 +207,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
Minimap modes Minimap modes
PROTOCOL VERSION 40: PROTOCOL VERSION 40:
TOCLIENT_MEDIA_PUSH changed, TOSERVER_HAVE_MEDIA added TOCLIENT_MEDIA_PUSH changed, TOSERVER_HAVE_MEDIA added
Added new particlespawner parameters (5.6.0)
*/ */
#define LATEST_PROTOCOL_VERSION 40 #define LATEST_PROTOCOL_VERSION 40
@ -511,11 +512,12 @@ enum ToClientCommand
TOCLIENT_SPAWN_PARTICLE = 0x46, TOCLIENT_SPAWN_PARTICLE = 0x46,
/* /*
v3f1000 pos -- struct range<T> { T min, T max, f32 bias };
v3f1000 velocity v3f pos
v3f1000 acceleration v3f velocity
f1000 expirationtime v3f acceleration
f1000 size f32 expirationtime
f32 size
u8 bool collisiondetection u8 bool collisiondetection
u32 len u32 len
u8[len] texture u8[len] texture
@ -524,22 +526,26 @@ enum ToClientCommand
TileAnimation animation TileAnimation animation
u8 glow u8 glow
u8 object_collision u8 object_collision
v3f drag
range<v3f> bounce
*/ */
TOCLIENT_ADD_PARTICLESPAWNER = 0x47, TOCLIENT_ADD_PARTICLESPAWNER = 0x47,
/* /*
-- struct range<T> { T min, T max, f32 bias };
-- struct tween<T> { T start, T end };
u16 amount u16 amount
f1000 spawntime f32 spawntime
v3f1000 minpos v3f minpos
v3f1000 maxpos v3f maxpos
v3f1000 minvel v3f minvel
v3f1000 maxvel v3f maxvel
v3f1000 minacc v3f minacc
v3f1000 maxacc v3f maxacc
f1000 minexptime f32 minexptime
f1000 maxexptime f32 maxexptime
f1000 minsize f32 minsize
f1000 maxsize f32 maxsize
u8 bool collisiondetection u8 bool collisiondetection
u32 len u32 len
u8[len] texture u8[len] texture
@ -549,6 +555,63 @@ enum ToClientCommand
TileAnimation animation TileAnimation animation
u8 glow u8 glow
u8 object_collision 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 TOCLIENT_DELETE_PARTICLESPAWNER_LEGACY = 0x48, // Obsolete

@ -18,7 +18,103 @@ with this program; if not, write to the Free Software Foundation, Inc.,
*/ */
#include "particles.h" #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 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, expirationtime);
writeF32(os, size); writeF32(os, size);
writeU8(os, collisiondetection); writeU8(os, collisiondetection);
os << serializeString32(texture); os << serializeString32(texture.string);
writeU8(os, vertical); writeU8(os, vertical);
writeU8(os, collision_removal); writeU8(os, collision_removal);
animation.serialize(os, 6); /* NOT the protocol ver */ 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); writeU16(os, node.param0);
writeU8(os, node.param2); writeU8(os, node.param2);
writeU8(os, node_tile); 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) 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); expirationtime = readF32(is);
size = readF32(is); size = readF32(is);
collisiondetection = readU8(is); collisiondetection = readU8(is);
texture = deSerializeString32(is); texture.string = deSerializeString32(is);
vertical = readU8(is); vertical = readU8(is);
collision_removal = readU8(is); collision_removal = readU8(is);
animation.deSerialize(is, 6); /* NOT the protocol ver */ animation.deSerialize(is, 6); /* NOT the protocol ver */
glow = readU8(is); glow = readU8(is);
object_collision = readU8(is); object_collision = readU8(is);
// This is kinda awful
u16 tmp_param0 = readU16(is); if (streamEndsBeforeParam<u16, readU16>(node.param0, is))
if (is.eof())
return; return;
node.param0 = tmp_param0;
node.param2 = readU8(is); node.param2 = readU8(is);
node_tile = readU8(is); node_tile = readU8(is);
if (streamEndsBeforeParam<v3f, readV3F32>(drag, is))
return;
jitter.deSerialize(is);
bounce.deSerialize(is);
} }

@ -20,19 +20,352 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#pragma once #pragma once
#include <string> #include <string>
#include <sstream>
#include <vector>
#include <ctgmath>
#include <type_traits>
#include "irrlichttypes_bloated.h" #include "irrlichttypes_bloated.h"
#include "tileanimation.h" #include "tileanimation.h"
#include "mapnode.h" #include "mapnode.h"
#include "util/serialize.h"
#include "util/numeric.h"
// This file defines the particle-related structures that both the server and // This file defines the particle-related structures that both the server and
// client need. The ParticleManager and rendering is in client/particles.h // 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 collisiondetection = false;
bool collision_removal = false; bool collision_removal = false;
bool object_collision = false; bool object_collision = false;
bool vertical = false; bool vertical = false;
std::string texture; ServerParticleTexture texture;
struct TileAnimationParams animation; struct TileAnimationParams animation;
u8 glow = 0; u8 glow = 0;
MapNode node; MapNode node;
@ -58,22 +391,42 @@ struct CommonParticleParams {
} }
}; };
struct ParticleParameters : CommonParticleParams { struct ParticleParameters : CommonParticleParams
v3f pos; {
v3f vel; v3f pos, vel, acc, drag;
v3f acc; f32 size = 1, expirationtime = 1;
f32 expirationtime = 1; ParticleParamTypes::f32Range bounce;
f32 size = 1; ParticleParamTypes::v3fRange jitter;
void serialize(std::ostream &os, u16 protocol_ver) const; void serialize(std::ostream &os, u16 protocol_ver) const;
void deSerialize(std::istream &is, u16 protocol_ver); void deSerialize(std::istream &is, u16 protocol_ver);
}; };
struct ParticleSpawnerParameters : CommonParticleParams { struct ParticleSpawnerParameters : CommonParticleParams
{
u16 amount = 1; u16 amount = 1;
v3f minpos, maxpos, minvel, maxvel, minacc, maxacc;
f32 time = 1; 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 // For historical reasons no (de-)serialization methods here
}; };

@ -42,7 +42,7 @@ struct EnumString es_TileAnimationType[] =
{TAT_NONE, "none"}, {TAT_NONE, "none"},
{TAT_VERTICAL_FRAMES, "vertical_frames"}, {TAT_VERTICAL_FRAMES, "vertical_frames"},
{TAT_SHEET_2D, "sheet_2d"}, {TAT_SHEET_2D, "sheet_2d"},
{0, NULL}, {0, nullptr},
}; };
/******************************************************************************/ /******************************************************************************/

@ -100,6 +100,7 @@ void setboolfield(lua_State *L, int table,
const char *fieldname, bool value); const char *fieldname, bool value);
v3f checkFloatPos (lua_State *L, int index); v3f checkFloatPos (lua_State *L, int index);
v2f check_v2f (lua_State *L, int index);
v3f check_v3f (lua_State *L, int index); v3f check_v3f (lua_State *L, int index);
v3s16 check_v3s16 (lua_State *L, int index); v3s16 check_v3s16 (lua_State *L, int index);

@ -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);
}

@ -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_particles.h"
#include "lua_api/l_object.h" #include "lua_api/l_object.h"
#include "lua_api/l_internal.h" #include "lua_api/l_internal.h"
#include "lua_api/l_particleparams.h"
#include "common/c_converter.h" #include "common/c_converter.h"
#include "common/c_content.h" #include "common/c_content.h"
#include "server.h" #include "server.h"
#include "particles.h" #include "particles.h"
// add_particle({pos=, velocity=, acceleration=, expirationtime=, void LuaParticleParams::readTexValue(lua_State* L, ServerParticleTexture& tex)
// size=, collisiondetection=, collision_removal=, object_collision=, {
// vertical=, texture=, player=}) StackUnroller unroll(L);
// pos/velocity/acceleration = {x=num, y=num, z=num}
// expirationtime = num (seconds) tex.animated = false;
// size = num if (lua_isstring(L, -1)) {
// collisiondetection = bool tex.string = lua_tostring(L, -1);
// collision_removal = bool return;
// object_collision = bool }
// vertical = bool
// texture = e.g."default_wood.png" luaL_checktype(L, -1, LUA_TTABLE);
// animation = TileAnimation definition lua_getfield(L, -1, "name");
// glow = num 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) int ModApiParticles::l_add_particle(lua_State *L)
{ {
NO_MAP_LOCK_REQUIRED; NO_MAP_LOCK_REQUIRED;
// Get parameters // Get parameters
struct ParticleParameters p; ParticleParameters p;
std::string playername; std::string playername;
if (lua_gettop(L) > 1) // deprecated 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.expirationtime = luaL_checknumber(L, 4);
p.size = luaL_checknumber(L, 5); p.size = luaL_checknumber(L, 5);
p.collisiondetection = readParam<bool>(L, 6); 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 if (lua_gettop(L) == 8) // only spawn for a single player
playername = luaL_checkstring(L, 8); playername = luaL_checkstring(L, 8);
} }
@ -108,7 +128,12 @@ int ModApiParticles::l_add_particle(lua_State *L)
p.animation = read_animation_definition(L, -1); p.animation = read_animation_definition(L, -1);
lua_pop(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); p.glow = getintfield_default(L, 1, "glow", p.glow);
lua_getfield(L, 1, "node"); 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); p.node_tile = getintfield_default(L, 1, "node_tile", p.node_tile);
playername = getstringfield_default(L, 1, "playername", ""); 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); getServer(L)->spawnParticle(playername, p);
return 1; return 1;
} }
// add_particlespawner({amount=, time=, // add_particlespawner({...})
// 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
int ModApiParticles::l_add_particlespawner(lua_State *L) int ModApiParticles::l_add_particlespawner(lua_State *L)
{ {
NO_MAP_LOCK_REQUIRED; NO_MAP_LOCK_REQUIRED;
@ -156,24 +173,31 @@ int ModApiParticles::l_add_particlespawner(lua_State *L)
ServerActiveObject *attached = NULL; ServerActiveObject *attached = NULL;
std::string playername; std::string playername;
using namespace ParticleParamTypes;
if (lua_gettop(L) > 1) //deprecated if (lua_gettop(L) > 1) //deprecated
{ {
log_deprecated(L, "Deprecated add_particlespawner call with " log_deprecated(L, "Deprecated add_particlespawner call with "
"individual parameters instead of definition"); "individual parameters instead of definition");
p.amount = luaL_checknumber(L, 1); p.amount = luaL_checknumber(L, 1);
p.time = luaL_checknumber(L, 2); p.time = luaL_checknumber(L, 2);
p.minpos = check_v3f(L, 3); auto minpos = check_v3f(L, 3);
p.maxpos = check_v3f(L, 4); auto maxpos = check_v3f(L, 4);
p.minvel = check_v3f(L, 5); auto minvel = check_v3f(L, 5);
p.maxvel = check_v3f(L, 6); auto maxvel = check_v3f(L, 6);
p.minacc = check_v3f(L, 7); auto minacc = check_v3f(L, 7);
p.maxacc = check_v3f(L, 8); auto maxacc = check_v3f(L, 8);
p.minexptime = luaL_checknumber(L, 9); auto minexptime = luaL_checknumber(L, 9);
p.maxexptime = luaL_checknumber(L, 10); auto maxexptime = luaL_checknumber(L, 10);
p.minsize = luaL_checknumber(L, 11); auto minsize = luaL_checknumber(L, 11);
p.maxsize = luaL_checknumber(L, 12); 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.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 if (lua_gettop(L) == 15) // only spawn for a single player
playername = luaL_checkstring(L, 15); 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.amount = getintfield_default(L, 1, "amount", p.amount);
p.time = getfloatfield_default(L, 1, "time", p.time); p.time = getfloatfield_default(L, 1, "time", p.time);
lua_getfield(L, 1, "minpos"); // set default values
if (lua_istable(L, -1)) p.exptime = 1;
p.minpos = check_v3f(L, -1); p.size = 1;
lua_pop(L, 1);
lua_getfield(L, 1, "maxpos"); // read spawner parameters from the table
if (lua_istable(L, -1)) LuaParticleParams::readTweenTable(L, "pos", p.pos);
p.maxpos = check_v3f(L, -1); LuaParticleParams::readTweenTable(L, "vel", p.vel);
lua_pop(L, 1); 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, "minvel"); lua_getfield(L, -1, "die_on_contact");
if (lua_istable(L, -1)) if (!lua_isnil(L, -1))
p.minvel = check_v3f(L, -1); p.attractor_kill = readParam<bool>(L, -1);
lua_pop(L, 1); lua_pop(L,1);
lua_getfield(L, 1, "maxvel"); if (p.attractor_kind != AttractorKind::none) {
if (lua_istable(L, -1)) LuaParticleParams::readTweenTable(L, "strength", p.attract);
p.maxvel = check_v3f(L, -1); LuaParticleParams::readTweenTable(L, "origin", p.attractor_origin);
lua_pop(L, 1); 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, "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);
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, p.collisiondetection = getboolfield_default(L, 1,
"collisiondetection", p.collisiondetection); "collisiondetection", p.collisiondetection);
p.collision_removal = getboolfield_default(L, 1, p.collision_removal = getboolfield_default(L, 1,
@ -234,11 +264,29 @@ int ModApiParticles::l_add_particlespawner(lua_State *L)
attached = ObjectRef::getobject(ref); 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.vertical = getboolfield_default(L, 1, "vertical", p.vertical);
p.texture = getstringfield_default(L, 1, "texture", p.texture);
playername = getstringfield_default(L, 1, "playername", ""); playername = getstringfield_default(L, 1, "playername", "");
p.glow = getintfield_default(L, 1, "glow", p.glow); 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"); lua_getfield(L, 1, "node");
if (lua_istable(L, -1)) if (lua_istable(L, -1))
p.node = readnode(L, -1, getGameDef(L)->ndef()); p.node = readnode(L, -1, getGameDef(L)->ndef());

@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "common/c_converter.h" #include "common/c_converter.h"
#include "lua_api/l_internal.h" #include "lua_api/l_internal.h"
#include "lua_api/l_object.h" #include "lua_api/l_object.h"
#include "lua_api/l_particleparams.h"
#include "client/particles.h" #include "client/particles.h"
#include "client/client.h" #include "client/client.h"
#include "client/clientevent.h" #include "client/clientevent.h"
@ -49,6 +50,19 @@ int ModApiParticlesLocal::l_add_particle(lua_State *L)
p.acc = check_v3f(L, -1); p.acc = check_v3f(L, -1);
lua_pop(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 = getfloatfield_default(L, 1, "expirationtime",
p.expirationtime); p.expirationtime);
p.size = getfloatfield_default(L, 1, "size", p.size); 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); p.animation = read_animation_definition(L, -1);
lua_pop(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); p.glow = getintfield_default(L, 1, "glow", p.glow);
lua_getfield(L, 1, "node"); lua_getfield(L, 1, "node");
@ -88,44 +106,50 @@ int ModApiParticlesLocal::l_add_particlespawner(lua_State *L)
// Get parameters // Get parameters
ParticleSpawnerParameters p; ParticleSpawnerParameters p;
p.amount = getintfield_default(L, 1, "amount", p.amount); p.amount = getintfield_default(L, 1, "amount", p.amount);
p.time = getfloatfield_default(L, 1, "time", p.time); p.time = getfloatfield_default(L, 1, "time", p.time);
lua_getfield(L, 1, "minpos"); // set default values
if (lua_istable(L, -1)) p.exptime = 1;
p.minpos = check_v3f(L, -1); p.size = 1;
lua_pop(L, 1);
lua_getfield(L, 1, "maxpos"); // read spawner parameters from the table
if (lua_istable(L, -1)) using namespace ParticleParamTypes;
p.maxpos = check_v3f(L, -1); LuaParticleParams::readTweenTable(L, "pos", p.pos);
lua_pop(L, 1); 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, "minvel"); lua_getfield(L, -1, "die_on_contact");
if (lua_istable(L, -1)) if (!lua_isnil(L, -1))
p.minvel = check_v3f(L, -1); p.attractor_kill = readParam<bool>(L, -1);
lua_pop(L, 1); lua_pop(L,1);
lua_getfield(L, 1, "maxvel"); if (p.attractor_kind != AttractorKind::none) {
if (lua_istable(L, -1)) LuaParticleParams::readTweenTable(L, "strength", p.attract);
p.maxvel = check_v3f(L, -1); LuaParticleParams::readTweenTable(L, "origin", p.attractor_origin);
lua_pop(L, 1); 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, "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);
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, p.collisiondetection = getboolfield_default(L, 1,
"collisiondetection", p.collisiondetection); "collisiondetection", p.collisiondetection);
p.collision_removal = getboolfield_default(L, 1, 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); p.animation = read_animation_definition(L, -1);
lua_pop(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.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); 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"); lua_getfield(L, 1, "node");
if (lua_istable(L, -1)) if (lua_istable(L, -1))
p.node = readnode(L, -1, getGameDef(L)->ndef()); p.node = readnode(L, -1, getGameDef(L)->ndef());

@ -160,6 +160,7 @@ v3f ServerPlayingSound::getPos(ServerEnvironment *env, bool *pos_exists) const
return sao->getBasePosition(); return sao->getBasePosition();
} }
} }
return v3f(0,0,0); 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) { if (peer_id == PEER_ID_INEXISTENT) {
std::vector<session_t> clients = m_clients.getClientIDs(); 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; const float radius_sq = radius * radius;
/* Don't send short-lived spawners to distant players. /* Don't send short-lived spawners to distant players.
* This could be replaced with proper tracking at some point. */ * 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); NetworkPacket pkt(TOCLIENT_ADD_PARTICLESPAWNER, 100, peer_id);
pkt << p.amount << p.time << p.minpos << p.maxpos << p.minvel pkt << p.amount << p.time;
<< p.maxvel << p.minacc << p.maxacc << p.minexptime << p.maxexptime { // serialize legacy fields
<< p.minsize << p.maxsize << p.collisiondetection; 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; 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.glow << p.object_collision;
pkt << p.node.param0 << p.node.param2 << p.node_tile; 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); Send(&pkt);
} }
@ -3692,8 +3751,8 @@ v3f Server::findSpawnPos()
s32 range = MYMIN(1 + i, range_max); s32 range = MYMIN(1 + i, range_max);
// We're going to try to throw the player to this position // We're going to try to throw the player to this position
v2s16 nodepos2d = v2s16( v2s16 nodepos2d = v2s16(
-range + (myrand() % (range * 2)), -range + myrand_range(0, range*2),
-range + (myrand() % (range * 2))); -range + myrand_range(0, range*2));
// Get spawn level at point // Get spawn level at point
s16 spawn_level = m_emerge->getSpawnLevelAtPoint(nodepos2d); s16 spawn_level = m_emerge->getSpawnLevelAtPoint(nodepos2d);
// Continue if MAX_MAP_GENERATION_LIMIT was returned by the mapgen to // Continue if MAX_MAP_GENERATION_LIMIT was returned by the mapgen to

@ -46,11 +46,22 @@ void myrand_bytes(void *out, size_t len)
g_pcgrand.bytes(out, 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) int myrand_range(int min, int max)
{ {
return g_pcgrand.range(min, 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 64-bit unaligned version of MurmurHash

@ -223,6 +223,8 @@ u32 myrand();
void mysrand(unsigned int seed); void mysrand(unsigned int seed);
void myrand_bytes(void *out, size_t len); void myrand_bytes(void *out, size_t len);
int myrand_range(int min, int max); int myrand_range(int min, int max);
float myrand_range(float min, float max);
float myrand_float();
/* /*
Miscellaneous functions 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.getGreen() * mod, 0, 255),
core::clamp<u32>(color.getBlue() * 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)
);
}

@ -45,7 +45,7 @@ public:
Buffer() Buffer()
{ {
m_size = 0; m_size = 0;
data = NULL; data = nullptr;
} }
Buffer(unsigned int size) Buffer(unsigned int size)
{ {
@ -53,7 +53,7 @@ public:
if(size != 0) if(size != 0)
data = new T[size]; data = new T[size];
else else
data = NULL; data = nullptr;
} }
// Disable class copy // Disable class copy
@ -82,7 +82,7 @@ public:
memcpy(data, t, size); memcpy(data, t, size);
} }
else else
data = NULL; data = nullptr;
} }
~Buffer() ~Buffer()
@ -166,7 +166,7 @@ public:
if(m_size != 0) if(m_size != 0)
data = new T[m_size]; data = new T[m_size];
else else
data = NULL; data = nullptr;
refcount = new unsigned int; refcount = new unsigned int;
memset(data,0,sizeof(T)*m_size); memset(data,0,sizeof(T)*m_size);
(*refcount) = 1; (*refcount) = 1;
@ -201,7 +201,7 @@ public:
memcpy(data, t, m_size); memcpy(data, t, m_size);
} }
else else
data = NULL; data = nullptr;
refcount = new unsigned int; refcount = new unsigned int;
(*refcount) = 1; (*refcount) = 1;
} }
@ -216,7 +216,7 @@ public:
memcpy(data, *buffer, buffer.getSize()); memcpy(data, *buffer, buffer.getSize());
} }
else else
data = NULL; data = nullptr;
refcount = new unsigned int; refcount = new unsigned int;
(*refcount) = 1; (*refcount) = 1;
} }
@ -256,3 +256,4 @@ private:
unsigned int m_size; unsigned int m_size;
unsigned int *refcount; unsigned int *refcount;
}; };