A headless, renderer-agnostic X3D domain-runtime SDK in C++. Load an X3D scene (XML, ClassicVRML, VRML97, or JSON; versions 3.0–4.1), run its event / behavior model tick-by-tick, and pull out renderer-ready geometry — meshes, materials, lights, camera, background — for your backend. No GPU, no windowing, no rendering opinion: the runtime stays spec-correct and backend-free, and you bring (or borrow) the renderer.
The C++ node layer is generated from the official X3D Unified Object Model (UOM), so every node and field is spec-correct by construction.
Every image below is real X3D, parsed by the SDK and drawn by the headless CPU rasterizer example — no GPU, no display, no system dependencies (image I/O is a vendored single-header stb, compiled in). It consumes the same renderer-agnostic extraction seam any GL/Vulkan/CAVE consumer would, which is the whole point: the runtime stays spec-correct and backend-free.
![]() |
![]() |
| A ~47k-triangle CC0 lion bust (Poly Haven), textured PBR (base + ORM + normal map) under a three-point rig. | Glossy PhysicalMaterial spheres inside a Background panorama skybox. |
![]() |
![]() |
| Metallic × roughness sweep — the analytic Cook-Torrance/GGX BRDF. | RGB three-point directional lighting accumulating on a sphere. |
The analytic primitives (Box, Sphere, Cone, Cylinder), each wearing a
different proc: texture, so the per-primitive texture-coordinate generation
reads off the surface — checker on the box faces, lat/long on the sphere, brick
up the cone, rainbow bars around the cylinder.
The Utah teapot (Martin Newell, 1975 — public domain),
its 32 bicubic Bézier patches expressed as order-4 NurbsPatchSurface nodes — a Bézier
patch is a NURBS patch (clamped knots, unit weights). Tessellated with analytic
normals (the cross product of the surface partial derivatives), so the ceramic shading
runs smoothly across all 32 patch seams. Scene: assets/gallery/hero_teapot_nurbs.x3d.
Reproduce them with mise run cpuraster, then render any scene under
examples/cpu_raster/assets/ (e.g. … assets/models/lion_head/lion_head_lit.x3d -o lion.png).
Seamless loops the headless CPU rasterizer produced by stepping simulation
time frame-by-frame (--animate) and muxing with ffmpeg — the same
TimeSensor → Interpolator → ROUTE machinery a browser runs, with no GPU or
display:
A guided-tour flythrough of a real, 600+ item X3D world — the NPS/MOVES
Kelp Forest Exhibit (Inline composition, PROTO fish & kelp, ElevationGrid
terrain, swimming/swaying/pumping animation) — rendered entirely headless.
Exhibit © NPS MOVES Institute (free use with credit); fetched, not bundled.
Full-quality WebM ·
how it's built. And the synthetic
interpolator demos:
Full-quality WebM:
dvd ·
position ·
orientation ·
color — regenerate everything with mise run demos; see
examples/cpu_raster/.
Build the tools (mise run build → build/x3d), then drive scenes from the
shell — convert between encodings, validate against the spec, headlessly
simulate behavior, or export geometry:
x3d convert scene.x3dv -o scene.x3d -f xml # ClassicVRML → XML (or vrml|json)
x3d validate scene.x3d --json # conformance + profile-fit diagnostics
x3d sim scene.x3d --ticks 120 # run the event/behavior loop, trace field changes
x3d extract scene.x3d -o scene.stl # geometry → binary STL
x3d canonicalize scene.x3d # X3D Canonical Form (X3DC14N)Link x3d_cpp::sdk and #include "x3d/sdk.hpp" — everything an embedder needs
is in namespace x3d::sdk. Parse once, tick each frame, consume the delta:
#include "x3d/sdk.hpp"
namespace sdk = x3d::sdk;
sdk::X3DDocument doc = sdk::parseFile("scene.x3d"); // 4 encodings + gzip, with diagnostics
sdk::X3DExecutionContext ctx;
ctx.buildSceneGraph(doc.getScene());
ctx.buildFrom(doc.getScene());
sdk::SceneExtractor ex(ctx, doc.getScene());
sdk::RenderDelta f0 = ex.fullSnapshot(); // upload f0.added (meshes/materials/lights)
while (running) {
ctx.tick(now); // advance time, routes, scripts, behaviors
sdk::RenderDelta d = ex.delta(); // apply d.added / removed / updated*
}The SDK does no file IO, image decoding, or rasterization — those are
embedder-supplied seams (AssetResolver, TextureResolver, FontMetrics,
ScriptEngine, …), each proven swappable by a second backend. See
docs/sdk/.
The examples/cpu_raster/ reference consumer turns the
extraction output into a PNG on the CPU — no GPU, no display — which is exactly
how the gallery shots are made:
mise run cpuraster # build + test the rasterizer (build-cpuraster/)
build-cpuraster/examples/cpu_raster/x3d_cpu_raster scene.x3d -o scene.pngThe node layer is regenerated from the X3D UOM (build-time codegen, not needed to use the SDK):
uv run x3d-cpp-gen --out ./generated_cpp_bindingsRuns from any working directory; the spec XML and Jinja templates ship with the package and are resolved relative to the install, not the CWD.
-s, --spec— path to the X3D UOM XML (default: packaged 4.0 model)-o, --out— output directory (default:./generated_cpp_bindings)--templates— Jinja templates directory (default: packaged)--clang-format— formatter executable (envCLANG_FORMAT; empty to disable)--compiler— C++ compiler for the smoke test (envCXX; empty to skip)--no-test— skip generating/compiling the smoke test
The generated smoke test (generated_cpp_bindings/test.cpp) is value-asserting:
for every concrete node it default-constructs an instance and asserts each
readable field with a spec default returns exactly that default (comparing the
field getter against the node's static getDefault<Name>()), plus explicit
literal pins for a few well-known nodes (e.g. Box size=={2,2,2}, solid==true).
uv run x3d-cpp-gen compiles and runs it.
The repo ships a mise.toml task runner:
mise run gen # regenerate the committed C++ bindings into generated_cpp_bindings/
mise run test # pytest (unit suite + full-tree golden-drift test)
mise run golden # golden-drift gate (regenerate to a temp dir, diff every *.hpp)
mise run build # cmake configure + build + ctest
mise run corpus-fetch # fetch the X3D test corpus the differential gates need (see "Test corpus")
mise run ci # full local pipeline: test + golden + conformance-gate + build +
# cli-gate-regression (the last needs a corpus — run corpus-fetch first)(scripts/check_golden.sh is the same golden gate, runnable directly.)
generated_cpp_bindings/*.hpp are golden: they are committed and treated as
the source of truth for codegen output. The only generation artifacts that are
NOT golden are test.cpp / test_exec (gitignored).
Codegen changes are therefore intentional and explicit:
- Change a template (
src/x3d_cpp_gen/templates/) or emitter. - Regenerate:
uv run x3d-cpp-gen --out generated_cpp_bindings(ormise run gen). - Review and commit the new headers.
The golden-drift gate (scripts/check_golden.sh, tests/test_golden_tree.py,
and the golden CI job) regenerates into a temp dir and fails on ANY *.hpp
difference, so uncommitted codegen drift can never land silently.
.github/workflows/ci.yml runs on demand (workflow_dispatch — the full
matrix is heavy; re-enable the push: / pull_request: triggers to make it
automatic). Forgejo Actions reads the same file if the repo is mirrored there:
- python —
uv sync+uv run pytest(unit suite + full-tree golden test). - golden — the golden-drift gate (regenerate + diff).
- cpp —
cmakebuild +ctestacross a compiler matrix that pins the baseline GCC 11 / Clang 14 and also runs the current distro compilers.
The SDK and its generator build and test with no external data — the
generated bindings and the X3D Unified Object Model are bundled, so nothing
here is required to build, test, or use the SDK (mise run test / golden /
build need none of it). A few optional developer tools and conformance gates
plug in via environment variables. The RAG and JDK seams skip cleanly when
unset; the corpus differential gates instead fail-closed when asked to run
without a corpus (a gate with no inputs must never green) — fetch one in seconds
with mise run corpus-fetch (see Test corpus):
| Variable | Used by | What plugs in |
|---|---|---|
X3D_CORPUS_DIR |
corpus sweep, CLI/canon gates (mise run corpus / cli-gate / canon-gate) |
Root of a local X3D example archive checkout. Defaults to .x3d-corpus/ populated by mise run corpus-fetch — see Test corpus. |
X3D_SPEC_PROSE_DIR |
scripts/spec_rag.py |
Directory of X3D normative-prose markdown (one *.md per section, mirrored from web3d.org). |
X3D_EMBED_URL |
scripts/spec_rag.py, scripts/code_rag.py |
OpenAI-compatible embeddings endpoint (POST {"model","input"} → {"data":[{"embedding":[…]}]}). Default http://localhost:8080/v1/embeddings. |
X3D_QDRANT_URL |
the RAG scripts | Base URL of a Qdrant vector store. Default http://localhost:6333. |
X3D_JDK_BIN / JAVA_HOME |
tools/x3d-cli/gen_canon_goldens.sh |
A JDK ≥ 25 (X3DJSAIL -canonical needs it). Auto-discovered via JAVA_HOME / mise / PATH if unset. |
The differential gates (mise run cli-gate / canon-gate / cli-gate-regression)
and the full sweep (mise run corpus) validate the SDK against the Web3D X3D
Example Archive. None of this is needed to build, test, or use the SDK — only
to run those gates.
Get it with one command:
mise run corpus-fetch # ~9 MB, a few seconds
mise run cli-gate-regression # now PASS (was fail-closed with no corpus)corpus-fetch (scripts/fetch_corpus.sh) downloads exactly what the gates need
— the committed curated subset (tools/x3d-cli/goldens/subset.txt) plus its
transitive Inline/EXTERNPROTO scene dependencies — into the layout the
gates expect:
.x3d-corpus/x3d-code/www.web3d.org/x3d/content/examples/...
It lands in .x3d-corpus/ (gitignored) by default, which the gate tasks pick up
automatically; set X3D_CORPUS_DIR to use a different location (e.g. a full
archive checkout). Notes:
- Subset vs. dependencies.
subset.txtlists the files to test; validating them needs the scenes they pull in viaInline/EXTERNPROTO, so the fetcher resolves that closure. A bare subset would produce false "regressions". - A few subset entries may be unavailable upstream (pruned since the subset was committed); the gates tolerate this and the fetcher reports which.
- Authoritative source. Master is the Web3D
x3dSourceForge SVN repo (svn.code.sf.net/p/x3d/code);corpus-fetchpulls the same files over HTTP fromwww.web3d.org. For a full, version-pinned archive,svn checkoutthat repo into$X3D_CORPUS_DIR/x3d-code/. - License. The example scenes are open-source under the BSD-style Web3D
Consortium Open-Source License for Models and Software (each carries
<meta name='license' content='../license.html'/>). The fetcher downloads from the source and never redistributes them, so they are not bundled here.
MIT © 2026 Ben Sandbrook. Bundled and optional third-party components (including the Web3D Consortium X3D Unified Object Model, under a BSD-style license) are credited in NOTICE.










