World Particle System

From The DarkMod Wiki
Jump to: navigation, search

The world particle system allows particles to exist in the world independent of any emitter entity. It enables effects like trailing smoke where the emitted smoke rises naturally instead of moving with the emitter, as well as fine-grained control of the emission of individual particle quads.

TDM 2.03 supports 3 particle systems: weather patches, that emit particles like rain or snow from a wide surface; func_emitters, entities that emit particles from a point; and the world particle system that's controlled by func_smokes and scripts. If you want a particle effect to be emitted from a stationary spot, use a func_emitter or a weather patch. If you want a particle that should always move with its emitter, for example a lamp glare, use a func_emitter bound to the light source. World particles are for effects that once released take their own path independent of the movement of any emitter, like trailing smoke, or for effects where you need fine-grained control of the release of individual particle quads.

Usage

Func_smoke

Func_smoke entities use the world particle system. Use them like a func_emitter, except that the particle effect is specified by the "smoke" spawnarg instead of "model".

Func_smokes can be triggered on and off like func_emitters. They also support the "start_off" spawnarg.

If bound to a moving object, func_smokes will leave their particles trailing behind them. The two flasks in the video are bound to rotating gears and have the same particle attached (mcu_lilpipesmoke.prt): the left one uses a func_emitter, the right one a func_smoke.

Video demo of the difference

Scripting

Script event sys.emitParticle is used for world particle system emissions.

From the script reference:

scriptEvent float emitParticle(string particle, float startTime, float diversity, vector origin, vector angle);

Start a particle effect in the world without using an entity emitter. Will emit one quad per particle stage when first called with sys.getTime() as the start time. Designed to be called once per frame with the same startTime each call to achieve a normal particle effect, or on demand with sys.getTime() as the startTime for finer grained control, 1 quad at a time. Returns True (1) if there are more particles to be emitted from the stage, False (0) if the stage has released all its quads.
particle: String: name of particle effect.
startTime: Game seconds since map start: use sys.getTime() for the first call unless you want to back-date the particle so that it starts part way through its cycle.
diversity: Randomizer value between 0 and 1. All particles with the same diversity will have the same path and rotation. Use sys.random(1) for a random path.
origin: Origin of the particle effect.
angle: Axis for the particle effect. Use $<entityname>.getAngles() to align the particle to an entity. use '0 0 0' for an upright (world-aligned) particle effect.
Spawnclasses responding to this event: idThread

"origin" is where the quad will be released from. You can use an entity origin as reference: using $player1.getOrigin(), for example, will emit the quad from between the player's feet. Using $guard2.getEyePos() will emit it from guard2's eyes.

Once you have emitted a particle quad, you have no further control over it. It will go its own way according to the path specified in the particle decl.

The script event is designed to be called every frame if you want to achieve "normal" particle emission as specified in the particle decl. It needs to be given a start time, which it uses to determine whether it should emit a quad or not right now.

Single quad emission

Calling emitParticle with the current time -- sys.getTime() -- as the startTime parameter will always emit a quad, the first that would be emitted by the specified particle effect. That's because particle effects always emit their first quad as soon as they start up no matter what timing is specified in the particle decl.

"Natural" timing

To make sys.emitParticle emit particle quads just like a func_smoke does, using the timings specified in the particle decl, call it with the same "startTime" every frame. Typically your script will start with the current time, and store that time so that you can pass the same time in every frame. Your code will continue doing that until emitParticle returns false (0), meaning that all quads for the particle effect have now been released:

float starttime = sys.getTime();
float more_to_come = 1;

while ( more_to_come ) { more_to_come = sys.emitParticle("tdm_smoke_candleout", starttime, sys.random(1), smoke_origin, '0 0 0'); sys.waitFrame(); }

For example, if you are using a particle effect that emits one quad every second, and you call sys.emitParticle every frame, i.e. 60 times per second, then it will emit 1 particle every 60th call. The 59 calls in between will not emit anything.

Unlike func_emitters and func_smokes, emitParticle does not support cycling particle effects. If you want your emissions to carry on forever, you have to repeat the above instructions for ever in your script, e.g:

while (1) // cycle for ever
{
    float starttime = sys.getTime();
    float more_to_come = 1;
 
    while ( more_to_come )
    {
        more_to_come = sys.emitParticle("tdm_smoke_candleout", starttime, sys.random(1), smoke_origin, '0 0 0');
        sys.waitFrame();
    }
}

Example

The following script emits a trailing smoke particle effect from an extinguished candle using the "natural" timing method and without cycling.

This is an exaggerated version of the standard 2.03 candle smoke effect, where the particle decl has been tweaked to increase the number, size, and duration of the particle quads.

The script runs through the particle effect once without cycling. The only change from the script given above is that it calculates the origin of the candle top every frame from the origin of the candle base and its current orientation, so if the player is moving the candle during the effect, the smoke comes out of the right place.

float candle_height = 17; // height of wick above candle holder 
 
float starttime = sys.getTime();
float more_to_come = 1;
while ( more_to_come )
{
    vector candle_top = sys.angToUp( $candle_holder.getAngles() ) * candle_height;
    vector smoke_origin = $candle_holder.getOrigin() + candle_top;
    more_to_come = sys.emitParticle("tdm_smoke_candleout", starttime, sys.random(1), smoke_origin, '0 0 0');
    sys.waitFrame();
}

Video of the result

YouTube video

Performance considerations

Particles emitted by weather patches and func_emitters are rendered only if the particle effect might appear in some area of the map that the player can see. Particle effects on the far side of the map will be "turned off". World particles are different: they are rendered every frame no matter whether the player has any chance of seeing them or not. They won't actually be drawn if not in view -- i.e. converted to screen pixels and coloured in -- but their positions will be calculated and passed to the graphics card every frame. If you have a thousand trailing particle emitters in your map, using a variety of different particle effects, you are likely to see an impact on FPS.

On the other hand, world particles enjoy some performance advantages over other types of particle. Weather patches and func_emitters each generate their own individual draw call every frame that they are in view. If you have two smoking torches next to one another, that's 2 draw calls each frame for the smoke.

All world particles of the same kind will be drawn in a single draw call, no matter how spread out they are in the map. So adding more world smoke emitters will increase calculations a bit, but it won't add more draw calls.