3D Game Development
This guide covers the core tools for building 3D games: loading models, building environments, PBR materials, lighting, shadows, and common 3D patterns.
WebGPU Required
All rendering in Web Engine Dev uses WebGPU exclusively: there is no WebGL2 fallback. If a browser does not support WebGPU, the engine will surface a clear error. Check Can I Use WebGPU for current browser support.
Loading 3D Models (glTF/GLB)
typescript
import { GLTFLoader, spawnGLTFScene } from '@web-engine-dev/gltf';
// Create a loader (device comes from engine.renderer.device)
const loader = new GLTFLoader(engine.renderer.device);
// Load a GLB file
const result = await loader.load('models/hero.glb');
// Instantiate as entities in the world
const { rootEntity } = spawnGLTFScene(world, result, {
position: [0, 0, 0],
rotation: [0, 0, 0, 1],
scale: [1, 1, 1],
});
// The model's hierarchy is preserved:
// rootEntity
// └── Armature
// ├── HeroMesh (MeshHandle)
// ├── WeaponSocket
// └── ShieldSocketTraversing the Instantiated Hierarchy
typescript
import { createHierarchy } from '@web-engine-dev/hierarchy';
// The hierarchy is set up via the hierarchy system registered by the engine;
// access it through the HierarchyResource if using ECS resources,
// or create a standalone instance for direct use:
const hierarchy = createHierarchy();
// Find a child bone socket by name
const weaponSocket = hierarchy.findByName(rootEntity, 'WeaponSocket');
// Load and attach a weapon to the socket
const swordResult = await loader.load('models/sword.glb');
const { rootEntity: swordEntity } = spawnGLTFScene(world, swordResult);
hierarchy.setParent(swordEntity, weaponSocket);PBR Materials
Web Engine Dev uses a physically-based rendering (PBR) pipeline. Materials are defined with metalness/roughness workflow:
typescript
import { MaterialRegistryResource, createMaterial, MeshHandle } from '@web-engine-dev/renderer';
// Create a PBR material
const stoneMaterial = createMaterial({
type: 'pbr',
albedo: { texture: 'textures/stone-albedo.png' },
normal: { texture: 'textures/stone-normal.png' },
metalRoughness: { texture: 'textures/stone-metal-rough.png' },
ao: { texture: 'textures/stone-ao.png' },
// Or use flat values instead of textures:
// albedo: { color: [0.5, 0.5, 0.5, 1] },
// metalness: 0.0,
// roughness: 0.8,
});
const materialId = world.getResource(MaterialRegistryResource).register(stoneMaterial);
// Apply to a mesh entity
world.insert(meshEntity, MeshHandle, {
mesh: 'meshes/stone-wall.mesh',
material: materialId,
});Emissive Materials (Glowing Objects)
typescript
const glowMaterial = createMaterial({
type: 'pbr',
albedo: { color: [0.1, 0.3, 1.0, 1] },
emissive: { color: [0.2, 0.6, 2.0] }, // HDR values supported
metalness: 0,
roughness: 0.3,
});
const glowMatId = world.getResource(MaterialRegistryResource).register(glowMaterial);Lighting
Directional Light (Sun/Moon)
typescript
import { DirectionalLightComponent, spawnDirectionalLight } from '@web-engine-dev/renderer';
import { Quaternion } from '@web-engine-dev/math';
const sunEntity = spawnDirectionalLight(world, {
color: [1.0, 0.97, 0.85], // warm sunlight
intensity: 3.0,
castShadows: true,
shadowResolution: 2048,
shadowBias: 0.001,
shadowCascades: 4, // CSM for large worlds
rotation: Quaternion.fromEuler(-45, 30, 0),
});Point Light
typescript
import { spawnPointLight } from '@web-engine-dev/renderer';
spawnPointLight(world, {
color: [1.0, 0.6, 0.2],
intensity: 20,
range: 8,
castShadows: false, // expensive: use sparingly
position: [0, 2, 0],
});Spot Light
typescript
import { spawnSpotLight } from '@web-engine-dev/renderer';
import { Quaternion } from '@web-engine-dev/math';
spawnSpotLight(world, {
color: [0.9, 0.9, 1.0],
intensity: 50,
range: 15,
innerAngle: 20, // degrees: fully lit inside this cone
outerAngle: 35, // degrees: fade to dark outside this cone
castShadows: true,
position: [5, 8, 0],
rotation: Quaternion.fromEuler(90, 0, 0),
});Environment Lighting (IBL)
typescript
import { EnvironmentMapResource, EnvironmentSetupSystem } from '@web-engine-dev/renderer';
import { CoreSchedule } from '@web-engine-dev/ecs';
// Register the environment setup system
engine.scheduler.addSystem(CoreSchedule.Update, EnvironmentSetupSystem);
// Use an HDR environment map for image-based lighting
world.insertResource(EnvironmentMapResource, {
type: 'hdri',
hdri: 'environments/outdoor-overcast.hdr',
intensity: 1.0,
rotation: 45, // rotate the sky in degrees
showSkybox: true,
});Terrain
typescript
import {
TerrainRoot,
TerrainPatch,
TerrainManagerResource,
TerrainLODUpdateSystem,
TerrainRenderSubmissionSystem,
} from '@web-engine-dev/terrain';
import { CoreSchedule } from '@web-engine-dev/ecs';
// Register terrain systems via the plugin/scheduler
engine.scheduler.addSystem(CoreSchedule.Update, TerrainLODUpdateSystem);
engine.scheduler.addSystem(CoreSchedule.Update, TerrainRenderSubmissionSystem);
// Create a terrain entity
const terrainEntity = world.spawn();
world.insert(terrainEntity, TerrainRoot, {
heightmap: 'terrain/heightmap.png',
width: 512, // world units
depth: 512,
maxHeight: 80,
collisionEnabled: true,
// Texture splatting: blend up to 4 textures based on height/slope
layers: [
{ texture: 'terrain/grass.png', tileSize: 4, normal: 'terrain/grass-n.png' },
{ texture: 'terrain/rock.png', tileSize: 8, normal: 'terrain/rock-n.png' },
{ texture: 'terrain/snow.png', tileSize: 6, normal: 'terrain/snow-n.png' },
{ texture: 'terrain/sand.png', tileSize: 4, normal: 'terrain/sand-n.png' },
],
// Splatmap: red=grass, green=rock, blue=snow, alpha=sand
splatmap: 'terrain/splatmap.png',
});
// Sample terrain height at a world position
const terrainMgr = world.getResource(TerrainManagerResource);
const height = terrainMgr.getHeightAt(terrainEntity, { x: 50, z: 30 });3D Character Controller
typescript
import { CharacterController3D } from '@web-engine-dev/character';
import { PhysicsWorld3DResource } from '@web-engine-dev/physics3d';
import { MainCameraResource } from '@web-engine-dev/renderer';
world.insert(playerEntity, CharacterController3D, {
height: 1.8,
radius: 0.4,
stepHeight: 0.3,
slopeLimit: 45,
gravity: 9.81,
skinWidth: 0.02,
});
function PlayerMovement3DSystem(world: World): void {
const input = world.getResource(InputState);
const { delta } = world.getResource(Time);
const physics = world.getResource(PhysicsWorld3DResource);
const camera = world.getResource(MainCameraResource);
const ctrlQ = world.query().with(CharacterController3D, Player).build();
for (const result of world.run(ctrlQ)) {
const [ctrl] = result.components;
const entity = result.entity;
// Camera-relative movement
const camForward = camera.getForwardXZ(); // horizontal forward vector
const camRight = camera.getRightXZ();
let moveDir = Vec3.zero();
const rawInput = world.getResource(InputStateResource) as InputState;
if (rawInput?.pressedKeys.has('KeyW')) moveDir = moveDir.add(camForward);
if (rawInput?.pressedKeys.has('KeyS')) moveDir = moveDir.sub(camForward);
if (rawInput?.pressedKeys.has('KeyD')) moveDir = moveDir.add(camRight);
if (rawInput?.pressedKeys.has('KeyA')) moveDir = moveDir.sub(camRight);
const speed = 5; // m/s
moveDir = moveDir.normalize().scale(speed);
// Jump
if (input.justPressed('jump') && ctrl.isGrounded) {
moveDir.y = 8; // jump velocity
}
// Apply gravity when airborne
if (!ctrl.isGrounded) {
moveDir.y -= 9.81 * delta;
}
physics.moveCharacter(entity, moveDir.scale(delta));
}
}Level of Detail (LOD)
typescript
import { LODMeshGroup, LODState } from '@web-engine-dev/renderer';
world.insert(treeEntity, LODMeshGroup, {
levels: [
{ distance: 0, meshUrl: 'models/tree-lod0.glb' }, // full detail
{ distance: 20, meshUrl: 'models/tree-lod1.glb' }, // medium
{ distance: 50, meshUrl: 'models/tree-lod2.glb' }, // low
{ distance: 100, meshUrl: null }, // culled
],
fadeTransition: true,
});Shadows Configuration
typescript
import { ShadowSettingsResource } from '@web-engine-dev/renderer';
// Configure shadow quality globally
world.insertResource(ShadowSettingsResource, {
cascadeShadowMaps: {
enabled: true,
cascades: 4,
maxDistance: 200,
lambda: 0.75,
resolution: 2048,
},
pointShadows: {
enabled: true,
resolution: 512,
maxLights: 4,
},
softShadows: true,
shadowBias: 0.001,
normalBias: 0.002,
});Fog
typescript
import { FogResource } from '@web-engine-dev/renderer';
world.insertResource(FogResource, {
type: 'exponential-squared',
color: [0.7, 0.8, 0.9],
density: 0.02,
startDistance: 50,
endDistance: 300,
});Next Steps
- Visual Effects, post-processing for 3D scenes
- Camera Systems, third-person and first-person cameras
- Physics, 3D physics rigidbodies and colliders