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 attribute views the WASM-owned particle buffer. This is the buffer
that changes 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. The bridge
then flags the attribute needsUpdate so Three.js re-uploads it to the GPU.
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.