diff --git a/index.html b/index.html index 0fb8b9c..f2bcd52 100644 --- a/index.html +++ b/index.html @@ -190,77 +190,43 @@ precision mediump float; varying vec3 v_fragPos; - varying vec2 v_uv; + varying vec3 v_normal; uniform vec3 eyePos; - uniform sampler2D displace_map; - vec3 lightPos = vec3(0.,0.,10.); //not used in diffuse. diffuse uses a directional light. It is only used for specular glittering. - vec3 lightColor = vec3(1.0,1.0,1.0); + vec3 lightColor = vec3(1.0, 1.0, 1.0); - //using forward difference - //Normal vectors are compute as: https://www.scratchapixel.com/lessons/procedural-generation-virtual-worlds/perlin-noise-part-2/perlin-noise-computing-derivatives - void main(void) { - vec4 displace = texture2D(displace_map, v_uv); - //calculate normal - float gridPointDelta = (1. / 256.); - vec3 currPoint = vec3(0.0,0.0,displace.x); - vec3 right = vec3(gridPointDelta,0.0,texture2D(displace_map,vec2(v_uv.x + gridPointDelta,v_uv.y)).x*(1./1.)); - vec3 left = vec3(-gridPointDelta,0.0,texture2D(displace_map,vec2(v_uv.x - gridPointDelta,v_uv.y)).x*(1./1.)); - vec3 up = vec3(0.,gridPointDelta,texture2D(displace_map,vec2(v_uv.x ,v_uv.y + gridPointDelta)).x*(1./1.)); - vec3 down = vec3(0.,-gridPointDelta,texture2D(displace_map,vec2(v_uv.x ,v_uv.y - gridPointDelta)).x*(1./1.)); + vec3 norm = normalize(v_normal); + vec3 lightDir = normalize(vec3(0.3, 0.5, 1.0)); // Sun direction - //vec3 tangent = normalize(right - currPoint); - //vec3 biTangent = normalize(up - currPoint); - vec3 tangent = normalize(vec3(gridPointDelta,0.,right.z-left.z)); - vec3 biTangent = normalize(vec3(0.,gridPointDelta,down.z-up.z)); - //vec3 normal = biTangent; - vec3 normal = cross(tangent, biTangent); - - vec3 norm = normalize(normal); - norm.y *= -1.; //Normal y direction is somehow inverted - //vec3 lightDir = normalize(lightPos - v_fragPos); - vec3 lightDir = normalize(-vec3(0.0,.0,-1.)); //sun shines in drection of -z - - float diff = max(dot(norm,lightDir),0.0); + float diff = max(dot(norm, lightDir), 0.0); vec3 diffuse = diff * lightColor; - vec3 result = (diffuse) * vec3(0.0,0.0,1.0); - //Old lightning - vec3 toCameraVector = normalize(v_fragPos - eyePos); - vec3 reflec = normalize(reflect(toCameraVector, norm)); + // View direction + vec3 toCameraVector = normalize(eyePos - v_fragPos); + vec3 reflec = normalize(reflect(-toCameraVector, norm)); - //Schlicks approximation to Fresnelfactor - float n1 = 1., n2 = 1.33333; - float R0 = pow((n1-n2)/(n1+n2), 2.); - float fresnel = R0 + (1. - R0)*pow((1.-dot(norm,reflec)),5.) ; + // Schlick's approximation to Fresnel factor + float n1 = 1.0, n2 = 1.33333; + float R0 = pow((n1 - n2) / (n1 + n2), 2.0); + float fresnel = R0 + (1.0 - R0) * pow(1.0 - max(dot(norm, toCameraVector), 0.0), 5.0); - //vec3 waterColor = vec3(34./255.,154./255.,211./255.); - vec3 oceanColor = vec3(0,.4,.4); // under-sea colour - vec3 skyColor = vec3(1.,1.,1.); + vec3 oceanColor = vec3(0.0, 0.3, 0.4); + vec3 skyColor = vec3(0.6, 0.8, 1.0); - //Subsurface scattering - vec3 sssSun = vec3(0.,-5.,-7.0); - vec3 tosssSunVec = normalize(sssSun - v_fragPos); - vec3 tosssSun = normalize(vec3(0.0,-100.,1.)); - float ssDistortion = 0.1; - float sssIntensity = 1.; - vec3 halfWay = normalize(tosssSun+norm*ssDistortion); - float ssScateringCoef = pow(clamp(dot(toCameraVector,-halfWay),0.0,1.0),5.) * sssIntensity; - //Sun glittering - float glitterFactor = max(0.0,dot(tosssSunVec,reflect(-toCameraVector,norm))); - if(!(glitterFactor > 0.98)) { - glitterFactor = 0.0; - } + // Specular highlights + vec3 halfwayDir = normalize(lightDir + toCameraVector); + float spec = pow(max(dot(norm, halfwayDir), 0.0), 256.0); + vec3 specular = spec * lightColor * 0.8; - //gl_FragColor = vec4(oceanColor + lightColor * glitterFactor,1.0); - //gl_FragColor=vec4(clamp(oceanColor + (oceanColor*ssScateringCoef),0.,1.0),1.0); //Display subsurfacecatterting component - //gl_FragColor = vec4((mix(oceanColor,skyColor,fresnel).xyz), 1.); //Just display reflection component - //gl_FragColor = vec4(diffuse * oceanColor,1.0); //Render only diffuse component - //gl_FragColor = vec4(normal,1.0); //show Normal map - //gl_FragColor = vec4(displace.x,displace.x,displace.x,1.0); //Show Perlin Noise texture deactivate vertex distrotion before - gl_FragColor = vec4((clamp(diffuse,0.97,1.0) * (mix(oceanColor + (oceanColor*ssScateringCoef),skyColor*0.8,fresnel).xyz))+ lightColor * glitterFactor, 1.0); //All combined + // Subsurface scattering approximation + float sss = pow(max(dot(toCameraVector, -lightDir), 0.0), 4.0) * 0.3; + vec3 sssColor = vec3(0.0, 0.5, 0.5) * sss; + + vec3 finalColor = mix(oceanColor, skyColor, fresnel) * clamp(diffuse, 0.4, 1.0) + specular + sssColor; + + gl_FragColor = vec4(finalColor, 1.0); } diff --git a/src/Grid.ts b/src/Grid.ts index 78a4790..b9a2b2a 100644 --- a/src/Grid.ts +++ b/src/Grid.ts @@ -1,23 +1,34 @@ /** Grid for the water surface */ export class Grid { private indices: number[] = []; + private lineIndices: number[] = []; private vertices: number[] = []; private vao: WebGLVertexArrayObject | null = null; + private lineVao: WebGLVertexArrayObject | null = null; private size: number; + private offsetX: number; + private offsetY: number; + private scale: number; - constructor(size: number = 128) { + constructor(size: number = 128, offsetX: number = 0, offsetY: number = 0, scale: number = 1) { this.size = size; + this.offsetX = offsetX; + this.offsetY = offsetY; + this.scale = scale; } generate(): void { this.indices = []; + this.lineIndices = []; this.vertices = []; for (let j = 0; j <= this.size; ++j) { for (let i = 0; i <= this.size; ++i) { - // Generate Vertices - const x = i / this.size; - const y = j / this.size; + // Generate Vertices normalized to 0-1, then scale and offset + const u = i / this.size; + const v = j / this.size; + const x = (u - 0.5) * this.scale + this.offsetX; + const y = (v - 0.5) * this.scale + this.offsetY; const z = 0; this.vertices.push(x, y, z); @@ -35,6 +46,16 @@ export class Grid { this.indices.push(row2 + i + 1); this.indices.push(row2 + i); } + + // Generate line indices for wireframe + if (i < this.size) { + const currentVertex = j * (this.size + 1) + i; + this.lineIndices.push(currentVertex, currentVertex + 1); + } + if (j < this.size) { + const currentVertex = j * (this.size + 1) + i; + this.lineIndices.push(currentVertex, currentVertex + (this.size + 1)); + } } } } @@ -42,6 +63,7 @@ export class Grid { initVAO(gl: WebGL2RenderingContext): void { this.generate(); + // Create VAO for filled triangles this.vao = gl.createVertexArray(); gl.bindVertexArray(this.vao); @@ -56,10 +78,28 @@ export class Grid { gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 3 * Float32Array.BYTES_PER_ELEMENT, 0); gl.enableVertexAttribArray(0); gl.bindVertexArray(null); + + // Create VAO for wireframe lines + this.lineVao = gl.createVertexArray(); + gl.bindVertexArray(this.lineVao); + + gl.bindBuffer(gl.ARRAY_BUFFER, vboGrid); + + const iboLine: WebGLBuffer | null = gl.createBuffer(); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, iboLine); + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint32Array(this.lineIndices), gl.STATIC_DRAW); + + gl.vertexAttribPointer(0, 3, gl.FLOAT, false, 3 * Float32Array.BYTES_PER_ELEMENT, 0); + gl.enableVertexAttribArray(0); + gl.bindVertexArray(null); } - draw(gl: WebGL2RenderingContext): void { - if (this.vao) { + draw(gl: WebGL2RenderingContext, wireframe: boolean = false): void { + if (wireframe && this.lineVao) { + gl.bindVertexArray(this.lineVao); + gl.drawElements(gl.LINES, this.lineIndices.length, gl.UNSIGNED_INT, 0); + gl.bindVertexArray(null); + } else if (this.vao) { gl.bindVertexArray(this.vao); gl.drawElements(gl.TRIANGLES, this.indices.length, gl.UNSIGNED_INT, 0); gl.bindVertexArray(null); diff --git a/src/OceanLOD.ts b/src/OceanLOD.ts new file mode 100644 index 0000000..7c736cb --- /dev/null +++ b/src/OceanLOD.ts @@ -0,0 +1,118 @@ +import { Grid } from './Grid'; +import { vec3 } from 'gl-matrix'; + +/** Manages multiple ocean grid patches with LOD based on camera distance */ +export class OceanLOD { + private grids: Array<{ + grid: Grid; + centerX: number; + centerY: number; + size: number; + lodLevel: number; + }> = []; + + 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 + ]; + + private readonly PATCH_SIZE = 2.0; // World size of each patch + private readonly PATCHES_PER_SIDE = 7; // 7x7 = 49 patches total + + constructor() { + this.createGridPatches(); + } + + private createGridPatches(): void { + const halfPatches = Math.floor(this.PATCHES_PER_SIDE / 2); + + for (let y = -halfPatches; y <= halfPatches; y++) { + for (let x = -halfPatches; x <= halfPatches; x++) { + const centerX = x * this.PATCH_SIZE; + const centerY = y * this.PATCH_SIZE; + + // Start with lowest detail - will be updated based on camera + const grid = new Grid( + this.LOD_LEVELS[3].gridSize, + centerX, + centerY, + this.PATCH_SIZE + ); + + this.grids.push({ + grid, + centerX, + centerY, + size: this.PATCH_SIZE, + lodLevel: 3 + }); + } + } + } + + /** Update LOD based on camera position */ + updateLOD(gl: WebGL2RenderingContext, cameraPos: vec3): void { + for (const patch of this.grids) { + // Calculate distance from camera to patch center + const dx = patch.centerX - cameraPos[0]; + const dy = patch.centerY - cameraPos[1]; + const distance = Math.sqrt(dx * dx + dy * dy); + + // Determine appropriate LOD level + let newLodLevel = 3; // Default to lowest detail + for (let i = 0; i < this.LOD_LEVELS.length; i++) { + if (distance < this.LOD_LEVELS[i].distance) { + newLodLevel = i; + break; + } + } + + // Only recreate grid if LOD level changed + if (newLodLevel !== patch.lodLevel) { + patch.lodLevel = newLodLevel; + patch.grid = new Grid( + this.LOD_LEVELS[newLodLevel].gridSize, + patch.centerX, + patch.centerY, + patch.size + ); + patch.grid.initVAO(gl); + } + } + } + + initVAO(gl: WebGL2RenderingContext): void { + for (const { grid } of this.grids) { + grid.initVAO(gl); + } + } + + draw(gl: WebGL2RenderingContext, wireframe: boolean = false): void { + for (const { grid } of this.grids) { + grid.draw(gl, wireframe); + } + } + + getGridCount(): number { + return this.grids.length; + } + + getTotalVertexCount(): number { + let total = 0; + for (const { grid } of this.grids) { + total += grid.getIndexCount() / 3; + } + return total; + } + + /** Get statistics about current LOD distribution */ + getLODStats(): { [key: number]: number } { + const stats: { [key: number]: number } = { 0: 0, 1: 0, 2: 0, 3: 0 }; + for (const patch of this.grids) { + stats[patch.lodLevel]++; + } + return stats; + } +} diff --git a/src/constants.ts b/src/constants.ts index 47c6364..089ccd9 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,7 +1,7 @@ // Configuration Constants export const GRID_SIZE = 128; -export const NOISE_TEXTURE_WIDTH = 256; -export const NOISE_TEXTURE_HEIGHT = 256; +export const NOISE_TEXTURE_WIDTH = 1024; +export const NOISE_TEXTURE_HEIGHT = 1024; export const CANVAS_WIDTH = 800; export const CANVAS_HEIGHT = 600; export const FOV = 1.0; diff --git a/src/main.ts b/src/main.ts index 7fe2c34..ab276e8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,6 @@ import { vec3, mat4 } from 'gl-matrix'; import { Camera } from './Camera'; -import { Grid } from './Grid'; +import { OceanLOD } from './OceanLOD'; import { createProgram } from './Shader'; import * as Config from './constants'; @@ -112,8 +112,8 @@ function initFBO() { // 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.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, textureFBO, 0); @@ -131,6 +131,7 @@ var lastTime = new Date().getTime(); var counter = 0.0; var fps = 0; var fpsDisplay: HTMLElement | null = null; +var lodStatsTimer = 0; /** Input states*/ var mouseXVel = 0; var mouseYVel = 0; @@ -140,14 +141,17 @@ var keyboardZoom = 0; var keysPressed: Set = new Set(); /** Objects and states*/ var camera: Camera; -var oceanGrid: Grid; +var oceanLOD: OceanLOD; var curRotX = Config.CAMERA_DEFAULT_ROT_X; var curRotY = Config.CAMERA_DEFAULT_ROT_Y; +var wireframeMode = false; function drawScene() { fps++; let now = new Date(); let delta = now.getTime() - lastTime; timeSpent += delta; + lodStatsTimer += delta; + if ((counter += delta) >= Config.FPS_UPDATE_INTERVAL) { counter = 0; if (fpsDisplay) { @@ -155,62 +159,38 @@ function drawScene() { } 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 - - //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 + + // Log LOD stats every 5 seconds + if (lodStatsTimer >= 5000) { + lodStatsTimer = 0; + const stats = oceanLOD.getLODStats(); + console.log(`LOD Stats - High:${stats[0]} Med:${stats[1]} Low:${stats[2]} VeryLow:${stats[3]}`); } + lastTime = now.getTime(); + // Single render pass with Gerstner waves computed in vertex shader - //--- Second render pass -> Geomtry with displacement by perlin noise texture --- + //--- Render pass -> Ocean with Gerstner wave displacement --- { 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 + 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)); var view = camera.getViewMatrix(); + // Update LOD based on camera position + oceanLOD.updateLOD(gl, camera.pos); + 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. + // No centering needed - grids are already positioned correctly in world space gl.useProgram(defaultProgram); let view_loc = gl.getUniformLocation(defaultProgram, "view"); @@ -221,11 +201,9 @@ function drawScene() { 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); + let uTime_loc = gl.getUniformLocation(defaultProgram, "uTime"); + gl.uniform1f(uTime_loc, timeSpent); + oceanLOD.draw(gl, wireframeMode); } requestAnimationFrame(drawScene); } @@ -317,13 +295,24 @@ function main() { window.addEventListener('resize', () => { updateCanvasSize(canvas); }); + + // Wireframe toggle handler + window.addEventListener('toggleWireframe', () => { + wireframeMode = !wireframeMode; + const wireframeBtn = document.getElementById('wireframe-toggle'); + if (wireframeBtn) { + wireframeBtn.textContent = `Wireframe: ${wireframeMode ? 'ON' : 'OFF'}`; + } + console.log(`Wireframe mode: ${wireframeMode ? 'ON' : 'OFF'}`); + }); initShaders(); initGeometry(); initFBO(); - oceanGrid = new Grid(Config.GRID_SIZE); - oceanGrid.initVAO(gl); + oceanLOD = new OceanLOD(); + oceanLOD.initVAO(gl); + console.log(`Ocean LOD initialized with ${oceanLOD.getGridCount()} patches`); camera = new Camera(); //Check if any errors apeared during init.