Files
WebGPU-FFT-Ocean-Demo/main.js

388 lines
7.3 KiB
JavaScript
Raw Normal View History

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( );