Skip to content

delta9000/x3d-cpp

Repository files navigation

x3d-cpp-gen

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.

Gallery — real X3D, rendered headless

Every image below is real X3D, parsed by the SDK and drawn by the headless CPU rasterizer exampleno 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.

Textured PBR lion bust PhysicalMaterial spheres in a skybox
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 grid RGB three-point lighting
Metallic × roughness sweep — the analytic Cook-Torrance/GGX BRDF. RGB three-point directional lighting accumulating on a sphere.

Primitive texcoords

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.

Utah teapot — 32 NURBS patches

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).

Animated — interpolators over time

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:

Kelp Forest flythrough

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:

X3D logo bounce grazing the corner uvgrid sphere orbiting a path
The X3D "DVD logo": a PositionInterpolator with true edge reflections that grazes the corner without hitting it. A textured sphere orbiting a closed path (PositionInterpolator → Transform.translation).
sphere cycling hue sphere spinning via SLERP
A ColorInterpolator cycling a material through the HSV hue arc. An OrientationInterpolator spinning a sphere via quaternion SLERP.

Full-quality WebM: dvd · position · orientation · color — regenerate everything with mise run demos; see examples/cpu_raster/.

Quickstart — three ways in

1. The x3d CLI (no code)

Build the tools (mise run buildbuild/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)

2. Embed the SDK (one header)

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/.

3. Render it headless (the gallery above)

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.png

Generating the C++ bindings

The 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_bindings

Runs from any working directory; the spec XML and Jinja templates ship with the package and are resolved relative to the install, not the CWD.

Options

  • -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 (env CLANG_FORMAT; empty to disable)
  • --compiler — C++ compiler for the smoke test (env CXX; 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.

Dev tasks (mise)

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.)

Golden-file policy

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:

  1. Change a template (src/x3d_cpp_gen/templates/) or emitter.
  2. Regenerate: uv run x3d-cpp-gen --out generated_cpp_bindings (or mise run gen).
  3. 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.

CI

.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:

  • pythonuv sync + uv run pytest (unit suite + full-tree golden test).
  • golden — the golden-drift gate (regenerate + diff).
  • cppcmake build + ctest across a compiler matrix that pins the baseline GCC 11 / Clang 14 and also runs the current distro compilers.

Configuration (optional external resources)

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.

Test corpus

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.txt lists the files to test; validating them needs the scenes they pull in via Inline/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 x3d SourceForge SVN repo (svn.code.sf.net/p/x3d/code); corpus-fetch pulls the same files over HTTP from www.web3d.org. For a full, version-pinned archive, svn checkout that 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.

License

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.

About

Headless, renderer-agnostic X3D 4.0 domain-runtime SDK for C++ — runtime + code generator.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors