Three.js renderer plugin for AR.js-core
npm install @ar-js-org/arjs-plugin-threejs> π§ͺ A Three.js renderer plugin for AR.js-core: mounts a WebGL canvas, consumes AR marker + camera events, and exposes perβmarker Three.js Group anchors 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, raw ar:getMarker, legacy ar: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 Group only when a marker first appears)
- π Debug helpers: scene & perβanchor AxesHelper
- π§ͺ Test-friendly: inject your own renderer via rendererFactory
- π Dual render triggers: engine:update or requestAnimationFrame fallback
- π‘ Confidence filtering on marker events
- π§Ή Clean disable/dispose lifecycle
> Note: the dist and types folders are not committed. If you modify the source, run npm install, then rebuild with npm run build:vite and npm run build:types before using the package or publishing.
``bash`
npm run build:vite
Outputs:
- ESM: dist/arjs-plugin-threejs.mjsdist/arjs-plugin-threejs.js
- CJS:
- Source maps included
Serve the example (choose one):
`bashIf example has its own dev scripts
cd examples/minimal
npm i
npm run dev
Quick start (Engine + Artoolkit + Three.js plugin) π
`js
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();
`Events handled π
| 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 / ar:markerUpdated / ar:markerLost | legacy shapes | Adapted internally to ar:marker |
| ar:camera | { projectionMatrix } | Sets camera projection |
| engine:update | any | Optional frame trigger (in addition to RAF) |Options βοΈ
| 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 | null | Inject custom renderer (testing) |Classic AR.js chain:
`
finalMatrix = R_y(Ο) R_z(Ο) modelViewMatrix * R_x(Ο/2)
`If
changeMatrixMode === 'cameraTransformMatrix', invert at the end.Camera Projection π―
`js
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 and how to add content π§±
Anchors are created lazily from the first pose event.
`js
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 });
}
});
`Testing π§ͺ
Run tests:
`bash
npm test
`Watch:
`bash
npm run test:watch
`Coverage 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:
`js
const fakeRenderer = {
domElement: document.createElement("canvas"),
setPixelRatio() {},
setClearColor() {},
setSize() {},
render() {},
dispose() {},
};
const plugin = new ThreeJSRendererPlugin({
rendererFactory: () => fakeRenderer,
});
`CI π€
GitHub Actions workflow (
.github/workflows/ci.yml) runs:- Install
- Build
- Tests (Node version defined in
.nvmrc file)
Badge above shows current status.Type Definitions π€π§Ύ
TypeScript declaration files are included in the
types folder. Prefer importing types from the provided declarations:-
types/index.d.ts
- types/threejs-renderer-plugin.d.tsSource maps (
.d.ts.map) are included for better editor/IDE support.Compatibility π
- Built & tested with Three.js 0.161.x
- Requires AR.js-core engine abstraction with an event bus (
on/off/emit`)- π 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 π