Visual Pipeline Architecture
Developer guide to megane's internal architecture: how molecular data flows from file parsing to pixels on screen, and how to extend the system.
Design Philosophy
megane's visual pipeline is built on three core principles:
Typed Data Flow
The pipeline is a directed acyclic graph where edges carry one of 7 typed data channels: particle, bond, cell, trajectory, label, mesh, and vector. Nodes declare typed input/output ports via NODE_PORTS in src/pipeline/types.ts. The UI prevents connections between incompatible port types at the graph level (see canConnect()), so executors can trust that incoming data has the expected shape.
Separation of Computation and Rendering
Pipeline execution produces a pure data structure — ViewportState — with no Three.js dependencies. A separate translation layer (applyViewportState() in src/pipeline/apply.ts) maps that data to renderer method calls. This means:
- The pipeline can be tested without a WebGL context.
- The renderer can be swapped without changing pipeline logic.
- Multiple front-ends (app, widget, docs embed) share the same pipeline engine.
GPU-Efficient Impostor Rendering
Instead of mesh-based spheres (32+ triangles each), atoms are rendered as screen-aligned quads (2 triangles) with ray-sphere intersection in the fragment shader. The base per-atom data costs 7 floats of GPU memory (position xyz, radius, color rgb), and the current ImpostorAtomMesh implementation adds 2 more floats for per-atom scale and opacity overrides (9 floats/atom total). All atoms draw in a single instanced call. This is what allows megane to render 1M+ atoms at 60 fps on mid-range hardware.
System Overview
┌─────────────────────────────────────────────────────────────┐
│ Rust / WASM Parsers (crates/megane-wasm/) │
│ PDB, GRO, XYZ, MOL, CIF, LAMMPS, XTC, .traj │
│ → Snapshot { positions, elements, bonds, box } │
└────────────────────────────┬────────────────────────────────┘
│ wasm-bindgen FFI
▼
┌─────────────────────────────────────────────────────────────┐
│ Pipeline Engine (src/pipeline/) │
│ │
│ executePipeline() → topological sort → per-node executor │
│ │
│ LoadStructure → Filter → Modify → AddBond → Viewport │
│ │ │ │ │ │ │
│ ParticleData ParticleData ... BondData ViewportState │
└────────────────────────────┬────────────────────────────────┘
│ applyViewportState()
▼
┌─────────────────────────────────────────────────────────────┐
│ Renderer (src/renderer/) │
│ │
│ MoleculeRenderer orchestrates: │
│ ImpostorAtomMesh (billboard quads + ray-sphere shader) │
│ ImpostorBondMesh (billboard quads + cylinder shader) │
│ CellRenderer, LabelOverlay, ArrowRenderer, ... │
│ StructureLayer (multi-structure overlay) │
└────────────────────────────────── ───────────────────────────┘
Pipeline Data Types
Seven typed channels flow through pipeline edges. Defined in src/pipeline/types.ts.
| Channel | Interface | Key Fields | Produced By | Consumed By |
|---|---|---|---|---|
particle | ParticleData | source (Snapshot), indices, scaleOverrides, opacityOverrides | LoadStructure, Streaming, Filter, Modify | Filter, Modify, AddBond, Labels, Polyhedra, Viewport |
bond | BondData | bondIndices, bondOrders, nBonds, scale, opacity | AddBond, Streaming, Filter, Modify | Filter, Modify, Viewport |
cell | CellData | box (3x3 Float32Array), visible, axesVisible | LoadStructure, Streaming | Viewport |
trajectory | TrajectoryData | provider (FrameProvider), meta | LoadStructure, LoadTrajectory, Streaming | Viewport |
label | LabelData | labels[], particleRef | LabelGenerator | Viewport |
mesh | MeshData | positions, indices, normals, colors | PolyhedronGenerator | Viewport |
vector | VectorData | frames (VectorFrame[]), scale | LoadVector, VectorOverlay | VectorOverlay, Viewport |
Each edge in the UI is color-coded by data type (DATA_TYPE_COLORS). Filter and Modify nodes are generic — they accept both particle and bond inputs via GENERIC_NODE_ACCEPTS.
Pipeline Execution
executePipeline() in src/pipeline/execute.ts drives the data flow:
- Topological sort —
topologicalSort()(Kahn's algorithm) insrc/pipeline/graph.tsdetermines execution order. - Per-node dispatch — A switch on
params.typecalls the appropriate executor fromsrc/pipeline/executors/. - Edge data propagation — An
EdgeOutputsmap (Map<nodeId, Map<portName, PipelineData>>) carries outputs between nodes.collectInputs()gathers upstream data for each node. - Disabled node passthrough — Disabled nodes forward their first input to a matching output port, preserving downstream flow.
- ViewportState assembly — The
viewportexecutor collects all 7 channels into a singleViewportState. - Error collection —
NodeErrorobjects (warning/error) accumulate per-node and are surfaced in the UI as badges.
Rendering Layer
MoleculeRenderer
src/renderer/MoleculeRenderer.ts (~1300 lines) is the main orchestrator. It owns the Three.js scene, camera (orthographic or perspective), OrbitControls, and all sub-renderers. Its API is imperative and framework-agnostic — React components call it via refs, and the widget/docs embed uses the same API.
Scene graph structure:
THREE.Scene
├── Lights (hemisphere + 3-point directional)
├── ImpostorAtomMesh.mesh (primary structure atoms)
├── ImpostorBondMesh.mesh (primary structure bonds)
├── CellRenderer (wireframe simulation box)
├── CellAxesRenderer (a, b, c axis arrows)
├── LabelOverlay (Canvas 2D text on top)
├─ ─ ArrowRenderer (per-atom vectors)
├── PolyhedronRenderer (coordination polyhedra)
├── Selection highlights
├── PivotMarker (rotation center crosshair)
└── StructureLayer[] (one per additional structure)
├── ImpostorAtomMesh
├── ImpostorBondMesh
└── CellRenderer
Impostor Technique
ImpostorAtomMesh (src/renderer/ImpostorAtomMesh.ts) uses InstancedBufferGeometry with a single 4-vertex quad:
Vertex Shader Fragment Shader
───────────── ───────────────
For each instance: For each fragment:
Expand quad to screen- Compute dist from center
aligned billboard sized If dist > 1.0 → discard
by radius × scale z = sqrt(1 - dist²)
Write correct gl_FragDepth
Apply lighting (dual diffuse
+ Blinn-Phong + Fresnel rim)
Instance attributes per atom:
instanceCenter(vec3) — world positioninstanceRadius(float) — vdW radius × BALL_STICK_ATOM_SCALEinstanceColor(vec3) — element color from CPK/VESTA schemeinstanceScaleOverride(float) — per-atom scale multiplier (default 1.0)instanceOpacityOverride(float) — per-atom opacity multiplier (default 1.0)
Global scale and opacity are O(1) uniform updates. Per-atom overrides are activated via uUsePerAtomOverrides uniform. Buffers are pre-allocated to maxAtoms (default 1M) and grow dynamically if needed.
Bond Rendering
ImpostorBondMesh (src/renderer/ImpostorBondMesh.ts) renders bonds as billboard quads stretched between atom pairs. Atom positions are stored in a DataTexture and read via texelFetch() in the vertex shader — this means per-frame position updates cost O(nAtoms) regardless of bond count.
Bond order visualization:
- Single — 1 cylinder
- Double — 2 parallel cylinders (perpendicular offset)
- Triple — 3 cylinders in triangular arrangement
- Aromatic — 1 solid + 1 dashed offset cylinder
Shader Architecture
All shaders are in src/renderer/shaders.ts as GLSL 3.0 ES strings, used with RawShaderMaterial. This requires explicit declaration of all uniforms and attributes (no Three.js auto-injection).
Lighting model:
- Dual directional diffuse (sky-blue key light + warm fill)
- Blinn-Phong specular highlights
- Fresnel rim lighting for depth cues
- Edge darkening for contour emphasis
AtomRenderer / BondRenderer Interfaces
src/types.ts defines renderer abstraction interfaces:
interface AtomRenderer {
readonly mesh: THREE.Object3D;
loadSnapshot(snapshot: Snapshot): void;
updatePositions(positions: Float32Array): void;
setScale?(scale: number, snapshot: Snapshot): void;
setOpacity?(opacity: number): void;
setScaleOverrides?(overrides: Float32Array): void;
setOpacityOverrides?(overrides: Float32Array): void;
clearOverrides?(): void;
dispose(): void;
}
ImpostorAtomMesh is the default implementation. The legacy AtomMesh (InstancedMesh + SphereGeometry) also exists. The interface is the swap point for alternative renderers.
Multi-Structure Overlay
StructureLayer (src/renderer/StructureLayer.ts) encapsulates an independent set of atom/bond/cell renderers per additional load_structure node. applyViewportState() routes the first particle source to the primary renderer and additional sources to their respective StructureLayers, keyed by sourceNodeId.
Extending megane
Adding a New Pipeline Node
The most common extension. Here are the steps, using a hypothetical "ColorByProperty" node as an example:
1. Define the node type in src/pipeline/types.ts:
- Add
"color_by_property"to thePipelineNodeTypeunion - Add a label in
NODE_TYPE_LABELS - Assign a category in
NODE_CATEGORY
2. Define parameters:
export interface ColorByPropertyParams {
type: "color_by_property";
property: "element" | "index" | "x" | "y" | "z";
colormap: string;
}
Add to the PipelineNodeParams union and defaultParams() switch.
3. Define ports in NODE_PORTS:
color_by_property: {
inputs: [{ name: "particle", dataType: "particle", label: "Particle" }],
outputs: [{ name: "particle", dataType: "particle", label: "Particle" }],
},
4. Write the executor in src/pipeline/executors/colorByProperty.ts:
export function executeColorByProperty(
params: ColorByPropertyParams,
inputs: Map<string, PipelineData[]>,
): Map<string, PipelineData> {
const particles = inputs.get("particle");
if (!particles?.length) return new Map();
const particle = particles[0] as ParticleData;
// ... compute color overrides ...
return new Map([["particle", { ...particle, /* modified fields */ }]]);
}
Follow existing patterns in filter.ts or modify.ts.
5. Register in the execution engine — Add a case "color_by_property": to the switch in executePipeline() (src/pipeline/execute.ts).
6. Create the UI node — Add src/components/nodes/ColorByPropertyNode.tsx wrapping NodeShell (src/components/nodes/NodeShell.tsx). Follow the pattern from FilterNode.tsx or ModifyNode.tsx.
7. Register in the node palette — Add to the node creation menu so users can drag it onto the canvas.
Adding a New Renderer Backend
Implement the AtomRenderer or BondRenderer interface from src/types.ts:
- Required:
meshproperty,loadSnapshot(),updatePositions(),dispose() - Optional:
setScale(),setOpacity(),setScaleOverrides(),setOpacityOverrides(),clearOverrides()
Wire it into MoleculeRenderer by replacing the renderer construction. See ImpostorAtomMesh and the legacy AtomMesh (src/renderer/AtomMesh.ts) as reference implementations.
Adding a New Data Channel Type
If the existing 7 channels don't cover your needs:
- Add to
PipelineDataTypeunion andDATA_TYPE_COLORSinsrc/pipeline/types.ts - Create the data interface (e.g.,
SurfaceData) - Add to the
PipelineDataunion - Add an input port to the viewport node's
NODE_PORTSentry - Add a field to
ViewportStateandDEFAULT_VIEWPORT_STATE - Handle the new field in
applyViewportState()(src/pipeline/apply.ts) - Implement the corresponding renderer
Modifying Shaders
Shaders live in src/renderer/shaders.ts as template literal strings. Because RawShaderMaterial is used, you must declare all uniform and in/out variables explicitly.
To add a new uniform:
- Declare it in the GLSL string (e.g.,
uniform float uMyParam;) - Add it to the
uniformsobject in the material constructor (inImpostorAtomMesh.tsorImpostorBondMesh.ts) - Set it from the renderer API:
this.material.uniforms.uMyParam.value = val
Adding a New File Format Parser
- Implement the parser in Rust in
crates/megane-core/src/ - Expose via WASM in
crates/megane-wasm/src/lib.rswith#[wasm_bindgen] - Expose via PyO3 in
crates/megane-python/src/lib.rswith#[pyfunction] - Add the file extension dispatch in
src/parsers/structure.ts(parseStructureFile) - Update the file format support list in
README.md
Key File Index
| Subsystem | Files |
|---|---|
| Core types | src/types.ts (Snapshot, Frame, AtomRenderer, BondRenderer) |
| Pipeline types | src/pipeline/types.ts (data channels, ports, node params, ViewportState) |
| Pipeline engine | src/pipeline/execute.ts, src/pipeline/graph.ts |
| Pipeline → Renderer | src/pipeline/apply.ts |
| Node executors | src/pipeline/executors/ (one file per node type) |
| Selection DSL | src/pipeline/selection.ts (recursive-descent parser/evaluator) |
| Main renderer | src/renderer/MoleculeRenderer.ts |
| Atom impostor | src/renderer/ImpostorAtomMesh.ts |
| Bond impostor | src/renderer/ImpostorBondMesh.ts |
| Shaders | src/renderer/shaders.ts |
| Multi-structure | src/renderer/StructureLayer.ts |
| Camera/controls | src/renderer/CameraManager.ts |
| Picking | src/renderer/Picking.ts |
| Element data | src/constants.ts (colors, vdW radii, bond params) |
| WASM parsers | crates/megane-core/, crates/megane-wasm/ |
| Python bindings | crates/megane-python/, python/megane/ |
| Pipeline store | src/pipeline/store.ts (Zustand) |
| Binary protocol | src/protocol/protocol.ts |