Skip to content

rioharper/Inkfield

Repository files navigation

InkField

publicly available at inkfield.studio

Tool Preview

InkField is a browser-based renderer for Gaussian splat scans. Load a .ply, .spz, .splat, .ksplat, or .sog scene, orbit around it, and view it as a field of text glyphs, hatch strokes, or flow-placed marks whose direction follows the structure of the scan. A cinematographer-facing lens model, an animatable camera rig, and an offline export pipeline turn those looks into stills, vector art, and video.

As you may know, splats are not smooth meshes like STLs, but rather thousands (or more) little splotches averaged out into what looks like a mesh. Crucially, these splats are not spheres, as one may assume. They are ellipsoids, with it's eccentricity determined by a 3D covariance matrix split into independent scaling factors and rotation quaternions to match the geometric contours of the target scene.

This means we can match specific glyphs and flow not arbitrarily, like a default brightness-to-ASCII filter, but assign neighboring splats a specific glpyh that matches the eccentricity and orientation in 3D space. Flow mode bakes object-space streamlines over the scan before stamping glyphs along them. Marks can flow along surfaces, silhouettes, table edges, folds, and other geometry in the scene while staying attached as the camera orbits.

What It Includes

  • Static Vite + TypeScript web app with no backend.
  • Three.js camera, scene, and orbit controls.
  • Spark-powered Gaussian splat loading and rendering.
  • Bundled public/sample.spz scene that loads on startup.
  • Local scan loading through a file picker.
  • Four render modes:
    • Photo: normal Spark splat rendering.
    • Glyph: structure-aware character rendering.
    • Hatch: pen-style hatching driven by the same orientation field.
    • Flow: object-space streamline placement with discrete glyph or hatch stamps that ride the surface, with relighting and depth of field.
  • Two orientation sources, switchable with the Stroke source control:
    • Per-frame (screen): per-frame direction from screen-space splat covariance.
    • Locked to surface: direction from a precomputed 3D surface frame so strokes stay locked to surfaces while orbiting.
  • A one-time surface/flow precompute that runs off the main thread in a Web Worker, with progress reported in the status readout and results cached per scan so reloads and mode toggles are instant.
  • A thin-lens camera model: 24 / 35 / 50 / 85 mm focal-length presets, aperture stops, and a focus distance, with a focus-plane gizmo and an optional focus- peaking preview.
  • A depth-of-field stage in Flow mode that modulates the marks by their circle of confusion instead of blurring pixels.
  • An animatable camera rig with two paths — Orbit (turntable) and Dolly in — plus play / scrub / duration / speed controls.
  • An export pipeline: high-resolution PNG, vector SVG (instanced marks), and a deterministic frame sequence that encodes to WebM video or a PNG ZIP, with an export-side Orbit / Dolly motion toggle.
  • An Auto-tune look button that picks reasonable grid, saturation, bin, orientation, and flow settings for the loaded scan.
  • A single roomy inspector of collapsible, self-summarizing sections, with the render mode promoted to a permanent left rail.
  • Debug overlays for the derived per-splat normal and tangent fields.
  • Temporal smoothing to reduce flicker while orbiting in per-frame mode.

How It Works

Glyph and Hatch modes render the scene in a small grid, not at full screen resolution. Each grid cell becomes one glyph or hatch tile.

In the first pass, Spark projects the splats into an offscreen accumulation target. For each splat, InkField estimates the stretched oval it makes in screen space. The oval's long axis gives a local direction, and its opacity, size, and elongation decide how much influence it should have.

Line direction has no sign: a stroke at 10 degrees is the same as one at 190 degrees. To average those directions correctly, InkField stores each orientation as a double-angle vector:

(cos 2theta, sin 2theta) * weight

Those vectors are additively blended per grid cell. The renderer also accumulates weighted color and total coverage. A temporal pass blends the current orientation field with the previous frame so orbiting stays stable.

In the final pass, InkField reads each cell:

  • low-structure or rounded cells become blob-like glyphs such as ., o, or @;
  • elongated, confident cells become directional glyphs or hatch strokes;
  • color comes from the average splat color in that cell;
  • density comes from accumulated coverage.

The hatching mode uses the same orientation data as the glyph mode. It only swaps the atlas used for the final draw.

Flow placement

Flow mode is different from the grid modes above. It does not decide one mark per screen cell. Instead, the worker uses the baked surface frame to trace evenly spaced streamlines in object space:

  1. seed and trace sign-continuous surface streamlines through the tangent field;
  2. keep lines evenly spaced with a proximity grid, stopping when they approach existing accepted lines, low-confidence patches, loops, or off-surface areas;
  3. trace a second cross-hatch set along normal x tangent;
  4. sample the nearest splat color and normal at every streamline point;
  5. assign nested density levels so darker tones reveal finer lines.

At render time those streamlines are projected through the current camera and splat transform, then converted into discrete atlas stamps. The active Mark style control chooses ASCII-like glyphs or hatch marks, and Mark spacing controls the densest screen-space stamp interval. If a view would exceed the stamp budget, the pass raises the effective gap for that frame rather than drawing continuous strokes.

Flow mode is also relightable. The light azimuth/elevation controls produce a world-space light direction. Each frame the flow pass transforms that light into object space and computes a Lambert tone from the baked normals. Darkness reveals marks by fixed thresholds: coarse primary marks appear first, finer marks fill in as regions darken, and cross-hatch marks join in shadow. Because the thresholds are fixed, moving the light only adds or fades marks instead of replacing the whole pattern.

Lens and depth of field

InkField carries a thin-lens camera model (thin-lens-camera.ts) with the controls a cinematographer expects: a focal-length preset (24 / 35 / 50 / 85 mm), an aperture in stops, and a focus distance. Focal length sets the camera's field of view in every mode, the way real glass does. Aperture and focus distance drive a depth-of-field stage that is live in Flow mode.

That stage never blurs pixels. Defocus is treated as a property of each mark: the pass gives every stamp a camera-space depth, turns it into a circle of confusion through the active lens, and resolves one shared per-mark rule:

  • out-of-focus marks grow, soften, and jitter their orientation slightly;
  • a stable, position-hashed thinning fades marks out as they defocus, so density drops where the lens is soft while staying exact at the focus plane;
  • bright, strongly defocused survivors bloom into discrete round bokeh discs (one tile swap per mark, never a continuous blur).

Every driver is monotone in defocus and deterministic per mark, so a focus pull glides without flicker and an exported sequence is reproducible frame to frame. A focus-plane ring marks the focus distance in the viewport, and an optional focus-peaking preview tints in-focus marks magenta to confirm the plane — it is a preview assist only and is never written into an export.

The depth-of-field math lives in src/depth-of-field/ as pure, Three.js-free, Node-testable modules (circle of confusion, defocus response, stable thinning, the combined per-mark rule, bokeh glyphs, the animatable lens track, scene-level CoC resolution, and cine presets), so the live pass and the offline export share one implementation.

Camera motion and export

The camera rig (camera-rig.ts) drives deterministic motion for both live preview and export. It samples a captured orbit basis — target, radius, elevation, starting azimuth — and offers two paths: Orbit, a turntable that advances the azimuth, and Dolly in, which holds the angle and eases the radius inward. Scrubbing to a given progress always lands on the same pose, which is what makes exports repeatable. Play, scrub, duration, and speed are all adjustable.

orbitexample.mp4
dollyexample.mp4

Export runs offline against an offscreen render target so output is independent of the on-screen canvas size:

  • Still image — a high-resolution PNG at a resolution multiplier, or a vector SVG of the instanced marks (Glyph, Hatch, and Flow modes).
  • Movie — choose the export Motion (Orbit or Dolly, independent of the live preview path), set a frame count and frame rate, render a deterministic frame sequence, then save it as a WebM video (encoded with WebCodecs) or, as a fallback, a ZIP of PNG frames.

Each frame is captured only after its render fully completes and is hashed, so the sequence is stable and the exported colors match the viewport byte-for-byte.

Screen vs. surface orientation

The directions above come from one of two sources, chosen by the Stroke source control.

In per-frame (Screen) mode, the direction of each splat is read from its projected covariance every frame. This reads well in stills, but strokes tend to swim across surfaces as the camera orbits, because the direction is a screen- space measurement rather than a property of the geometry.

In Locked to surface mode, InkField computes a coherent 3D surface frame once when a scan loads, in a background Web Worker:

  1. build a kd-tree over the splat centers and a k-nearest-neighbor graph;
  2. derive a per-splat normal from the smallest eigenvector of each splat's covariance, with a confidence score from its anisotropy;
  3. make the normal signs consistent across neighbors with minimum-spanning-tree propagation;
  4. smooth a tangent line field over the surface using the same nematic double-angle representation, so it has no 180-degree seams.

The resulting tangent, normal, and confidence are baked into per-splat textures. Each frame the baked tangent is projected into view space and fed into the same accumulation and glyph pass, blending back toward the screen-space direction where confidence is low. Because the direction now lives in 3D, it rotates with the camera and strokes stay attached to surfaces, so temporal smoothing is turned off in this mode. The same surface field also seeds the flow-glyph bake. The normal and tangent fields can be inspected directly through the Normals and Tangents overlays.

What It Could Be Used For

InkField is useful anywhere a 3D scan should be read as drawing, notation, or texture instead of photorealistic imagery:

  • stylized presentation of Gaussian splat captures;
  • visual experiments with structure-aware ASCII art and flow-placed glyphs;
  • sketch, hatch, and print-like render studies;
  • cinematic motion studies — turntables, dolly pushes, and rack-focus looks where defocus is drawn as marks rather than blur;
  • portfolio stills, vector exports, or short videos built from real scanned objects;
  • research sketches around orientation fields, screen-space splat analysis, and non-photorealistic rendering.

It is especially suited to scans with clear edges, surface flow, folds, furniture, architecture, plants, fabric, or objects whose shape should remain legible after being reduced to marks.

Getting Started

Install dependencies:

npm install

Run the development server:

npm run dev

Build the static site:

npm run build

Run tests:

npm test

The repo also carries offline "spike" scripts that render validation SVGs into output/ for the flow-glyph and depth-of-field pipelines (for example npm run dof:focus-pull or npm run flow:streamlines); npm run dof:test runs just the depth-of-field math tests. See the scripts block in package.json for the full list.

Project Shape

index.html                          Vite entry; mounts #app and loads src/main.ts

src/
  main.ts                           tiny entry: imports styles and calls
                                      bootstrapInkField()
  app/
    bootstrap.ts                    wires everything together: scene context, UI,
                                      passes, render loop, export controller, and
                                      the initial control values
    app-shell.ts                    builds the DOM (mode rail + collapsible
                                      inspector sections) into #app
    scene-context.ts                Three.js renderer / scene / camera / orbit
                                      controls, camera rig, thin-lens, focus-plane
    render-loop.ts                  the per-frame loop and viewport resize
    ui-bindings.ts                  connects every InkFieldUi event to its handler
    export-controller.ts            PNG / SVG / frame-sequence / video / PNG-ZIP
                                      export, and the export motion toggle

  control-panel.ts                  createUi / InkFieldUi: binds load buttons,
                                      mode/source radios, sliders, lens, motion,
                                      transform, export, and overlay controls,
                                      plus the status readouts and section summaries
  render-settings.ts                shared types and defaults for render mode, grid
                                      columns, direction bins, saturation,
                                      orientation source, flow controls, light
                                      direction, and splat transform
  auto-tune.ts                      derives reasonable look settings from the scan
  camera-rig.ts                     deterministic orbit/dolly motion for preview
                                      and export
  thin-lens-camera.ts               cinematographer-facing lens model: focal length,
                                      aperture, focus distance, FoV, presets
  splat-formats.ts                  supported splat extensions and accept strings
  splat-stage.ts                    Spark scene setup, sample/local loading, splat
                                      transform, debug overlays; launches the
                                      surface/flow worker and caches its results
  orientation-math.ts               nematic (double-angle) orientation helpers:
                                      angle<->vector, averaging, and bin quantization
  splat-attribute-shaders.ts        per-splat vec3 attribute texture and the Spark
                                      vertex-shader patch that reads it, plus
                                      covariance/eigensolve normal helpers

  depth-of-field/                   pure, Three.js-free, Node-testable DoF stack:
    circle-of-confusion.ts            thin-lens CoC per mark
    defocus-response.ts               CoC -> how a present mark is drawn
    stable-thinning.ts                fixed per-mark rank + soft drop
    defocused-mark.ts                 the single shared per-mark rule
    bokeh-glyph.ts                    bright + defocused survivors bloom to discs
    lens-track.ts                     animatable focus/aperture along the path
    scene-defocus.ts                  resolves a scene CoC for the live pass
    cine-presets.ts                   named lens/look presets
    focus-peaking.ts                  in-focus preview tint math
    focus-plane-indicator.ts          world-space focus-plane gizmo (viewport only)

  export/
    offline-renderer.ts             deterministic offscreen capture primitive
    frame-sequence-renderer.ts      walks a normalized camera path, one frame/pose
    video-sequence-export.ts        WebCodecs WebM encode + PNG-ZIP packaging
    png-export.ts                   single-frame PNG encode + download helpers
    orientation-svg-export.ts       glyph/hatch SVG document emit
    flow-svg-export.ts              flow-mode SVG document emit
    svg-glyph-instances.ts          shared instanced-symbol SVG builder

  surface-field/
    precompute.ts                   kd-tree + k-NN graph, covariance normals, MST
                                      sign orientation, nematic tangent smoothing
    worker.ts                       Web Worker entry for surface + flow precompute
    worker-client.ts                main-thread client: spawn, progress, results
    textures.ts                     packs normal/tangent/confidence into textures

  flow-glyphs/
    field/                          sampler and singularity / low-coherence masking
    streamlines/                    tracing, seeds, even spacing, proximity grid,
                                      and nested density assignment
    bake/                           worker-side object-space line bake + cache
    render/                         projection, discrete stamp placement, stamp
                                      reveal thresholds, and tonal reveal math
    math/vec.ts                     shared Vec2/Vec3 and scalar easing helpers
    flow-runtime-status.ts          flow bake/render status formatting

  passes/
    stylized-glyph-pass.ts          OrientationGlyphPass: accumulation + temporal +
                                      glyph/hatch draw (screen/surface)
    orientation-glyph-pass.ts       orientation accumulation pass entry
    orientation-glyph-shaders.ts    accumulation/draw shader sources
    brightness-glyph-pass.ts        brightness-ramp glyph scaffolding
    flow-glyph-pass.ts              Flow mode pass: projects baked streamlines,
                                      stamps glyph/hatch quads, applies relight and
                                      the live depth-of-field response

  atlas/
    glyph-atlas.ts                  builds glyph and hatch atlas textures + layout
    glyph-definitions.ts            brightness ramp, blob, and directional glyph sets

  style.css                         app styling

public/
  sample.spz                        bundled demo scan

scripts/                            offline spike renderers (flow-* and dof-*) that
                                      write validation SVGs into output/

tests/                              Node test runner specs mirroring the source:
  brightness-glyph-pass / orientation-glyph-pass / hatch-mode-and-ui
  splat-attribute-shaders / surface-field-precompute / auto-tune
  streamline-tracing / even-spacing / field-reliability / flow-glyph-stamping
  tonal-reveal / flow-bake-cache
  circle-of-confusion / stable-thinning / thin-lens-camera / defocus-response
  defocused-mark / bokeh-glyph / lens-track / focus-peaking / cine-presets
  scene-defocus
  offline + sequence: sequence-export / frame-sequence-renderer /
  video-sequence-export / png-export / flow-svg-export

About

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors