import { RenderPipeline } from "/framework/RenderPipeline.js"; import { Block } from "/framework/Block.js"; import Camera from "/framework/Camera.js"; import EventManager from "/framework/eventManager.js"; import { SpectrumPass } from "../passes/SpectrumPass.js"; import { RowFFTPass } from "../passes/RowFFTPass.js"; import { ColFFTPass } from "../passes/ColFFTPass.js"; import { OceanRenderPass } from "../passes/OceanRenderPass.js"; import { OceanSolidRenderPass } from "../passes/OceanSolidRenderPass.js"; import { SkySpherePass } from "../passes/SkySpherePass.js"; export class OceanPipeline extends RenderPipeline { constructor( engine, canvas ) { super( engine ); this.canvas = canvas; this.canvasConfigured = false; this.gridSize = 64; this.meshResolution = 64; this.offsetX = 0; this.offsetZ = 0; this.wavelengthScale = 1.0; this.tiling = 1; this.handleInput = true; this.renderMode = "solid"; this.shadingMode = "lighting"; this.isPaused = false; this.elapsedTime = 0; } async create( ) { this.patchSize = 80; this.heightScale = 54.0; this.timeScale = 0.35; this.startTime = performance.now(); const simSize = this.gridSize; this.memory.set( "h0Real", new Float32Array( simSize * simSize ) ); this.memory.set( "h0Imag", new Float32Array( simSize * simSize ) ); this.memory.set( "spectrumReal", new Float32Array( simSize * simSize ) ); this.memory.set( "spectrumImag", new Float32Array( simSize * simSize ) ); this.memory.set( "rowReal", new Float32Array( simSize * simSize ) ); this.memory.set( "rowImag", new Float32Array( simSize * simSize ) ); this.memory.set( "colReal", new Float32Array( simSize * simSize ) ); this.memory.set( "colImag", new Float32Array( simSize * simSize ) ); this.memory.set( "heightField", new Float32Array( simSize * simSize ) ); this.memory.set( "computeParams", new Float32Array( 4 ) ); this.memory.set( "renderParams", new Float32Array( [ simSize, this.patchSize, this.heightScale, 0, this.meshResolution, this.tiling, this.wavelengthScale ] ) ); this._seedSpectrum(); const geometry = this._buildGridGeometry( this.meshResolution, this.patchSize ); this.memory.set( "positions", geometry.positions ); this.memory.set( "uvs", geometry.uvs ); this.memory.set( "indices", geometry.indices ); this.memory.set( "lineIndices", geometry.lineIndices ); const skySphere = this._buildSkySphereGeometry( 220, 32, 64 ); this.memory.set( "skyPositions", skySphere.positions ); this.memory.set( "skyNormals", skySphere.normals ); this.memory.set( "skyIndices", skySphere.indices ); const cubeGeometry = this._buildCubeGeometry(); this.memory.set( "cubePositions", cubeGeometry.positions ); this.memory.set( "cubeColors", cubeGeometry.colors ); this.memory.set( "cubeIndices", cubeGeometry.indices ); const block = new Block( "ocean", this ); const spectrumPass = new SpectrumPass( ); const rowFFTPass = new RowFFTPass( ); const colFFTPass = new ColFFTPass( ); const renderWirePass = new OceanRenderPass( ); const renderSolidPass = new OceanSolidRenderPass( ); const skySpherePass = new SkySpherePass( ); block.addPass( "Spectrum", spectrumPass ); block.addPass( "RowFFT", rowFFTPass ); block.addPass( "ColFFT", colFFTPass ); block.addPass( "RenderWire", renderWirePass ); block.addPass( "RenderSolid", renderSolidPass ); block.addPass( "SkySphere", skySpherePass ); this.addBlock( block ); this.camera = new Camera( [ 0, 90, 120 ], [ 0, 0, 0 ], [ 0, 1, 0 ] ); this.camera.far = 6000.0; this.camera.pitch = Math.PI / 3; this.camera.update(); this.eventManager = new EventManager( ); if ( this.canvas && this.handleInput ) { this.eventManager.setup( this.canvas, this.camera ); this.eventManager.registerEventListeners(); } await super.create(); } setHeightScale( value ) { this.heightScale = value; this.memory.computeParams[ 2 ] = value; this.memory.renderParams[ 2 ] = value; } setWavelengthScale( value ) { if ( value <= 0 ) { value = 0.25; } this.wavelengthScale = value; if ( this.memory && this.memory.renderParams ) { this.memory.renderParams[ 6 ] = this.wavelengthScale; } this._seedSpectrum( ); const block = this.getBlockByName( "ocean" ); if ( block ) { const spectrumPass = block.getPass( "Spectrum" ); if ( spectrumPass && spectrumPass.shader ) { spectrumPass.shader.setVariable( "h0Real", this.memory.h0Real ); spectrumPass.shader.setVariable( "h0Imag", this.memory.h0Imag ); } } } setRenderMode( mode ) { if ( mode !== "wireframe" && mode !== "solid" ) { return; } this.renderMode = mode; } setShadingMode( mode ) { let code = 0; if ( mode === "normals" ) { code = 1; } else if ( mode === "solid" ) { code = 2; } else if ( mode === "height" ) { code = 3; } else if ( mode === "realistic" ) { code = 4; } else { mode = "lighting"; code = 0; } this.shadingMode = mode; this.memory.renderParams[ 3 ] = code; } setTiling( value ) { if ( value < 1 ) { value = 1; } this.tiling = value; this.memory.renderParams[ 5 ] = value; } setOffset( x, z ) { this.offsetX = x; this.offsetZ = z; // Offsets are now handled in the vertex shader via instancing. } setPaused( paused ) { if ( paused === this.isPaused ) { return; } this.isPaused = paused; if ( !paused ) { this.startTime = performance.now() - this.elapsedTime * 1000.0; } } async stepOnce( stepSeconds = 1 / 60 ) { if ( !this.isPaused ) { return; } this.elapsedTime += stepSeconds; this.memory.computeParams[ 0 ] = this.elapsedTime * this.timeScale; this.memory.computeParams[ 1 ] = this.gridSize; await this.bindBuffers( ); await this.execute( ); } async bindBuffers( ) { const nowSeconds = ( performance.now() - this.startTime ) / 1000; if ( !this.isPaused ) { this.elapsedTime = nowSeconds; } this.memory.computeParams[ 0 ] = this.elapsedTime * this.timeScale; this.memory.computeParams[ 1 ] = this.gridSize; const block = this.getBlockByName( "ocean" ); const spectrumPass = block.getPass( "Spectrum" ); const rowFFTPass = block.getPass( "RowFFT" ); const colFFTPass = block.getPass( "ColFFT" ); const renderWirePass = block.getPass( "RenderWire" ); const renderSolidPass = block.getPass( "RenderSolid" ); const skyPass = block.getPass( "SkySphere" ); await spectrumPass.bindBuffers( ); await rowFFTPass.bindBuffers( ); await colFFTPass.bindBuffers( ); if ( this.renderMode === "solid" ) { await renderSolidPass.bindBuffers( ); } else { await renderWirePass.bindBuffers( ); } if ( skyPass ) { await skyPass.bindBuffers( ); } } async execute( ) { const block = this.getBlockByName( "ocean" ); const spectrumPass = block.getPass( "Spectrum" ); const rowFFTPass = block.getPass( "RowFFT" ); const colFFTPass = block.getPass( "ColFFT" ); await spectrumPass.execute( ); await rowFFTPass.execute( ); await colFFTPass.execute( ); } _seedSpectrum( ) { const size = this.gridSize; const phillipsA = 0.0006; const windSpeed = 24.0; const windDir = { x: 0.8, y: 0.6 }; const baseL = windSpeed * windSpeed / 9.81; let wlScale = this.wavelengthScale; if ( wlScale <= 0 ) { wlScale = 0.25; } const L = baseL; for ( let y = 0; y < size; y++ ) { for ( let x = 0; x < size; x++ ) { const kx = ( x - size / 2 ) * ( Math.PI * 2 / this.patchSize ); const ky = ( y - size / 2 ) * ( Math.PI * 2 / this.patchSize ); const kLength = Math.sqrt( kx * kx + ky * ky ); if ( kLength === 0 ) { continue; } const kxNorm = kx / kLength; const kyNorm = ky / kLength; const windDotK = kxNorm * windDir.x + kyNorm * windDir.y; const kScaled = kLength / wlScale; const phillips = phillipsA * Math.exp( -1 / ( kScaled * kScaled * L * L ) ) / Math.pow( kScaled, 4 ) * ( windDotK * windDotK ); const gaussianR = this._gaussianRandom(); const gaussianI = this._gaussianRandom(); const amplitude = Math.sqrt( phillips ) * Math.SQRT1_2; const idx = y * size + x; this.memory.h0Real[ idx ] = gaussianR * amplitude; this.memory.h0Imag[ idx ] = gaussianI * amplitude; } } // enforce conjugate symmetry: h0(-k) = conj( h0(k) ) for a real height field for ( let y = 0; y < size; y++ ) { for ( let x = 0; x < size; x++ ) { const idx = y * size + x; const mx = ( size - x ) % size; const my = ( size - y ) % size; const mirrorIdx = my * size + mx; this.memory.h0Real[ mirrorIdx ] = this.memory.h0Real[ idx ]; this.memory.h0Imag[ mirrorIdx ] = -this.memory.h0Imag[ idx ]; } } this.memory.computeParams[ 0 ] = 0; this.memory.computeParams[ 1 ] = this.gridSize; this.memory.computeParams[ 2 ] = this.heightScale; this.memory.computeParams[ 3 ] = this.patchSize; this.memory.renderParams[ 2 ] = this.heightScale; } _gaussianRandom( ) { let u = 0, v = 0; while ( u === 0 ) u = Math.random(); while ( v === 0 ) v = Math.random(); return Math.sqrt( -2.0 * Math.log( u ) ) * Math.cos( 2.0 * Math.PI * v ); } _buildGridGeometry( size, span ) { const vertexCount = size * size; const positions = new Float32Array( vertexCount * 3 ); const uvs = new Float32Array( vertexCount * 2 ); const quadCount = ( size - 1 ) * ( size - 1 ); const indices = new Uint32Array( quadCount * 6 ); const horizontalLines = size * ( size - 1 ); const verticalLines = ( size - 1 ) * size; const lineIndices = new Uint32Array( ( horizontalLines + verticalLines ) * 2 ); let pi = 0; let ui = 0; for ( let y = 0; y < size; y++ ) { for ( let x = 0; x < size; x++ ) { const fx = x / ( size - 1 ); const fy = y / ( size - 1 ); const worldX = ( fx - 0.5 ) * span; const worldZ = ( fy - 0.5 ) * span; positions[ pi++ ] = worldX; positions[ pi++ ] = 0; positions[ pi++ ] = worldZ; uvs[ ui++ ] = x; uvs[ ui++ ] = y; } } let ii = 0; let li = 0; for ( let y = 0; y < size - 1; y++ ) { for ( let x = 0; x < size - 1; x++ ) { const topLeft = y * size + x; const topRight = topLeft + 1; const bottomLeft = topLeft + size; const bottomRight = bottomLeft + 1; indices[ ii++ ] = topLeft; indices[ ii++ ] = bottomLeft; indices[ ii++ ] = topRight; indices[ ii++ ] = topRight; indices[ ii++ ] = bottomLeft; indices[ ii++ ] = bottomRight; } // horizontal line for row y at each segment for ( let x = 0; x < size - 1; x++ ) { const a = y * size + x; const b = a + 1; lineIndices[ li++ ] = a; lineIndices[ li++ ] = b; } } // vertical lines for ( let x = 0; x < size; x++ ) { for ( let y = 0; y < size - 1; y++ ) { const a = y * size + x; const b = a + size; lineIndices[ li++ ] = a; lineIndices[ li++ ] = b; } } return { positions, uvs, indices, lineIndices }; } _buildSkySphereGeometry( radius, latSegments, lonSegments ) { const latCount = latSegments; const lonCount = lonSegments; const vertexCount = ( latCount + 1 ) * ( lonCount + 1 ); const positions = new Float32Array( vertexCount * 3 ); const normals = new Float32Array( vertexCount * 3 ); const uvs = new Float32Array( vertexCount * 2 ); const indices = new Uint32Array( latCount * lonCount * 6 ); let pi = 0; let ni = 0; let ui = 0; for ( let lat = 0; lat <= latCount; lat++ ) { const theta = lat * Math.PI / latCount; const sinTheta = Math.sin( theta ); const cosTheta = Math.cos( theta ); for ( let lon = 0; lon <= lonCount; lon++ ) { const phi = lon * Math.PI * 2.0 / lonCount; const sinPhi = Math.sin( phi ); const cosPhi = Math.cos( phi ); const x = sinTheta * cosPhi; const y = cosTheta; const z = sinTheta * sinPhi; positions[ pi++ ] = x * radius; positions[ pi++ ] = y * radius; positions[ pi++ ] = z * radius; const nx = -x; const ny = -y; const nz = -z; normals[ ni++ ] = nx; normals[ ni++ ] = ny; normals[ ni++ ] = nz; uvs[ ui++ ] = lon / lonCount; uvs[ ui++ ] = lat / latCount; } } let ii = 0; for ( let y = 0; y < latCount; y++ ) { for ( let x = 0; x < lonCount; x++ ) { const i0 = y * ( lonCount + 1 ) + x; const i1 = i0 + 1; const i2 = ( y + 1 ) * ( lonCount + 1 ) + x; const i3 = i2 + 1; indices[ ii++ ] = i0; indices[ ii++ ] = i2; indices[ ii++ ] = i1; indices[ ii++ ] = i1; indices[ ii++ ] = i2; indices[ ii++ ] = i3; } } return { positions, normals, uvs, indices }; } _buildCubeGeometry( ) { const positions = new Float32Array( [ // Front -1, -1, 1, 1, -1, 1, 1, 1, 1, -1, 1, 1, // Back -1, -1, -1, -1, 1, -1, 1, 1, -1, 1, -1, -1, // Top -1, 1, -1, -1, 1, 1, 1, 1, 1, 1, 1, -1, // Bottom -1, -1, -1, 1, -1, -1, 1, -1, 1, -1, -1, 1, // Right 1, -1, -1, 1, 1, -1, 1, 1, 1, 1, -1, 1, // Left -1, -1, -1, -1, -1, 1, -1, 1, 1, -1, 1, -1 ] ); const colors = new Float32Array( [ // Front 0.1, 0.6, 1.0, 0.1, 0.6, 1.0, 0.1, 0.6, 1.0, 0.1, 0.6, 1.0, // Back 0.1, 0.2, 0.8, 0.1, 0.2, 0.8, 0.1, 0.2, 0.8, 0.1, 0.2, 0.8, // Top 0.2, 0.8, 0.5, 0.2, 0.8, 0.5, 0.2, 0.8, 0.5, 0.2, 0.8, 0.5, // Bottom 0.9, 0.7, 0.2, 0.9, 0.7, 0.2, 0.9, 0.7, 0.2, 0.9, 0.7, 0.2, // Right 0.8, 0.3, 0.4, 0.8, 0.3, 0.4, 0.8, 0.3, 0.4, 0.8, 0.3, 0.4, // Left 0.4, 0.9, 0.7, 0.4, 0.9, 0.7, 0.4, 0.9, 0.7, 0.4, 0.9, 0.7 ] ); const indices = new Uint32Array( [ 0, 1, 2, 0, 2, 3, // Front 4, 5, 6, 4, 6, 7, // Back 8, 9, 10, 8, 10, 11, // Top 12, 13, 14, 12, 14, 15, // Bottom 16, 17, 18, 16, 18, 19, // Right 20, 21, 22, 20, 22, 23 // Left ] ); return { positions, colors, indices }; } }