diff --git a/index.html b/index.html index b69309d..a3fb7de 100644 --- a/index.html +++ b/index.html @@ -243,6 +243,7 @@ varying vec3 v_normal; varying float v_waveHeight; varying float v_foamFactor; + varying float v_distanceFade; uniform vec3 eyePos; uniform float uFoamIntensity; @@ -297,7 +298,7 @@ // Deep and shallow water colors vec3 deepColor = vec3(0.0, 0.08, 0.15); vec3 shallowColor = vec3(0.0, 0.35, 0.45); - vec3 skyColor = vec3(0.5, 0.7, 0.9); + vec3 skyColor = vec3(0.55, 0.7, 0.9); // Match skybox horizon color vec3 foamColor = vec3(0.95, 0.98, 1.0); // Blend between deep and shallow based on wave height @@ -343,8 +344,8 @@ // Softer edge fade based on foam factor foam *= smoothstep(0.0, 0.25, v_foamFactor); - // Additional soft fade at foam edges - foam = pow(foam, 0.7) * uFoamIntensity; + // Additional soft fade at foam edges and fade out at distance + foam = pow(foam, 0.7) * uFoamIntensity * v_distanceFade; // Combine all lighting vec3 reflectedColor = mix(oceanColor, skyColor, fresnel); @@ -353,15 +354,26 @@ // Blend foam on top with slight transparency variation vec3 finalColor = mix(waterColor, foamColor * clamp(diffuse + 0.4, 0.0, 1.0), foam * 0.85); - // Slight fog for distant water + // Atmospheric fog for distant water - blends to horizon float dist = length(eyePos - v_fragPos); - float fog = 1.0 - clamp(dist * 0.015, 0.0, 0.6); - finalColor = mix(skyColor * 0.85, finalColor, fog); + + // Exponential fog with aggressive horizon fade + float fogFactor = exp(-dist * 0.04); + // Fully fade at stretched horizon vertices + float horizonFade = smoothstep(40.0, 80.0, dist); + fogFactor *= (1.0 - horizonFade); + fogFactor = clamp(fogFactor, 0.0, 1.0); + + // Horizon color must exactly match skybox horizon + vec3 horizonColor = vec3(0.55, 0.7, 0.9); + finalColor = mix(horizonColor, finalColor, fogFactor); gl_FragColor = vec4(finalColor, 1.0); } diff --git a/src/Camera.ts b/src/Camera.ts index 742d15e..47c55ce 100644 --- a/src/Camera.ts +++ b/src/Camera.ts @@ -1,62 +1,87 @@ import { vec3, mat4, vec4 } from 'gl-matrix'; -/** A camera that always looks at the world origin. Can have an offset and be rotated. */ +/** FPS-style flight camera with free movement */ export class Camera { pos: vec3; target: vec3; up: vec3; - - xRot: number; - yRot: number; - offset: number; + + // FPS camera angles (in radians) + pitch: number; // Up/down rotation + yaw: number; // Left/right rotation + + // Direction vectors + forward: vec3; + right: vec3; constructor() { this.pos = vec3.create(); - vec3.set(this.pos, 0.0, 0.0, 0.0); + vec3.set(this.pos, 0.0, -3.0, 2.0); // Start above and behind origin this.target = vec3.create(); - vec3.set(this.target, 0.0, 0.0, 0.0); this.up = vec3.create(); - vec3.set(this.up, 0.0, 1.0, 0.0); - this.xRot = 0.0; - this.yRot = 0.0; - this.offset = 0.0; + vec3.set(this.up, 0.0, 0.0, 1.0); // Z is up + this.forward = vec3.create(); + this.right = vec3.create(); + this.pitch = -0.3; // Looking slightly down + this.yaw = Math.PI / 2; // Looking toward +Y + this.updateVectors(); } - setRotationX(rotX: number): void { - this.xRot = rotX; - this.updatePos(); + /** Rotate camera by mouse delta */ + rotate(deltaX: number, deltaY: number, sensitivity: number = 0.003): void { + this.yaw -= deltaX * sensitivity; + this.pitch -= deltaY * sensitivity; + + // Clamp pitch to avoid flipping + const maxPitch = Math.PI / 2 - 0.01; + this.pitch = Math.max(-maxPitch, Math.min(maxPitch, this.pitch)); + + this.updateVectors(); } - setRotationY(rotY: number): void { - this.yRot = rotY; - this.updatePos(); + /** Move camera in the direction it's looking */ + moveForward(amount: number): void { + vec3.scaleAndAdd(this.pos, this.pos, this.forward, amount); + this.updateVectors(); } - /** Sets the offset to world origin. */ - setOffset(off: number): void { - this.offset = off; - this.updatePos(); + moveRight(amount: number): void { + vec3.scaleAndAdd(this.pos, this.pos, this.right, amount); + this.updateVectors(); } - /** Recalculates the position according to xy-rotation and offset. */ - private updatePos(): void { - const transformation: mat4 = mat4.create(); - mat4.identity(transformation); + moveUp(amount: number): void { + // Move along world Z axis + this.pos[2] += amount; + this.updateVectors(); + } - //2. xy-Rotation - mat4.rotateX(transformation, transformation, this.xRot); - mat4.rotateY(transformation, transformation, this.yRot); + /** Move in the actual look direction (including vertical) */ + moveInLookDirection(amount: number): void { + vec3.scaleAndAdd(this.pos, this.pos, this.forward, amount); + this.updateVectors(); + } - //1. Translation - const translation = vec3.create(); - vec3.set(translation, 0.0, 0.0, this.offset); - mat4.translate(transformation, transformation, translation); - - const temp: vec4 = vec4.create(); - vec4.set(temp, 0.0, 0.0, 0.0, 1.0); - vec4.transformMat4(temp, temp, transformation); - - vec3.set(this.pos, temp[0], temp[1], temp[2]); + /** Update direction vectors from pitch/yaw */ + private updateVectors(): void { + // Calculate forward vector from pitch and yaw + // Z is up, so we use different axis mapping + this.forward[0] = Math.cos(this.pitch) * Math.cos(this.yaw); + this.forward[1] = Math.cos(this.pitch) * Math.sin(this.yaw); + this.forward[2] = Math.sin(this.pitch); + vec3.normalize(this.forward, this.forward); + + // Right vector is perpendicular to forward and world up + const worldUp = vec3.fromValues(0, 0, 1); + vec3.cross(this.right, this.forward, worldUp); + vec3.normalize(this.right, this.right); + + // Camera up is perpendicular to forward and right + vec3.cross(this.up, this.right, this.forward); + vec3.normalize(this.up, this.up); + + // Update target + vec3.add(this.target, this.pos, this.forward); } getViewMatrix(): mat4 { @@ -64,4 +89,9 @@ export class Camera { mat4.lookAt(ret, this.pos, this.target, this.up); return ret; } + + /** Get view direction for LOD calculations */ + getViewDirection(): vec3 { + return vec3.clone(this.forward); + } } diff --git a/src/OceanLOD.ts b/src/OceanLOD.ts index ad2f579..a6118e6 100644 --- a/src/OceanLOD.ts +++ b/src/OceanLOD.ts @@ -13,14 +13,16 @@ export class OceanLOD { }> = []; private readonly LOD_LEVELS = [ - { distance: 2.0, gridSize: 128 }, // Closest - highest detail - { distance: 5.0, gridSize: 64 }, // Medium distance - { distance: 10.0, gridSize: 32 }, // Far distance - { distance: 20.0, gridSize: 16 }, // Very far - lowest detail + { distance: 5.0, gridSize: 128 }, // Closest - highest detail + { distance: 15.0, gridSize: 64 }, // Medium distance + { distance: 40.0, gridSize: 32 }, // Far distance + { distance: 80.0, gridSize: 16 }, // Very far + { distance: 150.0, gridSize: 8 }, // Horizon - minimal detail + { distance: Infinity, gridSize: 4 },// Beyond horizon - lowest detail ]; - private readonly PATCH_SIZE = 2.0; // World size of each patch - private readonly PATCHES_PER_SIDE = 7; // 7x7 = 49 patches total + private readonly PATCH_SIZE = 8.0; // World size of each patch (larger for horizon coverage) + private readonly PATCHES_PER_SIDE = 51; // 51x51 = 2601 patches (covers ~200 units in each direction) private readonly VIEW_CONE_COS = Math.cos(Math.PI * 0.45); // ~81 degree half-angle (wider than typical FOV) constructor() { @@ -37,7 +39,7 @@ export class OceanLOD { // Start with lowest detail - will be updated based on camera const grid = new Grid( - this.LOD_LEVELS[3].gridSize, + this.LOD_LEVELS[5].gridSize, centerX, centerY, this.PATCH_SIZE @@ -48,7 +50,7 @@ export class OceanLOD { centerX, centerY, size: this.PATCH_SIZE, - lodLevel: 3, + lodLevel: 5, visible: true }); } @@ -86,11 +88,11 @@ export class OceanLOD { patch.visible = isInFront; // Calculate LOD level - let newLodLevel = 3; // Default to lowest detail + let newLodLevel = 5; // Default to lowest detail if (!isInFront) { // Behind camera - skip (will not be drawn) - newLodLevel = 3; + newLodLevel = 5; } else if (isInViewCone) { // In view cone - use distance-based LOD for (let i = 0; i < this.LOD_LEVELS.length; i++) { @@ -103,7 +105,7 @@ export class OceanLOD { // In front but outside view cone - reduce detail by 1-2 levels for (let i = 0; i < this.LOD_LEVELS.length; i++) { if (distance < this.LOD_LEVELS[i].distance) { - newLodLevel = Math.min(i + 2, 3); // Reduce detail + newLodLevel = Math.min(i + 2, 5); // Reduce detail break; } } @@ -151,7 +153,7 @@ export class OceanLOD { /** Get statistics about current LOD distribution */ getLODStats(): { [key: number]: number } { - const stats: { [key: number]: number } = { 0: 0, 1: 0, 2: 0, 3: 0 }; + const stats: { [key: number]: number } = { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 }; for (const patch of this.grids) { stats[patch.lodLevel]++; } diff --git a/src/main.ts b/src/main.ts index 7526cc6..bc5e325 100644 --- a/src/main.ts +++ b/src/main.ts @@ -138,17 +138,15 @@ var lodStatsTimer = 0; /** 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: Camera; var oceanLOD: OceanLOD; var skybox: Skybox; -var curRotX = Config.CAMERA_DEFAULT_ROT_X; -var curRotY = Config.CAMERA_DEFAULT_ROT_Y; var wireframeMode = false; +/** Camera movement speed */ +var moveSpeed = 0.15; +var fastMoveSpeed = 0.4; /** Ocean shader settings */ var waveHeight = 1.0; var waveSpeed = 1.0; @@ -190,9 +188,16 @@ function drawScene() { var projection = mat4.create(); mat4.perspective(projection, Config.FOV, viewportWidth / viewportHeight, Config.NEAR_PLANE, Config.FAR_PLANE); - camera.setOffset(Config.CAMERA_DEFAULT_OFFSET + keyboardZoom); - camera.setRotationX((curRotX += mouseYVel * Config.MOUSE_SENSITIVITY + keyboardRotationX)); - camera.setRotationY((curRotY += mouseXVel * Config.MOUSE_SENSITIVITY + keyboardRotationY)); + // Handle FPS camera movement + handleCameraMovement(); + + // Apply mouse rotation + if (mouseXVel !== 0 || mouseYVel !== 0) { + camera.rotate(mouseXVel, mouseYVel); + mouseXVel = 0; + mouseYVel = 0; + } + var view = camera.getViewMatrix(); // Draw skybox first with depth test disabled (always behind everything) @@ -245,28 +250,38 @@ function drawScene() { requestAnimationFrame(drawScene); } -/** Handle keyboard input for camera controls */ -function handleKeyboardInput() { - keyboardRotationX = 0; - keyboardRotationY = 0; +/** Handle FPS camera movement */ +function handleCameraMovement() { + const speed = keysPressed.has('Shift') ? fastMoveSpeed : moveSpeed; - if (keysPressed.has('w') || keysPressed.has('W') || keysPressed.has('ArrowUp')) { - keyboardRotationX = Config.KEYBOARD_ROTATION_SPEED; + // WASD for horizontal movement + if (keysPressed.has('w') || keysPressed.has('W')) { + camera.moveForward(speed); } - if (keysPressed.has('s') || keysPressed.has('S') || keysPressed.has('ArrowDown')) { - keyboardRotationX = -Config.KEYBOARD_ROTATION_SPEED; + if (keysPressed.has('s') || keysPressed.has('S')) { + camera.moveForward(-speed); } - if (keysPressed.has('a') || keysPressed.has('A') || keysPressed.has('ArrowLeft')) { - keyboardRotationY = Config.KEYBOARD_ROTATION_SPEED; + if (keysPressed.has('a') || keysPressed.has('A')) { + camera.moveRight(-speed); } - if (keysPressed.has('d') || keysPressed.has('D') || keysPressed.has('ArrowRight')) { - keyboardRotationY = -Config.KEYBOARD_ROTATION_SPEED; + if (keysPressed.has('d') || keysPressed.has('D')) { + camera.moveRight(speed); } - if (keysPressed.has('q') || keysPressed.has('Q') || keysPressed.has('+')) { - keyboardZoom -= Config.KEYBOARD_ZOOM_SPEED; + + // Q/E for vertical movement + if (keysPressed.has('q') || keysPressed.has('Q')) { + camera.moveUp(-speed); } - if (keysPressed.has('e') || keysPressed.has('E') || keysPressed.has('-')) { - keyboardZoom += Config.KEYBOARD_ZOOM_SPEED; + if (keysPressed.has('e') || keysPressed.has('E')) { + camera.moveUp(speed); + } + + // Space to go up, Ctrl to go down + if (keysPressed.has(' ')) { + camera.moveUp(speed); + } + if (keysPressed.has('Control')) { + camera.moveUp(-speed); } } @@ -313,19 +328,20 @@ function main() { // Keyboard controls window.addEventListener('keydown', (evt) => { keysPressed.add(evt.key); - handleKeyboardInput(); // Reset camera on 'R' key if (evt.key === 'r' || evt.key === 'R') { - curRotX = Config.CAMERA_DEFAULT_ROT_X; - curRotY = Config.CAMERA_DEFAULT_ROT_Y; - keyboardZoom = 0; + camera = new Camera(); // Reset to initial position + } + + // Prevent default for space to avoid page scroll + if (evt.key === ' ') { + evt.preventDefault(); } }); window.addEventListener('keyup', (evt) => { keysPressed.delete(evt.key); - handleKeyboardInput(); }); // Window resize handler