Glitter
Glitter is LibrarianLib’s high-performance particle module, which is inspired primarily by Unreal Engine’s Niagara particle systems.
As an example of the performance, here are over sixty thousand particles with full world collision running at a solid 30 FPS. Part of the reason the system is so performant is that it’s designed to do everything with nearly zero object allocations, meaning there’s barely any memory churn even with tens of thousands of particles active at once.
For reference and as an easy jumping off point, here’s a complete particle system. All of this will be explained further down.
~import com.teamwizardry.librarianlib.glitter.ParticleSystem;
~import com.teamwizardry.librarianlib.glitter.bindings.ConstantBinding;
~import com.teamwizardry.librarianlib.glitter.bindings.StoredBinding;
~import com.teamwizardry.librarianlib.glitter.modules.BasicPhysicsUpdateModule;
~import com.teamwizardry.librarianlib.glitter.modules.SpriteRenderModule;
~import net.minecraft.util.ResourceLocation;
~
public class QuickstartSystem extends ParticleSystem {
@Override
public void configure() {
StoredBinding position = bind(3);
StoredBinding previousPosition = bind(3);
StoredBinding velocity = bind(3);
StoredBinding color = bind(4);
getUpdateModules().add(new BasicPhysicsUpdateModule(
position, previousPosition, velocity
));
getRenderModules().add(
SpriteRenderModule.build(
new ResourceLocation("modid", "textures/particle/sprite.png"),
position
)
.previousPosition(previousPosition)
.color(color)
.size(0.25)
.build()
);
}
public void spawn(
double x, double y, double z,
double vx, double vy, double vz,
float r, float g, float b, float a
) {
this.addParticle(
20, // lifetime
x, y, z, // position
x, y, z, // previousPosition
vx, vy, vz, // velocity
r, g, b, a // color
);
}
}
Systems
Systems are the core of Glitter. They define the data (bindings), behavior (update modules), and appearance (render modules) of particles.
Particle systems are configured by overriding the configure()
method, which may be called
multiple times (reloading textures reloads particle systems, to facilitate hot swapping). There
should only be one instance of a particle system, and it should be registered using the
addToGame
method.
Particles are created using the protected addParticle
method. Subclasses should provide custom
spawn
methods that have meaningful parameters (addParticle
just accepts a bunch of double
s
for reasons I’ll go into in the next section). However, do refrain from using Vector3d
parameters
if you’ll be spawning a large number of particles, since that’s exactly the kind of memory churn
glitter tries to avoid.
Bindings
In order for a particle system to do anything useful, each particle needs to hold some data. In
Glitter particles aren’t represented by instances of classes, since classes introduce unnecessary
overhead. Instead, each particle is represented as a double[]
, and bindings provide access and
meaning to those array elements.
Bindings can be created in the configure()
method using the bind(int)
method. Each binding
is allocated the specified number of elements in the particle array. However, the only time this
layout will actually matter to you is when spawning particles.
~import com.teamwizardry.librarianlib.glitter.ParticleSystem;
~import com.teamwizardry.librarianlib.glitter.bindings.StoredBinding;
~
public class BindingExampleSystem extends ParticleSystem {
@Override
public void configure() {
StoredBinding position = bind(3);
StoredBinding color = bind(4);
// The resulting particle array layout:
// [
// age,
// lifetime,
// pos, pos, pos,
// color, color, color, color
// ]
}
}
To use a binding, pass the particle array into the binding’s load(double[])
method. Once the
data has been loaded into the binding, it can be accessed from the getContents()
array. If the
binding is writable then passing the particle into the store(double[])
method will store the
modified values back into particle array.
Because of this abstraction (using load
/store
), bindings don’t necessarily have to be
directly accessing values from the particle array. Glitter has a number of built in bindings that
do more than directly index into the particles.
ConstantBinding
One of the more common bindings, this just always reports the same valueEaseBinding
This will ease between two values based on the contents of other bindingsPathBinding
This will ease a value along aParticlePath
(e.g.BezierPath
andEllipsePath
)VariableBinding
A binding that can store temporary state for use between multiple modules.
Update Modules
Without update modules, the most a particle can do is exist in a current state. It can’t move, change color, or really change at all. Every tick, each particle is passed to the configured update modules for processing. (e.g. particle 0 will be passed to modules A, B, then C, then particle 1 will be passed to modules A, B, then C, …)
The most commonly used update module is the BasicPhysicsUpdateModule
. This module handles
everything needed for most physics-based particles, including gravitational acceleration, block
collision, bouncing, and friction. All these parameters are configurable through bindings, but have
sensible defaults that you likely won’t need to change.
~import com.teamwizardry.librarianlib.glitter.ParticleSystem;
~import com.teamwizardry.librarianlib.glitter.bindings.StoredBinding;
~import com.teamwizardry.librarianlib.glitter.modules.BasicPhysicsUpdateModule;
~
public class BasicPhysicsExampleSystem extends ParticleSystem {
@Override
public void configure() {
StoredBinding position = bind(3);
StoredBinding previousPosition = bind(3);
StoredBinding velocity = bind(3);
getUpdateModules().add(new BasicPhysicsUpdateModule(
position, previousPosition, velocity
));
}
}
Render Modules
Render modules are what take your particles and render them on the screen. A system can have any number of render modules, meaning you can overlay multiple render effects on top of each other (e.g. an opaque sprite with an additive halo sprite).
You can either implement your own or use the built-in SpriteRenderModule
. At its simplest, the
SpriteRenderModule
takes just a render type and position binding. The other two most commonly
used parameters are the particle color and particle size, but it has several other useful features.
~import com.teamwizardry.librarianlib.glitter.ParticleSystem;
~import com.teamwizardry.librarianlib.glitter.bindings.ConstantBinding;
~import com.teamwizardry.librarianlib.glitter.bindings.StoredBinding;
~import com.teamwizardry.librarianlib.glitter.modules.SpriteRenderModule;
~import net.minecraft.util.ResourceLocation;
~
public class SpriteRenderExampleSystem extends ParticleSystem {
@Override
public void configure() {
StoredBinding position = bind(3);
StoredBinding previousPosition = bind(3);
StoredBinding color = bind(4);
getRenderModules().add(
SpriteRenderModule.build(
new ResourceLocation("modid", "textures/particle/sprite.png"),
position
)
.previousPosition(previousPosition)
.color(color)
.size(0.25)
.build()
);
}
}
Advanced SpriteRenderModule
Features
Alpha Multiplier
The easiest to understand would be the alphaMultiplier
. This acts as an additional modifier on
the color’s alpha channel, allowing easier opacity manipulation.
Sprite Sheet
Often you’ll have a large number of particle systems that are identical in every way except the
texture. It can be tedious, repetitive, and downright inefficient to have separate systems for
each texture, so the SpriteRenderModule
has a setting to fix that: the spriteSheet(size, index)
.
The sprite sheet size must be a power of two (2, 4, 8, ...), and the sprite sheet index is an index
in left to right, top to bottom order. This index can even be an EaseBinding
to support
animations.
Facing Vector
One of the most interesting features of the sprite renderer is support for particles with sprites
that don’t directly face the player. By default, sprites are always rotated such that they face
directly toward the camera, essentially appearing 2D. However, by specifying a custom 3D binding for
the facingVector
, particles can be made to face any direction you want. By a fortunate quirk of
the math, the facing vector doesn’t even need to be normalized.
In this example I’ve set the facing vector to the velocity binding.
Depth Sorting & Other Modules
There are two other types of modules which are more rarely used: global update modules
(getGlobalUpdateModules()
) and render prep modules (getRenderPrepModules()
). The
DepthSortModule
is a global update module, and likely the one you’ll use most.
Global Update Modules
Global update modules are given the entire backing list of particles before each frame, and are useful for things like depth sorting.
Render Prep Modules
These modules are identical to update modules, but are run during the render phase. These have limited usefulness, but are available if you need them. Note that frames are often rendered much faster than the tick rate, and their impact gets larger and larger the higher the frame rate, so these modules are even more performance critical than the update modules.