Writing custom compute materials

The asset package covers common cases, but custom behaviour requires writing your own compute shader. This page covers the three slots you’ll most commonly write against (Emission Material, Self Affect Material (and the sibling ParticleAffector world-space affector), and Collider Material) plus the standard Studio workflow for turning a shader into a bindable material.

For the other slots, the authoring pattern is the same: pick an asset-package material whose shape is close to what you want, duplicate its material type, replace the shader, and press Sync with Uniforms to regenerate property-type bindings.

Emission materials

Every emission material is a compute shader that implements a single function, EmitParticle. The Particles/emitter.glsl include provides the workgroup setup, the particle data buffer binding, the hash function, the dispatch loop, and the main function: your shader only fills in EmitParticle.

The wrapper pre-writes sane defaults to the particle’s slot before calling your function (position at the emitter origin from getEmitterTransform()[3].xyz, velocity 0, mass 1, lifetime 1, scale 1, hash randomized). Your function reads or overrides any of those via the getParticle* / setParticle* accessor surface. After your function returns, the wrapper finalises prevPosition, currentLifetime, deltaTime, and prevDeltaTime: don’t touch those.

Emission shader template

Create a new compute shader file in your project (e.g. Emitter_Rising.comp.glsl) and paste this template:

#version 450 core
#include "Particles/emitter.glsl"

// Uniforms declared here appear as bindable properties in Studio after
// pressing "Sync with Uniforms" in the Material Type inspector.

void EmitParticle(uint index, uint emitIndex)
{
    // Your custom emission logic.
    // The slot at `index` is already populated with sensible defaults.
    // Override only what you need via setParticle* / addParticle* helpers.
}

The two parameters are:

  • index: the particle’s slot in the buffer. Pass to getParticle*(index) / setParticle*(index, …) accessors.

  • emitIndex: 0-based index of this particle within this frame’s emission batch. Useful for sub-frame timing patterns and event-driven subemitters that index into the source event buffer.

Add custom logic: upward velocity

Replace the empty body with an upward push:

void EmitParticle(uint index, uint emitIndex)
{
    setParticleVelocity(index, vec3(0.0, 1.0, 0.0));
    setParticleTotalLifetime(index, 2.0);
}

Make the velocity tunable with a uniform

Promote the constant into a uniform so designers can tune it in Kanzi Studio:

#version 450 core
#include "Particles/emitter.glsl"

uniform float RiseSpeed;
uniform float Lifetime;

void EmitParticle(uint index, uint emitIndex)
{
    setParticleVelocity(index, vec3(0.0, RiseSpeed, 0.0));
    setParticleTotalLifetime(index, Lifetime);
}

Hook up the shader in Kanzi Studio

Note

Why duplicate instead of creating from scratch? Creating a new material type in Kanzi Studio generates a vertex and fragment shader pair by default. Particle materials are compute shaders, so it’s easier to start from a working compute material type and swap the shader.

  1. In the LibraryMaterial Types, find the Emitter_DiscVolume material type you imported from the asset package. Right-click it and select Duplicate. Rename the copy to Emitter_Rising.

  2. Open the duplicated material type and replace its Compute Shader with your new Emitter_Rising.comp.glsl.

  3. In the Properties panel click the Sync with Uniforms button.

    Kanzi Studio scans the shader for uniforms and generates matching property types (RiseSpeed, Lifetime) in the library. It also creates binding entries that wire each property type to the corresponding uniform. Bindings from the old DiscVolume uniforms that no longer exist in the shader are removed automatically.

  4. For each generated property type (RiseSpeed, Lifetime), open its editor:

    • Set the Category to Emission. This groups it under the Emission heading when adding properties to a node.

    • Set sensible Min, Max, and Default values. For example, RiseSpeed → min 0, max 20, default 5. Lifetime → min 0.1, max 10, default 2.

    • Optionally set a Display Name and Tooltip.

  5. Create a Material instance of the new material type (e.g. Emitter_Rising_Default).

  6. Select your ParticleEmitter and set its Emission Material property to the new material.

  7. On the emitter node, add the Emission.RiseSpeed and Emission.Lifetime properties. Sliders appear in the properties panel.

  8. Press Preview and drag the sliders: particles rise faster or slower in real time.

Affector materials

Two node slots consume affector materials:

  • ParticleEmitter.SelfAffectMaterial: runs once per particle per frame, only on the particles spawned by this emitter. Use for per-emitter effects (buoyancy local to a smoke plume, drag that only applies to this emitter’s particles).

  • ParticleAffector.AffectorMaterial: runs once per particle per frame across every emitter in the same ParticleSystem, positioned by the affector node’s transform. Use for global forces (world-space gravity, a region of wind, a point attractor anywhere in the scene).

Both slots accept the same shader shape. The difference is purely where in the scene the compute is dispatched from.

Self-affector shader template

#version 450 core
#include "Particles/affector.glsl"

// Uniforms: tunable in Studio after "Sync with Uniforms".

void Affector(uint index)
{
    // Mutate the particle via accessor helpers:
    //   addParticleAcceleration(index, a)  : body force (gravity-like)
    //   addParticleForce       (index, F)  : mass-aware force (wind / drag)
    //   addParticleVelocity    (index, dv) : direct velocity bump
    //   setParticleVelocity    (index, v)  : clamp / reset velocity
    //   setParticlePosition    (index, p)  : teleport
    //   killParticle           (index)     : mark as dead
    //
    // Read state via getParticle*(index): getParticlePosition, getParticleVelocity,
    // getParticleMass, getParticleCurrentLifetime, etc. Read the affector node's
    // transform via getAffectorTransform(); the current frame's dt via getFrameInterval().
}

The shader has no return value: every per-particle effect is applied through the accessor helpers. See Affectors for the addParticleAcceleration vs addParticleForce choice.

Hooking up a self-affector

Follow the same Kanzi Studio flow as the emission example above: duplicate an existing affector material type rather than creating one from scratch:

  1. In the LibraryMaterial Types, duplicate one of the imported affector material types (e.g. Affector_Gravity) and rename the copy.

  2. Replace its Compute Shader with your affector shader.

  3. Press Sync with Uniforms.

  4. Set property-type categories to Affector.

  5. Create a material instance.

  6. Assign it to the emitter node’s Self Affect Material property.

  7. Add the relevant property types to the emitter node.

The asset package ships production-ready affector shaders (Affector_Buoyancy, Affector_Directional, Affector_Gravity, Affector_Drag, Affector_PointAttractor, Affector_Wind, Affector_KillZone, Affector_CurlNoise, Affector_TimeDilation, Affector_VelocityClamp). Use them as starting references.

World-space affectors

A world-space affector uses the same shader as a self-affector but runs as a separate ParticleAffector node in the scene, so its region of influence is positioned by the affector node’s transform.

  1. In the Node Tree, right-click the ParticleSystem and select CreateParticle Affector.

  2. Select the new affector node.

  3. Set its Affector Material property to the same affector material you built for the self-affector case.

  4. Add the relevant Affector.* property types.

Move the affector node in the scene to reposition its region of influence. getAffectorTransform() returns the affector node’s transform, so shaders can implement radius falloff or directional effects based on the node’s position and orientation.

See also

Affectors for the stock affector catalog and the order in which self-affectors, tree-affectors, and integration run per frame.

Collider materials

A collider tests each particle for penetration into a surface and reports a hit point, normal, and parametric hit time. The collider include handles the physical response (reflect velocity, apply bounce and friction, advance remaining sub-step time); your shader only implements the shape test.

See Colliders for the stock Collider_* catalog and how Bounce and Friction interact with the custom shape test.

Collider shader template

#version 450 core
#include "Particles/collider.glsl"

// Uniforms: tunable in Studio after "Sync with Uniforms".

HitInfo Collide(vec3 position, vec3 prevPosition)
{
    // Test whether the segment prevPosition -> position penetrates your shape.
    // Read the collider node's transform via getColliderTransform().
    //
    // Set hit = true if the particle is inside; the wrapper applies bounce / friction
    // and sub-step advancement. tHit is the parametric hit time along the segment in
    // [0, 1]: 0 means "already inside at prevPosition", 1 means "reached the surface
    // exactly at position".
    HitInfo info;
    info.hit      = false;
    info.tHit     = 0.0;
    info.normal   = vec3(0.0, 1.0, 0.0);
    info.position = position;
    return info;
}

The collider snippet only answers the geometric question. The wrapper handles the physical response (reflect velocity, apply bounce / friction, advance sub-step time). position and prevPosition are the segment endpoints: they may be sub-iteration values during multi-bounce resolution, not the particle’s frame-start position, so they’re passed as parameters rather than re-derivable from the particle index.

Hooking up a collider

  1. Duplicate an existing collider material type (e.g. Collider_Plane) and rename the copy.

  2. Replace its Compute Shader with your collider shader.

  3. Press Sync with Uniforms and set property-type categories to Collider.

  4. Create a material instance.

  5. Create a ParticleCollider node under the ParticleSystem and assign your new material to its Collider Material property.

  6. Add the relevant Collider.* property types plus ParticleCollider.Bounce and ParticleCollider.Friction.

The node’s world transform is available to the shader via getColliderTransform(), so move, rotate, or scale the ParticleCollider to reposition the collision volume.

MaterialTypePropertyTypes declaration

The property types surfaced by Sync with Uniforms are stored on the material type in a MaterialTypePropertyTypes list. Each entry names a property type; Kanzi Studio resolves those names against the project’s property-type library and generates the shader-binding entries.

You rarely need to touch this list directly (Sync with Uniforms maintains it) but it’s worth knowing about when:

  • A material type needs to consume a property type whose name differs from its shader uniform (e.g. a buffer-typed consumer property like Emission.SourceEventBuffer, which doesn’t map to a conventional scalar uniform).

  • You’re copying a material type by editing the .kzm file directly rather than duplicating in Kanzi Studio.

In those cases, edit the MaterialTypePropertyTypes.Value.PropertyTypeNames list to add the property-type names the shader expects to read from. The generator that drives Particle editor reference reads this exact list to produce the per-material property tables.

See also