Skip to content

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
//       └── ShieldSocket

Traversing 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

Proprietary software. All rights reserved.