Rendering Overview

Web Engine's high-performance rendering system built on Three.js with WebGL/WebGPU support, advanced culling, and modern graphics features.

The rendering system provides a production-grade graphics pipeline with automatic optimization, GPU-driven rendering, and support for modern graphics APIs. Built on Three.js, it combines ease of use with high-performance rendering capabilities.

Key Features#

WebGL & WebGPU

Dual renderer support with automatic fallback. WebGPU for modern devices, WebGL for compatibility.

GPU Culling

Frustum and occlusion culling executed on GPU for maximum throughput with 100k+ entities.

Render Graph

Multi-frame buffering with automatic pass scheduling and resource management.

Material Instancing

Automatic material sharing and batching to reduce draw calls and state changes.

LOD System

Automatic level-of-detail switching for meshes and instances based on distance.

Post-Processing

HDR bloom, tone mapping, SSAO, depth of field, and custom effect chains.

Architecture#

The rendering system is built on a modern, performance-focused architecture with several key components working together:

Renderer Factory#

The renderer factory creates WebGL or WebGPU renderers with optimal settings based on device capabilities:

import { createRenderer, isWebGPUSupported } from '@web-engine-dev/core/engine/rendering';
// Check WebGPU support
const hasWebGPU = await isWebGPUSupported();
// Create renderer with automatic backend selection
const renderer = createRenderer(
hasWebGPU ? 'webgpu' : 'webgl',
canvasElement,
{
antialias: true,
powerPreference: 'high-performance',
alpha: false,
stencil: true,
}
);

Scene Renderer#

The SceneRenderer orchestrates the entire rendering pipeline, executing passes in the optimal order:

import { getSceneRenderer } from '@web-engine-dev/core/engine/rendering';
// Initialize scene renderer
const sceneRenderer = getSceneRenderer({
enableShadows: true,
enableDepthPrepass: true,
enableGPUCulling: true,
enablePostProcessing: true,
});
// In your game loop
function gameLoop(world, delta, time) {
// Render frame with all optimizations
sceneRenderer.render(world, delta, time);
// Get performance stats
const stats = sceneRenderer.getStats();
console.log(`Frame: ${stats.frameTime.toFixed(2)}ms`);
}

Render Graph#

The render graph manages pass execution with automatic dependency resolution and multi-frame buffering:

import { getRenderGraph } from '@web-engine-dev/core/engine/rendering';
const renderGraph = getRenderGraph();
// Render graph compiles passes in dependency order:
// 1. Shadow Pass (produces ShadowMap)
// 2. Depth Prepass (produces depth buffer)
// 3. Opaque Pass (uses ShadowMap + depth)
// 4. Transparent Pass (uses depth for sorting)
// 5. Post-Process Pass (uses final color buffer)
// Triple buffering: GPU executes frame N while CPU builds N+1
renderGraph.beginFrame(world, delta, time);
// ... add passes ...
renderGraph.endFrame();
renderGraph.submitReady();

Rendering Pipeline#

The engine executes a series of render passes each frame, optimized for performance and visual quality:

PassPurposeOutput
Shadow PassRender cascaded shadow maps for directional lightsShadow depth textures
Depth PrepassEarly-Z rejection to reduce overdrawDepth buffer + Hi-Z pyramid
Opaque PassRender solid objects front-to-backColor + depth buffer
Transparent PassRender transparent objects back-to-frontBlended color buffer
Post-Process PassApply bloom, tone mapping, effectsFinal screen image

Three.js Integration#

The engine seamlessly integrates with Three.js, allowing you to use any Three.js feature while benefiting from automatic optimization:

Scene Graph Synchronization#

The RenderSystem automatically syncs ECS entities to Three.js scene objects:

// ECS components are automatically synced to Three.js
const entity = createEntity(world);
addComponent(world, entity, Transform);
addComponent(world, entity, MeshRenderer);
// Transform changes sync to Three.js Object3D
Transform.position[entity][0] = 10;
Transform.position[entity][1] = 5;
Transform.position[entity][2] = 0;
// RenderSystem updates Three.js mesh.position automatically
// No manual synchronization needed!

Custom Three.js Materials#

You can use any Three.js material, including custom shaders:

import * as THREE from 'three';
import { MaterialFactory } from '@web-engine-dev/core/engine/materials';
// Use built-in materials
const standardMat = new THREE.MeshStandardMaterial({
color: 0x00ff00,
roughness: 0.5,
metalness: 0.8,
});
// Or create custom shader materials
const customMat = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
uColor: { value: new THREE.Color(0xff0000) },
},
vertexShader: /* glsl */ `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: /* glsl */ `
uniform float uTime;
uniform vec3 uColor;
varying vec2 vUv;
void main() {
float pulse = sin(uTime * 2.0) * 0.5 + 0.5;
gl_FragColor = vec4(uColor * pulse, 1.0);
}
`,
});

Performance Targets#

The rendering system is optimized to meet aggressive performance targets:

MetricTargetNotes
Frame time @ 1080p< 8msAllows headroom for 60 FPS
Draw calls< 500Through batching and instancing
State changes< 100Material and shader reuse
GPU memory< 1 GBTexture streaming and pooling
Culling time< 0.5msGPU frustum + occlusion
Entities rendered100k+With GPU instancing

Performance Monitoring

Use sceneRenderer.getStats() to monitor frame time, draw calls, and culling efficiency. The engine automatically adjusts LOD and culling settings based on performance.

WebGPU vs WebGL#

The engine supports both rendering backends with automatic fallback:

FeatureWebGPUWebGL
Browser SupportChrome 113+, Edge 113+All modern browsers
Compute ShadersYesLimited (transform feedback)
GPU InstancingUnlimited instancesLimited by uniforms
Texture Arrays16k layersLimited support
Performance10-30% fasterBaseline
ValidationExcellent errorsLimited errors

Automatic Backend Selection

The engine automatically selects WebGPU when available and falls back to WebGL for maximum compatibility. Both backends share the same API, so your code works identically on either.

Best Practices#

Reduce Draw Calls#

  • Use instanced rendering for repeated objects (trees, rocks, etc.)
  • Share materials between meshes when possible
  • Enable automatic batching for static geometry
  • Use texture atlases instead of individual textures

Optimize Materials#

  • Reuse material instances instead of creating new ones
  • Use MeshBasicMaterial for unlit objects
  • Disable features you don't need (e.g., transparent: false)
  • Compress textures using KTX2/Basis Universal

Leverage LOD#

  • Create multiple detail levels for complex meshes
  • Use impostor billboards for distant objects
  • Configure LOD thresholds based on object importance
  • Test LOD transitions to avoid pop-in

Manage GPU Memory#

  • Dispose unused geometries and materials
  • Use texture compression (KTX2, DDS)
  • Stream textures for large worlds
  • Monitor memory with renderer.info.memory

Mobile Considerations

Mobile GPUs have less memory and bandwidth. Use lower resolution textures, reduce shadow map size, disable post-processing on low-end devices, and prefer simpler materials.

Next Steps#

Now that you understand the rendering architecture, explore specific topics:

Rendering | Web Engine Docs | Web Engine Docs