Building a Scannable 3D QR Code Generator with WebGPU

I built Every QR Code to answer a narrow technical question: can a URL grow into a memorable 3D world without making the QR code less trustworthy? Enter a URL in the Studio above and it becomes a Tree or Terrain. Reveal the code, and the same scene returns to a canonical QR matrix that points to the original destination.

That distinction matters. I do not ask a scanner to interpret the decorative silhouette as a QR code. The artwork and the machine-readable code are two views of one identity. The QR matrix stays the source of truth; the 3D form is derived from it.

The implementation is split into four parts:

Plain text
URL normalization
  → canonical QR matrix + role map
  → deterministic Link DNA + QR-derived fields
  → Tree or Terrain model
  → WebGPU scene ⇄ exact flat QR view
                          ↘ static SVG fallback

This article explains how that separation works, where determinism comes from, and which limitations I deliberately kept instead of hiding them behind a “creative QR” label.

Keep the QR matrix as an invariant

Every QR Code first turns the input into an HTTP or HTTPS URL. It trims whitespace, supplies https:// for host-like input, removes a default port, and rejects credentials or unsupported schemes. The normalized payload then goes through a conventional QR encoder with error correction level M.

The current QR profile accepts symbol versions 1 through 6. That cap is intentional: it keeps the matrix small enough to become legible 3D geometry. A very long or complex URL fails with a clear error instead of silently producing a dense world whose modules are too small to use.

The encoder output is copied into a plain binary matrix. I also build a role map that identifies the finder patterns, separators, timing patterns, alignment patterns, format bits, fixed dark module, data bits, and remainder bits. The code can therefore treat structural QR modules differently from ordinary data without rewriting any of them.

The flat output is equally conservative. Consecutive dark modules are compacted into one SVG path, surrounded by the standard four-module quiet zone. Both the SVG fallback and the flat WebGPU view come from this same matrix.

In other words, styling never feeds back into encoding:

Plain text
payload URL → QR encoder → immutable dark/light cells
                               │
                               ├─ flat QR view
                               ├─ SVG fallback
                               └─ fields used to grow the 3D world

That one-way data flow is the main reason I can make the artwork expressive without asking it to carry the link by approximation.

Build a stable identity without letting tracking parameters control it

A QR matrix must encode the destination exactly, but visual identity benefits from a more deliberate definition. I separate the link into three identities:

Identity Input Why it exists
Family Registrable domain Related hosts can share a broad visual family
Site Hostname Paths on one site can keep the same site-level procedural DNA
Page Normalized page URL A page can request its own repeatable world

The page identity removes the fragment, drops common tracking parameters such as utm_*, gclid, and fbclid, and sorts the remaining query parameters. Those changes affect the visual seed only; the QR payload still preserves the normalized destination. A campaign link can therefore keep its campaign parameters without allowing them to define an unrelated visual family.

Each identity is hashed with SHA-256. A named channel such as seed/v1/shape or tree/v2/archetype hashes the source digest and channel label again, then turns the result into four 32-bit values for an SFC32 random stream. Separate channels prevent a new color choice from consuming the random number that previously selected geometry.

The default component scope is site. It uses the hostname for detailed visual seeds while still encoding each complete URL in its own QR matrix. Passing identityScope="url" switches those detail seeds to the normalized page identity. Family-level channels remain useful for keeping related pages visually recognizable.

Determinism also needs a version boundary. A generator version is a drawing recipe, not an npm package version. Share links and saved Gallery records store that version, so a future package can add a new recipe without silently changing a world that was created with version 1.

Turn a binary matrix into Tree and Terrain fields

Using dark cells as a list of cubes produced a QR-shaped object, but not a coherent world. I needed continuous signals that could guide form without changing the cells themselves. From the matrix I derive:

  • local dark-module density in 3×3 and 5×5 neighborhoods;
  • a separable five-tap Gaussian blur;
  • distance to the nearest dark and light modules;
  • an edge field computed from the blurred matrix; and
  • the structural role of every QR module.

These fields let the renderer ask higher-level questions. Is this module inside a dense region? Is it close to a boundary? Does it belong to a finder pattern? The answers influence relief, material, feature placement, and palette slots, while the binary matrix remains unchanged.

For every dark module, the seed model stores two positions. The first is its exact coordinate on the flat QR ground plane. The second maps the same module onto the selected 3D topology. The renderer can therefore interpolate between a world and its canonical QR target instead of reconstructing the QR code at the end of an animation.

Tree and Terrain use the same identity but interpret the derived fields differently:

  • Tree keeps QR-anchored base columns, then grows trunk, branch, canopy, and blossom structures above them. The URL-derived channels choose a stable archetype and morphology.
  • Terrain converts dark-module relief into a height field, smooths it for three passes, and attenuates the outer edge. Raising a module changes only its height; its QR column does not drift.

Tests enforce the invariants behind that description: every dark cell has one finite surface and QR target, the ground targets stay on one plane, raised layers remain in their original columns, and Terrain cannot mutate the canonical QR field. Those tests are more valuable than comparing a few screenshots because they check the relationship the product promises.

Keep WebGPU behind a lazy rendering boundary

QR generation does not require WebGPU. The public core package owns URL parsing, hashing, the QR matrix, role map, derived fields, and SVG path. The renderer package owns the WebGPU scene. React and Web Component adapters connect the two:

Package Responsibility
@every-qrcode/core Link identity, canonical QR data, and SVG path
@every-qrcode/renderer-webgpu Tree and Terrain models plus the GPU renderer
@every-qrcode/react Typed React component and interaction state
@every-qrcode/web-component Framework-independent custom element

The adapters dynamically import the renderer only when they prepare a 3D seed. The Gallery observes cards before mounting them, and the product build keeps Tree and Terrain in separate lazy chunks so switching models is the point at which the other model is downloaded.

If WebGPU initialization fails, the adapter replaces the canvas with the canonical SVG QR path. The fallback is deliberately static: unsupported GPU hardware should reduce visual richness, not remove the destination. Generation happens in the browser, and the reusable components do not need a server call to create the identity or render the code.

Use it from React or without a framework

The React component exposes the product boundary rather than the renderer internals:

Bash
pnpm add @every-qrcode/react
TypeScript
import { EveryQRCode } from '@every-qrcode/react';

export function WebsiteIdentity() {
	return <EveryQRCode identityScope="site" model="tree" url="https://example.com" />;
}

For a page without React, the Web Component uses the same core and renderer:

Bash
pnpm add @every-qrcode/web-component
JavaScript
import '@every-qrcode/web-component/auto';
HTML, XML
<every-qr-code model="terrain" url="https://example.com"></every-qr-code>

The packages and their tests are available in the Every QR Code source repository under the MIT License. The Gallery and Studio are free to use.

The tradeoffs I would keep

This architecture makes several constraints explicit:

  1. The decorative world is not itself the scanning surface. It must reveal the flat QR view.
  2. Long URLs are bounded by QR profile v1. Shortening a link is better than rendering an unreadably dense 3D field.
  3. Interactive 3D depends on WebGPU. The fallback preserves the QR code, not the animation.
  4. Deterministic output must be versioned. Changing a procedural recipe without retaining its version would break saved identities even if the URL stayed the same.
  5. Visual similarity and destination identity are different. Family and site scopes can keep related pages recognizable, while the canonical matrix still represents the complete payload.

The result is less magical than claiming that any illustration can be scanned, but it is easier to trust. A standard QR matrix owns the destination; deterministic fields make it memorable; WebGPU adds the world; and a plain SVG remains available when the GPU path cannot run.

For another system that separates canonical source data from browser rendering, read how I built the OpenStreetMap data pipeline behind EveryCityMap.