poka.sh

Docs

How poka-3d-viewer draws a model

explanation

The wasm-only rule (owner, 2026-10-16) says every viewer uses wasm: the host may only present what a wasm module produced. Until poka-viewers#96 the 3D viewer broke it. Both hosts decoded the parser's Scene3DAst themselves and drew it with their own Three.js code (viewer-scene3d.js in poka-interfaces), and this crate's wasm only checked the scene and printed a summary. The owner decided to move the drawing into the wasm, and this page explains what moved, what is left in script and why.

What the wasm does

poka-3d-viewer reads the Scene3DAst like any other viewer reads its *Ast and writes HTML. In that HTML it puts everything a draw needs, worked out once from the file's own data (src/scene.rs):

  • Geometry. One batch per mesh: its positions as f32 x, y, z, relative to the centre of the scene bounds (subtracted in f64 first, so a georeferenced model in the millions keeps its precision on the GPU), its indices as u32, and which primitive it is (triangles, line segments or points, with the old name test for a point cloud from a parser that predates Mesh3D.primitive). A trailing incomplete triangle or segment is dropped and named on the card.
  • Normals. The file's own when it gives one per vertex; otherwise area-weighted vertex normals for an indexed mesh and flat face normals for a triangle soup. They are written as i8 x, y, z.
  • Colour and material. The material's base colour, then per-vertex colours, then the mesh colour, then the default for the primitive; an alpha of 0 means opaque, and a material's opacity, metallic and roughness are carried with the batch.
  • Layers, bounds and views. Meshes grouped by element type (the part of the name before ·), with a swatch and a count; the file's lights as one marker layer; the bounds of the scene and of every batch; the saved views in the same local coordinates.
  • Point sizes. Each cloud's size as a share of the view, from its own point spacing.

The buffers go into one binary blob, base64 in a <script type="application/octet-stream" id="s3-blob">, and the description of every batch (where its buffers are in the blob, its colour, its layer and bounds) is JSON in <script type="application/json" id="s3-scene">. Strings from the file reach the page only through that JSON, escaped so a < cannot close the element, and the script puts them in with textContent.

Below the stage is the shared information card with what the scene holds.

What stays in script, and why

The viewer ABI runs a wasm once per preview (*Ast in, HTML out), so the wasm cannot be asked again for every mouse move. The HTML therefore carries a small presenter (PRESENTER_JS in src/present.rs). It knows nothing about any file format. It:

  • uploads the buffers to WebGL exactly as the wasm wrote them and draws each batch with the colour, opacity and material terms the wasm chose;
  • turns pointer and wheel input into a camera (orbit, pan, zoom to the cursor) and builds the view and projection matrices from it each frame;
  • shades surfaces in its GLSL (a sky and ground fill, a key light over the viewer's shoulder, a cool rim and a filmic tone curve, the same lighting the Three.js viewer used); lines and points keep their exact colours;
  • reads the depth under the cursor for picking (double-click to orbit about a point, and the measure tool), and the toolbar's saved views, Fit, Flip and the layer toggles, all from the JSON.

That is the presentation bridge the issue allows: a canvas and a GPU draw of buffers a wasm produced. Three.js is not used.

What is not drawn yet

Textures and the sky image. Both are encoded images (Texture3D), and letting the browser decode them would break the same rule; surfaces show their base colour and the card names the textures it left out. They come back when a viewer can decode images in wasm (poka-parsers' image work).

Limits

  • One preview carries at most 44 MiB of vertex data (the base64 is 4/3 of that, and the desktop host caps a module's stdout at 64 MiB). Past that the card says the model is too large to draw here.
  • The wasm memory cap is 256 MiB, enough for the scene, its copy in the buffers and the output, streamed.

Hosts

The hosts still special-case the viewer id until poka-interfaces switches to running this wasm like every other viewer, and showing its HTML in the preview frame. Until then a change here does not reach the app. A new field in Scene3DAst or Mesh3D reaches the app through the hosts' own decoders as well, so follow Add a field to an AST and expect to touch them too.

Checking it

  • cargo test -p poka-3d-viewer: the scene builder and the HTML.
  • python3 build/scripts/check-scene3d-corpus.py --parsers ../poka-parsers --html-out DIR: runs every 3D parser and this wasm on the corpus with wasmtime and checks the counts and bounds the wasm wrote against the Scene3DAst and, for STL, OBJ, OFF, PLY and glTF, against a reader of the file itself.
  • node build/scripts/scene3d-present-check.mjs --html DIR --interfaces ../poka-interfaces: loads each page in the host's own preview frame in headless Chromium and checks that the presenter draws the counts the wasm planned.