So this was a fun deep dive getting to know what drives the particle systems, particularly the GPU particle systems, in Babylon. Here’s the setup and the PG
- Models with animations that have pre-generated VAT to create entity instances for scale
- Item models that the entities will hold in their hands (swords, axes, shields etc.) that use the same VAT manager to follow animations
- Particles that emit around the item (like a flaming sword) and follow animations
- Particles that emit from left and right hands of the entity for a spell casting particle effect
There’s a good bit of code in this PG and the entity instancing/item instancing have been showcased in other projects, what I wanted to demonstrate was a lean way to have GPUParticleSystem emitters follow and conform to bone positions and rotations.
The relevant code:
// Alignment matrix to translate the emitter shape UP
// And offset slightly to the hilt of the weapon
const qAlign = BABYLON.Quaternion.RotationAxis(
new BABYLON.Vector3(0, 0, 1),
-Math.PI / 2
);
const alignMat = new BABYLON.Matrix();
BABYLON.Matrix.FromQuaternionToRef(qAlign, alignMat);
alignMat.addTranslationFromFloats(-1.65, 0, 0);
// One scoped world Matrix to avoid GC
const worldMat = new BABYLON.Matrix();
// A hack to the particle system's emitter to conform to the interface
// Needed to be recognized as an object that has a world matrix.
itemParticleSystem.emitter = {
position: true,
isEnabled() {
return true;
},
// Entrypoint for on-demand CPU matrix compute
// As lean as possible
getWorldMatrix() {
// Determine current frame for VAT manager
const fromFrame = animationBuffer.x;
const toFrame = animationBuffer.y;
const total = toFrame - fromFrame + 1;
const t = manager.time * animationBuffer.w;
// Sample a random bone anchor based on time so we can generate particles emitting
// from multiple points over time.
const anchorIdx = (t % boneAnchors.length) | 0;
const offsetBase = boneAnchors[anchorIdx];
const off = (fromFrame + Math.floor(t % total)) * floatsPerFrame + offsetBase;
// Fill in matrix based on the offset in the raw VAT data
BABYLON.Matrix.FromArrayToRef(vatData as any, off, worldMat);
if (canUseFloat16) {
for (let i = 0; i < 16; i++) {
(worldMat.m as any)[i] = BABYLON.FromHalfFloat(vatData[off + i]);
}
}
alignMat.multiplyToRef(worldMat, worldMat);
// Add to original transformNode position to keep it "moving" with an entity
(worldMat.m as any)[12] += transformNode.position.x;
(worldMat.m as any)[13] += transformNode.position.y;
(worldMat.m as any)[14] += transformNode.position.z;
return worldMat;
}
} as any;
Here is a short clip of rendering 105 separate uniquely animating entities with random held items and alternating between a held flame effect or spell casting