Case Study: Geometry Nodes Closures for Particle Emitters

Closures and Bundles are new concepts in Geometry Nodes that have recently been added to the main branch as an experimental feature. You can read more about the basic idea in the pull request #128340 or the workshop blogpost. In this post i want to explore how closures can be used to implement emitters for particle systems as a way to establish best practices and maybe find shortcomings that need to be addressed in the future.

Blend file: emitter-closures.blend
Important:

  • Use a recent build that has the closures-and-bundles feature (4. April or later).
  • Enable the “Bundle and Closure Nodes” feature in Preferences > Experimental > New Features


Bundle

Closure

A particle system typically starts out empty and then uses one or more emitters to generate particles and insert them into the system. The emitter only defines the initial location, velocity, and other properties of particles. Afterwards any new particles are released and simulated alongside existing particles using physics, collision, or any number of other features. I won’t go into detail about these other particle system components here to keep the post focused.

Since there are many different possible types of emitters and many ways in which users may want to customize them it’s virtually impossible to make a particle system node group that satisfies all needs. Take the existing particle system implementation in Blender: It implements a wide range of particle distribution methods and customization with textures, and yet still becomes insufficient for all but the simplest cases. The wide range of possible feature combinations makes it hard to test and maintain, and some combinations of settings (e.g. anything involving rotations) are known to fail more often than not.

In my test scenario i’ve implemented a fairly standard physics-based particle system with basic movement, collision, and forces. Particles pass through a number of stages in each time step, the first of which is to generate new particles and combine them with existing ones. In a conventional node setup this would require a user to modify the primary simulation node asset and insert their own nodes. Instead, i’m now using an Evaluate Closure node to delegate particle emission to an external closure. This closure outputs a set of points, which are joined with the existing particles.


Particle system stages

Particle system node from the outside

Evaluating the emitter closure

Big closure with multiple emitters

:information_source: The inputs and outputs of a closure must match the expected inputs and outputs at the point of evaluation. This needs to be communicated outside of the node setup itself in some way. While there is some basic feedback about missing sockets, knowing upfront what an emitter closure should do is important. Templates and example implementations can help, but some other way of documenting the closure “contract” would be helpful in the future.

Point Emitter

One of the simplest emitter types is a point emitter. Particles spawn at a single point in space. This point can remain fixed, or it can be animated over time.

Values in a closure, such as the emitter position, can be animated either by directly animating node properties (preferably through modifier buttons) or by animating an object and reading from an Object Info node.

:information_source: One might assume that animated values inside a closure can be a problem because the closure “binds” these values. However, the closure itself is not part of the simulation state stored by the simulation zone. Every time the particle system updates the closure is constructed from scratch using the current (animated) value of node sockets and/or scene state.

The number of particles emitted can be defined in different ways:

  1. Burst: A fixed number of particles is emitted at a specific point in time.
  2. Rate: The amount of particles emitted depends on the time step size. The rate can change over time.

With rate-based emission a naive implementation might simply multiply the rate by the time step size and output this amount of particles. However, if this amount is less than 1, no particle would ever be emitted! Any fractional amount of emission would also be ignored, leading to an actual emission rate that is smaller than the nominal rate.

To fix this, the emitter should add up the fractional amount of particles emitted, so that any time the accumulated amount crosses an integer value a particle gets emitted. This way even very low emission rates are possible, spawning a particle only every couple of seconds or so.


Left side: Low emission rate leads to cutoff below 24 (less than 1 particle per frame)
Right side: Cumulative emission count supports arbitrarily low emission rates

Keeping track of emitter state requires some extra data input/output. The closure itself cannot store the emitter state, so the particle system has to store this state information instead. It can store a generic Bundle for all emitter state data, and the closure then reads the necessary value for each emitter from the bundle. The closure returns the modified emitter bundle and the simulation stores it for the next iteration.


Read state for various emitters

Add emission to the state

Write state to bundle

Sphere Emitter and Other Shapes

Only a small change is needed to go from point emitters to sphere emitters and similar geometric shapes. Instead of placing each new particle at the same point, we now randomly distribute particles on the surface of a sphere.

The math for chosing a point on a sphere at random won’t be derived in detail here, suffice to say that a uniform density on a sphere can be achieved by picking a uniform z coordinate in [-1..1] range, radius r = \sqrt{1 - z^2}, and rotation around Z by a random angle. Other geometry primitives such as cylinders, cones, etc. have their own methods for uniform random distributions.

In order to change the random position for every new particle we have to make sure that the ID value for each particle is unique. The “local” index of the new particles will always start at zero, so the randomized positions would also be the same for each new batch of particles. To avoid this we need to assigne a unique ID to each new particle, which the Random utility nodes can use. A simple way to compute a unique ID is to take the “local” index and add the cumulative emission count. The Seed value can also be varied for each emitter to give it a unique distribution.


Unique ID for each particle

Mesh Emitter

A mesh emitter places particles at random locations on the mesh surface. The density of particles should be uniform by default, with an optional factor that can be used to limit emission to certain areas, for example by painting an emission map or a vertex attribute.


Varying face area on Suzanne

Cumulative emission amounts

The overall amount of particles to emit in a single time step is determined by integrating the density over the entire mesh surface. This can be approximated by linear interpolation in each face and a sum over all faces. Since sampling general Ngons is hard, the mesh is first triangulated to simplify the problem. It should be noted that the total emission count is the only value necessary to remember from previous iterations, it is not necessary to store a count per triangle.

:information_source: The Distribute Points on Surface node can be used for a simple mesh emitter implementation. However, it suffers from the same low-rate cutoff problem described for point emitters: if the emission rate is too low, too few or no particles at all will be emitted. There is no way of emitting an exact number of particles with this node, so it isn’t suitable for an accurate emitter at this point.


Total amount is the new emitter state

Chosing a specific mesh face with varying probability would require evaluating the quantile function of the distribution, which is difficult. An easier approach is to determine the amount of emission for each triangle separately, and then generate the required number of particles on each triangle. The emission count is accumulated over all triangles so that particles are distributed over the whole surface. Starting at the total count of the previous frame, like we did for point emitters, ensures a steady increment at low rates. Additionally shuffling the order of mesh faces avoids emitting particles in the same locations every time.

Once the actual integer amount of particles for each face in a time step is computed, a For Each Element zone is used to generate per-triangle points. Attributes for each triangle point, including the position, are interpolated between the three corners of the triangle. A set of 3 barycentric weights is computed and each attribute is mixed using these factors. In the current implementation the weights are chosen uniformly, leading to a “flat” distribution across the face. This is fine for relatively dense meshes, but if actual density interpolation on each triangle is necessary a different technique could be used (e.g. J. Portsmouth, “Efficient barycentric point sampling on meshes”, 2017). As an additional optimization, the corner attribute vectors are packed into a single matrix attribute, which can be interpolated with a single vector transformation (reduces number of sockets by factor 3).


Using a For-Each zone to generate particles

Sample weights for a triangle point

Curve emitter

Similar to a mesh emitter, on a curve emitter the density can be variable. To enable efficient sampling the curve is first converted into a series of simple linear segments, equivalent to triangulating a surface mesh. The “evaluated” curve points are the natural choice for this.

Shuffling curve segments is not possible directly because the curve has a fixed order of points, but this can be circumvented by converting the curve to a point cloud first. Each point represents a small linear curve segment. Any attributes we may need later, such as the curve normal direction, must be captured before conversion.

Similar to mesh sampling, each segment generates a number of points according to its density factor. Two weights are chosen and the attributes of the vertices are interpolated.

Conclusion

The good stuff:

  • You can now customize behavior of complex node-based assets without having to modify the original asset.
  • Only necessary features need to be made available. Node asset UI remains lean instead of providing mostly-unused buttons for every possible scenario.
  • Users can choose between different implementations and balance performance, capabilities, and complexity based on individual needs.

However, they also still have some rough corners and can add more complexity:

  • Closures require a clearly documented signature of inputs and outputs. Templates or example implementations can help with that, but better support in the asset system may be required for those.
  • Currently a closure must always be defined, otherwise the evaluation produces no output. Having a default “pass-through” implementation or a boolean socket to indicate a “null” closure on evaluation would make sure the system works out-of-the-box without having to set up dummy closures.
    Note: With upcoming field support it won’t be necessary to store named attributes as often. An empty field yielding zeroes will often be sufficient as a “default” implementation.
  • Combining multiple emitters requires manually separating and re-combining data in a single closure. Future support for lists could make this more user-friendly. Implementing a quasi-list with a closure is also feasible, but increases the complexity significantly.
  • Having to pass around extra bundles for storage can make things complicated. It’s up to users to make sure the right geometry is passed to a given emitter.
    Note: I’ve tested putting a simulation zone into a closure to cache state internally, but this is unsupported.
    Note 2: It may be possible to put both the closure and supplemental data in the same bundle to only have a single socket, but bundles with closures don’t currently work in simulation zones.

Closures are a valuable addition to geometry nodes. Currently they are a tool for technical users more than artists, but they should become more accessible over time. I hope this initial set of emitters can grow into a truly node-based particle system in the future.

30 Likes

Hi,

I find this progress both surprising and very convincing. Congratulations on your hard work, it’s truly impressive to see what this new approach with closures can achieve.

That said, I’d like to point out a few limitations, particularly regarding performance, Geometry Nodes have difficulty running above 20 FPS on simple meshes, and can drop to 1-2 FPS on curves or denser geometries. (On My config Ryzen 7 7745h + RTX 4070 8 GO)

Beyond performance, one of the biggest hurdles remains the complexity of use. Even by compacting the nodes into custom groups, it remains very difficult for many users to grasp.
I’ve seen add-ons offering similar systems with only 4 or 5 nodes, which are much more accessible. It might be an interesting avenue to develop “specialized” or simplified nodes for common tasks, to make everything more intuitive.

Because, let’s be honest, a very large part of the community has to go through tutorials to understand how to use these tools, as they can sometimes seem so “illogical” or unfinished. And sometimes we just want to understand things simply without having to dig around left and right trying to understand, but in the end, we don’t get it and we just stupidly use “As long as it works.” This limits creativity, and it’s a shame given the incredible potential of this tool.
I would really like a “Simple” and intuitive version of Blender’s GN, and not a big pile of nodes and go figure it out yourself!
There should be a “Noob” version and an “Expert” version. Here you’re highlighting that the expert version, although incredible, is far too complex! And we get lost trying to enter nodes within nodes within nodes… It’s not intuitive… in terms of UX, it’s just abominable.

Thank you for taking this comment into consideration, even though it’s intended for LARP projects, not just this feature!

3 Likes

That said, I’d like to point out a few limitations, particularly regarding performance, Geometry Nodes have difficulty running above 20 FPS on simple meshes, and can drop to 1-2 FPS on curves or denser geometries.

Are you referring to the actual particle emitters here or geometry nodes in general? I know that the mesh emitter in particular is not very fast atm. I don’t know exactly what causes this because there is no timing info inside closures yet (this is planned), but i suspect randomizing face order by sorting may be the main issue. I haven’t put much effort into optimizing yet.

Beyond performance, one of the biggest hurdles remains the complexity of use. Even by compacting the nodes into custom groups, it remains very difficult for many users to grasp.

I mention a few sore points in my conclusion at the end. This post is specifically about testing closures, and ease of use is certainly not their strong point yet. I would like to use assets more extensively, but until asset embedding is sorted out it’s not a very viable solution during active development.

4 Likes

For the performance issue i’m been talking about the closure zone (Emitters) :wink: (Even the complex GN are not fast too idk why :thinking: :innocent:).

With you’re Blend file i got 15 FPS with you’re default scene… and Extremly laggy viewport !