Skip to content

Script


The Script layer enables direct writing of GLSL, with additional variables and functions to simplify the process.

This allows you to create custom GPU particle modifiers.


Selecting a Script layer from the nxQuestion list will open the script editor.

This will contain the simple default script.

The default script sets the particle color to yellow if the age is greater than 1 second.

if (particle.age > 1.0)
{
particle.color = vec3(1.0, 1.0, 0.0);
}

The script editor features simple editing (with undo and clipboard) and syntax highlighting.

It has the common key shortcuts:

  • Ctrl C, X, V, Z for copy, cut, paste, undo
  • Double-click a word to select
  • Shift + cursor to extend selection
  • Tab / Shift-Tab with a selection to increase or decrease indent

There is a right-click menu, which has the usual editing and load/save functions.

nxQ_Script_RClick_v01.png

Right-click menu options.

There are Find and Replace tools, with the usual options.

This field allows you to search for defined words in the script.

Use the additional buttons to match case, or match whole word only.

nxQ_Script_Find_v03.png

Using the Find parameter, ‘time’ is being searched for (and discovered, highlighted).

This parameter will become available once an item is discovered and highlighted.

It allows you to replace defined elements of the script with whatever you have input into the field.

Use the additional buttons to replace next, or replace all.

nxQ_Script_Replace_v03.png

Utilizing the Replace option, ‘time’ is replaced with ‘frame’.


The script can optionally contain a main() function.

This is then called, by default, for this script.

void main()
{
if (doc.time > 1.0)
{
particle.color = vec3(1.0, 1.0, 0.0);
}
}

A main() does not need to be defined; all the script code is embedded into the currently processing code (for example, within a Question layer, or at the top level, within the shader’s own main function).

Scripts can define functions, which can then be called within the script or other scripts.

Order matters; functions must be defined before they are called.

The final shader script is built in the order it appears within nxQuestion.

If you define a user variable within nxQuestion after you try to use it, the result may not be as expected, and you may get an error shown in the Cinema 4D console.

Functions should be defined at the top of the script. Any global variables or #define macros should appear before the first function definition.

On finding the first function, anything declared before it will also be included.


There is a built-in print() function to print strings and simple variables back to the Cinema 4D console.

print() takes first a format string, then any variables or values.

For example, print("hello") will print hello to the console. However, it will print numerous times because scripts execute per particle, so be very cautious when printing.

print("hello");

The above will produce output like the following in the console, once per particle:

hello
hello
hello
hello
...

It is best to do an index check and limit prints to a specific particle, or use a low particle count when debugging.

if (index == 0)
{
print("hello");
}

With the index check, only one line is printed:

hello

The print() function signature is:

print("format string", parameters...)

Format specifiers begin with %. The supported specifiers are:

SpecifierOutput
%d, %i, %lSigned integer
%uUnsigned integer
%fFloat
%<width>.<places>fFloat with decimal places, e.g. %.3f or %1.3f. The <width> is unused.
%.<components>v<places>fVector components, e.g. %.3v3f prints all three components of a vec3 with 3 decimal places. <components> is the number of floats in the vector.

If errors are found when the GLSL is compiled, these will print into the console.

Generally this will be due to script errors, but any error compiling nxQuestion will be shown.

For example, a typo in a function name:

if (index == 0)
{
wrint("hello");
}

Produces the following in the console:

nxQuestion Error in Script at line 3 : 'wrint' : no matching overloaded function found

When making script changes, it is highly recommended to test first with a single (or low) particle count and a single frame step.

GPUs are not happy when you make mistakes.


There are helper variables built in, which are converted from keywords at compile time.

You can access the current particle’s data with particle and any particle by index using particles[particle_index].

Scripts execute per particle in parallel on the GPU. The reserved variable index holds the current particle’s index.

Access: R = read only, RW = read/write.

These variables access data for the currently processing particle.

VariableDescriptionTypeAccess
particle.age / particle.timeParticle age in secondsfloatRW
particle.frameParticle age in framesintR
particle.speedParticle speed. Writing scales velocity magnitude while keeping direction.floatRW
particle.groupParticle groupintRW
particle.radiusParticle radiusfloatRW
particle.massParticle massfloatRW
particle.lifeParticle lifespan in secondsfloatRW
particle.idParticle IDintR
particle.flagsParticle flagsintRW
particle.distanceDistance the particle has travelledfloatR
particle.frictionParticle frictionfloatR
particle.bounceParticle bouncefloatR
particle.emitterParticle emitter indexintR
particle.colorParticle RGB colorvec3RW
particle.positionParticle positionvec3RW
particle.velocityParticle velocityvec3RW
particle.rotationParticle rotationvec3RW
particle.scaleParticle scalevec3RW
particle.originParticle position at the start of the current framevec3R
particle.uvwParticle UVW coordinatesvec3R
particle.vertexParticle vertex weightfloatR
particle.neighbors(distance)Number of particles within distanceintR
particle.densityParticle density, calculated livefloatR
particle.bornTrue if the particle was born this frameboolR
particle.fieldFalloff/field value for the particlefloatR
particle.randomRandom value derived from the particle IDfloatR
particle.indexParticle index on the GPUintR
particle.smokeParticle smoke value (nxExplosiaFX)floatRW
particle.temperatureParticle temperature value (nxExplosiaFX)floatRW
particle.fuelParticle fuel value (nxExplosiaFX)floatRW

Any particle can be accessed using particles[particle_index].<variable>, for example:

vec3 otherPos = particles[0].position;
float otherAge = particles[0].age;

All of the same properties listed above are available on particles[n], with the same types, but all are read-only. To get the speed of another particle use length(particles[n].velocity) rather than particles[n].speed.

VariableDescriptionTypeAccess
doc.timeDocument time in secondsfloatR
doc.deltaTime elapsed since the last frame in secondsfloatR
doc.frameCurrent document frameintR
doc.fpsDocument frame rateintR
VariableDescriptionTypeAccess
math.random(seed)Random value generated from seed (int)floatR
VariableDescriptionTypeAccess
compute.iterationCurrent nxQuestion iterationintR
compute.iterationcountTotal number of iterations set in nxQuestionintR
VariableDescriptionTypeAccess
emitters.countNumber of emitters on the GPU. May differ from the document count, as CPU-only emitters are not present.intR
emitter[n].firstIndex of the first particle for this emitter. On the GPU all particles share a single array.intR
emitter[n].countNumber of particles for this emitterintR
emitter[n].seedRandom seed for this emitterintR
emitter[n].create(position, velocity, color, radius, life, flags, userdata)Creates a new particle on the GPU. Returns the new particle’s index for use with newparticle[]. flags should be 0. userdata can be any integer value. n must be a literal integer; using particle.emitter here will cause a type error.int

Particles created with emitter[n].create() are written into a separate output buffer. The returned index is used to set their properties via newparticle[new_index].

VariableDescriptionTypeAccess
newparticle[n].userdataUser-defined integer valueintRW
newparticle[n].emitterGPU emitter index used to create the CPU-side particle. Set to -1 to skip CPU-side creation.intRW
newparticle[n].positionParticle positionvec3RW
newparticle[n].velocityParticle velocityvec3RW
newparticle[n].radiusParticle radiusfloatRW
newparticle[n].colorParticle RGB color. If the x component is less than 0.0 the color is ignored.vec3RW
newparticle[n].lifeParticle lifespan. Set to 0.0 or less to ignore.floatRW
newparticle[n].flagsParticle flags. Set to 0.intRW

Particles are colored based on their speed. Slow particles appear blue and fast particles appear red.

void main()
{
// normalize speed to 0-1 range; adjust the multiplier to suit your particle velocities
float speed = clamp(particle.speed * 0.2, 0.0, 1.0);
// slow = blue, fast = red
particle.color = vec3(speed, 0.0, 1.0 - speed);
}

Particle color fades from orange to blue over the particle’s lifetime.

void main()
{
// 0.0 at birth, 1.0 at end of life
float lifeRatio = particle.age / particle.life;
// blend from orange to blue
particle.color = mix(vec3(1.0, 0.5, 0.0), vec3(0.0, 0.2, 1.0), lifeRatio);
}

Particles are pushed tangentially around the world origin in the XZ plane. Increase or decrease the multiplier to control orbit speed.

void main()
{
// flatten to XZ plane to orbit around the Y axis
vec3 pos = vec3(particle.position.x, 0.0, particle.position.z);
float dist = length(pos);
if (dist > 0.001)
{
// tangent is perpendicular to the radial direction and the Y axis
vec3 tangent = cross(normalize(pos), vec3(0.0, 1.0, 0.0));
// doc.delta makes the force frame-rate independent
particle.velocity += tangent * 300.0 * doc.delta;
}
}

There is no built-in way to reset particle.distance, but particle.smoke, particle.temperature and particle.fuel are writable floats that persist in the particle buffer between frames. When not used for nxExplosiaFX, any of them can serve as a custom per-particle accumulator.

This script uses particle.smoke to accumulate distance travelled. Using mod() to sample cyclically means no reset is needed; the value grows freely but the color repeats every 200 units. Three sin waves offset by 120 degrees each produce a smooth continuous cycle through the full color wheel.

void main()
{
// accumulate distance travelled; particle.smoke persists between frames
particle.smoke += particle.speed * doc.delta;
// mod() cycles the value without needing a reset
float t = mod(particle.smoke, 200.0) / 200.0;
// three sin waves offset by 120 degrees cycle smoothly through the color wheel
particle.color = vec3(
sin(t * 6.28318) * 0.5 + 0.5,
sin(t * 6.28318 + 2.09440) * 0.5 + 0.5,
sin(t * 6.28318 + 4.18879) * 0.5 + 0.5
);
}

Use particle.temperature or particle.fuel if you need additional independent counters or if nxExplosiaFX is active in the scene.

When each particle is born a smaller child particle is spawned from the same position with an upward velocity offset and half the lifespan. The emitter index is passed as a literal; change 0 to match the GPU index of your target emitter.

void main()
{
// particle.born is true only on the frame the particle is created
if (particle.born)
{
emitter[0].create(
particle.position,
particle.velocity + vec3(0.0, 50.0, 0.0), // inherit velocity plus upward offset
vec3(1.0, 0.8, 0.0), // orange
particle.radius * 0.5, // half the parent radius
particle.life * 0.5, // half the parent lifespan
0, // flags (always 0)
0 // userdata
);
}
}

A time-animated trig field steers particles along flowing organic paths, similar in look to curl noise but with no solver required. The three field components are offset across spatial and time axes so each axis flows independently. Particle color reflects the current field direction, mapping XYZ to RGB.

Adjust scale to match your scene size (smaller values give larger slower waves) and the force multiplier to taste.

void main()
{
// scale position into field space
float scale = 0.004;
vec3 p = particle.position * scale;
// animate the field over time
float t = doc.time * 0.3;
// each component uses offset axes to prevent the field aligning to a single direction
vec3 field = vec3(
cos(p.y + t) - sin(p.z + t),
cos(p.z + t) - sin(p.x + t),
cos(p.x + t) - sin(p.y + t)
);
particle.velocity += field * 200.0 * doc.delta;
// map field direction to color
particle.color = abs(normalize(field));
}

A wave front expands outward from the world origin at a set speed, repeating every wavePeriod seconds. Particles are pushed radially as the wave passes through them and briefly lit up against a dark base color. Adjust waveSpeed, wavePeriod and waveWidth to control the timing and sharpness of the pulse.

void main()
{
float waveSpeed = 400.0; // units per second
float wavePeriod = 3.0; // seconds between pulses
float waveWidth = 40.0; // sharpness of the pulse edge
// current radius of the wave front
float waveRadius = mod(doc.time, wavePeriod) * waveSpeed;
float dist = length(particle.position);
// Gaussian falloff centered on the wave front
float wave = exp(-pow(dist - waveRadius, 2.0) / (waveWidth * waveWidth));
if (dist > 0.001)
{
// push particles outward proportional to wave intensity
particle.velocity += normalize(particle.position) * wave * 2000.0 * doc.delta;
}
// blend from dark base color to bright orange at the wave front
particle.color = mix(vec3(0.05, 0.05, 0.1), vec3(1.0, 0.6, 0.1), wave);
}

Particles are attracted to the surface of a sphere and simultaneously pushed tangentially, causing them to flow across it. Particles on the surface appear cyan and those far from it appear orange. Set targetRadius to match the scale of your emitter.

void main()
{
// set this to match the scale of your emitter
float targetRadius = 300.0;
float dist = length(particle.position);
if (dist > 0.001)
{
vec3 normal = particle.position / dist;
// attract toward the sphere surface; strength scales with distance from it
particle.velocity += normal * (targetRadius - dist) * 3.0 * doc.delta;
// push tangentially to create surface flow around the Y axis
vec3 tangent = cross(normal, vec3(0.0, 1.0, 0.0));
particle.velocity += tangent * 200.0 * doc.delta;
// cyan on the surface, orange when far from it
float t = clamp(abs(dist - targetRadius) / 100.0, 0.0, 1.0);
particle.color = mix(vec3(0.2, 0.8, 1.0), vec3(1.0, 0.2, 0.0), t);
}
}

AI assistants are well suited to writing nxQuestion scripts. Because the scripting environment is a defined subset of GLSL with a fixed set of variables, giving an AI the right context up front produces accurate and immediately usable results.

Claude works particularly well for this. Paste the context block below at the start of your conversation, then describe what you want in plain language.

Copy this into Claude before making any script requests.

You are writing scripts for the nxQuestion Script layer in NeXus for Cinema 4D.
Scripts are written in GLSL and run once per particle per frame on the GPU in parallel.
The script can use a void main() function or plain inline code.
PARTICLE VARIABLES (current particle):
particle.age / particle.time float RW age in seconds
particle.frame int R age in frames
particle.speed float RW speed; writing scales velocity while keeping direction
particle.group int RW
particle.radius float RW
particle.mass float RW
particle.life float RW lifespan in seconds
particle.id int R
particle.flags int RW
particle.distance float R total distance travelled, cannot be reset
particle.friction float R
particle.bounce float R
particle.emitter int R GPU emitter index
particle.color vec3 RW RGB
particle.position vec3 RW
particle.velocity vec3 RW
particle.rotation vec3 RW
particle.scale vec3 RW
particle.origin vec3 R position at start of current frame
particle.uvw vec3 R
particle.vertex float R vertex weight
particle.neighbors(dist) int R particle count within radius; requires nxFluids
particle.density float R requires nxFluids
particle.born bool R true only on birth frame
particle.field float R falloff value
particle.random float R stable random value from particle ID
particle.index int R GPU array index
particle.smoke float RW nxExplosiaFX or repurpose as custom float storage
particle.temperature float RW nxExplosiaFX or repurpose as custom float storage
particle.fuel float RW nxExplosiaFX or repurpose as custom float storage
OTHER PARTICLES (read access):
particles[n].property same properties as above
DOCUMENT:
doc.time float seconds
doc.delta float seconds since last frame; use this to make forces frame-rate independent
doc.frame int
doc.fps int
MATH:
math.random(seed int) float random value from integer seed
COMPUTE:
compute.iteration int current iteration
compute.iterationcount int total iterations
EMITTERS:
emitters.count int GPU emitter count
emitter[n].first int index of first particle for emitter n
emitter[n].count int particle count for emitter n
emitter[n].seed int random seed for emitter n
emitter[n].create(position vec3, velocity vec3, color vec3, radius float, life float, flags int, userdata int)
int spawns a new particle; returns its index for use with newparticle[]
always use a literal integer for n, not particle.emitter
NEW PARTICLES (after emitter[n].create()):
newparticle[n].position vec3 RW
newparticle[n].velocity vec3 RW
newparticle[n].color vec3 RW ignored if x < 0.0
newparticle[n].radius float RW
newparticle[n].life float RW ignored if <= 0.0
newparticle[n].flags int RW always 0
newparticle[n].userdata int RW
newparticle[n].emitter int RW GPU emitter index for CPU-side creation; -1 to skip
RULES:
- Use doc.delta to keep forces frame-rate independent
- particle.smoke, particle.temperature and particle.fuel persist between frames and can store custom per-particle float values when nxExplosiaFX is not in use
- particle.neighbors() and particle.density only work when nxFluids is active
- The variable `index` is the current particle's GPU array index
- Standard GLSL functions (sin, cos, length, normalize, cross, mix, clamp, mod, exp, abs, etc.) are all available

Prompt:

Make particles orbit the Y axis with inner particles spinning faster than outer ones. Push particles outward from the Y axis over time so they spread out. Zero the velocity to avoid drift. Color them red close to the centre and blue farther out.

The result below is one example of what this prompt produced. AI responses vary each time, so the same prompt may return a different but equally valid script. If the first result is not quite right, refine the prompt or ask the AI to adjust specific parts.

Result:

void main() {
float dist = length(particle.position.xz);
// outward radial push -- spreads particles away from the Y axis over time
float pushStrength = 20.0;
vec2 outward = normalize(particle.position.xz + vec2(0.0001)) * pushStrength * doc.delta;
particle.position.x += outward.x;
particle.position.z += outward.y;
// recalculate dist after push
dist = length(particle.position.xz);
// angular velocity decreases with distance; inner particles spin faster
float omega = 8.0 / (dist + 0.001);
float angle = omega * doc.delta;
float cosA = cos(angle);
float sinA = sin(angle);
// rotate position directly in XZ using a 2D rotation matrix
float x = particle.position.x;
float z = particle.position.z;
particle.position.x = x * cosA - z * sinA;
particle.position.z = x * sinA + z * cosA;
// zero velocity to prevent drift from physics integration
particle.velocity = vec3(0.0);
// red at the centre, blue toward the outside
float minDist = 5.0;
float maxDist = 200.0;
float t = clamp((dist - minDist) / (maxDist - minDist), 0.0, 1.0);
particle.color = mix(vec3(1.0, 0.0, 0.0), vec3(0.0, 0.0, 1.0), t);
}

Copyright © 2026 INSYDIUM LTD. All Rights Reserved.