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 entity
const cameraEntity = createEntity(world);
addComponent(world, cameraEntity, Transform);
addComponent(world, cameraEntity, Camera);
addComponent(world, cameraEntity, CameraTag);
// Position camera
Transform.position[cameraEntity][0] = 0;
Transform.position[cameraEntity][1] = 5;
Transform.position[cameraEntity][2] = 10;
// Configure perspective camera
Camera.fov[cameraEntity] = 60; // Field of view (degrees)
Camera.near[cameraEntity] = 0.1; // Near clipping plane
Camera.far[cameraEntity] = 1000; // Far clipping plane
Camera.focus[cameraEntity] = 10; // Focus distance (for DOF)
// Aspect ratio is automatically calculated from viewport

Perspective Camera#

Perspective cameras provide realistic 3D rendering with depth perception:

PropertyDescriptionRecommended
fovField of view in degrees (vertical)60-75 for games, 35-50 for cinematic
nearNear clipping plane distance0.1 for indoor, 1.0 for outdoor
farFar clipping plane distance100-1000 based on world size
focusFocus distance for depth of fieldDistance to main subject
// Standard game camera
Camera.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 camera
Camera.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 point
const 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 offset
const targetEntity = playerEntity;
const offset = new THREE.Vector3(0, 2, 5); // Behind and above
const 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 component
addComponent(world, cameraEntity, CameraShake);
// Trigger shake
function 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 nearby
const 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 time

Multi-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 cameras
const camera1 = createCamera(world, { x: 0, y: 5, z: 10 });
const camera2 = createCamera(world, { x: 10, y: 5, z: 0 });
// Configure multi-view renderer
const 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 views
function 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 object
mesh.frustumCulled = true; // Enable (default)
mesh.frustumCulled = false; // Disable (always render)
// For custom culling logic
const 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 visible
function isVisible(object) {
return frustum.intersectsObject(object);
}
// Test if sphere is visible
const 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 movement
function 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 transition
function 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 perspective
Camera.fov[cameraEntity] = 35;
// Follow target with offset
const offset = new THREE.Vector3(-5, 3, -8); // Behind, up, left
// Smooth tracking
const 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 angle
const distance = 20;
Transform.position[cameraEntity][0] = distance;
Transform.position[cameraEntity][1] = distance;
Transform.position[cameraEntity][2] = distance;
// Look at origin
const 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 cameras

Security Camera#

// Fixed position, rotating view
const centerPos = new THREE.Vector3(0, 0, 0);
const angle = time * 0.5; // Slow rotation
const 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 aesthetic
const scanlineShader = /* ... */;
Rendering | Web Engine Docs | Web Engine Docs