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 = 34 const SEA_HEIGHT = 52 const SEA_CENTER_Y = 6 export class Sea { readonly object = new THREE.Group() private readonly water: THREE.Mesh private readonly foam: THREE.Points private readonly baseX: Float32Array private readonly baseY: Float32Array private time = 0 private scroll = 0 constructor() { const geometry = new THREE.PlaneGeometry(SEA_WIDTH, SEA_HEIGHT, 34, 52) 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]! const y = this.baseY[i]! + this.scroll const swell = Math.sin(x * 0.6 + this.time * 1.3) * Math.cos(y * 0.45 + this.time * 0.9) const chop = Math.sin(y * 0.9 + x * 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 minY = SEA_CENTER_Y - SEA_HEIGHT / 2 for (let i = 0; i < FOAM_COUNT; i++) { let y = foamPositions.getY(i) - SCROLL_SPEED * dt if (y < minY) { y += SEA_HEIGHT foamPositions.setX(i, THREE.MathUtils.randFloatSpread(SEA_WIDTH)) } foamPositions.setY(i, y) } foamPositions.needsUpdate = true } }