Source file: /examples/lights/multiple_spot_lights.js

import {
  MeshMaterial3D,
  WebGLRenderer,
  TextureLoader,
  PerspectiveProjection,
  Camera,
  WebGLRenderDevice,
  PlaneMeshBuilder,
  OrbitCameraControls,
  MeshMaterialPlugin,
  ShadowPlugin,
  LightPlugin,
  DirectionalLight,
  AmbientLight,
  TextureType,
  SkyBox,
  UVSphereMeshBuilder,
  CanvasTarget,
  CuboidMeshBuilder,
  LambertMaterial,
  SkyboxPlugin,
  SpotLight,
  SpotLightShadow,
  Affine3,
  Vector3,
  BasicMaterial,
  Color,
  Object3D,
  CameraPlugin
} from "webgllis"
import { GUI } from "dat.gui"

const canvas = document.createElement('canvas')
const renderTarget = new CanvasTarget(canvas)
const renderDevice = new WebGLRenderDevice(canvas, {
  depth: true
})
const renderer = new WebGLRenderer({
  plugins: [
    new ShadowPlugin(),
    new LightPlugin(),
    new SkyboxPlugin(),
    new MeshMaterialPlugin(),
    new CameraPlugin()
  ]
})

// assets and loaders
const textureLoader = new TextureLoader()
const texture = textureLoader.load({
  paths: ["/assets/images/uv.jpg"],
})
const skyboxTexture = textureLoader.load({
  paths: [
    "/assets/images/skybox/miramar_right.png",
    "/assets/images/skybox/miramar_left.png",
    "/assets/images/skybox/miramar_top.png",
    "/assets/images/skybox/miramar_bottom.png",
    "/assets/images/skybox/miramar_back.png",
    "/assets/images/skybox/miramar_front.png",
  ],
  type: TextureType.TextureCubeMap,
})
const material = new LambertMaterial({
  mainTexture: texture
})
const arrowBuilder = new CuboidMeshBuilder()
const meshBuilder = new PlaneMeshBuilder()

meshBuilder.width = 10
meshBuilder.height = 10
arrowBuilder.width = 0.1
arrowBuilder.height = 0.1
arrowBuilder.depth = 1

// objects
const ambientLight = new AmbientLight()
const sun = new DirectionalLight()
const spotShadow = new SpotLightShadow()
const lights = createLights()
const camera = new Camera(renderTarget)
const cameraControls = new OrbitCameraControls(camera, canvas)

const skyBox = new SkyBox({
  day: skyboxTexture
})
const ground = new MeshMaterial3D(meshBuilder.build(), material)
const objects = createObjects()

ambientLight.intensity = 0.15
sun.transform.position.y = 2
sun.transform.position.z = 0
sun.transform.orientation.rotateX(- Math.PI / 2)
sun.intensity = 0.01

skyBox.transform.orientation.rotateY(Math.PI)
ground.transform.orientation.rotateX(-Math.PI / 2)

//set up the camera
cameraControls.distance = 3
if (camera.projection instanceof PerspectiveProjection) {
  camera.projection.fov = Math.PI / 180 * 75
  camera.projection.aspect = innerWidth / innerHeight
}

document.body.append(canvas)
addEventListener("resize", updateView)
updateView()
requestAnimationFrame(update)

function createObjects() {
  const results = []
  const meshBuilder2 = new UVSphereMeshBuilder()


  meshBuilder2.radius = 0.25
  const sphereMesh = meshBuilder2.build()

  for (let x = -5; x < 5; x++) {
    for (let y = -5; y < 5; y++) {
      const object = new MeshMaterial3D(sphereMesh, material)

      object.transform.position.x = x
      object.transform.position.y = 0.5
      object.transform.position.z = y
      results.push(object)
    }
  }

  return results
}

function createLights() {
  const builder = new CuboidMeshBuilder()
  const width = 10
  const height = 10
  const tallness = 4
  const scale = 0.1
  const lights = new Object3D()

  builder.width = 0.1
  builder.height = 0.1
  builder.depth = 1

  const mesh = builder.build()
  for (let x = -width / 2; x <= width / 2; x += width) {
    for (let y = -height / 2; y <= height / 2; y += height) {
      const light = new SpotLight()
      const helper = new MeshMaterial3D(mesh, new BasicMaterial({
        color: Color.RED.clone()
      }))
      const space = new Affine3()

      space.translate(new Vector3(x, tallness, y))
      space.lookAt(new Vector3(x * scale, 0, y * scale), Vector3.Y)

      const [position, orientation, _scale] = space.decompose()

      helper.transform.position.z -= 0.5
      light.intensity = 1
      light.range = 20
      light.outerAngle = Math.PI / 8
      light.shadow = spotShadow
      light.transform.position.copy(position)
      light.transform.orientation.copy(orientation)
      light.add(helper)
      lights.add(light)
    }
  }
  return lights
}

function update() {
  cameraControls.update()
  lights.transform.orientation.rotateY(0.01)
  renderer.render([ground, ...objects, sun, lights, ambientLight, skyBox, camera], renderDevice)
  requestAnimationFrame(update)
}

function updateView() {
  canvas.style.width = innerWidth + "px"
  canvas.style.height = innerHeight + "px"
  canvas.width = innerWidth * devicePixelRatio
  canvas.height = innerHeight * devicePixelRatio

  if (camera.projection instanceof PerspectiveProjection) {
    camera.projection.aspect = innerWidth / innerHeight
  }
}

const settings = {
  shadow: true
}
const controls = new GUI()
const shadowFolder = controls.addFolder("Shadows")

shadowFolder
  .add(settings, 'shadow')
  .name("Enable Shadow")
  .onChange(toggleShadows)
shadowFolder.open()

/**
 * @param {boolean} value
 */
function toggleShadows(value) {
  if (value) {
    lights.traverseDFS(light => {
      if(light instanceof SpotLight){
        light.shadow = spotShadow
      }
      return true
    })
  } else {
    lights.traverseDFS(light => {
      if(light instanceof SpotLight){
        light.shadow = undefined
      }
      return true
    })
  }
}