Camera System
Camera types, controls, multi-view rendering, frustum culling, and camera effects for creating dynamic viewpoints.
Web Engine's camera system provides flexible viewpoint control through the ECS architecture. Create perspective and orthographic cameras, implement custom controls, and render multiple views simultaneously.
Camera Types#
Perspective Camera
Realistic 3D camera with field of view and depth
Orthographic Camera
Parallel projection for isometric and 2D views
Multi-View
Render multiple cameras simultaneously (splitscreen)
Camera Effects
Shake, zoom, depth of field, and transitions
Creating a Camera#
Add the Camera component to an entity to create a camera:
import { createEntity, addComponent } from 'bitecs';import { Transform, Camera, CameraTag} from '@web-engine-dev/core/engine/ecs/components'; // Create camera entityconst cameraEntity = createEntity(world);addComponent(world, cameraEntity, Transform);addComponent(world, cameraEntity, Camera);addComponent(world, cameraEntity, CameraTag); // Position cameraTransform.position[cameraEntity][0] = 0;Transform.position[cameraEntity][1] = 5;Transform.position[cameraEntity][2] = 10; // Configure perspective cameraCamera.fov[cameraEntity] = 60; // Field of view (degrees)Camera.near[cameraEntity] = 0.1; // Near clipping planeCamera.far[cameraEntity] = 1000; // Far clipping planeCamera.focus[cameraEntity] = 10; // Focus distance (for DOF) // Aspect ratio is automatically calculated from viewportPerspective Camera#
Perspective cameras provide realistic 3D rendering with depth perception:
| Property | Description | Recommended |
|---|---|---|
| fov | Field of view in degrees (vertical) | 60-75 for games, 35-50 for cinematic |
| near | Near clipping plane distance | 0.1 for indoor, 1.0 for outdoor |
| far | Far clipping plane distance | 100-1000 based on world size |
| focus | Focus distance for depth of field | Distance to main subject |
// Standard game cameraCamera.fov[cameraEntity] = 70;Camera.near[cameraEntity] = 0.1;Camera.far[cameraEntity] = 1000; // Cinematic camera (narrower FOV)Camera.fov[cameraEntity] = 40;Camera.near[cameraEntity] = 1;Camera.far[cameraEntity] = 500; // Wide-angle cameraCamera.fov[cameraEntity] = 90;Camera.near[cameraEntity] = 0.5;Camera.far[cameraEntity] = 2000;Field of View
Lower FOV (35-50) creates a compressed, cinematic look. Higher FOV (80-90) provides a wider view but can cause distortion at the edges. Most games use 60-75 degrees as a balanced default.
Camera Controls#
Implement camera movement and rotation through the Transform component:
Orbit Controls#
// Orbit camera around a target pointconst target = new THREE.Vector3(0, 0, 0);const distance = 10;const angle = 0;const height = 5; function updateOrbitCamera(delta, input) { // Rotate around target angle += input.mouseDeltaX * 0.005; // Position camera Transform.position[cameraEntity][0] = target.x + Math.sin(angle) * distance; Transform.position[cameraEntity][1] = height; Transform.position[cameraEntity][2] = target.z + Math.cos(angle) * distance; // Look at target const camera = SystemContext.camera; camera.lookAt(target); // Sync rotation back to ECS Transform.rotation[cameraEntity][0] = camera.rotation.x; Transform.rotation[cameraEntity][1] = camera.rotation.y; Transform.rotation[cameraEntity][2] = camera.rotation.z;}First-Person Controls#
let yaw = 0;let pitch = 0; function updateFirstPersonCamera(delta, input) { // Mouse look yaw -= input.mouseDeltaX * 0.002; pitch -= input.mouseDeltaY * 0.002; pitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, pitch)); // Movement (WASD) const forward = new THREE.Vector3( Math.sin(yaw), 0, Math.cos(yaw) ); const right = new THREE.Vector3( Math.cos(yaw), 0, -Math.sin(yaw) ); const speed = 5; if (input.forward) { Transform.position[cameraEntity][0] += forward.x * speed * delta; Transform.position[cameraEntity][2] += forward.z * speed * delta; } if (input.backward) { Transform.position[cameraEntity][0] -= forward.x * speed * delta; Transform.position[cameraEntity][2] -= forward.z * speed * delta; } if (input.right) { Transform.position[cameraEntity][0] += right.x * speed * delta; Transform.position[cameraEntity][2] += right.z * speed * delta; } if (input.left) { Transform.position[cameraEntity][0] -= right.x * speed * delta; Transform.position[cameraEntity][2] -= right.z * speed * delta; } // Apply rotation Transform.rotation[cameraEntity][0] = pitch; Transform.rotation[cameraEntity][1] = yaw; Transform.rotation[cameraEntity][2] = 0;}Third-Person Controls#
// Follow a target entity with offsetconst targetEntity = playerEntity;const offset = new THREE.Vector3(0, 2, 5); // Behind and aboveconst smoothing = 0.1; // Camera lag function updateThirdPersonCamera(delta) { // Target position const targetPos = new THREE.Vector3( Transform.position[targetEntity][0], Transform.position[targetEntity][1], Transform.position[targetEntity][2] ); // Desired camera position const desiredPos = targetPos.clone().add(offset); // Smooth follow const currentPos = new THREE.Vector3( Transform.position[cameraEntity][0], Transform.position[cameraEntity][1], Transform.position[cameraEntity][2] ); currentPos.lerp(desiredPos, smoothing); Transform.position[cameraEntity][0] = currentPos.x; Transform.position[cameraEntity][1] = currentPos.y; Transform.position[cameraEntity][2] = currentPos.z; // Look at target const camera = SystemContext.camera; camera.lookAt(targetPos);}Camera Shake#
Add impact and intensity to your game with camera shake:
import { CameraShake } from '@web-engine-dev/core/engine/ecs/components'; // Add camera shake componentaddComponent(world, cameraEntity, CameraShake); // Trigger shakefunction triggerShake(intensity = 1.0, duration = 0.5) { CameraShake.intensity[cameraEntity] = intensity; CameraShake.duration[cameraEntity] = duration; CameraShake.frequency[cameraEntity] = 20; // Shake speed CameraShake.decay[cameraEntity] = 2.0; // Exponential decay} // Example: Explosion nearbyconst distance = 10;const maxDistance = 50;const intensity = 1.0 - (distance / maxDistance);triggerShake(intensity * 2.0, 0.8); // CameraShakeSystem automatically applies shake to camera// and decays intensity over timeMulti-View Rendering#
Render multiple camera views simultaneously for split-screen or picture-in-picture:
import { MultiViewRenderer } from '@web-engine-dev/core/engine/rendering'; // Create multiple camerasconst camera1 = createCamera(world, { x: 0, y: 5, z: 10 });const camera2 = createCamera(world, { x: 10, y: 5, z: 0 }); // Configure multi-view rendererconst multiView = new MultiViewRenderer({ views: [ { camera: camera1, viewport: { x: 0, y: 0, width: 0.5, height: 1 }, // Left half }, { camera: camera2, viewport: { x: 0.5, y: 0, width: 0.5, height: 1 }, // Right half }, ],}); // Render all viewsfunction render() { multiView.render(scene, renderer);} // Common layouts:// Split-screen horizontal: { width: 0.5, height: 1 }// Split-screen vertical: { width: 1, height: 0.5 }// Picture-in-picture: { x: 0.7, y: 0.7, width: 0.25, height: 0.25 }// Quad view: 4 cameras at { width: 0.5, height: 0.5 }Frustum Culling#
The engine automatically culls objects outside the camera's view frustum:
// Frustum culling is automatic, but you can control it per objectmesh.frustumCulled = true; // Enable (default)mesh.frustumCulled = false; // Disable (always render) // For custom culling logicconst frustum = new THREE.Frustum();const cameraViewProjection = new THREE.Matrix4(); function updateFrustum(camera) { cameraViewProjection.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse ); frustum.setFromProjectionMatrix(cameraViewProjection);} // Test if object is visiblefunction isVisible(object) { return frustum.intersectsObject(object);} // Test if sphere is visibleconst sphere = new THREE.Sphere( new THREE.Vector3(0, 0, 0), 5 // radius);const visible = frustum.intersectsSphere(sphere);Camera Transitions#
Smoothly transition between camera positions or settings:
// Smooth camera movementfunction transitionCamera( from: THREE.Vector3, to: THREE.Vector3, duration: number) { const startTime = performance.now(); function animate() { const elapsed = performance.now() - startTime; const t = Math.min(elapsed / (duration * 1000), 1); // Ease in-out const eased = t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2; // Interpolate position const pos = from.clone().lerp(to, eased); Transform.position[cameraEntity][0] = pos.x; Transform.position[cameraEntity][1] = pos.y; Transform.position[cameraEntity][2] = pos.z; if (t < 1) { requestAnimationFrame(animate); } } animate();} // Smooth FOV transitionfunction transitionFOV(fromFOV: number, toFOV: number, duration: number) { const startTime = performance.now(); function animate() { const elapsed = performance.now() - startTime; const t = Math.min(elapsed / (duration * 1000), 1); const fov = fromFOV + (toFOV - fromFOV) * t; Camera.fov[cameraEntity] = fov; if (t < 1) { requestAnimationFrame(animate); } } animate();}Best Practices#
Camera Setup#
- Use appropriate near/far planes to maximize depth precision
- Keep near plane as far as possible (0.1-1.0) to reduce z-fighting
- Set far plane to just beyond visible distance to improve culling
- Match FOV to your game's style (narrow for tactical, wide for action)
- Test camera at different aspect ratios (16:9, 16:10, 21:9, mobile)
Camera Controls#
- Add smoothing/interpolation for natural camera movement
- Clamp pitch to prevent camera flipping upside down
- Use separate sensitivity settings for mouse and gamepad
- Implement camera collision to prevent clipping through walls
- Provide accessibility options for motion sensitivity
Performance#
- Enable frustum culling for all objects (default behavior)
- Use appropriate far plane to cull distant objects
- Implement LOD system to switch models based on distance
- Avoid rendering multiple high-resolution views simultaneously
- Use occlusion culling for complex scenes
Z-Fighting
If you see flickering between overlapping surfaces, increase the near plane value. Z-fighting occurs when the depth precision is insufficient for the near/far ratio. Keep near as high as possible and far as low as reasonable.
Common Camera Patterns#
Cinematic Camera#
// Narrow FOV for compressed perspectiveCamera.fov[cameraEntity] = 35; // Follow target with offsetconst offset = new THREE.Vector3(-5, 3, -8); // Behind, up, left // Smooth trackingconst lookAheadDistance = 5;const targetPos = getTargetPosition();const lookAtPos = targetPos.clone().add( targetVelocity.clone().normalize().multiplyScalar(lookAheadDistance)); camera.lookAt(lookAtPos);Isometric Camera#
// Position camera at 45-degree angleconst distance = 20;Transform.position[cameraEntity][0] = distance;Transform.position[cameraEntity][1] = distance;Transform.position[cameraEntity][2] = distance; // Look at originconst camera = SystemContext.camera;camera.lookAt(0, 0, 0); // Use orthographic projection (not implemented in basic Camera component)// For true isometric, you'd need to extend the camera system// to support orthographic camerasSecurity Camera#
// Fixed position, rotating viewconst centerPos = new THREE.Vector3(0, 0, 0);const angle = time * 0.5; // Slow rotationconst radius = 10; Transform.position[cameraEntity][0] = Math.sin(angle) * radius;Transform.position[cameraEntity][1] = 5;Transform.position[cameraEntity][2] = Math.cos(angle) * radius; camera.lookAt(centerPos); // Add scanline effect for security camera aestheticconst scanlineShader = /* ... */;