PopcornFX DEV. NOTE #2

PopcornFX - realtime fx fractal fracture

Using a density texture to spawn particles on a mesh

We are going to see how to use a new PopcornFX scripting function called “sampleSurfacePCoordsFromUV” to spawn particles on certain parts of a mesh. This function takes a texture coordinate as parameter and returns a parametric coordinate on a mesh surface. Let’s see how to use that to create our effect.
This is the final effect we are trying to achieve:

Using a density texture to spawn particles on a mesh

You can see that particles are only spawning on specific parts of the mesh, which are defined by a density texture.
Let’s go through the creation process now:

  • First we will create the effect using a non-optimized technique, allowing us to dynamically override the mesh and texture on which we are spawning particles.
  • Then, we will create the optimized effect using the new “sampleSurfacePCoordsFromUV” script function. Please note that you cannot dynamically override the mesh or texture with this function, meaning that you will not be able to have multiple instances of this effect with different meshes and textures.

The naive technique

The first step of this technique consists of spawning particles uniformly on the surface of a mesh.
Create a new attribute sampler shape as below:

Create a new shape attribute sampler

The attribute samplers have the advantage of being overridable in the game engine in which you will import your effect, meaning that you will be able to reuse the same effect with different meshes.
Here is what the attribute samplers will look like when imported in Unity for example:

PopcornFX effect instantiated in Unity 3D

Please notice that it gives you the opportunity to choose a mesh and a texture from your Unity assets to override the samplers “SpawnMesh” and “SpawnDensity”, meaning that you will be able to have multiple instances of this effects with different meshes and textures.
Back to the effect in the PopcornFX editor: Let’s set the mesh you want to test your effect with as “3D Layers (Effect)” backdrop:

Create a mesh backdrop

Backdrops are just here for the sake of seeing meshes in the editor, to test the physical interactions of your particles and be used as default values for the attribute samplers. They will not appear in the game engine once your effect is exported.

Note that if you bring in a custom FBX mesh, you will need to locate it in the content-browser and right-click ‘Build asset’ to be able to use it in the effect editor.
In the attribute panel, select the backdrop in the drop-down “Editor binding”

Overriding an attribute sampler with a backdrop

When you will import the effect in the game engine used for production, you will be able to choose a mesh from your scene to override the “SpawnMesh” sampler.
Now, create a new layer and add this code to the spawn script:

Position = SpawnMesh.samplePosition();

Your particles should now be spawning on the surface of the mesh.

Spawn the particles to match the density of the texture

First of all, create a texture containing any spawn information you need. Please note that, in this example, texture color is just used as a mask to decide whether to spawn a particle or not:

PopcornFX density texture

Secondly, add a texture attribute sampler to your effect:

Create a new texture attribute sampler

You can now sample this texture from the mesh UV to get the color info.
Here is the new spawn script we did (SpawnDensity is the name of the texture attribute sampler):

function void    Eval()
{
    Size = 0.01;
    Color = float4(1.0);

    // Sample a random coordinate on the mesh:
    int3 coords = SpawnMesh.samplePCoords();

    // Set the position of the particles:
    Position = SpawnMesh.samplePosition(coords);

    // Set the velocity so that the particles slowly get away from the mesh surface:
    Velocity = SpawnMesh.sampleNormal(coords) * 0.1;

    // Get the UV for this coordinate on the mesh:
    float2 uv = SpawnMesh.sampleTexcoord(coords);

    // Use the UV to sample the texture color:
    float4 textureColor = SpawnDensity.sample(uv, textureFilter.Linear, textureAddr.Wrap);

    // Here we use a "random select" to choose the life duration of the particle.
    // We choose either 0.0 (the particle is not spawned) or 1.0 second depending on the
    // intensity of the red channel of the texture color (textureColor.x)
    // So, the more red there is on a pixel of the texture,
    // the more the particle spawned on this pixel has a chance to live.
    Life = randsel(0.0, 1.0, textureColor.x);
}

 

In the example below, most of the particles will be killed as soon as they are spawned because their life will be set to zero, so you have to set the spawn rate to a large value to get a nice result. Here, to get ~33,000 particles, we had to set the spawn count to 1,000,000 particles per second:

Changing the spawn count on a layer

You can see here that the Newborns take around 40% of the update time in the performance graph of the viewport HUD:

Performance view with a lot of discarded particles

This means that more than 40% of the time is spent spawning particles among which only about 3% will actually live their full life. All the others will be killed instantly on the first frame, because they spawned outside the white pixels of the density texture. The optimized way of doing this is to only actually spawn particles on the part of the texture we are interested in, so no particle has to be killed at spawn.

Optimized way of sampling the texture density

To start with, take the same effect and remove the mesh attribute sampler. It needs to be set as a regular sampler. The same goes for the texture attribute sampler:

Create a regular shape sampler

You can remove the mesh backdrop and just select the mesh you want to use in the “MeshResource” field of the sampler.

Selecting a mesh for a shape sampler

Make sure that the “Sampler” property of the texture sampler is either set to “Density” or “Both”.

Changing the texture sampling mode to Density

This will allow you to call the “sampleDensity” function to get texture coordinates depending on the texture color intensity.
See the texture sampler online documentation for more details.

Then, open the mesh in the PopcornFX content browser, check the “WriteAccelUV2PC” property, and rebuild the asset from the FBX to create the “UV to PCoords” acceleration data structure:

Baking a mesh with the UV to PCoords acceleration structure

Besides, here are the reasons why you can no longer use attribute samplers:

  • A “probability density function” is built for the texture sampler when your effect is loaded, this is what allows you to call “sampleDensity“.
  • An acceleration structure is baked inside the mesh that will allow you to get parametric coordinates on the mesh from a texture coordinate using “sampleSurfacePCoordsFromUV“, as long as your mesh doesn’t have multiple triangles with overlapping UVs (the same texture coordinate is mapped on multiple triangles of the mesh).

View of the quadtree used to convert UVs to parametric coordinates on a mesh

The acceleration structure used by “sampleSurfacePCoordsFromUV“. You can see the unwrap of the mesh triangles in UV space as well as the tiles from the quadtree used to optimize the search.
Being able to override those samplers would mean that we need to rebuild these two data structures at runtime, which would be too costly.
(In the previous example, it was the opposite and we retrieved the texture coordinates from the parametric coordinates with “sampleTexcoord“)
Now, set the spawn rate of the layer to ~33 000 particles/second, and type this code in your spawn script:

function void    Eval()
{
    Size = 0.01;
    Color = float4(1.0);

    // We start by getting a UV depending on the texture density:
    float2 uv = SpawnDensity.sampleDensity();

    // Get the coordinates on the mesh corresponding to this UV:
    int3 coords = SpawnMesh.sampleSurfacePCoordsFromUV(uv);

    // Set the position and velocity of the particles:
    Position = SpawnMesh.samplePosition(coords);
    Velocity = SpawnMesh.sampleNormal(coords) * 0.1;

    // The particles are only spawned where they need to, so we do not
    // have to instant-kill the particles that should not be spawned:
    Life = 1.0;
}

 

As no particles are killed during the first frame anymore, all the 33 000 particles will live their full life, so the effect will look the same, but you should now have way better performance:

Performance view using sampleSurfacePCoordsFromUV

Here the newborn particles only take about 3% of the time. Much better.
Now let’s see how this influences the global performance of the effect. The green graph below represents the CPU time per frame.
On the left, it’s the first “non-optimized” version of this effect with the discarded particles and on the right the second method:

Non-optimized version of the effect

Optimized version of the effect

 

Note that we divided the update time by more than two, going from 0.32 ms to 0.14 ms.
Also, performance is way more stable with the second technique: you can see that there are less spikes on the graph.

Conclusion

Be aware that this technique is still a bit limited: you will not be able to override the texture or the mesh and your mesh needs to have non-overlapping texture coordinates for the “sampleSurfacePCoordsFromUV“.
However, it is really efficient, allowing users to avoid a lot of unnecessary particle spawns and drastically improving performance!

 

Paul Baron

Graphics programmer at PopcornFX,
Persistant Studios