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:
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: aFloat32Arrayview 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:
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.