Rendering overview
Meep builds a clustered Forward+ pipeline on top of Three.js, integrating lights and meshes through the ECS so scenes with many dynamic lights stay fast.
Meep renders through Three.js but adds a significant layer on top: a clustered Forward+ pipeline that makes hundreds of dynamic lights practical, an ECS integration so every light and mesh is a component, and a BVH-driven visibility system that avoids processing geometry outside the view. This page explains how those pieces fit together; the linked pages cover each subsystem in depth.
Three.js as infrastructure
The GraphicsEngine creates a standard THREE.WebGLRenderer and THREE.Scene. Three.js handles the WebGL state machine, shader compilation, buffer uploads, and the underlying draw-call API. Meep does not replace any of that. What it replaces is how the scene is populated and how lighting is computed at the fragment level.
Three.js’s built-in lighting path iterates every light for every draw call, which limits practical scene counts to a handful of lights. Meep sidesteps that with its own clustering pass.
The clustered Forward+ pipeline
Before each frame, ForwardPlusRenderingPlugin runs LightManager.buildTiles(camera). The process has four stages:
- Frustum construction. The view frustum is computed from the active camera’s projection and world matrices, and its eight corner points are recorded.
- Visible-light culling. All registered lights are held in a BVH. The frustum is tested against that BVH to produce a set of potentially-visible lights; each point light is then tested against the frustum more precisely using its sphere radius. Lights outside the frustum are discarded.
- Cluster assignment. The frustum is divided into an
x × y × zgrid of sub-frustums (the default plugin resolution is 12 × 6 × 4). For each cluster cell, the visible-light BVH is queried again to find which lights overlap that cell’s volume. The result — a lookup offset and a count — is written into aDataTexture3D(the cluster texture). - Shader upload. Three GPU textures are bound to every Forward+ material each frame: the cluster texture, a flat light-data texture (position, radius, color, intensity packed as float32), and a lookup texture (the indirection table from cluster offset to light index).
At shading time, a fragment’s clip-space position identifies its cluster cell, the cluster texture gives the light range for that cell, and the fragment loops over only those lights. A scene with 500 lights spread across a large environment costs only the lights that overlap the fragment’s local cell — typically far fewer than the total count.
Cluster resolution
The resolution of the cluster grid trades GPU memory and CPU tile-building time against shading accuracy:
import { ForwardPlusRenderingPlugin } from "@woosh/meep-engine/src/engine/graphics/render/forward_plus/plugin/ForwardPlusRenderingPlugin.js";
const fp = new ForwardPlusRenderingPlugin();
fp.setClusterResolution(16, 8, 8); // x, y, z subdivisions
engine.plugins.add(fp);
Higher resolution means fewer lights per cluster cell (faster shading) but more work building the tile texture each frame. The default (12 × 6 × 4) is a reasonable starting point. setClusterResolution can be called before or after startup.
ECS integration
All rendering state lives on ECS components, and systems manage the relationship between those components and the underlying Three.js and Forward+ objects.
| ECS side | Rendering side |
|---|---|
Light + Transform | Three.js light object + Forward+ PointLight (via LightSystem) |
ShadedGeometry + Transform | Three.js Mesh / instanced draw / merged geometry (via ShadedGeometrySystem) |
Camera | Three.js PerspectiveCamera (via CameraSystem) |
Adding LightSystem and ShadedGeometrySystem to the entity manager is the minimal setup for a lit 3-D scene:
import { LightSystem } from "@woosh/meep-engine/src/engine/graphics/ecs/light/LightSystem.js";
import { ShadedGeometrySystem } from "@woosh/meep-engine/src/engine/graphics/ecs/mesh-v2/ShadedGeometrySystem.js";
em.addSystem(new LightSystem(engine));
em.addSystem(new ShadedGeometrySystem(engine));
ForwardPlusRenderingPlugin is acquired automatically by LightSystem at startup; you do not need to register it separately.
Visibility and BVH
ShadedGeometrySystem maintains a BVH over the world-space bounding boxes of all active ShadedGeometry components. Each frame, the system queries that BVH against the camera frustum to produce the visible set rather than submitting every entity for frustum testing. The same BVH is available for raycasts:
const hits = shadedGeometrySystem.raycast(ox, oy, oz, dx, dy, dz);
// or, nearest only:
const result = shadedGeometrySystem.raycastNearest(contact, ox, oy, oz, dx, dy, dz);
Both return entity IDs and the originating ShadedGeometry so gameplay code can identify what was hit.
Shadow maps
The engine enables Three.js shadow maps at startup and sets the shadow type to VSMShadowMap (Variance Shadow Maps). Meep’s shadow camera fitting — done in LightSystem each frame — projects the view frustum into the directional light’s local space, computes its bounding box, and snaps the orthographic shadow camera to texel-aligned discrete positions to eliminate shadow swimming. See Lights & shadows for the full details.
Where to go next
- Lights & shadows — the
Lightcomponent, light types, how they feed the Forward+ cluster, and variance shadow maps. - Meshes & materials —
ShadedGeometry, flags, hardware instancing, static geometry merging, and MikkT tangent generation.