2025-12-31 14:30:18 +01:00
|
|
|
import { Engine } from "./framework/Engine.js";
|
2025-12-31 14:22:45 +01:00
|
|
|
|
2025-12-31 14:30:18 +01:00
|
|
|
import { Scene } from "./framework/Scene.js";
|
2025-12-31 14:22:45 +01:00
|
|
|
|
2025-12-31 14:30:18 +01:00
|
|
|
import { Mesh } from "./framework/Mesh.js";
|
2025-12-31 14:22:45 +01:00
|
|
|
|
|
|
|
|
import { OceanPipeline } from "./pipelines/OceanPipeline.js";
|
|
|
|
|
|
|
|
|
|
import { OceanTests } from "./tests/OceanTests.js";
|
|
|
|
|
|
|
|
|
|
import { events } from "./events.js";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OceanApp {
|
|
|
|
|
|
|
|
|
|
static bitReverse( x, bits ) {
|
|
|
|
|
|
|
|
|
|
let n = x;
|
|
|
|
|
let r = 0;
|
|
|
|
|
|
|
|
|
|
for ( let i = 0; i < bits; i++ ) {
|
|
|
|
|
|
|
|
|
|
r = ( r << 1 ) | ( n & 1 );
|
|
|
|
|
n = n >> 1;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return r;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async run( ) {
|
|
|
|
|
|
|
|
|
|
const eventsHandler = new events( );
|
|
|
|
|
|
|
|
|
|
const domElements = eventsHandler.getDomElements( );
|
|
|
|
|
|
|
|
|
|
const statusElement = domElements.statusElement;
|
|
|
|
|
|
|
|
|
|
const canvas = domElements.canvas;
|
|
|
|
|
|
|
|
|
|
const waveHeightSlider = domElements.waveHeightSlider;
|
|
|
|
|
|
|
|
|
|
const waveHeightValue = domElements.waveHeightValue;
|
|
|
|
|
|
|
|
|
|
const wavelengthSlider = domElements.wavelengthSlider;
|
|
|
|
|
|
|
|
|
|
const wavelengthValue = domElements.wavelengthValue;
|
|
|
|
|
|
|
|
|
|
const resolutionSlider = domElements.resolutionSlider;
|
|
|
|
|
|
|
|
|
|
const resolutionValue = domElements.resolutionValue;
|
|
|
|
|
|
|
|
|
|
const tilingSlider = domElements.tilingSlider;
|
|
|
|
|
|
|
|
|
|
const tilingValue = domElements.tilingValue;
|
|
|
|
|
|
|
|
|
|
const wireframeToggle = domElements.wireframeToggle;
|
|
|
|
|
|
|
|
|
|
const pauseToggle = domElements.pauseToggle;
|
|
|
|
|
|
|
|
|
|
const shadingModeSelect = domElements.shadingModeSelect;
|
|
|
|
|
|
|
|
|
|
const stepButton = domElements.stepButton;
|
|
|
|
|
|
|
|
|
|
const dumpHeightButton = domElements.dumpHeightButton;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function resizeCanvasToDisplay( ) {
|
|
|
|
|
|
|
|
|
|
const width = window.innerWidth;
|
|
|
|
|
const height = window.innerHeight;
|
|
|
|
|
|
|
|
|
|
if ( canvas.width !== width || canvas.height !== height ) {
|
|
|
|
|
|
|
|
|
|
canvas.width = width;
|
|
|
|
|
canvas.height = height;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resizeCanvasToDisplay( );
|
|
|
|
|
|
|
|
|
|
if ( !navigator.gpu ) {
|
|
|
|
|
statusElement.textContent = "WebGPU not supported in this browser.";
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
statusElement.textContent = "Requesting WebGPU device…";
|
|
|
|
|
|
|
|
|
|
const adapter = await navigator.gpu.requestAdapter( );
|
|
|
|
|
|
|
|
|
|
const device = await adapter.requestDevice( );
|
|
|
|
|
|
|
|
|
|
const engine = new Engine( device );
|
|
|
|
|
|
|
|
|
|
const renderSystem = engine.createRenderSystem( canvas );
|
|
|
|
|
|
|
|
|
|
let scene = new Scene( );
|
|
|
|
|
|
|
|
|
|
let pipelines = [];
|
|
|
|
|
|
|
|
|
|
let currentMeshResolution = 1024;
|
|
|
|
|
|
|
|
|
|
let currentWavelengthScale = 1.0;
|
|
|
|
|
|
|
|
|
|
let currentTiling = 1;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function getPrimaryPipeline( ) {
|
|
|
|
|
|
|
|
|
|
return pipelines.length > 0 ? pipelines[ 0 ] : null;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function rebuildScene( ) {
|
|
|
|
|
|
|
|
|
|
const previousPrimary = getPrimaryPipeline( );
|
|
|
|
|
|
|
|
|
|
statusElement.textContent = "Rebuilding pipelines…";
|
|
|
|
|
|
|
|
|
|
const newScene = new Scene( );
|
|
|
|
|
|
|
|
|
|
const meshRes = currentMeshResolution;
|
|
|
|
|
|
|
|
|
|
const tiling = currentTiling;
|
|
|
|
|
|
|
|
|
|
const pipeline = new OceanPipeline( engine, canvas );
|
|
|
|
|
|
|
|
|
|
pipeline.gridSize = 64;
|
|
|
|
|
pipeline.meshResolution = meshRes;
|
|
|
|
|
|
|
|
|
|
pipeline.memory.set( "canvasRef", canvas );
|
|
|
|
|
|
|
|
|
|
await pipeline.create( );
|
|
|
|
|
|
|
|
|
|
// restore camera from previous primary pipeline if available
|
|
|
|
|
if ( previousPrimary && previousPrimary.camera && pipeline.camera ) {
|
|
|
|
|
|
|
|
|
|
const prevCam = previousPrimary.camera;
|
|
|
|
|
const newCam = pipeline.camera;
|
|
|
|
|
|
|
|
|
|
newCam.yaw = prevCam.yaw;
|
|
|
|
|
newCam.pitch = prevCam.pitch;
|
|
|
|
|
newCam.distance = prevCam.distance;
|
|
|
|
|
newCam.fovRadians = prevCam.fovRadians;
|
|
|
|
|
newCam.near = prevCam.near;
|
|
|
|
|
newCam.far = prevCam.far;
|
|
|
|
|
|
|
|
|
|
if ( prevCam.target && newCam.target ) {
|
|
|
|
|
|
|
|
|
|
newCam.target.x = prevCam.target.x;
|
|
|
|
|
newCam.target.y = prevCam.target.y;
|
|
|
|
|
newCam.target.z = prevCam.target.z;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( prevCam.up && newCam.up ) {
|
|
|
|
|
|
|
|
|
|
newCam.up.x = prevCam.up.x;
|
|
|
|
|
newCam.up.y = prevCam.up.y;
|
|
|
|
|
newCam.up.z = prevCam.up.z;
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( typeof newCam.update === "function" ) {
|
|
|
|
|
|
|
|
|
|
newCam.update( );
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// global settings
|
|
|
|
|
if ( waveHeightSlider && waveHeightValue ) {
|
|
|
|
|
|
|
|
|
|
const hValue = parseFloat( waveHeightSlider.value );
|
|
|
|
|
|
|
|
|
|
if ( !isNaN( hValue ) ) {
|
|
|
|
|
|
|
|
|
|
pipeline.setHeightScale( hValue );
|
|
|
|
|
|
|
|
|
|
waveHeightValue.textContent = hValue.toFixed( 0 );
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( wavelengthSlider && wavelengthValue && typeof pipeline.setWavelengthScale === "function" ) {
|
|
|
|
|
|
|
|
|
|
const wValue = parseFloat( wavelengthSlider.value );
|
|
|
|
|
|
|
|
|
|
if ( !isNaN( wValue ) && wValue > 0 ) {
|
|
|
|
|
|
|
|
|
|
pipeline.setWavelengthScale( wValue );
|
|
|
|
|
|
|
|
|
|
wavelengthValue.textContent = wValue.toFixed( 2 );
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( wireframeToggle ) {
|
|
|
|
|
|
|
|
|
|
if ( wireframeToggle.checked ) {
|
|
|
|
|
|
|
|
|
|
pipeline.setRenderMode( "wireframe" );
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
|
|
|
|
|
pipeline.setRenderMode( "solid" );
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( shadingModeSelect ) {
|
|
|
|
|
|
|
|
|
|
pipeline.setShadingMode( shadingModeSelect.value || "lighting" );
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( pauseToggle ) {
|
|
|
|
|
|
|
|
|
|
pipeline.setPaused( pauseToggle.checked );
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// tiling (number of tiles around center)
|
|
|
|
|
pipeline.setTiling( tiling );
|
|
|
|
|
|
|
|
|
|
const tileRange = Math.max( 0, tiling - 1 );
|
|
|
|
|
const tilesPerRow = tileRange * 2 + 1;
|
|
|
|
|
const tileCount = tilesPerRow * tilesPerRow;
|
|
|
|
|
|
|
|
|
|
const block = pipeline.getBlockByName( "ocean" );
|
|
|
|
|
|
|
|
|
|
if ( block ) {
|
|
|
|
|
|
|
|
|
|
const skyPass = block.getPass( "SkySphere" );
|
|
|
|
|
|
|
|
|
|
if ( skyPass && skyPass.shader ) {
|
|
|
|
|
|
|
|
|
|
const skyMesh = new Mesh( );
|
|
|
|
|
|
|
|
|
|
skyMesh.addShader( skyPass.shader );
|
|
|
|
|
|
|
|
|
|
newScene.addMesh( skyMesh );
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const renderPassName = wireframeToggle && wireframeToggle.checked ? "RenderWire" : "RenderSolid";
|
|
|
|
|
|
|
|
|
|
const renderPass = block.getPass( renderPassName );
|
|
|
|
|
|
|
|
|
|
if ( renderPass && renderPass.shader ) {
|
|
|
|
|
|
|
|
|
|
const mesh = new Mesh( );
|
|
|
|
|
|
|
|
|
|
mesh.addShader( renderPass.shader );
|
|
|
|
|
|
|
|
|
|
mesh.setInstanceCount( tileCount );
|
|
|
|
|
|
|
|
|
|
newScene.addMesh( mesh );
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pipelines = [ pipeline ];
|
|
|
|
|
scene = newScene;
|
|
|
|
|
|
|
|
|
|
statusElement.textContent = "Simulating ocean (drag to orbit, scroll to zoom)";
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
await rebuildScene( );
|
|
|
|
|
|
|
|
|
|
eventsHandler.resizeCanvasToDisplay = resizeCanvasToDisplay;
|
|
|
|
|
eventsHandler.getPrimaryPipeline = getPrimaryPipeline;
|
|
|
|
|
eventsHandler.getPipelines = function( ) { return pipelines; };
|
|
|
|
|
eventsHandler.rebuildScene = rebuildScene;
|
|
|
|
|
eventsHandler.getScene = function( ) { return scene; };
|
|
|
|
|
eventsHandler.getRenderSystem = function( ) { return renderSystem; };
|
|
|
|
|
eventsHandler.setCurrentMeshResolution = function( value ) { currentMeshResolution = value; };
|
|
|
|
|
eventsHandler.setCurrentWavelengthScale = function( value ) { currentWavelengthScale = value; };
|
|
|
|
|
eventsHandler.setCurrentTiling = function( value ) { currentTiling = value; };
|
|
|
|
|
eventsHandler.setAutoRotate = function( value ) { autoRotate = value; };
|
|
|
|
|
|
|
|
|
|
eventsHandler.setup( );
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// expose FFT test helper
|
|
|
|
|
const oceanTests = new OceanTests( getPrimaryPipeline );
|
|
|
|
|
|
|
|
|
|
window.testFft = async function( ) {
|
|
|
|
|
|
|
|
|
|
return oceanTests.testFft( );
|
|
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
window.testSpectrum = async function( ) {
|
|
|
|
|
|
|
|
|
|
return oceanTests.testSpectrum( );
|
|
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let autoRotate = true;
|
|
|
|
|
|
|
|
|
|
let lastAutoRotateTime = performance.now();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async function frame( ) {
|
|
|
|
|
|
|
|
|
|
const now = performance.now();
|
|
|
|
|
|
|
|
|
|
if ( autoRotate ) {
|
|
|
|
|
|
|
|
|
|
const dt = ( now - lastAutoRotateTime ) / 1000;
|
|
|
|
|
|
|
|
|
|
const primary = getPrimaryPipeline();
|
|
|
|
|
|
|
|
|
|
if ( primary && primary.camera ) {
|
|
|
|
|
|
|
|
|
|
const rotationSpeed = 0.15;
|
|
|
|
|
|
|
|
|
|
primary.camera.yaw += rotationSpeed * dt;
|
|
|
|
|
|
|
|
|
|
primary.camera.update();
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lastAutoRotateTime = now;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const list = pipelines.slice( );
|
|
|
|
|
|
|
|
|
|
const run = async function( ) {
|
|
|
|
|
|
|
|
|
|
for ( const p of list ) {
|
|
|
|
|
|
|
|
|
|
await p.bindBuffers( );
|
|
|
|
|
await p.execute( );
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( scene && renderSystem ) {
|
|
|
|
|
|
|
|
|
|
renderSystem.render( scene );
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
|
|
|
|
await run( );
|
|
|
|
|
|
|
|
|
|
requestAnimationFrame( frame );
|
|
|
|
|
|
|
|
|
|
} catch ( error ) {
|
|
|
|
|
|
|
|
|
|
console.error( error );
|
|
|
|
|
statusElement.textContent = "Error while drawing ocean.";
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
requestAnimationFrame( frame );
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const app = new OceanApp( );
|
|
|
|
|
|
|
|
|
|
app.run( );
|