Particle collisions and cutoff

From The DarkMod Wiki
Revision as of 16:46, 9 December 2020 by Stgatilov (talk | contribs) (Added section about particle models.)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigationJump to search

This article describes the collision and cutoff features of particle systems, introduced in TDM 2.08. If you want to know what various particle keywords mean, please read Particle Editor article.

Particle lifetime cutoff

The whole feature relies on the idea of lifetime "cutoff".

Normally, every emitted particle lives for the duration specified by time keyword in the particle stage declaration. Different particle are emitted at different time moments (controlled by bunching keyword), but every particle lives for the same time. A particle can go through various transformations during its lifetime, including fading in and out, animations, shape and color changes, etc.

This feature introduces an additional event in particle lifetime called cutoff. A particle is no longer rendered after the cutoff moment. Most importantly, this cutoff moment is specified by cutoff texture and varies among particles. This flexibility allows to bake collisions with static objects into the texture.


Here are all the necessary bits:

  • surface used as emitter in .map
  • particle-emitting material in .mtr
  • particle effect definition in .prt
  • cutoff texture (not needed with collisionStatic)

Specify cutoff texture manually

One approach is to specify cutoff texture manually. Note that you should never do this if you simply want particles to stop at collisions: use collisionStatic keyword instead! However, this would serve as a good warmup before the next section. Besides, this way can be used to set cutoff for some artistic reasons not related to collisions.

First of all, draw a particle-emitting surface in DarkRadiant. It can be either a patch or a brush with 5 nodraw sides.

In most cases the emitting surface must have [0..1] x [0..1] texture coordinates. So select the created surface (i.e. the patch or single side of the brush), open Surface Inspector, and click on Fit button on Fix Texture: 1.00 x 1.00 line.

Now it remains only to apply a particle-emitting material to this surface, but we have to create it first. Please look into materials/ file for an example of particle-emitting material:

       deform particle tdm_rain2_heavy
       qer_editorimage textures/editor/rain
       {  //needed to emit particles
           blend   filter
           map      _white

Copy this definition into your own material and rename to something customized to avoid confusion in future. Note that the first line references a particle definition, which we have not created yet.

IMPORTANT: Use deform particle and never deform particle2, since the latter does not ensure uniform particle distribution across the surface!

The next step is to create a particle definition. Open particles/tdm_weather2.prt file, and copy tdm_rain2_heavy particle definition from it into some .prt file in your FM. Better rename the copied particle definition to something custom to avoid confusion.

   particle tdm_rain2_heavy {
           count               10
           material            textures/particles/drop2
           time                0.500
           cycles              0.000
           bunching            1.000
           distribution        rect 0.000 0.000 0.000
           direction           cone "0.000"
           orientation         aimed 0.000 0.040
           speed               "1000.000"
           size                "0.500" 
           aspect              "1.000" 
           randomDistribution  0.000
           fadeIn              0.200
           fadeOut             0.000
           color               0.040 0.040 0.040 1.000
           fadeColor           0.000 0.000 0.000 1.000
           offset              0.000 0.000 0.000
           gravity             0.000

IMPORTANT: Ensure that your particle stage has no additional randomization of spawn location, i.e. it contains the exact line: distribution rect 0.000 0.000 0.000 (the aforementioned rain2 definitions are already OK). Otherwise you will have hard time understanding what happens.

In order to attach cutoff texture, you have to add two keywords to particle stage:

  • cutoffTimeMap {path/to/texture/image.tga}: specifies which texture to use as cutoff.
  • mapLayout texture {W} {H}: specifies how the texture should be applied. {W} and {H} must equal width and height of the texture.

Here is an example of added keywords:

           cutoffTimeMap		textures/cutoff/shapedrain.tga
           mapLayout			texture 256 128

Lastly, draw the cutoff texture itself. This texture would be stretched onto the particle-emitting surface, and the color at each point would define the cutoff moment for all particles emitted from it.

More precisely, the color defines which ratio of the lifetime a particle lives before cutoff. If you set full-black color, then particle will die immediately after spawning, you won't see it. If you set full-white color, then particle will live through its whole lifetime without cutoff. Middle-gray color (128 128 128) will make particles disappear after they live through half of their lifetime.

For the purpose of creating manual cutoff textures, only red channel of the texture matters. The green and blue channels are used for additional precision beyond 8 bits.

For a tutorial, you might want to start with some simple black-and-white image and check how it affects the particle system in-game, then continue to some grayscale texture.

Auto-generate cutoff texture from collisions

Now that you understand how cutoff texture works, let's see how it can be auto-generated in order to stop particles on collisions.

The generic workflow is like this:

  • Copy particle definition you like from the core game into your FM. You'll have to modify it.
  • Add collisionStatic keyword to the particle stages that should have collisions.
  • Set mapLayout texture or mapLayout linear keyword to define how collisions will be precomputed. These two modes are described in detail below.

However, the particle keywords are not enough on their own, they only mark the particle system as using collision detection. In order to compute the cutoff maps, you must run the runParticle compiler tool in the game engine. This tool is very similar to dmap in nature and usage. Here are the steps:

  1. dmap {mapname} --- makes sure your .proc file is correct, since runParticle uses it.
  2. runParticle {mapname} --- computes the cutoff maps for all collisionStatic particle systems in the map.

All the generated textures will be saved into textures/_prt_gen/ directory.

IMPORTANT: Never store anything important there! runParticle starts with clearing this directory every time you run it.

The texture filename is composed of the name of the model in .proc file (which is either world area or entity), the index of the surface in the model, and the index of the particle stage in the particle definition file. Well, that's how it works for particle deform surfaces...

If you decide to inspect these maps, better mask off green and blue channels in your image viewer, and look only at the red channel, since it contains the high bits of the cutoff ratio.

The runParticle tool takes everything which looks like static (i.e. never moves or disappears) and solid (e.g. not a light flare) geometry and considers them blockers. In reality, it is quite hard to reliably distinguish static geometry. While the tool has some complicated ruled built in, in some cases it might be necessary for tweak something manually. Of course, better keep such tweaking at minimum level.

There are two entity spawnargs which override automatic decisions:

  • particle_collision_static_blocker {1 or 0 or 2}: consider this entity a blocker / not a blocker for particle collision computations / force it as a blocker
  • particle_collision_static_emitter {1 or 0}: compute collisions for particle-emitting surfaces of this entity / don't compute them

Aside from that, you can add collisionStaticWorldOnly keyword to particle definition file: in this case only world geometry (brushes and patches) will be considered blockers.

Texture layout

This mode was the only available in TDM 2.08. We will consider rain as an example.

Copy particle definition tdm_rain2_heavy from particles/tdm_weather2.prt file into your FM. Then add the following keywords:

  • collisionStatic: says that the particles should stop on collisions with static objects.
  • mapLayout texture {W} {H}: specifies that cutoff is applied by texcoords, and sets resolution of the cutoff texture.

After that use dmap + runParticle as described above.

The texture resolution affects how precise collisions would be depending on spawn location. For instance, if you apply one 1000 x 1000 rain-emitting patch over the whole map of size 3000 x 3000, then all particles emitted 3 length units from each other will stop at the same height. Such precision is more than enough for rain. If you create small patches for something more localized, better use lower resolution to avoid wasted storage and performance consequences.

The mapLayout texture mode imposes many restrictions on the particle definition. The tdm_rain2_XXX definitions adhere to all these restrictions. Here are they for the reference:

  • distribution rect 0 0 0, i.e. no additional randomization of spawn location
  • direction cone 0 and gravity 0, so that all particles travel along parallel lines
  • worldAxis is forbidden

Another important rule for mapLayout texture layout is that different locations on the surface emitter must have different texture coordinates. The tool will throw error if it detects that several locations have same texture coordinates, since it cannot write two values into one texel.

On the other hand, it is perfectly normal if some parts of the cutoff texture are not used by the emitter. Given that dmap carves away some parts of the patch which are inside opaque brushes, this happens almost always. The generated texture has all-zero color at such unused locations, including zero alpha channel.

Another interesting fact is that dmap splits a patch which spans over several areas. In a typical usage scenario, mapper applies a single huge rain-emitting patch over the whole map, and this patch gets splitted into many pieces: one piece per every area it covers. A separate texture is generated for every piece. It is worth noting that runParticle computes minimum bounding rectangle of all the used texels and does not save anything outside of it. So don't be surprised that texture resolution for those pieces is much lower than the resolution you specified: you set resolution for the whole [0..1] x [0..1] region.

The optimization mentioned above can be used to create non-rectangular surface emitters. Let's suppose that you have a map with a very tall building in the center, and outdoors area surrounding it. If you create one rain patch crossing the building, then rain would go inside building. To fix this, you can use CSG/boolean operation to subtract a rectangular region from the patch (well, I'm afraid DR cannot do this, so consider it a mind experiment). The remaining region would consist of many patches with various texcoords rectangles, but everything would work normally, and the effective cutoff texture resolution would remain the same.

Linear layout

This mode was added in TDM 2.09. We will consider snow as an example.

Copy particle definition tdm_snow_moderate from particles/tdm_weather.prt file into your FM. Then add the following keywords into every particle stage:

  • collisionStatic: says that the particles should stop on collisions with static objects.
  • mapLayout linear: specifies that cutoff would be applied by particle index and cycle number.
  • diversityPeriod {K}: says that particle system should exactly repeat itself every K-th cycle.
  • collisionStaticTimeSteps {T}: defines that trajectory of a particle should be approximated with a polyline with T segments.

After that use dmap + runParticle as described above.

While texture layout precomputes how cutoff depends on texcoords, linear layout works completely differently and exploits deterministic nature of PRNG. It iterates over all possible particle trajectories of a given system and precomputes cutoff for each of them.

Without diversityPeriod keyword, the number of possible trajectories would be almost unlimited, because every cycle particles have different random trajectories. That's why this keyword is required. You should set it to minimum positive integer such that the periodic behavior of the system is not apparent when you look at it. For long-living particles it should be set to 1. For a particle system with half-second cycle, it might be necessary to set it to 10 to avoid noticeable repetition.

The collisionStaticTimeSteps keyword specifies how to approximate trajectories. The particles can easily move along curved paths, and the engine cannot collide such paths exactly with the world. So unless particles move along straight lines, runParticle tool approximates their trajectories with evenly-sampled polyline having T line segments. Collision is checked independently for every line segment, so high value can result in arbitrarily long time needed for runParticle to finish. On the other hand, setting low value can result in particles stopping near obstacle boundaries while they should continue flying (or vice versa).

Suppose that you have N particles in your particle system, diversityPeriod is K and collisionStaticTimeSteps is T. runParticle tool iterates over each of N particles and each of K cycles, and computes one cutoff value for them by colliding T line segments against the world. So:

  • Generated cutoff texture would have K * N texels.
  • runParticle will have to intersect K * N * T line segments with the world in order to compute it.

This simple math should be enough to convince everyone that values K and T should not be set too high =)

Inspecting linear-layout cutoff maps is pretty useless: the trajectories are packed in random order there, so you would only see random noise. Compressing such textures is pretty useless too.

Unlike texture layout, the linear layout mode is rather unstable. If you change some parameters in particle definition even slightly, the old cutoff texture won't work anymore.

Particle models

Everything described above applies only to surface emitters, i.e. patches/brushes with a material with "particle deform" applied to it. Starting from TDM 2.09, collisions are partly supported for more popular "particle models" too. Only mapLayout linear makes sense in this case.

However, there are many limitations for particle models (see also #5437):

  • Only particle models explicitly listed in .map file work. So there must be a map entity with "model" spawnarg having "XXX.prt" value. Particle models inside def_attach-ed entities are ignored.
  • Entity with several models pointing to same .prt don't work (like chandeliers).
  • If entity's diversity is randomized by some in-game code, then precomputed collisions won't match. Randomization is done at least for func_emitter-s and in light scripts.

It is not yet clear how this area would develop in future.


  • runParticle complains about overlapping texcoords.

Check if you really have this problem, and fix it if you have. Alternatively, you can switch to mapLayout linear, which does not care about texcoords.

  • Generated textures have missing triangles.

Such errors happened on first implementations, caused by dmap issues. They resulted in triangular zones without rain, which can be detected in-game using r_showTris 1. Also, they are easy to see in generated textures, since dropped triangles have zero color and alpha. If you have some problems, report them on forums.

  • Rain does not reach ground, i.e. stops before collision.

The collisionStatic feature allows to hide particles prematurely, but cannot extend their lifetime. If rain does not reach ground, then you have to increase time keyword in particle definition. Keep in mind that increasing lifetime in K times effectively decreases particle density in K times, so you might need increasing count too.

  • Cannot see any particles.

Check that emitting patch is facing the right direction: it should be visible in DR from the side where particles go to. Also, try r_showTris 1 to be sure that they are not invisible.

  • Rain falls through water, even with particle_collision_static_blocker 1.

This happens if the material of the water surface does not have the "water" contents/keyword. A surface is allowed to block particles only if it has either "solid" or "water" content flag, other materials never block particles.

  • runParticle does not finish in reasonable time.

With mapLayout linear, quite a lot of precomputation can be needed. Check the math above. Note that count value in particle definition defines number of emitter particle per 64x64 square, so for a large emitter total the number of particles N is much larger. You might want to disable collisionStatic and see how it works/looks, after that set minimum values of count, diversityPeriod, and collisionStaticTimeSteps. From there, increase them slightly as needed.

  • Collisions don't work with mapLayout linear, particle pass through.

As explained in linear-layout section, this mode is quite unstable. Any difference between the situation during precomputation and during gameplay may result in cutoff texture mismatch. Make sure you have done dmap and runParticle properly, and if it does not help, ask for investigation on forums. If this is "particle model", then also read appropriate section about limitations.

  • Rain disappears under some viewing angles (old TDM).

This was a bug in bounding box computation of particle systems. It should be fixed in TDM 2.09 (#5136). Before that, the problem was hacked around by setting a large boundsExpansion value to particle definition. This hack breaks culling of particle systems, hence degrades performance --- don't use it any more.

  • cutoffTimeMap does not work (old TDM)

Previously cutoffTimeMap did not work with com_smp 1 (Multi Core Enhancement). It should be fixed in TDM 2.09 (#5141).

Possible future improvements

The following extensions are planned for future:

  • Animate to different material after cutoff, which would allow to generate splashes from rain.
  • Full support for particle models, including attachments, func_emitters, etc.


You can download a sample test map with collision-aware rain and snow here.

Related threads on developers-only forums: