Skip to content

src/sea.ts

90 lines · 2.9 KB · TypeScript
Raw
import * as THREE from 'three'

const SURFACE_Z = -0.2
const WAVE_HEIGHT = 0.22
const SCROLL_SPEED = 5
const FOAM_COUNT = 120
// Covers everything the tilted camera can see, which reaches well past the play field.
const SEA_WIDTH = 50
const SEA_HEIGHT = 28
const SEA_CENTER_Y = 2.5

export class Sea {
  readonly object = new THREE.Group()
  private readonly water: THREE.Mesh<THREE.PlaneGeometry, THREE.MeshStandardMaterial>
  private readonly foam: THREE.Points<THREE.BufferGeometry, THREE.PointsMaterial>
  private readonly baseX: Float32Array
  private readonly baseY: Float32Array
  private time = 0
  private scroll = 0

  constructor() {
    const geometry = new THREE.PlaneGeometry(SEA_WIDTH, SEA_HEIGHT, 50, 28)
    geometry.translate(0, SEA_CENTER_Y, 0)
    const positions = geometry.attributes.position!
    this.baseX = new Float32Array(positions.count)
    this.baseY = new Float32Array(positions.count)
    for (let i = 0; i < positions.count; i++) {
      this.baseX[i] = positions.getX(i)
      this.baseY[i] = positions.getY(i)
    }

    this.water = new THREE.Mesh(
      geometry,
      new THREE.MeshStandardMaterial({
        color: 0x0e6aa8,
        flatShading: true,
        roughness: 0.6,
      }),
    )
    this.water.position.z = SURFACE_Z
    this.object.add(this.water)

    const foamPositions = new Float32Array(FOAM_COUNT * 3)
    for (let i = 0; i < FOAM_COUNT; i++) {
      foamPositions[i * 3] = THREE.MathUtils.randFloatSpread(SEA_WIDTH)
      foamPositions[i * 3 + 1] = SEA_CENTER_Y + THREE.MathUtils.randFloatSpread(SEA_HEIGHT)
      foamPositions[i * 3 + 2] = SURFACE_Z + WAVE_HEIGHT + 0.05
    }
    const foamGeometry = new THREE.BufferGeometry()
    foamGeometry.setAttribute('position', new THREE.BufferAttribute(foamPositions, 3))
    this.foam = new THREE.Points(
      foamGeometry,
      new THREE.PointsMaterial({
        color: 0xe0f2fe,
        size: 0.25,
        transparent: true,
        opacity: 0.5,
        depthWrite: false,
      }),
    )
    this.object.add(this.foam)
  }

  update(dt: number) {
    this.time += dt
    this.scroll += dt * SCROLL_SPEED

    const positions = this.water.geometry.attributes.position!
    for (let i = 0; i < positions.count; i++) {
      const x = this.baseX[i]! + this.scroll
      const y = this.baseY[i]!
      const swell = Math.sin(y * 0.6 + this.time * 1.3) * Math.cos(x * 0.45 + this.time * 0.9)
      const chop = Math.sin(x * 0.9 + y * 0.3 + this.time * 2) * 0.4
      positions.setZ(i, (swell + chop) * WAVE_HEIGHT)
    }
    positions.needsUpdate = true

    const foamPositions = this.foam.geometry.attributes.position!
    const minX = -SEA_WIDTH / 2
    for (let i = 0; i < FOAM_COUNT; i++) {
      let x = foamPositions.getX(i) - SCROLL_SPEED * dt
      if (x < minX) {
        x += SEA_WIDTH
        foamPositions.setY(i, SEA_CENTER_Y + THREE.MathUtils.randFloatSpread(SEA_HEIGHT))
      }
      foamPositions.setX(i, x)
    }
    foamPositions.needsUpdate = true
  }
}