🧪 A Three.js renderer plugin for AR.js-core: mounts a WebGL canvas, consumes AR marker + camera events, and exposes per‑marker Three.js
Groupanchors for you to attach content.
🔧 Defaults replicate classic AR.js axis handling.
🚀 Designed for extensibility, testability (renderer injection), and modern ESM builds.
- Features
- Install / Build
- Quick Start
- Events
- Options
- Camera Projection
- Anchors & Adding Content
- Testing
- CI
- Compatibility
- Roadmap Ideas
- License
- ✅ Unified handling for
ar:marker, rawar:getMarker, legacyar:markerFound / Updated / Lost - 🔄 Automatic AR.js classic axis transform chain (
R_y(π) * R_z(π) * modelViewMatrix * R_x(π/2)) - 🧬 Optional experimental path (
invertModelView,applyAxisFix) - 🪝 Lazy anchor creation (create Three.js
Grouponly when a marker first appears) - 🎛 Debug helpers: scene & per‑anchor
AxesHelper - 🧪 Test-friendly: inject your own renderer via
rendererFactory - 🏃 Dual render triggers:
engine:updateorrequestAnimationFramefallback - 🛡 Confidence filtering on marker events
- 🧹 Clean disable/dispose lifecycle
npm run build:viteOutputs:
- ESM:
dist/arjs-plugin-threejs.mjs - CJS:
dist/arjs-plugin-threejs.js - Source maps included
Serve the example (choose one):
# If example has its own dev scripts cd examples/minimal npm i npm run dev # OR from repo root (so relative dist path works) npx http-server . # Open: http://localhost:8080/examples/minimal/import { Engine, webcamPlugin, defaultProfilePlugin } from "ar.js-core"; import { ThreeJSRendererPlugin } from "@AR-js-org/arjs-plugin-threejs"; // 1) Engine & core plugins const engine = new Engine(); engine.pluginManager.register(defaultProfilePlugin.id, defaultProfilePlugin); engine.pluginManager.register(webcamPlugin.id, webcamPlugin); const ctx = engine.getContext(); await engine.pluginManager.enable(defaultProfilePlugin.id, ctx); await engine.pluginManager.enable(webcamPlugin.id, ctx); // 2) Artoolkit plugin const { ArtoolkitPlugin } = await import( "./vendor/arjs-plugin-artoolkit/arjs-plugin-artoolkit.esm.js" ); const artoolkit = new ArtoolkitPlugin({ cameraParametersUrl: "/path/to/camera_para.dat", minConfidence: 0.6, }); await artoolkit.init(ctx); await artoolkit.enable(); // 3) Projection const proj = artoolkit.getProjectionMatrix?.(); const arr = proj?.toArray ? proj.toArray() : proj; if (Array.isArray(arr) && arr.length === 16) { engine.eventBus.emit("ar:camera", { projectionMatrix: arr }); } // 4) Three.js plugin const threePlugin = new ThreeJSRendererPlugin({ container: document.getElementById("viewport"), useLegacyAxisChain: true, changeMatrixMode: "modelViewMatrix", preferRAF: true, // debugSceneAxes: true, // debugAnchorAxes: true, }); await threePlugin.init(engine); await threePlugin.enable(); // 5) Start engine loop engine.start();| Event | Payload | Purpose |
|---|---|---|
ar:marker | { id, matrix?, visible? } | Unified high-level marker pose/visibility |
ar:getMarker | { matrix, marker: {...} } | Raw worker-level pose (plugin extracts ID/confidence) |
ar:markerFound / Updated / Lost | legacy shapes | Adapted internally to ar:marker |
ar:camera | { projectionMatrix } | Sets camera projection |
engine:update | any | Optional frame trigger (in addition to RAF) |
| Option | Type | Default | Description |
|---|---|---|---|
container | HTMLElement | document.body | Mount target for canvas |
preferRAF | boolean | true | Render each RAF even w/o engine:update |
minConfidence | number | 0 | Ignore ar:getMarker below confidence |
useLegacyAxisChain | boolean | true | Use classic AR.js transform chain |
changeMatrixMode | string | modelViewMatrix | Or cameraTransformMatrix (inverts) |
invertModelView | boolean | false | Experimental (disabled if legacy chain on) |
applyAxisFix | boolean | false | Experimental axis correction (Y/Z π) |
debugSceneAxes | boolean | false | Show AxesHelper at scene origin |
sceneAxesSize | number | 2 | Size for scene axes helper |
debugAnchorAxes | boolean | false | Add AxesHelper per anchor |
anchorAxesSize | number | 0.5 | Size for anchor axes helper |
rendererFactory | function | null | Inject custom renderer (testing) |
Classic AR.js chain:
finalMatrix = R_y(π) * R_z(π) * modelViewMatrix * R_x(π/2) If changeMatrixMode === 'cameraTransformMatrix', invert at end.
const proj = artoolkit.getProjectionMatrix(); const arr = proj?.toArray ? proj.toArray() : proj; if (Array.isArray(arr) && arr.length === 16) { engine.eventBus.emit("ar:camera", { projectionMatrix: arr }); }Look for log: Projection applied.
Anchors are created lazily from the first pose event.
engine.eventBus.on("ar:getMarker", (d) => { const id = String( d?.marker?.markerId ?? d?.marker?.id ?? d?.marker?.pattHandle ?? d?.marker?.uid ?? d?.marker?.index ?? "0", ); // Add content once anchor exists setTimeout(() => { const anchor = threePlugin.getAnchor(id); if (anchor && !anchor.userData._content) { anchor.userData._content = true; const cube = new THREE.Mesh( new THREE.BoxGeometry(0.5, 0.5, 0.5), new THREE.MeshBasicMaterial({ color: 0xff00ff }), ); cube.position.y = 0.25; anchor.add(cube); } }, 0); // Bridge raw to unified if (Array.isArray(d?.matrix) && d.matrix.length === 16) { engine.eventBus.emit("ar:marker", { id, matrix: d.matrix, visible: true }); } });Run tests:
npm testWatch:
npm run test:watchCoverage includes:
- Axis chain vs experimental path
- Inversion & axis fix effects
- Confidence filtering
- Anchor lifecycle (create, reuse, visibility)
- RAF fallback vs engine:update
- Projection & inverse
- Disable/Dispose cleanup
- Debug helpers presence
- Matrix invariants (
matrixAutoUpdate=false)
Test renderer injection example:
const fakeRenderer = { domElement: document.createElement("canvas"), setPixelRatio() {}, setClearColor() {}, setSize() {}, render() {}, dispose() {}, }; const plugin = new ThreeJSRendererPlugin({ rendererFactory: () => fakeRenderer, });GitHub Actions workflow (.github/workflows/ci.yml) runs:
- Install
- Build
- Tests (Node version defined in
.nvmrcfile) Badge above shows current status.
TypeScript declaration files are included in the types folder. Prefer importing types from the provided declarations:
types/index.d.tstypes/threejs-renderer-plugin.d.ts
Source maps (.d.ts.map) are included for better editor/IDE support.
- Built & tested with Three.js 0.161.x
- Requires AR.js-core engine abstraction with an event bus (
on/off/emit) - Should work with any tracking plugin that can emit marker IDs + 4x4 pose matrices
- 🔌 Additional renderer plugins (Babylon / PlayCanvas)
- 🧷 Multi-marker composition helpers
- 🌀 Pose smoothing module (optional add-on)
- 💡 Example gallery with animated models & GLTF loader integration
- 🧪 Visual regression tests (screenshot-based) in CI
MIT © AR.js Org
Made with ❤️ for Web AR. Contributions welcome! Open an issue / PR 🛠