// ===================================================================== // 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. // ---------------------------------------------------------------------