Skip to main content

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.

ChannelInterfaceKey FieldsProduced ByConsumed By
particleParticleDatasource (Snapshot), indices, scaleOverrides, opacityOverridesLoadStructure, Streaming, Filter, ModifyFilter, Modify, AddBond, Labels, Polyhedra, Viewport
bondBondDatabondIndices, bondOrders, nBonds, scale, opacityAddBond, Streaming, Filter, ModifyFilter, Modify, Viewport
cellCellDatabox (3x3 Float32Array), visible, axesVisibleLoadStructure, StreamingViewport
trajectoryTrajectoryDataprovider (FrameProvider), metaLoadStructure, LoadTrajectory, StreamingViewport
labelLabelDatalabels[], particleRefLabelGeneratorViewport
meshMeshDatapositions, indices, normals, colorsPolyhedronGeneratorViewport
vectorVectorDataframes (VectorFrame[]), scaleLoadVector, VectorOverlayVectorOverlay, 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:

  1. Topological sorttopologicalSort() (Kahn's algorithm) in src/pipeline/graph.ts determines execution order.
  2. Per-node dispatch — A switch on params.type calls the appropriate executor from src/pipeline/executors/.
  3. Edge data propagation — An EdgeOutputs map (Map<nodeId, Map<portName, PipelineData>>) carries outputs between nodes. collectInputs() gathers upstream data for each node.
  4. Disabled node passthrough — Disabled nodes forward their first input to a matching output port, preserving downstream flow.
  5. ViewportState assembly — The viewport executor collects all 7 channels into a single ViewportState.
  6. Error collectionNodeError objects (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 position
  • instanceRadius (float) — vdW radius × BALL_STICK_ATOM_SCALE
  • instanceColor (vec3) — element color from CPK/VESTA scheme
  • instanceScaleOverride (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 the PipelineNodeType union
  • 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: mesh property, 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:

  1. Add to PipelineDataType union and DATA_TYPE_COLORS in src/pipeline/types.ts
  2. Create the data interface (e.g., SurfaceData)
  3. Add to the PipelineData union
  4. Add an input port to the viewport node's NODE_PORTS entry
  5. Add a field to ViewportState and DEFAULT_VIEWPORT_STATE
  6. Handle the new field in applyViewportState() (src/pipeline/apply.ts)
  7. 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:

  1. Declare it in the GLSL string (e.g., uniform float uMyParam;)
  2. Add it to the uniforms object in the material constructor (in ImpostorAtomMesh.ts or ImpostorBondMesh.ts)
  3. Set it from the renderer API: this.material.uniforms.uMyParam.value = val

Adding a New File Format Parser

  1. Implement the parser in Rust in crates/megane-core/src/
  2. Expose via WASM in crates/megane-wasm/src/lib.rs with #[wasm_bindgen]
  3. Expose via PyO3 in crates/megane-python/src/lib.rs with #[pyfunction]
  4. Add the file extension dispatch in src/parsers/structure.ts (parseStructureFile)
  5. Update the file format support list in README.md

Key File Index

SubsystemFiles
Core typessrc/types.ts (Snapshot, Frame, AtomRenderer, BondRenderer)
Pipeline typessrc/pipeline/types.ts (data channels, ports, node params, ViewportState)
Pipeline enginesrc/pipeline/execute.ts, src/pipeline/graph.ts
Pipeline → Renderersrc/pipeline/apply.ts
Node executorssrc/pipeline/executors/ (one file per node type)
Selection DSLsrc/pipeline/selection.ts (recursive-descent parser/evaluator)
Main renderersrc/renderer/MoleculeRenderer.ts
Atom impostorsrc/renderer/ImpostorAtomMesh.ts
Bond impostorsrc/renderer/ImpostorBondMesh.ts
Shaderssrc/renderer/shaders.ts
Multi-structuresrc/renderer/StructureLayer.ts
Camera/controlssrc/renderer/CameraManager.ts
Pickingsrc/renderer/Picking.ts
Element datasrc/constants.ts (colors, vdW radii, bond params)
WASM parserscrates/megane-core/, crates/megane-wasm/
Python bindingscrates/megane-python/, python/megane/
Pipeline storesrc/pipeline/store.ts (Zustand)
Binary protocolsrc/protocol/protocol.ts