# How ArmorPaint’s Particle System Handles GPU-Side Simulation and Rendering

> Discover how ArmorPaint's particle system leverages GPU shaders for brush impact rendering while utilizing CPU for physics simulation via Bullet. Optimize your scene performance.

- Repository: [Armory 3D/armorpaint](https://github.com/armory3d/armorpaint)
- Tags: internals
- Published: 2026-09-14

---

**ArmorPaint delegates physics simulation to the CPU using Bullet physics while offloading brush impact rendering to lightweight GPU shaders generated per particle.**

The **ArmorPaint particle system** splits workload across two core modules: [`util_particle.c`](https://github.com/armory3d/armorpaint/blob/main/util_particle.c) manages CPU-side spawning and physics, while [`make_particle.c`](https://github.com/armory3d/armorpaint/blob/main/make_particle.c) generates GPU shaders that convert particle trajectories into paint stamps. This architecture keeps heavy physics calculations on the host while minimizing GPU overhead through procedural mask generation rather than full particle simulation.

## CPU-Side Particle Management in [`util_particle.c`](https://github.com/armory3d/armorpaint/blob/main/util_particle.c)

The CPU layer handles the entire particle lifecycle—from instantiation to destruction—using the Bullet physics engine to calculate trajectories and detect collisions with the mesh surface.

### Initialization and Spawning

When the particle tool activates, ArmorPaint initializes a physics world via `sim_init()` and creates a paint-body mesh using `physics_body_create()` (lines 4-18). On mouse click, the system spawns a sphere object by name `.Sphere`, converts it to a bullet mesh, and attaches a physics body with `physics_body_create(mo->base, PHYSICS_SHAPE_SPHERE, g_context->particle_mass)` (lines 67-107). A randomized impulse vector applied via `physics_body_apply_impulse()` launches the particle toward the mesh surface:

```c
object_t *obj = scene_spawn_object(".Sphere", NULL, true);
mesh_object_t *mo = obj->ext;
mo->material = make_particle_get_bullet_material();
physics_body_t *body = physics_body_create(mo->base, PHYSICS_SHAPE_SPHERE, g_context->particle_mass);
physics_body_apply_impulse(body->_body,
    (vec4_t){dx * impulse, dy * impulse, dz * impulse, 0.0});
g_context->particles[slot].body = body;
g_context->particles[slot].bullet = mo->base;
g_context->particles[slot].timer = tween_timer(g_context->particle_lifetime, NULL, NULL);

```

### Physics Simulation and Contact Detection

Each frame, `physics_world_update()` advances the Bullet simulation step (lines 25-28). The system retrieves collision data through `physics_get_contact_pairs()`, extracting hit position, surface normal, and depth for every active particle (lines 121-138). These values populate the global `g_context->particles[i]` array, maintaining the particle state between frames without GPU round-trips.

### Lifetime Management and Uniform Preparation

Particles expire based on a `tween_timer` (lines 29-35). Upon timeout, the CPU removes both the physics body and mesh instance, clearing the slot for reuse. Before the render pass, the system copies critical hit data—**current position**, **last position**, and **radius**—into global uniforms (`g_context->particle_hit_x`, etc.) that the GPU shader will read (lines 144-152). This bridges the CPU physics state to the GPU render pipeline.

## GPU-Side Shader Generation in [`make_particle.c`](https://github.com/armory3d/armorpaint/blob/main/make_particle.c)

Rather than simulating particles on the GPU, ArmorPaint generates a **mask shader** per particle that evaluates how the particle’s trajectory influences the paint buffer.

### Bullet Material Setup

The `make_particle_get_bullet_material()` function (lines 4-69) creates a simple cached material `particle_bullet` used only for visualizing collision spheres during debugging. This remains separate from the actual paint impact logic.

### Mask Shader Construction

The `make_particle_mask()` function (lines 71-86) injects three uniforms into the fragment shader:

- `particle_hit` – current world-space contact point
- `particle_hit_last` – previous frame contact point (for trajectory vectors)
- `particle_radius` – brush size multiplier

The shader builder writes GLSL-like code via `node_shader_write_frag()`, constructing a distance field that measures how far a fragment lies from the line segment connecting the two hit points.

### Distance-Based Fragment Calculation

During the paint render pass (`render_path_paint_commands_particle()` in [`render_path_paint.c`](https://github.com/armory3d/armorpaint/blob/main/render_path_paint.c), lines 134-152), the GPU executes the generated shader. The fragment code calculates perpendicular distance from the pixel to the particle’s motion vector using vector projection:

```c
node_shader_add_constant(kong, "particle_hit: float3", "_particle_hit");
node_shader_add_constant(kong, "particle_hit_last: float3", "_particle_hit_last");
node_shader_add_constant(kong, "particle_radius: float", "_particle_radius");

node_shader_write_frag(kong,
    "var pa: float3 = input.wpos.xyz - constants.particle_hit;");
node_shader_write_frag(kong,
    "var ba: float3 = constants.particle_hit_last - constants.particle_hit;");
node_shader_write_frag(kong,
    "var h: float = clamp(dot(pa, ba) / max(dot(ba, ba), 0.00000001), 0.0, 1.0);");
node_shader_write_frag(kong,
    "dist = length(pa - ba * h) * (5.0 / constants.particle_radius);");

```

Fragments where `dist > 1.0` are discarded; survivors calculate a falloff strength based on distance, brush hardness, and opacity, blending into the paint texture to create smooth stroke stamps that follow the physics-simulated trajectory.

## Bridging CPU Physics and GPU Rendering

The architecture maintains strict separation of concerns:

- **CPU ([`util_particle.c`](https://github.com/armory3d/armorpaint/blob/main/util_particle.c))** – Handles heavy physics integration via Bullet, collision detection, object lifetime, and prepares uniform data.
- **GPU ([`make_particle.c`](https://github.com/armory3d/armorpaint/blob/main/make_particle.c))** – Receives pre-calculated trajectory endpoints and computes influence masks using efficient fragment shaders.

This design allows **ArmorPaint** to support hundreds of simultaneous particles without GPU memory overhead for particle buffers or simulation state, since the render pipeline treats each particle as a transient shader execution rather than a persistent GPU object.

## Summary

- **CPU-side simulation** in [`util_particle.c`](https://github.com/armory3d/armorpaint/blob/main/util_particle.c) spawns sphere objects, runs Bullet physics updates, and caches hit positions in global context uniforms.
- **GPU-side rendering** in [`make_particle.c`](https://github.com/armory3d/armorpaint/blob/main/make_particle.c) generates procedural mask shaders that measure distance from fragments to particle trajectory segments.
- The system uses `physics_body_apply_impulse()` for launch vectors and `physics_get_contact_pairs()` for surface collision data.
- Shader uniforms bridge the two domains, passing `particle_hit`, `particle_hit_last`, and `particle_radius` to the fragment stage.
- Distance falloff calculations happen per-fragment using vector projection against the particle’s motion line, creating trajectory-following brush stamps.

## Frequently Asked Questions

### Does ArmorPaint simulate particles entirely on the GPU?

No. According to the **armorpaint** source code, physics simulation runs on the CPU using the Bullet engine within [`util_particle.c`](https://github.com/armory3d/armorpaint/blob/main/util_particle.c). The GPU only executes fragment shaders that determine paint influence based on pre-calculated collision points passed via uniforms.

### How does the particle system convert physics collisions into paint strokes?

The CPU stores hit positions and normals in `g_context->particles[]`, then copies the current and previous hit positions to shader uniforms. The GPU shader generated by `make_particle_mask()` computes the shortest distance from each fragment to the line segment between these two points, using the result as a brush strength multiplier.

### What file controls the particle paint render pass?

The function `render_path_paint_commands_particle()` inside [`paint/sources/render/render_path_paint.c`](https://github.com/armory3d/armorpaint/blob/main/paint/sources/render/render_path_paint.c) (lines 134-152) orchestrates the GPU execution. It binds the particle-specific uniforms and invokes the bullet mesh draw call with the generated mask shader, integrating particle effects into the standard paint rendering pipeline.

### Why does the system use sphere objects for particles?

The code in [`util_particle.c`](https://github.com/armory3d/armorpaint/blob/main/util_particle.c) instantiates `.Sphere` objects because the **Bullet physics shape** `PHYSICS_SHAPE_SPHERE` provides efficient collision detection against high-resolution meshes. Spheres roll naturally across surfaces, creating continuous contact points that translate into smooth brush strokes without complex rotational physics.