Three.js / React-Three-Fiber Integration¶
Use @ifc-lite/geometry with Three.js or react-three-fiber instead of the built-in WebGPU renderer.
Why Three.js?¶
The built-in @ifc-lite/renderer uses WebGPU for maximum performance, but you may want Three.js when:
- WebGPU is unavailable — Three.js works with WebGL (wider browser support)
- Existing Three.js ecosystem — integrate with your existing 3D scene, post-processing, controls
- react-three-fiber — declarative 3D with React
- Custom rendering — shadow maps, custom shaders, physics, XR
Prefer Babylon.js?
The same MeshData typed arrays work with any engine. See the Babylon.js Integration tutorial for the Babylon.js equivalent.
Architecture¶
The key insight: @ifc-lite/geometry outputs plain typed arrays that are engine-agnostic:
interface MeshData {
expressId: number;
ifcType?: string; // "IfcWall", "IfcDoor", etc.
positions: Float32Array; // [x,y,z, x,y,z, ...]
normals: Float32Array; // [nx,ny,nz, ...]
indices: Uint32Array; // Triangle indices
color: [number, number, number, number]; // RGBA (0-1)
}
You only need @ifc-lite/geometry (and its dependency @ifc-lite/wasm) — skip @ifc-lite/renderer entirely.
Quick Start¶
Scaffold a project¶
Or install manually¶
Convert MeshData to Three.js¶
import * as THREE from 'three';
import type { MeshData } from '@ifc-lite/geometry';
function meshDataToThree(mesh: MeshData): THREE.Mesh {
const geometry = new THREE.BufferGeometry();
geometry.setAttribute(
'position',
new THREE.BufferAttribute(mesh.positions, 3),
);
geometry.setAttribute(
'normal',
new THREE.BufferAttribute(mesh.normals, 3),
);
geometry.setIndex(new THREE.BufferAttribute(mesh.indices, 1));
const [r, g, b, a] = mesh.color;
const material = new THREE.MeshStandardMaterial({
color: new THREE.Color(r, g, b),
transparent: a < 1,
opacity: a,
side: a < 1 ? THREE.DoubleSide : THREE.FrontSide,
depthWrite: a >= 1,
});
const threeMesh = new THREE.Mesh(geometry, material);
threeMesh.userData.expressId = mesh.expressId;
return threeMesh;
}
Load and Display¶
import { GeometryProcessor } from '@ifc-lite/geometry';
const processor = new GeometryProcessor();
await processor.init();
const buffer = new Uint8Array(await file.arrayBuffer());
const result = await processor.process(buffer);
for (const mesh of result.meshes) {
scene.add(meshDataToThree(mesh));
}
Streaming (Progressive Display)¶
For large files, use streaming to show geometry as it loads:
for await (const event of processor.processStreaming(buffer)) {
switch (event.type) {
case 'batch':
for (const mesh of event.meshes) {
scene.add(meshDataToThree(mesh));
}
renderer.render(scene, camera); // Show progress
break;
case 'complete':
console.log(`Loaded ${event.totalMeshes} meshes`);
break;
}
}
Batching for Performance¶
Individual meshes per entity give you picking granularity but many draw calls. For large models, batch by color:
function batchMeshesByColor(
meshes: MeshData[],
): THREE.Mesh[] {
// Group by color
const buckets = new Map<string, MeshData[]>();
for (const m of meshes) {
const key = m.color.join(',');
if (!buckets.has(key)) buckets.set(key, []);
buckets.get(key)!.push(m);
}
const result: THREE.Mesh[] = [];
for (const [, group] of buckets) {
// Merge geometry
let totalPos = 0, totalIdx = 0;
for (const m of group) {
totalPos += m.positions.length;
totalIdx += m.indices.length;
}
const positions = new Float32Array(totalPos);
const normals = new Float32Array(totalPos);
const indices = new Uint32Array(totalIdx);
let pOff = 0, iOff = 0, vOff = 0;
for (const m of group) {
positions.set(m.positions, pOff);
normals.set(m.normals, pOff);
for (let i = 0; i < m.indices.length; i++) {
indices[iOff + i] = m.indices[i] + vOff;
}
pOff += m.positions.length;
iOff += m.indices.length;
vOff += m.positions.length / 3;
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geo.setAttribute('normal', new THREE.BufferAttribute(normals, 3));
geo.setIndex(new THREE.BufferAttribute(indices, 1));
const [r, g, b, a] = group[0].color;
const mat = new THREE.MeshStandardMaterial({
color: new THREE.Color(r, g, b),
transparent: a < 1,
opacity: a,
depthWrite: a >= 1,
});
result.push(new THREE.Mesh(geo, mat));
}
return result;
}
React-Three-Fiber (R3F)¶
Install¶
IFC Component¶
import { useEffect, useState, useMemo } from 'react';
import { Canvas } from '@react-three/fiber';
import { OrbitControls } from '@react-three/drei';
import * as THREE from 'three';
import { GeometryProcessor, type MeshData } from '@ifc-lite/geometry';
function IfcMesh({ mesh }: { mesh: MeshData }) {
const geometry = useMemo(() => {
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(mesh.positions, 3));
geo.setAttribute('normal', new THREE.BufferAttribute(mesh.normals, 3));
geo.setIndex(new THREE.BufferAttribute(mesh.indices, 1));
return geo;
}, [mesh]);
const [r, g, b, a] = mesh.color;
return (
<mesh geometry={geometry}>
<meshStandardMaterial
color={new THREE.Color(r, g, b)}
transparent={a < 1}
opacity={a}
side={a < 1 ? THREE.DoubleSide : THREE.FrontSide}
/>
</mesh>
);
}
function IfcModel({ url }: { url: string }) {
const [meshes, setMeshes] = useState<MeshData[]>([]);
useEffect(() => {
let cancelled = false;
async function load() {
const res = await fetch(url);
const buffer = new Uint8Array(await res.arrayBuffer());
const processor = new GeometryProcessor();
await processor.init();
const result = await processor.process(buffer);
if (!cancelled) setMeshes(result.meshes);
}
load();
return () => { cancelled = true; };
}, [url]);
return (
<group>
{meshes.map((mesh) => (
<IfcMesh key={mesh.expressId} mesh={mesh} />
))}
</group>
);
}
export default function App() {
return (
<Canvas camera={{ position: [20, 15, 20], fov: 50 }}>
<ambientLight intensity={0.6} />
<directionalLight position={[50, 80, 50]} intensity={0.8} />
<IfcModel url="/model.ifc" />
<OrbitControls />
</Canvas>
);
}
Entity Picking¶
Raycasting¶
const raycaster = new THREE.Raycaster();
const pointer = new THREE.Vector2();
canvas.addEventListener('click', (event) => {
const rect = canvas.getBoundingClientRect();
pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster.intersectObjects(scene.children, true);
if (intersects.length > 0) {
const hit = intersects[0].object;
const expressId = hit.userData.expressId;
console.log('Selected entity:', expressId);
}
});
Highlight on Hover¶
let hoveredMesh: THREE.Mesh | null = null;
const highlightColor = new THREE.Color(0x4f46e5);
canvas.addEventListener('pointermove', (event) => {
const rect = canvas.getBoundingClientRect();
pointer.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
pointer.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
// Reset previous
if (hoveredMesh) {
const orig = hoveredMesh.userData.originalColor;
(hoveredMesh.material as THREE.MeshStandardMaterial).emissive.copy(orig);
}
raycaster.setFromCamera(pointer, camera);
const intersects = raycaster.intersectObjects(scene.children, true);
if (intersects.length > 0) {
hoveredMesh = intersects[0].object as THREE.Mesh;
const mat = hoveredMesh.material as THREE.MeshStandardMaterial;
if (!hoveredMesh.userData.originalColor) {
hoveredMesh.userData.originalColor = mat.emissive.clone();
}
mat.emissive.copy(highlightColor);
} else {
hoveredMesh = null;
}
});
Coordinate Handling¶
IFC files may use large georeferenced coordinates. The geometry processor handles this automatically:
const result = await processor.process(buffer);
if (result.coordinateInfo.hasLargeCoordinates) {
const shift = result.coordinateInfo.originShift;
console.log(`Coordinates shifted by (${shift.x}, ${shift.y}, ${shift.z})`);
// If you need world coordinates for geolocation:
function toWorld(local: THREE.Vector3): THREE.Vector3 {
return new THREE.Vector3(
local.x - shift.x,
local.y - shift.y,
local.z - shift.z,
);
}
}
Performance Tips¶
| Strategy | When to use | Benefit |
|---|---|---|
| Individual meshes | Small models, need picking | Simple, per-entity control |
| Color batching | Medium models (1k-10k entities) | Fewer draw calls |
| THREE.InstancedMesh | Repeated elements (columns, furniture) | GPU instancing |
| LOD | Large models | Reduce triangle count at distance |
| Frustum culling | Large scenes | Only render visible geometry |
Using InstancedMesh for Repeated Elements¶
import { GeometryProcessor, type InstancedGeometry } from '@ifc-lite/geometry';
const processor = new GeometryProcessor();
await processor.init();
const buffer = new Uint8Array(await file.arrayBuffer());
for await (const event of processor.processInstancedStreaming(buffer)) {
if (event.type === 'batch') {
for (const instanced of event.geometries) {
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(instanced.positions, 3));
geo.setAttribute('normal', new THREE.BufferAttribute(instanced.normals, 3));
geo.setIndex(new THREE.BufferAttribute(instanced.indices, 1));
const mat = new THREE.MeshStandardMaterial();
const mesh = new THREE.InstancedMesh(geo, mat, instanced.instance_count);
for (let i = 0; i < instanced.instance_count; i++) {
const inst = instanced.get_instance(i);
if (!inst) continue;
const matrix = new THREE.Matrix4().fromArray(inst.transform);
mesh.setMatrixAt(i, matrix);
const [r, g, b] = inst.color;
mesh.setColorAt(i, new THREE.Color(r, g, b));
}
mesh.instanceMatrix.needsUpdate = true;
if (mesh.instanceColor) mesh.instanceColor.needsUpdate = true;
scene.add(mesh);
}
}
}
Extracting IFC Object Data¶
Geometry alone is just triangles — to build a useful BIM viewer you also need entity attributes, property sets, quantity sets, and the spatial hierarchy. The @ifc-lite/parser package provides all of this from the same IFC buffer, no server required.
Both pipelines run in parallel from the same buffer — geometry streams into the scene while the parser builds its data index in the background:
Building the Data Store¶
The data store scans the raw IFC STEP text once and creates a columnar index for fast lookups. It is pure JavaScript — no WASM required.
import {
StepTokenizer,
ColumnarParser,
type IfcDataStore,
extractEntityAttributesOnDemand,
extractPropertiesOnDemand,
extractQuantitiesOnDemand,
} from '@ifc-lite/parser';
async function buildDataStore(buffer: ArrayBuffer): Promise<IfcDataStore> {
const tokenizer = new StepTokenizer(new Uint8Array(buffer));
const entityRefs = [];
for (const ref of tokenizer.scanEntities()) {
entityRefs.push({
expressId: ref.expressId,
type: ref.type,
byteOffset: ref.offset,
byteLength: ref.length,
lineNumber: ref.line,
});
}
return new ColumnarParser().parseLite(buffer, entityRefs);
}
Querying Entity Data¶
Once the store is built, extract attributes, property sets, and quantity sets for any entity:
// Attributes (GlobalId, Name, Description, ObjectType, Tag)
const attrs = extractEntityAttributesOnDemand(store, expressId);
console.log(attrs.name); // "Basic Wall:Generic - 200mm"
console.log(attrs.globalId); // "2O2Fr$t4X7Zf8NOew3FLOH"
// Property sets (e.g. Pset_WallCommon → IsExternal, FireRating, …)
const psets = extractPropertiesOnDemand(store, expressId);
for (const pset of psets) {
for (const prop of pset.properties) {
console.log(`${pset.name}: ${prop.name} = ${prop.value}`);
}
}
// Quantity sets (e.g. Qto_WallBaseQuantities → NetSideArea, GrossVolume, …)
const qsets = extractQuantitiesOnDemand(store, expressId);
for (const qset of qsets) {
for (const q of qset.quantities) {
console.log(`${qset.name}: ${q.name} = ${q.value}`);
}
}
Performance
extractEntityAttributesOnDemand re-parses the source buffer on each call. For bulk lookups, use store.entities.getName(id) and store.entities.getTypeName(id) which read from the columnar index in O(1).
Click → Properties Panel¶
Combine picking with data extraction:
canvas.addEventListener('click', (event) => {
const expressId = pickAt(event.clientX, event.clientY);
if (expressId == null || !dataStore) return;
const attrs = extractEntityAttributesOnDemand(dataStore, expressId);
const psets = extractPropertiesOnDemand(dataStore, expressId);
const qsets = extractQuantitiesOnDemand(dataStore, expressId);
// Render attrs, psets, qsets into your UI panel
});
Extracting Spatial Structure¶
The data store includes a pre-parsed spatial hierarchy — the Project → Site → Building → Storey → Elements tree shown in the model tree panel of BIM viewers.
import { type SpatialNode } from '@ifc-lite/data';
const hierarchy = store.spatialHierarchy;
if (hierarchy) {
const project = hierarchy.project; // SpatialNode (root)
// Walk: project.children → sites → buildings → storeys
for (const storey of building.children) {
console.log(`${storey.name} (${storey.elevation}m) — ${storey.elements.length} elements`);
}
}
Each SpatialNode has:
expressId,name,type(IfcProject / IfcSite / IfcBuilding / IfcBuildingStorey / IfcSpace)elevation— metres above project base (storeys only)children— child spatial containerselements— expressIds of directly contained elements
Storey Lookup and Selection Sync¶
The hierarchy provides a reverse map from any element to its containing storey, enabling two-way sync between the 3D view and a spatial tree UI:
// Find which storey contains an element
const storeyId = hierarchy.elementToStorey.get(expressId);
// 3D click → reveal in tree
function onEntityClicked(expressId: number) {
applyHighlight(meshDataByExpressId.get(expressId));
const storeyId = hierarchy.elementToStorey.get(expressId);
if (storeyId != null) expandTreeNode(storeyId);
highlightTreeRow(expressId);
}
// Tree click → highlight in 3D
function onTreeElementClicked(expressId: number) {
applyHighlight(meshDataByExpressId.get(expressId));
showPropertiesPanel(expressId);
}
For a full implementation of the spatial tree UI (grouping elements by IFC type, search/filter, expand/collapse), see examples/threejs-viewer/src/ifc-data.ts and the renderSpatialPanel() function in src/main.ts.
Complete Data Flow¶
Vite Configuration¶
The WASM module needs these headers for SharedArrayBuffer:
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
optimizeDeps: {
exclude: ['@ifc-lite/wasm'],
},
server: {
headers: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
},
},
});
Full Example¶
See examples/threejs-viewer/ for a complete, runnable example with geometry streaming, object picking, a properties panel, and a spatial tree — all wired together.
Next Steps¶
- Babylon.js Integration — Same workflow with Babylon.js
- Querying Data — Fluent query API, SQL queries, and direct data access
- Building a Viewer — Full viewer with WebGPU
- Geometry Processing — Geometry API details
- API Reference — Complete API docs