| name | thatopen-impl-viewer |
| description | Use when setting up a ThatOpen BIM viewer from scratch, creating worlds, configuring renderers, or initializing the 3D viewport. Prevents missing init() calls and incorrect world setup order. Covers complete viewer setup: Worlds.create(), SimpleScene, SimpleRenderer, PostproductionRenderer, OrthoPerspectiveCamera, Grids, components.init(), container binding, dispose lifecycle. Keywords: viewer, world, scene, renderer, camera, grid, setup, init, dispose, postproduction, viewport, container, webgl, create viewer, embed viewer, viewer not rendering, blank canvas.
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires @thatopen/components 3.3.x. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
ThatOpen Viewer Setup
Overview
This skill covers the complete setup of a ThatOpen BIM viewer: creating a
World (scene + camera + renderer), configuring the container element,
starting the render loop, and proper cleanup. It includes both the basic
SimpleRenderer path and the advanced PostproductionRenderer path with
ambient occlusion and edge detection.
Version: @thatopen/components 3.3.x, @thatopen/components-front 3.3.x
Prerequisite: thatopen-core-architecture (component system, lifecycle)
Setup Order
ALWAYS follow this exact order when creating a viewer:
1. new Components()
2. components.get(Worlds)
3. worlds.create<Scene, Camera, Renderer>()
4. world.scene = new SimpleScene(components)
5. world.scene.setup()
6. world.renderer = new SimpleRenderer(components, container)
— OR new PostproductionRenderer(components, container)
7. world.camera = new OrthoPerspectiveCamera(components)
8. components.get(Grids).create(world) [optional]
9. components.init() [REQUIRED]
NEVER reorder these steps. The scene MUST be assigned before the renderer.
The camera MUST be assigned last. components.init() MUST be the final
call after all world setup is complete.
Container Element
The HTML container element is the DOM element where the WebGL canvas is
rendered. It MUST satisfy these requirements:
- MUST be a block-level element (typically
<div>).
- MUST have explicit width and height via CSS. Zero-size containers
produce a black or invisible viewport.
- MUST exist in the DOM before passing it to the renderer constructor.
- MUST NOT be
display: none at construction time.
<div id="viewer" style="width: 100%; height: 100vh;"></div>
const container = document.getElementById("viewer") as HTMLDivElement;
if (!container) throw new Error("Viewer container not found");
The renderer creates a <canvas> element inside the container and manages
its lifecycle. NEVER manually create or manipulate this canvas.
World Creation
A World connects a Scene, Camera, and Renderer into a single viewport.
Use the Worlds component to create worlds.
import * as OBC from "@thatopen/components";
const components = new OBC.Components();
const worlds = components.get(OBC.Worlds);
const world = worlds.create<
OBC.SimpleScene,
OBC.OrthoPerspectiveCamera,
OBC.SimpleRenderer
>();
- ALWAYS use
components.get(OBC.Worlds) to access the Worlds manager.
NEVER instantiate Worlds directly.
- The type parameters are for TypeScript type narrowing only. You MUST
still assign the concrete instances manually (see next sections).
- Multiple worlds are supported. Each world gets its own scene, camera,
and renderer. All worlds update in the same
components.init() loop.
Scene Setup
SimpleScene wraps a THREE.Scene and provides default lighting and
background setup via the Configurable interface.
world.scene = new OBC.SimpleScene(components);
world.scene.setup();
SimpleScene implements Configurable. ALWAYS call setup() after
assigning it to get default lights and background.
- Access the underlying Three.js scene:
world.scene.three.
- To customize lighting, modify the Three.js scene after
setup():
world.scene.setup();
const scene = world.scene.three;
scene.background = new THREE.Color(0x202932);
- To add custom Three.js objects:
const mesh = new THREE.Mesh(geometry, material);
world.scene.three.add(mesh);
world.meshes.add(mesh);
Renderer: SimpleRenderer
SimpleRenderer wraps THREE.WebGLRenderer. It handles canvas creation,
resizing, and per-frame rendering.
const container = document.getElementById("viewer") as HTMLDivElement;
world.renderer = new OBC.SimpleRenderer(components, container);
- The renderer creates a WebGL canvas inside the container element.
- It implements
Resizeable and auto-resizes via ResizeObserver.
- Access the underlying Three.js renderer:
world.renderer.three.
- To customize WebGL parameters:
world.renderer = new OBC.SimpleRenderer(components, container, {
antialias: true,
alpha: true,
});
Renderer: PostproductionRenderer
PostproductionRenderer extends the renderer with post-processing effects:
ambient occlusion (AO), edge detection, outline rendering, and SMAA
anti-aliasing. It is in @thatopen/components-front (browser-only).
import * as OBCF from "@thatopen/components-front";
const world = worlds.create<
OBC.SimpleScene,
OBC.OrthoPerspectiveCamera,
OBCF.PostproductionRenderer
>();
world.renderer = new OBCF.PostproductionRenderer(components, container);
Enabling Post-Processing
Post-processing is disabled by default. ALWAYS enable it explicitly:
world.renderer.postproduction.enabled = true;
Post-Processing Features
Access features via world.renderer.postproduction:
| Feature | Property | Default | Effect |
|---|
| Ambient Occlusion | ao | enabled | Darkens crevices and edges |
| Edge Detection | customEdges | enabled | Draws outlines on geometry |
| SMAA | Part of pipeline | enabled | Anti-aliasing |
| Gloss Pass | glossEnabled | false | Reflective surface pass |
const pp = world.renderer.postproduction;
pp.enabled = true;
pp.ao = true;
pp.customEdges = true;
PostproductionRenderer + Grid Interaction
When using PostproductionRenderer, the grid MUST be excluded from
post-processing to avoid visual artifacts:
const grids = components.get(OBC.Grids);
const grid = grids.create(world);
world.renderer.postproduction.exclude.add(grid.three);
ALWAYS exclude the grid mesh from post-processing. Failing to do so
causes the grid to render with incorrect edges and AO artifacts.
Camera: OrthoPerspectiveCamera
OrthoPerspectiveCamera provides dual perspective/orthographic cameras
with orbit, first-person, and plan navigation modes.
world.camera = new OBC.OrthoPerspectiveCamera(components);
- ALWAYS assign the camera after the scene and renderer.
- It wraps
camera-controls for orbit/pan/zoom interaction.
- Access the underlying cameras:
world.camera.threePersp — THREE.PerspectiveCamera
world.camera.threeOrtho — THREE.OrthographicCamera
world.camera.three — whichever is currently active
Navigation Modes
const camera = world.camera as OBC.OrthoPerspectiveCamera;
camera.set("Orbit");
camera.set("FirstPerson");
camera.set("Plan");
Framing Objects
world.camera.fit(world.meshes);
world.camera.fit(world.meshes, 1.5);
Grid
Grids creates an infinite ground grid for spatial reference.
const grids = components.get(OBC.Grids);
const grid = grids.create(world);
- ALWAYS create the grid BEFORE calling
components.init().
- The grid auto-fades based on camera distance.
- Access the underlying Three.js mesh:
grid.three.
Starting the Render Loop
components.init();
- ALWAYS call
components.init() AFTER all world setup is complete.
- This starts the
requestAnimationFrame loop.
- All
Updateable components (renderer, camera, etc.) receive
update(delta) calls each frame.
- NEVER call
init() before assigning scene, renderer, and camera.
The update loop will attempt to render an incomplete world.
- Calling
init() multiple times is safe but unnecessary.
Disposal
components.dispose();
- ALWAYS call
components.dispose() when the viewer is no longer needed.
- This disposes ALL components, worlds, renderers, scenes, and cameras.
FragmentsManager is disposed last to prevent dangling references.
- After calling
dispose(), NEVER use any component or world references.
- In frameworks (React, Vue, Angular), call dispose in the unmount/destroy
lifecycle hook.
Framework Integration Example (React)
useEffect(() => {
const components = new OBC.Components();
components.init();
return () => {
components.dispose();
};
}, []);
Bundler Configuration (Vite)
ThatOpen requires specific Vite configuration for WASM files, web workers,
and cross-origin isolation headers (needed for SharedArrayBuffer).
import { defineConfig } from "vite";
export default defineConfig({
plugins: [
{
name: "coop-coep",
configureServer(server) {
server.middlewares.use((_req, res, next) => {
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
next();
});
},
},
],
optimizeDeps: {
exclude: ["web-ifc"],
},
worker: {
format: "es",
},
});
COOP/COEP Headers
Cross-Origin-Opener-Policy: same-origin and
Cross-Origin-Embedder-Policy: require-corp are REQUIRED for
SharedArrayBuffer, which web-ifc uses internally.
- Without these headers, WASM operations may fail silently or throw.
- For production, configure these headers on your web server or CDN.
WASM Files
- web-ifc WASM files (
web-ifc.wasm, web-ifc-mt.wasm) MUST be
accessible at runtime.
IfcLoader.setup() with default settings auto-resolves WASM paths.
- For custom paths, copy WASM files to your
public/ directory and
configure IfcLoader accordingly.
Worker Files
- Fragment processing uses web workers for off-main-thread performance.
- ALWAYS initialize
FragmentsManager with a worker URL:
fragments.init(workerURL).
- Vite handles worker bundling via
worker: { format: "es" }.
Minimal Viewer (Copy-Paste Ready)
import * as OBC from "@thatopen/components";
const container = document.getElementById("viewer") as HTMLDivElement;
const components = new OBC.Components();
const worlds = components.get(OBC.Worlds);
const world = worlds.create<
OBC.SimpleScene,
OBC.OrthoPerspectiveCamera,
OBC.SimpleRenderer
>();
world.scene = new OBC.SimpleScene(components);
world.scene.setup();
world.renderer = new OBC.SimpleRenderer(components, container);
world.camera = new OBC.OrthoPerspectiveCamera(components);
components.(.).(world);
components.();
.(, {
components.();
});
PostproductionRenderer Viewer (Copy-Paste Ready)
import * as OBC from "@thatopen/components";
import * as OBCF from "@thatopen/components-front";
const container = document.getElementById("viewer") as HTMLDivElement;
const components = new OBC.Components();
const worlds = components.get(OBC.Worlds);
const world = worlds.create<
OBC.SimpleScene,
OBC.OrthoPerspectiveCamera,
OBCF.PostproductionRenderer
>();
world.scene = new OBC.SimpleScene(components);
world.scene.setup();
world.renderer = new OBCF.PostproductionRenderer(components, container);
world.camera = new OBC.OrthoPerspectiveCamera(components);
world.renderer.postproduction. = ;
grids = components.(.);
grid = grids.(world);
world....(grid.);
components.();
.(, {
components.();
});
Critical Rules
- ALWAYS call
components.init() after world setup. Without it,
nothing renders.
- ALWAYS call
components.dispose() on cleanup. BIM models consume
hundreds of MB of GPU memory.
- ALWAYS call
world.scene.setup() after assigning the scene.
Without it, there are no lights.
- ALWAYS exclude the grid from PostproductionRenderer.
- ALWAYS ensure the container has non-zero dimensions before
creating the renderer.
- NEVER reorder the setup sequence: scene first, then renderer,
then camera.
- NEVER use component references after calling
components.dispose().
- NEVER import
@thatopen/components-front in Node.js environments.
- NEVER create the renderer with a container that has
display: none.
- NEVER call
components.init() before assigning all three world
components (scene, renderer, camera).
Reference Files
Source Verification
All API signatures verified against:
- GitHub:
ThatOpen/engine_components main branch
- npm:
@thatopen/components@3.3.3, @thatopen/components-front@3.3.3
- Research:
docs/research/vooronderzoek-thatopen.md
- Skill:
thatopen-core-architecture (SKILL.md, references/)