Skip to main content
Version: Next

Rendering pipeline

EXEPERT draws three things: neurons (points), axons (line segments), and signals (a pooled particle system). All three bind their geometry to WASM memory; the difference is how often that memory changes.

The render loop

src/run.ts owns the requestAnimationFrame loop. Because scene.ts sets renderer.autoClear = false (so the background color stays configurable through the GUI), the loop clears manually each frame:

src/run.ts
function frame(): void {
requestAnimationFrame(frame)
clearColor.set(sceneSettings.bgColor)
renderer.setClearColor(clearColor, 1)
renderer.clear()
update() // brain.update(dt) when not paused
renderer.render(scene, camera) // synchronous: classic WebGLRenderer
stats.update()
tickFrameCount()
updateStatusFps()
updateBrainStatusBar()
}

The render call is synchronous because the renderer is a classic WebGLRenderer. (Earlier WebGPU drafts needed an awaited render; that is no longer the case.)

Neurons: Points

Built in Brain.buildNeuronGeometry(). The geometry has three attributes:

  • position: a Float32Array view into WASM memory (static after build).
  • color: per-neuron RGB, written from the configured neuron color.
  • size: a per-neuron random size in [0.75, 3.0).

Neurons render with the custom neuron ShaderMaterial (src/materials/neuron.ts): additive blending, a sprite texture, and a distance-attenuated point size. See Materials.

Axons: LineSegments

Built in Brain.buildAxonGeometry(). The Rust core samples each axon's cubic Bezier curve into 9 vertices (8 subdivisions) and exposes three buffers:

  • position: sampled curve vertices.
  • index: line-segment connectivity (setIndex).
  • opacity: a per-vertex opacity used by the axon shader.

These buffers are uploaded once. Axons render with the axon ShaderMaterial (src/materials/axon.ts): per-vertex opacity times a global multiplier, with additive blending.

Signals: particle pool

The ParticlePoolBridge (inside src/brain.ts) manages a Points cloud whose position and color attributes view WASM-owned particle buffers. These are the buffers that change every frame:

src/brain.ts
update(deltaTime: number): void {
this.refreshIfMemoryGrew()
this.world.step_cpu(deltaTime) // writes particle positions into WASM memory
this.particlePool.update() // flags needsUpdate for the GL upload
}

world.step_cpu(dt) advances each live signal along its axon, samples the Bezier curve, and writes the resulting position into WASM memory. Emotion and stimulus metadata also write per-signal RGB values into particle_colors_ptr(). The bridge then flags both attributes needsUpdate so Three.js re-uploads position and color to the GPU.

The affect-reactive chat layer passes a hex color on injected stimuli. Rust stores that color on the seeded signal and propagates it through neuron firing, so simultaneous signals can show different emotion colors on the same brain canvas instead of relying on one global material tint.

Why memory growth matters here

Of the three buffers, the particle buffer is the one most affected by WASM memory growth, so ParticlePoolBridge re-checks wasm.memory.buffer every frame and rewraps its view if the buffer changed. The neuron view is rebuilt by Brain.refreshIfMemoryGrew(); axon attributes are static. See Overview for the mechanism.