import { Grid } from './Grid'; import { vec3 } from 'gl-matrix'; /** Manages multiple ocean grid patches with LOD based on camera distance and view cone */ export class OceanLOD { private grids: Array<{ grid: Grid; centerX: number; centerY: number; size: number; lodLevel: number; visible: boolean; }> = []; private readonly LOD_LEVELS = [ { distance: 3.0, gridSize: 256 }, // Very close - ultra detail { distance: 8.0, gridSize: 128 }, // Close - high detail { distance: 20.0, gridSize: 64 }, // Medium distance { distance: 40.0, gridSize: 16 }, // Far - low detail { distance: 80.0, gridSize: 8 }, // Very far - minimal { distance: Infinity, gridSize: 4 },// Horizon - lowest (will be stretched anyway) ]; private readonly PATCH_SIZE = 10.0; // Larger patches = fewer needed private readonly PATCHES_PER_SIDE = 21; // 21x21 = 441 patches (covers ~200 units) private readonly VIEW_CONE_COS = Math.cos(Math.PI * 0.45); // ~81 degree half-angle (wider than typical FOV) // Track the grid origin to re-center when camera moves private gridOriginX: number = 0; private gridOriginY: number = 0; 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 + this.gridOriginX; const centerY = y * this.PATCH_SIZE + this.gridOriginY; // Start with lowest detail - will be updated based on camera const grid = new Grid( this.LOD_LEVELS[5].gridSize, centerX, centerY, this.PATCH_SIZE ); this.grids.push({ grid, centerX, centerY, size: this.PATCH_SIZE, lodLevel: 5, visible: true }); } } } /** Re-center the grid around a new origin */ private recenterGrid(gl: WebGL2RenderingContext, newOriginX: number, newOriginY: number): void { this.gridOriginX = newOriginX; this.gridOriginY = newOriginY; const halfPatches = Math.floor(this.PATCHES_PER_SIDE / 2); let i = 0; for (let y = -halfPatches; y <= halfPatches; y++) { for (let x = -halfPatches; x <= halfPatches; x++) { const patch = this.grids[i]; const newCenterX = x * this.PATCH_SIZE + this.gridOriginX; const newCenterY = y * this.PATCH_SIZE + this.gridOriginY; // Only update if patch position changed if (patch.centerX !== newCenterX || patch.centerY !== newCenterY) { patch.centerX = newCenterX; patch.centerY = newCenterY; // Force LOD recalculation patch.lodLevel = -1; } i++; } } } /** Update LOD based on camera position and view direction */ updateLOD(gl: WebGL2RenderingContext, cameraPos: vec3, cameraTarget: vec3): void { // Calculate view direction (normalized) const viewDir = vec3.create(); vec3.subtract(viewDir, cameraTarget, cameraPos); vec3.normalize(viewDir, viewDir); // Check if we need to recenter the grid (camera moved more than one patch size from origin) const cameraGridX = Math.floor(cameraPos[0] / this.PATCH_SIZE) * this.PATCH_SIZE; const cameraGridY = Math.floor(cameraPos[1] / this.PATCH_SIZE) * this.PATCH_SIZE; if (cameraGridX !== this.gridOriginX || cameraGridY !== this.gridOriginY) { this.recenterGrid(gl, cameraGridX, cameraGridY); } for (const patch of this.grids) { // Calculate vector from camera to patch center (on XY plane, Z=0 for ocean surface) const toPatch = vec3.fromValues( patch.centerX - cameraPos[0], patch.centerY - cameraPos[1], 0 - cameraPos[2] // Ocean is at Z=0 ); const distance = vec3.length(toPatch); // Normalize direction to patch const toPatchDir = vec3.create(); vec3.normalize(toPatchDir, toPatch); // Calculate dot product with view direction (how aligned is patch with where we're looking) const dotProduct = vec3.dot(viewDir, toPatchDir); // Determine if patch is in front of camera and within view cone const isInFront = dotProduct > -0.3; // Slightly behind is ok for edge cases const isInViewCone = dotProduct > this.VIEW_CONE_COS; // Frustum culling - don't draw patches behind camera patch.visible = isInFront; // Calculate LOD level let newLodLevel = 5; // Default to lowest detail if (!isInFront) { // Behind camera - skip (will not be drawn) newLodLevel = 5; } else if (isInViewCone) { // In view cone - use distance-based LOD for (let i = 0; i < this.LOD_LEVELS.length; i++) { if (distance < this.LOD_LEVELS[i].distance) { newLodLevel = i; break; } } } else { // 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, 5); // Reduce detail 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 patch of this.grids) { if (patch.visible) { patch.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, 4: 0, 5: 0 }; for (const patch of this.grids) { stats[patch.lodLevel]++; } return stats; } }