Particle Emitter
Smoke, steam, dust, spray, fireflies and fire. Every dial named in plain words, and six recipes so it works before you understand any of them.
What it does
It runs the region's particle system with settings you can actually read. Set RECIPE to a number between one and six and you have believable smoke, steam, dust, waterfall spray, fireflies or fire straight away. Set it to 0 and the twenty-one dials below are yours, each one with a sentence saying what it changes and what a sensible value looks like. The recipes write into those same dials, so you can also just read one as a worked example of what good numbers look like.
What you can build with it
The chimney on a cottage, steam off a kettle, dust hanging in a shaft of light, spray at the foot of a waterfall, fireflies over a meadow at dusk, a campfire, a smoking wreck, a fog bank, sparks off a forge. Almost every region that feels alive is doing it with a handful of these and nothing else.
How to use it
Settings you can change
24Edit these at the top of the script. Everything works on the defaults · change them only when you need to.
// =====================================================================
// Particle Emitter
// skimi3d script library
// https://grid.skimi3d.com/scripts/particle-emitter
//
// Smoke, steam, dust, spray, fireflies, fire. One emitter with every
// dial named in plain words, and six recipes so you can have something
// believable running before you understand any of them.
//
// Almost everybody's first particle script is one they were given, and
// most people keep using it for years without ever finding out what the
// numbers do, because the usual version is a wall of constants with no
// names. This one is meant to be the last one you need to copy: set
// RECIPE to a number and it works; set RECIPE to 0 and every dial below
// is yours.
//
// Plain LSL.
// =====================================================================
// ---------------------------------------------------------------------
// The quick way
// ---------------------------------------------------------------------
// 0 = ignore this line, use exactly what is written below
// 1 = smoke, from a chimney
// 2 = steam, from a kettle or a vent
// 3 = dust, motes hanging in a sunbeam
// 4 = spray, at the foot of a waterfall
// 5 = fireflies, on a summer evening
// 6 = fire
//
// Any number but 0 overwrites the settings below when the script starts.
// Change the number, save, and look. When one of them is nearly right,
// set RECIPE back to 0 and tune the lines by hand.
integer RECIPE = 1;
// ---------------------------------------------------------------------
// The dials
// ---------------------------------------------------------------------
// Name of a texture in this prim's Contents. Left empty you get the
// default soft blob, which is the right answer far more often than
// people expect: smoke and steam are blobs.
string TEXTURE = "";
// The colour each particle starts and ends as. It fades from one to the
// other across its life.
vector START_COLOUR = <0.25, 0.25, 0.25>;
vector END_COLOUR = <0.60, 0.60, 0.60>;
// How solid it is at birth and at death, from 0.0 to 1.0. Ending at 0.0
// is what makes a puff dissolve instead of blinking out.
float START_ALPHA = 0.6;
float END_ALPHA = 0.0;
// How big each particle is at birth and at death, in metres. The third
// number is ignored; particles are flat squares facing you. Nothing
// above 4.0 has any effect.
vector START_SIZE = <0.4, 0.4, 0.0>;
vector END_SIZE = <3.5, 3.5, 0.0>;
// How many seconds a single particle lives. This and SPEED together
// decide how far the plume reaches.
float PARTICLE_LIFE = 10.0;
// Seconds between puffs, and how many particles come out in each one.
// A small RATE with a small PER_BURST is smoother, and cheaper, than a
// large one of either.
float RATE = 0.25;
integer PER_BURST = 1;
// How wide the mouth of the emitter is, in metres. 0.0 emits from a
// point; a few metres scatters the birthplaces over an area, which is
// how you fill a room with dust rather than a corner.
float RADIUS = 0.05;
// How fast particles leave, in metres a second. Giving the two numbers
// different values is most of what makes a plume look natural.
float SPEED_MIN = 0.15;
float SPEED_MAX = 0.30;
// A steady push on every particle after it is born, in metres a second
// squared. <0.0, 0.0, -9.8> is real gravity, which is far too strong
// for anything but rain. Up is how smoke rises, down is how spray falls.
vector PUSH = <0.0, 0.0, 0.12>;
// Which way they come out:
// 1 = straight down, no push at all, for a leak or a drip
// 2 = a cone, aimed along the prim's Z. Turn the prim to aim it.
// 3 = every direction at once, for dust and fireflies
// 4 = a hollow cone, a ring rather than a beam
integer PATTERN = 2;
// The cone, in degrees, from its middle outwards. 0 and 10 is a narrow
// plume; 0 and 90 is a hemisphere; 40 and 45 is a thin ring.
float CONE_INNER = 0.0;
float CONE_OUTER = 10.0;
// Let the region's wind carry them. On for anything airborne and slow,
// off for anything with a mind of its own.
integer FOLLOW_WIND = TRUE;
// Ignore the region's light, so the particles stay bright at midnight.
// Right for fire, fireflies and neon; wrong for smoke and dust, which
// should go dark when the sun does.
integer BRIGHT = FALSE;
// Extra glow, from 0.0 to 1.0. Small numbers only.
float GLOW = 0.0;
// Is it emitting as soon as it is rezzed?
integer ON_AT_START = TRUE;
// Touch to stop and start it.
integer TOUCH_TO_TOGGLE = FALSE;
// Who may touch it. 0 = everyone, 1 = only the owner, 2 = same group.
integer WHO_MAY_TOUCH = 1;
// ---------------------------------------------------------------------
// Everything below here is the machinery
// ---------------------------------------------------------------------
integer gOn = FALSE;
// The recipes write straight into the dials above, so that everything
// downstream only ever reads one set of numbers. It also means you can
// read a recipe as an example of what sensible values look like.
cook(integer n)
{
if (n == 1)
{
// Smoke · a chimney. Grey, slow, growing as it climbs, and let
// the wind bend it.
TEXTURE = "";
START_COLOUR = <0.25, 0.25, 0.25>; END_COLOUR = <0.60, 0.60, 0.60>;
START_ALPHA = 0.60; END_ALPHA = 0.0;
START_SIZE = <0.40, 0.40, 0.0>; END_SIZE = <3.50, 3.50, 0.0>;
PARTICLE_LIFE = 10.0;
RATE = 0.25; PER_BURST = 1; RADIUS = 0.05;
SPEED_MIN = 0.15; SPEED_MAX = 0.30;
PUSH = <0.0, 0.0, 0.12>;
PATTERN = 2; CONE_INNER = 0.0; CONE_OUTER = 10.0;
FOLLOW_WIND = TRUE; BRIGHT = FALSE; GLOW = 0.0;
}
else if (n == 2)
{
// Steam · a kettle or a vent. White, quick, gone in a moment.
TEXTURE = "";
START_COLOUR = <1.0, 1.0, 1.0>; END_COLOUR = <1.0, 1.0, 1.0>;
START_ALPHA = 0.70; END_ALPHA = 0.0;
START_SIZE = <0.15, 0.15, 0.0>; END_SIZE = <1.20, 1.20, 0.0>;
PARTICLE_LIFE = 3.0;
RATE = 0.08; PER_BURST = 2; RADIUS = 0.02;
SPEED_MIN = 0.40; SPEED_MAX = 0.80;
PUSH = <0.0, 0.0, 0.40>;
PATTERN = 2; CONE_INNER = 0.0; CONE_OUTER = 12.0;
FOLLOW_WIND = TRUE; BRIGHT = FALSE; GLOW = 0.0;
}
else if (n == 3)
{
// Dust · motes in a sunbeam. Barely moving, barely there, and
// scattered over a wide birthplace so they fill a room.
TEXTURE = "";
START_COLOUR = <1.0, 0.95, 0.85>; END_COLOUR = <1.0, 0.95, 0.85>;
START_ALPHA = 0.35; END_ALPHA = 0.0;
START_SIZE = <0.05, 0.05, 0.0>; END_SIZE = <0.05, 0.05, 0.0>;
PARTICLE_LIFE = 20.0;
RATE = 0.40; PER_BURST = 4; RADIUS = 2.50;
SPEED_MIN = 0.01; SPEED_MAX = 0.06;
PUSH = <0.0, 0.0, -0.005>;
PATTERN = 3; CONE_INNER = 0.0; CONE_OUTER = 0.0;
FOLLOW_WIND = TRUE; BRIGHT = TRUE; GLOW = 0.02;
}
else if (n == 4)
{
// Spray · the foot of a waterfall. Many, short-lived, thrown up
// and pulled hard back down.
TEXTURE = "";
START_COLOUR = <0.85, 0.92, 1.0>; END_COLOUR = <1.0, 1.0, 1.0>;
START_ALPHA = 0.50; END_ALPHA = 0.0;
START_SIZE = <0.25, 0.25, 0.0>; END_SIZE = <0.90, 0.90, 0.0>;
PARTICLE_LIFE = 2.5;
RATE = 0.04; PER_BURST = 6; RADIUS = 0.60;
SPEED_MIN = 0.30; SPEED_MAX = 1.20;
PUSH = <0.0, 0.0, -2.50>;
PATTERN = 2; CONE_INNER = 0.0; CONE_OUTER = 25.0;
FOLLOW_WIND = FALSE; BRIGHT = FALSE; GLOW = 0.0;
}
else if (n == 5)
{
// Fireflies · a summer evening. Few, slow, glowing, and spread
// over several metres so they read as a swarm and not a fountain.
TEXTURE = "";
START_COLOUR = <0.90, 1.0, 0.40>; END_COLOUR = <0.40, 0.80, 0.20>;
START_ALPHA = 1.0; END_ALPHA = 0.0;
START_SIZE = <0.06, 0.06, 0.0>; END_SIZE = <0.03, 0.03, 0.0>;
PARTICLE_LIFE = 12.0;
RATE = 0.50; PER_BURST = 2; RADIUS = 4.0;
SPEED_MIN = 0.05; SPEED_MAX = 0.25;
PUSH = <0.0, 0.0, 0.0>;
PATTERN = 3; CONE_INNER = 0.0; CONE_OUTER = 0.0;
FOLLOW_WIND = FALSE; BRIGHT = TRUE; GLOW = 0.25;
}
else if (n == 6)
{
// Fire · orange at the base, red as it dies, shrinking rather
// than growing. Shrinking is the part people leave out, and it
// is why their fire looks like orange smoke.
TEXTURE = "";
START_COLOUR = <1.0, 0.75, 0.25>; END_COLOUR = <0.90, 0.15, 0.05>;
START_ALPHA = 0.85; END_ALPHA = 0.0;
START_SIZE = <0.35, 0.35, 0.0>; END_SIZE = <0.08, 0.08, 0.0>;
PARTICLE_LIFE = 1.4;
RATE = 0.05; PER_BURST = 3; RADIUS = 0.12;
SPEED_MIN = 0.25; SPEED_MAX = 0.55;
PUSH = <0.0, 0.0, 0.60>;
PATTERN = 2; CONE_INNER = 0.0; CONE_OUTER = 15.0;
FOLLOW_WIND = FALSE; BRIGHT = TRUE; GLOW = 0.12;
}
}
emit(integer on)
{
if (!on)
{
// An empty list is how you stop. There is no separate off.
llParticleSystem([]);
gOn = FALSE;
return;
}
integer flags = PSYS_PART_INTERP_COLOR_MASK | PSYS_PART_INTERP_SCALE_MASK;
if (BRIGHT) flags = flags | PSYS_PART_EMISSIVE_MASK;
if (FOLLOW_WIND) flags = flags | PSYS_PART_WIND_MASK;
integer pattern = PSYS_SRC_PATTERN_ANGLE_CONE;
if (PATTERN == 1) pattern = PSYS_SRC_PATTERN_DROP;
if (PATTERN == 3) pattern = PSYS_SRC_PATTERN_EXPLODE;
if (PATTERN == 4) pattern = PSYS_SRC_PATTERN_ANGLE_CONE_EMPTY;
string tex = "";
if (TEXTURE != "")
{
if (llGetInventoryType(TEXTURE) == INVENTORY_TEXTURE)
{
tex = TEXTURE;
}
else
{
// Naming a texture that is not there gives you the default
// blob and no explanation, which is a bad afternoon.
llOwnerSay("There is no texture called \"" + TEXTURE + "\" in my Contents, so I am using the default one.");
}
}
llParticleSystem([
PSYS_PART_FLAGS, flags,
PSYS_SRC_PATTERN, pattern,
PSYS_SRC_TEXTURE, tex,
PSYS_PART_START_COLOR, START_COLOUR,
PSYS_PART_END_COLOR, END_COLOUR,
PSYS_PART_START_ALPHA, START_ALPHA,
PSYS_PART_END_ALPHA, END_ALPHA,
PSYS_PART_START_SCALE, START_SIZE,
PSYS_PART_END_SCALE, END_SIZE,
PSYS_PART_START_GLOW, GLOW,
PSYS_PART_END_GLOW, GLOW,
PSYS_PART_MAX_AGE, PARTICLE_LIFE,
PSYS_SRC_BURST_RATE, RATE,
PSYS_SRC_BURST_PART_COUNT, PER_BURST,
PSYS_SRC_BURST_RADIUS, RADIUS,
PSYS_SRC_BURST_SPEED_MIN, SPEED_MIN,
PSYS_SRC_BURST_SPEED_MAX, SPEED_MAX,
PSYS_SRC_ACCEL, PUSH,
PSYS_SRC_ANGLE_BEGIN, CONE_INNER * DEG_TO_RAD,
PSYS_SRC_ANGLE_END, CONE_OUTER * DEG_TO_RAD,
// Zero means the emitter itself never stops. Everything else
// here is about the particles, this one line is about the
// source.
PSYS_SRC_MAX_AGE, 0.0
]);
gOn = TRUE;
}
integer mayTouch(key who)
{
if (WHO_MAY_TOUCH == 1) return who == llGetOwner();
if (WHO_MAY_TOUCH == 2) return llSameGroup(who);
return TRUE;
}
default
{
state_entry()
{
if (RECIPE != 0) cook(RECIPE);
emit(ON_AT_START);
}
touch_start(integer total)
{
if (!TOUCH_TO_TOGGLE) return;
key who = llDetectedKey(0);
if (!mayTouch(who))
{
llRegionSayTo(who, 0, "This one is not yours to switch.");
return;
}
emit(!gOn);
}
changed(integer what)
{
// A texture dropped in after the fact should be picked up
// without anyone having to remember to reset the script.
if (what & CHANGED_INVENTORY)
{
if (gOn) emit(TRUE);
}
}
on_rez(integer start)
{
llResetScript();
}
}
// ---------------------------------------------------------------------
// Three things worth knowing before you spend an evening on this
//
// Particles are drawn by the viewer, not by the region, so they cost
// the sim almost nothing and cost the person looking at them quite a
// lot. A hundred emitters is not a lag problem for your region; it is a
// lag problem for everyone who visits it. RATE and PER_BURST are the
// two numbers that decide how expensive you are being.
//
// Every viewer has a limit on how many particles it will draw at once,
// and visitors set it themselves. Your careful plume may be thinner for
// somebody else, or absent. Never build something that only reads
// correctly if the particles are there.
//
// A particle is always a flat square facing the camera. It cannot be
// turned, it cannot be lit, and it does not go behind things properly
// when it overlaps other transparent surfaces. Fighting any of those
// three is how afternoons disappear.
// ---------------------------------------------------------------------
Copy it, make a new script inworld, paste over what is there and save. Nothing here phones home.