Lights and Cameras using Components in Babylon.js
We've successfully loaded our game assets and implemented a robust movement system. Now, let's shed some light on our virtual world and provide players with a window to experience it all. We'll introduce Camera and Lighting components, along with their corresponding systems, to manage the visual aspects of our game.
The World Data
Let's add a World Entity and give it some components in the same way we did for the Player entity.
"entities": {
  "World": {
    "components": {
      "Asset": {
        "name": "World",
        "path": "glb/world.glb"
      },
      "Lighting": {
        "types": ["ambient", "directional"],
        "offset": [5, 10, -10]
      }
    }
  },
The LightingSystem:  Let There Be Light!
Now, let's create a LightingSystem to handle light creation and updates:
import {
  HemisphericLight,
  Vector3,
  DirectionalLight,
  ShadowGenerator,
} from "@babylonjs/core";
import { LightingComponent } from "../components/LightingComponent";
import { AssetComponent } from "../components/AssetComponent";
import { CameraComponent } from "../components/CameraComponent";
import { MovementComponent } from "../components/MovementComponent";
import { Entity, EntityManager, System } from "../lib";
import { WorldComponent } from "../components/WorldComponent";
import { TerrainSystem } from "./TerrainSystem";
import { TerrainComponent } from "../components/TerrainComponent";
export class LightingSystem extends System {
  public shadows: ShadowGenerator | null = null;
  constructor(
    entityManager: EntityManager,
    componentClasses = [LightingComponent],
  ) {
    super(entityManager, componentClasses);
    console.log("LightingSystem initialized");
  }
  processEntity(entity: Entity, deltaTime: number) {
    const playerEntity = this.entityManager.playerEntity;
    const playerAssets = playerEntity.getComponent(AssetComponent);
    const worldAssets = entity.getComponent(AssetComponent);
    if (!playerAssets.meshes.size || !worldAssets.meshes.size) return;
    const camera = playerEntity.getComponent(CameraComponent).camera;
    const lightingComponent = entity.getComponent(LightingComponent);
    const { lights, types, loading, offset } = lightingComponent;
    const lightOffset = Vector3.FromArray(offset) || new Vector3(0, 5, 0);
    if (!loading && lights.length < types.length) {
      lightingComponent.loading = true;
      types.forEach((type, index) => {
        if (type === "ambient") {
          const light = new HemisphericLight(
            `HemisphericLight-${lights.length}`,
            new Vector3(0, -1, 0),
            this.scene,
          );
          light.intensity = 4;
          lightingComponent.lights.push(light);
        }
        if (type === "directional") {
          const light = new DirectionalLight(
            `DirectionalLight-${lights.length}`,
            new Vector3(1, -10, 0),
            this.scene,
          );
          light.intensity = 0.8;
          light.position = new Vector3(0, 50, -3);
          light.autoCalcShadowZBounds = true;
          lightingComponent.lights.push(light);
        }
      });
      this.shadows = new ShadowGenerator(
        4096,
        lightingComponent.lights[1],
        true,
      );
      this.shadows.useBlurExponentialShadowMap = true;
      this.shadows.blurKernel = 32;
      this.shadows.enableSoftTransparentShadow = true;
      lightingComponent.loading = false;
      return;
    }
    const mesh = playerAssets.mainMesh;
    if (mesh) {
      lights.forEach((light) => {
        light.position = mesh.position.add(lightOffset);
      });
    }
    const pos = playerEntity.getComponent(MovementComponent).position;
    // lightingComponent.lights[1].setDirectionToTarget(pos);
    this.manageShadowMap();
  }
  manageShadowMap() {
    const { scene, shadows } = this;
    for (let i in scene.meshes) {
      var mesh = scene.meshes[i];
      if (mesh.castsShadows || mesh.name.includes("ground")) continue;
      if (scene.activeCamera.isInFrustum(mesh) && mesh.isVisible) {
        if (!mesh.name.includes("-UI")) {
          shadows.addShadowCaster(mesh, true);
          shadows.getShadowMap().renderList.push(mesh);
        }
        mesh.castsShadows = true;
      }
    }
  }
}
Explanation:
- The LightingSystemprocesses entities withLightingComponentcomponents.
- It creates lights based on the typesspecified in the component (e.g.,HemisphericLight,DirectionalLight).
- The processEntitymethod updates the lights' positions, potentially based on other entities like the player.
Camera Component on the Player Entity
Now, let's update our scene.json to include Camera and Lighting components for our player entity:
{
  "entities": {
    // ... other entities
    "Player": {
      "components": {
        // ... other components
        "Camera": {
          "type": "follow",
          "offset": [0, 7, -7]
        },
      }
    }
  }
}
The CameraComponent:  Capturing the View
Let's define a CameraComponent to store information about our game camera:
// CameraComponent.ts
import { Camera, Vector3 } from "@babylonjs/core";
import { Component } from "../lib";
export interface CameraComponentInput {
  type: "follow";
  offset: [0, 0, 10];
}
export class CameraComponent extends Component {
  camera: Camera;
  camera2: Camera;
  offset: Vector3;
  constructor(data: CameraComponentInput) {
    super(data);
    this.offset = Vector3.FromArray(data.offset);
  }
}
Key Properties:
- camera: A reference to the Babylon.js camera object.
- offset: A- Vector3representing the camera's offset relative to the entity it's attached to.
The LightingComponent: Setting the Mood
Next, let's define a LightingComponent to manage our game's lighting:
// LightComponent.ts
import { Component } from "../lib";
export interface LightingComponentInput {
  types: ["ambient", "directional"];
}
export class LightingComponent extends Component {
  types = ["ambient", "directional"];
  lights = [];
  loading = false;
  constructor(data: LightingComponentInput) {
    super(data);
    this.types = data.types;
  }
}
Key Properties:
- types: An array of strings specifying the types of lights to create (e.g., "ambient," "directional").
- lights: An array to store references to the created Babylon.js light objects.
The CameraSystem:  Keeping an Eye on the Action
Now, let's create a CameraSystem to handle camera creation, positioning, and updates:
// CameraSystem.ts
import { Vector3, FreeCamera, Viewport, Camera } from "@babylonjs/core";
import { Entity, EntityManager, System } from "../lib";
import { MovementComponent } from "../components/MovementComponent";
import { CameraComponent } from "../components/CameraComponent";
export class CameraSystem extends System {
  entityManager: EntityManager;
  viewport1: Viewport;
  viewport2: Viewport;
  viewportFull: Viewport;
  saveCamera2: Camera;
  constructor(
    entityManager: EntityManager,
    componentClasses = [CameraComponent, MovementComponent],
  ) {
    super(entityManager, componentClasses);
    this.componentClasses = componentClasses;
    this.entityManager = entityManager;
    this.viewport1 = new Viewport(0, 0, 0.5, 1.0); // Left half
    this.viewport2 = new Viewport(0.5, 0, 0.5, 1.0); // Right half
    this.viewportFull = new Viewport(0, 0, 1.0, 1.0); // Full screen
    console.log("CameraSystem initialized");
  }
  adjustCameraHeight(camera, players) {
    let minX = Infinity,
      maxX = -Infinity;
    let minZ = Infinity,
      maxZ = -Infinity;
    players.forEach((player) => {
      const position = player.getComponent(MovementComponent).position;
      const { x, y, z } = position;
      if (x < minX) minX = x;
      if (z < minZ) minZ = z;
      if (x > maxX) maxX = x;
      if (z > maxZ) maxZ = z;
    });
    const boundingBoxSize = Math.max(maxX - minX, maxZ - minZ);
    const distance = boundingBoxSize / (2 * Math.tan(camera.fov / 4));
    camera.position.y = Math.min(distance, 30);
    return distance;
  }
  processEntity(entity: Entity, deltaTime: number): void {
    const movementComponent = entity.getComponent(MovementComponent);
    const cameraComponent = entity.getComponent(CameraComponent);
    const { position } = movementComponent;
    const { offset } = cameraComponent;
    const targetPosition = position;
    const cameraPosition = position.add(offset);
    if (!cameraComponent.camera) {
      const camera = new FreeCamera("Camera", cameraPosition, this.scene);
      cameraComponent.camera = camera;
      // Add both cameras to the scene
      const ac = this.scene.activeCameras;
      // if (ac.length == 0) {
      camera.viewport = this.viewportFull;
      // } else if (ac.length == 1) {
      //   camera.viewport = this.viewport2;
      //   ac[0].viewport = this.viewport1;
      // }
      // this.scene.activeCameras.push(camera);
      // const canvas = this.scene.getEngine().getRenderingCanvas();
      // camera.attachControl(canvas, true);
    }
    // Assuming the player's position is where we want the camera to look at
    const players = this.entityManager.getPlayers();
    const camera = cameraComponent.camera;
    if (players[0] && players[1]) {
      const distance = this.adjustCameraHeight(cameraComponent.camera, players);
      if (distance < 10 && this.scene.activeCameras.length > 1) {
        this.saveCamera2 = this.scene.activeCameras[1];
        this.scene.activeCameras = [camera];
        camera.viewport = this.viewportFull;
      } else {
        camera.viewport = this.viewport1;
        this.saveCamera2.viewport = this.viewport2;
        this.scene.activeCameras = [camera, this.saveCamera2];
      }
    }
    // Update camera's target to the player's position
    this.updateCameraPosition(camera, targetPosition, cameraPosition);
    // this.updateCameraPosition(camera2, target2Position, camera2Position);
  }
  updateCameraPosition(
    camera: Camera,
    targetsPosition: Vector3,
    cameraPosition: Vector3,
  ) {
    camera.position = cameraPosition;
    let cy = camera.position.y;
    camera.setTarget(targetsPosition);
    // cy = Math.max(cy, 4);
    // cy = Math.min(cy, 100);
    // z = Math.max(z, 4);
    // z = Math.min(z, 12);
    // camera.position.y = cy;
  }
}
Explanation:
- The CameraSystemprocesses entities with bothCameraComponentandMovementComponentcomponents.
- It creates a FreeCamera(you can choose different camera types) if one doesn't exist for the entity.
- The processEntitymethod updates the camera's position based on the entity's position and the camera's offset.
- The camera is set to follow the entity by targeting its position.
In Sum
We've successfully added cameras and lights to our game world using a data-driven approach! By defining camera and lighting properties in our scene.json and using components and systems, we've enhanced our game's visual presentation and provided players with a way to experience our virtual world. Keep experimenting, keep refining, and watch your game world become more immersive and engaging!