Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 66 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 10 additions & 12 deletions packages/inspector/docs/authoring-a-renderer.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
The Decentraland inspector is **renderer-agnostic**. It owns the scene as
`@dcl/ecs` state (a CRDT-synced entity/component graph) and talks to whatever
draws that scene through a single interface, `IRenderer`. Babylon.js is the
built-in renderer. You can add your own (Unity, Bevy, PlayCanvas, a custom
WebGL/WebGPU engine, …) without modifying the inspector core.
default renderer; Three.js ships as a second one. You can add your own (Unity,
Bevy, PlayCanvas, a custom WebGL/WebGPU engine, …) without modifying the
inspector core.

This document is the contract spec. The public API lives at `@dcl/inspector`
(see `src/lib/renderer/index.ts`).
Expand Down Expand Up @@ -41,14 +42,12 @@ those are the inspector's. You implement drawing + input.

## Coordinate system & conventions

- **Right-handed, Y-up, meters.** Same as the SDK glTF convention. (Babylon is
left-handed internally; its adapter converts. Your renderer is responsible for
mapping the SDK convention to its own.)
- **Right-handed, Y-up, meters.** Same as the SDK. (Babylon is left-handed
internally; its adapter converts. Three.js is right-handed and maps directly.)
- **Vectors on the boundary are plain data**: `{ x, y, z }` (`@dcl/ecs-math`
`Vector3`) and `{ x, y, z, w }` quaternions. **Never** pass a live engine
object (a `BABYLON.Mesh`, a WASM handle, any scene node) across the contract —
only IDs, scalars, and plain vectors. This is what lets a renderer run
out-of-process.
object (a `THREE.Object3D`, a `BABYLON.Mesh`) across the contract — only IDs,
scalars, and plain vectors. This is what lets a renderer run out-of-process.
- **Entities are numbers** (`@dcl/ecs` `Entity`). The root is entity `0`.

---
Expand Down Expand Up @@ -154,10 +153,9 @@ toolbar picker; selecting yours persists the choice and reloads with it active.
data layer — use it instead of `fetch`; the inspector owns where assets live.

The forward path: connect your `engine` to CRDT (the inspector does this from
your returned `engine`), and project component changes. The pattern is a CRDT
subscriber — your `@dcl/ecs` engine's `onChangeFunction` translates the SDK
components you support (`Transform`, `GltfContainer`, `MeshRenderer`, …) into
your engine's scene graph. The built-in Babylon `SceneContext` is the reference.
your returned `engine`), and project component changes. The cleanest template is
`src/lib/renderer/three/ThreeSceneContext.ts` — a ~150-line CRDT subscriber that
turns `Transform`/`GltfContainer`/`MeshRenderer` into a Three scene graph.

---

Expand Down
2 changes: 2 additions & 0 deletions packages/inspector/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"dependencies": {
"@babel/parser": "7.28.5",
"@dcl/asset-packs": "file:../asset-packs",
"three": "^0.169.0",
"ts-deepmerge": "^7.0.0"
},
"devDependencies": {
Expand All @@ -25,6 +26,7 @@
"@types/react-dom": "18.3.0",
"@types/react-modal": "^3.16.0",
"@types/redux-saga": "^0.10.5",
"@types/three": "^0.169.0",
"@types/uuid": "^9.0.5",
"@vscode/webview-ui-toolkit": "^1.2.2",
"@well-known-components/pushable-channel": "^1.0.3",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ export default withSdk<Props>(({ sdk }) => {
}

// Spawn-point handles go through the renderer-agnostic contract. A renderer
// without in-scene handles no-ops them; the panel still edits spawn-point
// values via the form. (Named *controller* to avoid colliding with the
// `spawnPoints` array-state above.)
// without in-scene handles (e.g. three) no-ops them; the panel still edits
// spawn-point values via the form. (Named *controller* to avoid colliding
// with the `spawnPoints` array-state above.)
const spawnPointController = sdk.renderer.spawnPoints;

const [selectedSpawnPointIndex, setSelectedSpawnPointIndex] = useState<number | null>(() =>
Expand Down
2 changes: 1 addition & 1 deletion packages/inspector/src/components/Renderer/Renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ const Renderer: React.FC = () => {

const getDropPosition = async () => {
// Renderer-agnostic: ask the active renderer where the pointer hits the
// ground, then snap. Works for any renderer.
// ground, then snap. Works for any renderer (Babylon, three, …).
const point = (await sdk!.renderer.getPointerWorldPoint()) ?? { x: 0, y: 0, z: 0 };
return snapPosition(new Vector3(fixedNumber(point.x), 0, fixedNumber(point.z)));
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,13 @@ import type { RendererId } from '../../../lib/renderer/controller';
import './RendererPicker.css';

/**
* Toolbar control to choose the active 3D renderer from the registered plugins
* (see {@link registerRenderer}). With only the built-in Babylon renderer
* registered the picker lists a single option; it lights up automatically as
* renderers are registered.
* Toolbar control to choose the active 3D renderer (Babylon.js / Three.js).
*
* Selecting a renderer persists the choice and reloads the inspector so it
* initializes with that engine — the editor UI is wired to the scene in places,
* so a clean reload is simpler and safer than a live swap.
* A test tool for the pluggable-renderer boundary. Selecting a renderer persists
* the choice and reloads the inspector so it initializes with that engine — the
* editor UI is wired to the Babylon scene in places, so a clean reload is
* simpler and safer than a live swap. Three.js is the minimal-proof renderer
* (entities + camera + pick); editor extras won't appear there.
*/
const RendererPicker = withSdk(() => {
const current = getSelectedRenderer();
Expand Down
4 changes: 2 additions & 2 deletions packages/inspector/src/lib/renderer/conformance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,8 @@ export function createRendererConformanceSuite(options: RendererConformanceOptio
it('ingests an engine tick without throwing', async () => {
// The contract doesn't expose the scene graph, so the kit only asserts
// the renderer ingests engine changes cleanly. Authors should add their
// own scene-graph assertions for full forward-path coverage — a green
// run here is necessary, not sufficient.
// own scene-graph assertions (see ThreeSceneContext.spec.ts) for full
// forward-path coverage — a green run here is necessary, not sufficient.
engine.addEntity();
await engine.update(1);
// reaching here = no throw
Expand Down
37 changes: 35 additions & 2 deletions packages/inspector/src/lib/renderer/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { getHardcodedLoadableScene } from '../sdk/test-local-scene';
import type { AssetPack } from '../logic/catalog';
import type { InspectorPreferences } from '../logic/preferences/types';
import { BabylonRenderer } from './babylon/BabylonRenderer';
import { ThreeRenderer } from './three/ThreeRenderer';
import { connectReverseChannel } from './reverse-channel';
import { getRegisteredRenderers, getRendererPlugin, registerRenderer } from './plugin';
import type { MountedRenderer, RendererMountContext } from './plugin';
Expand Down Expand Up @@ -100,8 +101,7 @@ export async function buildRenderer(
}

// --- Built-in renderer plugins ---------------------------------------------
// The built-in Babylon renderer registers through the same public API a
// third-party renderer uses (see docs/authoring-a-renderer.md).
// Babylon and Three.js register through the same public API a third party uses.

let builtInsRegistered = false;

Expand Down Expand Up @@ -140,6 +140,39 @@ export function registerBuiltInRenderers(
return built;
},
});

registerRenderer({
id: 'three',
label: 'Three.js',
mount: ({ canvas, container, loadAsset }) => {
const threeCanvas = document.createElement('canvas');
threeCanvas.className = 'three-canvas';
threeCanvas.style.width = '100%';
threeCanvas.style.height = '100%';
canvas.style.display = 'none';
container.appendChild(threeCanvas);

const three = new ThreeRenderer(threeCanvas, loadAsset);
const disconnect = connectReverseChannel({
engine: three.context.engine,
operations: three.context.operations,
editorComponents: three.context.editorComponents,
Transform: three.context.Transform,
rendererEvents: three.events,
});

return {
renderer: three,
engine: three.context.engine,
dispose: () => {
disconnect();
three.dispose();
threeCanvas.remove();
canvas.style.display = '';
},
};
},
});
}

export type { MountedRenderer };
Expand Down
10 changes: 5 additions & 5 deletions packages/inspector/src/lib/renderer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
*
* The inspector is renderer-agnostic: it owns the scene as `@dcl/ecs` state and
* talks to whatever draws it through {@link IRenderer}. To add a renderer
* (Unity, Bevy, a custom WebGL/WebGPU engine, …) implement `IRenderer` and
* {@link registerRenderer} it. The built-in Babylon renderer uses this exact API.
* (Three.js, Unity, Bevy, …) implement `IRenderer` and {@link registerRenderer}
* it. Built-in Babylon and Three.js use this exact API.
*
* See `docs/authoring-a-renderer.md` for the contract semantics, and
* {@link createRendererConformanceSuite} (in `./conformance`) to verify an
* implementation.
* See `docs/authoring-a-renderer.md` for the contract semantics and a worked
* example, and {@link createRendererConformanceSuite} (in `./conformance`) to
* verify an implementation.
*/

// The contract every renderer implements.
Expand Down
14 changes: 7 additions & 7 deletions packages/inspector/src/lib/renderer/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import type { IRenderer } from './types';
*
* A renderer author implements {@link IRenderer} (see docs/authoring-a-renderer.md)
* and registers it with {@link registerRenderer}. The inspector then offers it in
* the renderer picker and mounts it like the built-in Babylon renderer, which is
* itself registered through this same API, with no special casing.
* the renderer picker and mounts it like any built-in Babylon and Three.js are
* themselves registered through this same API, with no special casing.
*
* The dependency on a concrete engine (Babylon, Unity, Bevy, …) is entirely the
* The dependency on a concrete engine (Babylon, Three, …) is entirely the
* plugin's; the inspector core knows only this descriptor and {@link IRenderer}.
*/

Expand All @@ -21,8 +21,8 @@ export interface RendererMountContext {
/**
* The inspector's shared viewport canvas. An in-process renderer may render
* into it directly, or create its own canvas inside `container` and leave
* this one hidden. An out-of-process renderer typically ignores it and uses
* an iframe in `container`.
* this one hidden (as the three renderer does). An out-of-process renderer
* typically ignores it and uses an iframe in `container`.
*/
canvas: HTMLCanvasElement;
/** The viewport container element — attach extra canvases/iframes here. */
Expand All @@ -44,7 +44,7 @@ export interface MountedRenderer {
/**
* The renderer's `@dcl/ecs` engine — the inspector connects it to the CRDT
* stream so the scene state flows in. Every renderer drives its scene from
* its own engine fed by CRDT (see the Babylon SceneContext).
* its own engine fed by CRDT (see ThreeSceneContext / Babylon SceneContext).
*/
engine: IEngine;
/** Tear everything down (renderer, reverse channel, any canvas/iframe). */
Expand All @@ -53,7 +53,7 @@ export interface MountedRenderer {

/** A registerable renderer. */
export interface RendererPlugin {
/** Stable unique id (e.g. 'babylon', 'my-org.my-renderer'). */
/** Stable unique id (e.g. 'babylon', 'three', 'my-org.my-renderer'). */
id: string;
/** Human label shown in the renderer picker. */
label: string;
Expand Down
8 changes: 4 additions & 4 deletions packages/inspector/src/lib/renderer/reverse-channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import type { PickTarget, RendererEvents } from './types';

/**
* The minimal surface the reverse-channel handler needs from a renderer's scene
* engine: an `@dcl/ecs` engine plus engine-bound operations, editor components,
* the Transform component, and an event bus. Babylon's SceneContext satisfies
* this; any renderer provides the same shape. Keeping it an interface — not the
* Babylon SceneContext — is what lets any renderer reuse the handler.
* engine. Babylon's SceneContext satisfies this; the three renderer's
* ThreeSceneContext provides the same (engine + engine-bound operations,
* editor components, Transform) plus its own event bus. Keeping it an interface
* — not the Babylon SceneContext — is what lets any renderer reuse the handler.
*/
export interface ReverseChannelTarget {
engine: IEngine;
Expand Down
Loading
Loading