import { vec3, mat4 } from 'gl-matrix'; import { ICamera } from './ICamera'; import { OrbitalCamera } from './Camera'; import { FPSCamera } from './FPSCamera'; import { Grid } from './Grid'; import { Skybox } from './Skybox'; import { createProgram } from './Shader'; import * as Config from './constants'; var gl: WebGL2RenderingContext; var viewportWidth = 0; var viewportHeight = 0; /** A camera that always looks at the world origin. Can have an offset and be rotated. */ // Moved to Camera.ts /** Init OpenGL and gets the viewport/canvas sizes */ function initGL(canvas: HTMLCanvasElement) { // Helper function for canvas resize const updateCanvasSize = (canvas: HTMLCanvasElement) => { const displayWidth = window.innerWidth; const displayHeight = window.innerHeight; if (canvas.width !== displayWidth || canvas.height !== displayHeight) { canvas.width = displayWidth; canvas.height = displayHeight; viewportWidth = displayWidth; viewportHeight = displayHeight; if (gl) { gl.viewport(0, 0, viewportWidth, viewportHeight); } } }; var gltemp; try { gltemp = canvas.getContext("webgl2"); if (!gltemp) gltemp = canvas.getContext("experimental-webgl2"); if (gltemp != null) { updateCanvasSize(canvas); } } catch (e) { } // Not the best error detection logic. // Redirect to http://get.webgl.org in failure case. if (gltemp == null) { console.error("Unable to initialize WebGL2. Your browser or machine may not support it."); return; } gl = gltemp; //WebGL2 supports floating point textures by default but it does not support filtering them or rendering to them by default. Note: 16bit filtering is included 32bit not if (!gl.getExtension('EXT_color_buffer_float')) { console.error("32Bit/16Bit single Color render Buffers not available."); } //allow 16bit texture as framebuffer target gl.enable(gl.DEPTH_TEST); return updateCanvasSize; } /** Update canvas size to fill window */ // Moved inline below /** Grid for the watersurface */ // Moved to Grid.ts /** Init Geometry for a Triangle */ var VBO: WebGLBuffer | null = null; function initGeometry() { VBO = gl.createBuffer(); //Vertex data represent fullscreen quad in NDC-Space // X, Y, Z, U, V let vertexData = [-1.0, -1.0, 0.0, /*BOTTOM LEFT*/ 0.0, 0.0, 1.0, -1.0, 0.0, /*BOTTOM RIGHT*/ 1.0, 0.0, -1.0, 1.0, 0.0, /*TOP LEFT */ 0.0, 1.0, 1.0, -1.0, 0.0, /*BOTTOM RIGHT */ 1.0, 0.0, -1.0, 1.0, 0.0, /*TOP LEFT */ 0.0, 1.0, 1.0, 1.0, 0.0, /*TOP RIGHT */ 1.0, 1.0 ]; gl.bindBuffer(gl.ARRAY_BUFFER, VBO); gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexData), gl.STATIC_DRAW); } /** Get shader source by HTML-Element */ // Moved to Shader.ts /** Init all Shaders that are needed */ var perlinNoiseProgram: WebGLProgram | null; var defaultProgram: WebGLProgram | null; var textureProgram: WebGLProgram | null; var skyProgram: WebGLProgram | null; function initShaders() { perlinNoiseProgram = createProgram(gl, "ndc-vs", "noise-fs", "Perlin Noise"); defaultProgram = createProgram(gl, "default-vs", "default-fs", "Default"); textureProgram = createProgram(gl, "texture-vs", "texture-fs", "Texture"); skyProgram = createProgram(gl, "sky-vs", "sky-fs", "Sky"); } /** Init an FBO used for the first render pass / perlin noise */ var perlinNoiseFBO: WebGLFramebuffer | null = null; var textureFBO: WebGLTexture | null = null; var perlinNoiseFBOWidth = Config.NOISE_TEXTURE_WIDTH; var perlinNoiseFBOHeight = Config.NOISE_TEXTURE_HEIGHT; function initFBO() { perlinNoiseFBO = gl.createFramebuffer(); gl.bindFramebuffer(gl.FRAMEBUFFER, perlinNoiseFBO); // Add attachments textureFBO = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, textureFBO); //last 3 parameter not intertesting becuase we are not supplying data gl.texImage2D(gl.TEXTURE_2D, 0, gl.R16F, perlinNoiseFBOWidth, perlinNoiseFBOHeight, 0, gl.RED, gl.HALF_FLOAT, null); // set the filtering so we don't need mips gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, textureFBO, 0); if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) != gl.FRAMEBUFFER_COMPLETE) { console.log("Framebuffer creation failed."); } gl.bindFramebuffer(gl.FRAMEBUFFER, null); //Reset to default framebuffer } /** Update/Draw function.*/ /** Framerate measurement variables */ var timeSpent = 0.0; var lastTime = new Date().getTime(); var counter = 0.0; var fps = 0; var fpsDisplay: HTMLElement | null = null; /** Input states*/ var mouseXVel = 0; var mouseYVel = 0; var keyboardRotationX = 0; var keyboardRotationY = 0; var keyboardZoom = 0; var keysPressed: Set = new Set(); /** Objects and states*/ var camera: ICamera; var orbitalCamera: OrbitalCamera; var fpsCamera: FPSCamera; var oceanGrid: Grid; var skybox: Skybox; var curRotX = Config.CAMERA_DEFAULT_ROT_X; var curRotY = Config.CAMERA_DEFAULT_ROT_Y; /** Camera modes */ var cameraMode: 'orbital' | 'fps' = 'fps'; var moveSpeed = 0.15; var fastMoveSpeed = 0.4; function drawScene() { fps++; let now = new Date(); let delta = now.getTime() - lastTime; timeSpent += delta; if ((counter += delta) >= Config.FPS_UPDATE_INTERVAL) { counter = 0; if (fpsDisplay) { fpsDisplay.textContent = `FPS: ${fps}`; } fps = 0; } lastTime = now.getTime(); // Two Rendering passes. The first one generates a perlin noise // texture. Second one uses the textur for vertex displacement // of a grid representing the water surface. //--- First render pass -> Perlin Noise (it updates the perlin noise texture) { gl.bindFramebuffer(gl.FRAMEBUFFER, perlinNoiseFBO); gl.viewport(0, 0, perlinNoiseFBOWidth, perlinNoiseFBOHeight); //Clear buffer content gl.clearColor(1.0, 1.0, 1.0, 1); gl.clear(gl.COLOR_BUFFER_BIT); //No depth buffer // Disable face culling for fullscreen quad gl.disable(gl.CULL_FACE); //draw a fullscreen quad gl.bindBuffer(gl.ARRAY_BUFFER, VBO); // There are 7 floating-point values per vertex let stride = 5 * Float32Array.BYTES_PER_ELEMENT; // Set up position stream gl.vertexAttribPointer(0, 3, gl.FLOAT, false, stride, 0); gl.enableVertexAttribArray(0); gl.vertexAttribPointer(1, 2, gl.FLOAT, false, stride, 3 * Float32Array.BYTES_PER_ELEMENT); gl.enableVertexAttribArray(1); gl.useProgram(perlinNoiseProgram); let uTime = gl.getUniformLocation(perlinNoiseProgram, "uTime"); gl.uniform1f(uTime, timeSpent); gl.drawArrays(gl.TRIANGLES, 0, 6); // Draw fullscreen quad } //--- Second render pass -> Geomtry with displacement by perlin noise texture --- { gl.bindFramebuffer(gl.FRAMEBUFFER, null); //Bind default framebuffer gl.viewport(0, 0, viewportWidth, viewportHeight); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); gl.activeTexture(gl.TEXTURE0); //Binds the texture to 0 gl.bindTexture(gl.TEXTURE_2D, textureFBO); var projection = mat4.create(); mat4.identity(projection); mat4.perspective(projection, Config.FOV, viewportWidth / viewportHeight, Config.NEAR_PLANE, Config.FAR_PLANE); //projection mode should actually be camera specific // Handle camera movement and rotation based on mode if (cameraMode === 'fps') { // FPS camera - direct movement camera = fpsCamera; handleFPSCameraMovement(); // Apply mouse rotation for FPS mode if (mouseXVel !== 0 || mouseYVel !== 0) { fpsCamera.rotate(mouseXVel, mouseYVel); mouseXVel = 0; mouseYVel = 0; } } else { // Orbital camera - original behavior camera = orbitalCamera; orbitalCamera.setOffset(Config.CAMERA_DEFAULT_OFFSET + keyboardZoom); orbitalCamera.setRotationX((curRotX += mouseYVel * Config.MOUSE_SENSITIVITY + keyboardRotationX)); orbitalCamera.setRotationY((curRotY += mouseXVel * Config.MOUSE_SENSITIVITY + keyboardRotationY)); } var view = camera.getViewMatrix(); // Sun direction (matches the one in ocean shader) const sunDirection = vec3.fromValues(0.3, 0.5, 0.8); vec3.normalize(sunDirection, sunDirection); // Draw skybox first with depth test disabled (always behind everything) gl.depthMask(false); gl.disable(gl.DEPTH_TEST); gl.disable(gl.CULL_FACE); // Disable face culling for skybox (we're inside) gl.useProgram(skyProgram); let sky_view_loc = gl.getUniformLocation(skyProgram, "view"); gl.uniformMatrix4fv(sky_view_loc, false, view); let sky_projection_loc = gl.getUniformLocation(skyProgram, "projection"); gl.uniformMatrix4fv(sky_projection_loc, false, projection); let sky_sun_loc = gl.getUniformLocation(skyProgram, "uSunDirection"); gl.uniform3fv(sky_sun_loc, sunDirection); skybox.draw(gl); gl.enable(gl.DEPTH_TEST); gl.depthMask(true); gl.enable(gl.CULL_FACE); // Re-enable face culling for ocean gl.cullFace(gl.BACK); // Cull back faces for ocean var model = mat4.create(); mat4.identity(model); let translationCentering = vec3.create(); vec3.set(translationCentering, -0.5, -0.5, 0.0); mat4.translate(model, model, translationCentering); //1. First Center the Surface in the origin. gl.useProgram(defaultProgram); let view_loc = gl.getUniformLocation(defaultProgram, "view"); gl.uniformMatrix4fv(view_loc, false, view); let model_loc = gl.getUniformLocation(defaultProgram, "model"); gl.uniformMatrix4fv(model_loc, false, model); let projection_loc = gl.getUniformLocation(defaultProgram, "projection"); gl.uniformMatrix4fv(projection_loc, false, projection); let eye_loc = gl.getUniformLocation(defaultProgram, "eyePos"); gl.uniform3fv(eye_loc, camera.pos); //let uTime_loc = gl.getUniformLocation(defaultProgram, "uTime"); //gl.uniform1f(uTime_loc, timeSpent); let displacementMap_loc = gl.getUniformLocation(defaultProgram, "displace_map"); gl.uniform1i(displacementMap_loc, 0); //Get texture from slot 0 oceanGrid.draw(gl); } requestAnimationFrame(drawScene); } /** Handle FPS camera movement */ function handleFPSCameraMovement() { const speed = keysPressed.has('Shift') ? fastMoveSpeed : moveSpeed; // WASD for horizontal movement if (keysPressed.has('w') || keysPressed.has('W')) { fpsCamera.moveForward(speed); } if (keysPressed.has('s') || keysPressed.has('S')) { fpsCamera.moveForward(-speed); } if (keysPressed.has('a') || keysPressed.has('A')) { fpsCamera.moveRight(-speed); } if (keysPressed.has('d') || keysPressed.has('D')) { fpsCamera.moveRight(speed); } // Q/E for vertical movement if (keysPressed.has('q') || keysPressed.has('Q')) { fpsCamera.moveUp(-speed); } if (keysPressed.has('e') || keysPressed.has('E')) { fpsCamera.moveUp(speed); } // Space to go up, Ctrl to go down if (keysPressed.has(' ')) { fpsCamera.moveUp(speed); } if (keysPressed.has('Control')) { fpsCamera.moveUp(-speed); } } /** Handle keyboard input for camera controls */ function handleKeyboardInput() { keyboardRotationX = 0; keyboardRotationY = 0; if (keysPressed.has('w') || keysPressed.has('W') || keysPressed.has('ArrowUp')) { keyboardRotationX = Config.KEYBOARD_ROTATION_SPEED; } if (keysPressed.has('s') || keysPressed.has('S') || keysPressed.has('ArrowDown')) { keyboardRotationX = -Config.KEYBOARD_ROTATION_SPEED; } if (keysPressed.has('a') || keysPressed.has('A') || keysPressed.has('ArrowLeft')) { keyboardRotationY = Config.KEYBOARD_ROTATION_SPEED; } if (keysPressed.has('d') || keysPressed.has('D') || keysPressed.has('ArrowRight')) { keyboardRotationY = -Config.KEYBOARD_ROTATION_SPEED; } if (keysPressed.has('q') || keysPressed.has('Q') || keysPressed.has('+')) { keyboardZoom -= Config.KEYBOARD_ZOOM_SPEED; } if (keysPressed.has('e') || keysPressed.has('E') || keysPressed.has('-')) { keyboardZoom += Config.KEYBOARD_ZOOM_SPEED; } } function main() { const canvas: HTMLCanvasElement = document.getElementById("window"); fpsDisplay = document.getElementById("fps-counter"); const updateCanvasSize = initGL(canvas); if (!updateCanvasSize) { console.error("Failed to initialize WebGL"); return; } var drag = false; var previousPosX: number | null; var previousPosY: number | null; canvas.addEventListener('mousedown', function (evt) { drag = true; }, false); canvas.addEventListener('mousemove', function (evt) { if (drag) { if (previousPosX == null || previousPosY == null) { previousPosX = evt.x; previousPosY = evt.y; } var mousePosX = evt.x; var mousePosY = evt.y; mouseXVel = (mousePosX - previousPosX); mouseYVel = (mousePosY - previousPosY); previousPosX = mousePosX; previousPosY = mousePosY; } }, false); var deactivateMouseMovement = function () { previousPosX = null; previousPosY = null; mouseXVel = 0.0; mouseYVel = 0.0; drag = false; } canvas.addEventListener('mouseup', deactivateMouseMovement, false); canvas.addEventListener('mouseleave', deactivateMouseMovement, false); // Keyboard controls window.addEventListener('keydown', (evt) => { keysPressed.add(evt.key); // Toggle camera mode with 'C' key if (evt.key === 'c' || evt.key === 'C') { cameraMode = cameraMode === 'fps' ? 'orbital' : 'fps'; console.log(`Camera mode: ${cameraMode.toUpperCase()}`); // Update FPS display to show camera mode if (fpsDisplay) { const modeText = document.createElement('div'); modeText.id = 'camera-mode'; modeText.style.cssText = 'position: absolute; top: 40px; left: 10px; color: white; font-family: monospace;'; modeText.textContent = `Camera: ${cameraMode.toUpperCase()}`; const existingMode = document.getElementById('camera-mode'); if (existingMode) { existingMode.textContent = `Camera: ${cameraMode.toUpperCase()}`; } else { document.body.appendChild(modeText); } } } // Reset camera on 'R' key if (evt.key === 'r' || evt.key === 'R') { if (cameraMode === 'fps') { fpsCamera = new FPSCamera(); // Reset to initial FPS position camera = fpsCamera; console.log('Camera reset to FPS default position'); } else { curRotX = Config.CAMERA_DEFAULT_ROT_X; curRotY = Config.CAMERA_DEFAULT_ROT_Y; keyboardZoom = 0; console.log('Camera reset to orbital default position'); } } // Prevent default for space to avoid page scroll if (evt.key === ' ' && cameraMode === 'fps') { evt.preventDefault(); } // Handle orbital camera keyboard input if (cameraMode === 'orbital') { handleKeyboardInput(); } }); window.addEventListener('keyup', (evt) => { keysPressed.delete(evt.key); if (cameraMode === 'orbital') { handleKeyboardInput(); } }); // Window resize handler window.addEventListener('resize', () => { updateCanvasSize(canvas); }); // Camera mode toggle from UI controls window.addEventListener('toggleCameraMode', () => { cameraMode = cameraMode === 'fps' ? 'orbital' : 'fps'; camera = cameraMode === 'fps' ? fpsCamera : orbitalCamera; console.log(`Camera mode switched to: ${cameraMode.toUpperCase()}`); // Update display const modeText = document.createElement('div'); modeText.id = 'camera-mode'; modeText.style.cssText = 'position: absolute; top: 40px; left: 10px; color: white; font-family: monospace;'; modeText.textContent = `Camera: ${cameraMode.toUpperCase()}`; const existingMode = document.getElementById('camera-mode'); if (existingMode) { existingMode.textContent = `Camera: ${cameraMode.toUpperCase()}`; } else { document.body.appendChild(modeText); } }); initShaders(); initGeometry(); initFBO(); oceanGrid = new Grid(Config.GRID_SIZE); oceanGrid.initVAO(gl); skybox = new Skybox(); skybox.initVAO(gl); // Initialize both cameras orbitalCamera = new OrbitalCamera(); fpsCamera = new FPSCamera(); camera = fpsCamera; // Start with FPS camera console.log('Cameras initialized - Press C to toggle between FPS and Orbital modes'); //Check if any errors apeared during init. if (gl.getError() != gl.NO_ERROR) { console.log("OpenGL Error!: "); } drawScene(); } main();