201 lines
7.7 KiB
TypeScript
201 lines
7.7 KiB
TypeScript
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;
|
|
}
|
|
}
|