diff --git a/src/engine/components/terrain/heightmap.js b/src/engine/components/terrain/heightmap.js index 345fdd3..f3eba22 100644 --- a/src/engine/components/terrain/heightmap.js +++ b/src/engine/components/terrain/heightmap.js @@ -40,6 +40,20 @@ class HeightMap { vec3.normalize(normal, normal) return normal } + + // Vertex position based getters with LOD subnode support + + getHeightPlaneMesh (mesh, vx, vz, absolute) { + let pos = mesh.pos + if (absolute) pos = mesh.absolutePosition + return this.getHeight(pos[0] + vx, pos[2] + vz) + } + + getNormalPlaneMesh (mesh, vx, vz, absolute) { + let pos = mesh.pos + if (absolute) pos = mesh.absolutePosition + return this.getNormal(pos[0] + vx, pos[2] + vz) + } } class SimplexHeightMap extends HeightMap { diff --git a/src/engine/components/terrain/lod.js b/src/engine/components/terrain/lod.js index 4796871..0d13cfd 100644 --- a/src/engine/components/terrain/lod.js +++ b/src/engine/components/terrain/lod.js @@ -4,6 +4,8 @@ import { Mesh } from '../../mesh' import { vec3 } from 'gl-matrix' import { addv3 } from '../../utility' +const LOD_SEPARATOR = 16 + class TerrainNode extends Node { constructor (root, pos, level) { super(pos) @@ -14,18 +16,20 @@ class TerrainNode extends Node { this.mesh = null } - createMesh (gl) { + updateLODMesh (gl) { + // If this mesh has children (we're a branch), generate their meshes instead if (this.children.length) { let generated = 0 for (let i in this.children) { if (generated >= this.root.genPerTick) break let child = this.children[i] if (!(child instanceof TerrainNode)) continue - generated += this.children[i].createMesh(gl) + generated += this.children[i].updateLODMesh(gl) } return generated } + // If we already have a mesh, we can skip if (this.mesh) return 0 let VERTICES = this.root.resolution @@ -37,18 +41,22 @@ class TerrainNode extends Node { let vertexPointer = 0 let divisionLevel = Math.pow(2, this.level) + // Create vertices dynamically for (let i = 0; i < VERTICES; i++) { for (let j = 0; j < VERTICES; j++) { let vertDivj = j / (VERTICES - 1) let vertDivi = i / (VERTICES - 1) + // Calculate relative resolution let pj = vertDivj * this.root.width / divisionLevel let pi = vertDivi * this.root.height / divisionLevel + // Generator takes meshes' absolute vertex position in the world when using height values + vertices[vertexPointer * 3] = pj - vertices[vertexPointer * 3 + 1] = this.root.generator.getHeight(this.absolutePosition[0] + pj, this.absolutePosition[2] + pi) + vertices[vertexPointer * 3 + 1] = this.root.generator.getHeightPlaneMesh(this, pj, pi, true) vertices[vertexPointer * 3 + 2] = pi - let normal = this.root.generator.getNormal(this.absolutePosition[0] + pj, this.absolutePosition[2] + pi) + let normal = this.root.generator.getNormalPlaneMesh(this, pj, pi, true) normals[vertexPointer * 3] = normal[0] normals[vertexPointer * 3 + 1] = normal[1] normals[vertexPointer * 3 + 2] = normal[2] @@ -58,6 +66,7 @@ class TerrainNode extends Node { } } + // Create indices dynamically let pointer = 0 for (let gz = 0; gz < VERTICES - 1; gz++) { for (let gx = 0; gx < VERTICES - 1; gx++) { @@ -78,14 +87,16 @@ class TerrainNode extends Node { this.mesh.material = this.root.material this.bounds = BoundingBox.fromMesh(this.mesh) + // Reduces flickering when subdividing if (this.parent && this.parent.mesh) { - this.parent.dispose() + this.parent.dispose(gl) } return 1 } draw (gl, shader) { + // Draw child nodes super.draw(gl, shader) if (!this.mesh) return // Set model transform matrix uniform @@ -100,51 +111,69 @@ class TerrainNode extends Node { return this.mesh != null } - dispose () { + dispose (gl) { + if (!this.mesh) return + this.mesh.dispose(gl) this.mesh = null } - merge () { + merge (gl) { + // Can't merge if we have nothing to merge! if (this.children.length === 0) return + + // Merge children and dispose their meshes for (let i in this.children) { - this.children[i].merge() - this.children[i].dispose() + this.children[i].merge(gl) + this.children[i].dispose(gl) } + // Delete all children, (hopefully) they will be garbage collected this.children = [] } subdivide () { + // Do not divide when we're already at the limit if (this.level === this.root.maxDetail) return let lv = this.level + 1 let stepLeft = this.root.width / Math.pow(2, lv) let stepForward = this.root.height / Math.pow(2, lv) + // Child nodes take relative positions because they are parented this.addChild(new TerrainNode(this.root, [0, 0, 0], lv)) this.addChild(new TerrainNode(this.root, [stepLeft, 0, 0], lv)) this.addChild(new TerrainNode(this.root, [0, 0, stepForward], lv)) this.addChild(new TerrainNode(this.root, [stepLeft, 0, stepForward], lv)) } - update (camera) { + // Divide and merge meshes depending on camera position and lodDistance + update (gl, camera) { if (!this.bounds) return false + // Get camera distance from the center of this mesh + // TODO: use edges let distCamera = vec3.distance(camera.pos, addv3(this.bounds.center, this.absolutePosition)) - let lodDistance = this.root.lodDistance - (Math.pow(2, this.level) * 16) + + // Get LOD change distance based on current division level and a fixed distance + let lodDistance = this.root.lodDistance - (Math.pow(2, this.level) * LOD_SEPARATOR) + + // If this node has children, either merge if too far away or update the children if (this.children.length) { - if (distCamera > lodDistance + 16 && this.level >= 1) { - this.merge() + // Merge the node if the node is further than lodDistance, plus a set offset + if (distCamera > lodDistance + LOD_SEPARATOR && this.level >= 1) { + this.merge(gl) return true } + // Update the children let acted = false for (let i in this.children) { let child = this.children[i] - acted = child.update(camera) + acted = child.update(gl, camera) } return acted } + // If the camera is close enough to this node and we still have room to divide, divide! if (distCamera < lodDistance && this.level < this.root.maxDetail) { this.subdivide() @@ -162,11 +191,13 @@ class LODTerrain extends Node { this.width = sWidth this.height = sHeight + // LOD parameters this.genPerTick = genPerTick this.lodDistance = lodDistance this.maxDetail = maxDetail this.resolution = resolution + // Create four initial nodes this.addChild(new TerrainNode(this, pos, 1)) this.addChild(new TerrainNode(this, [pos[0] + sWidth / 2, pos[1], pos[2]], 1)) this.addChild(new TerrainNode(this, [pos[0], pos[1], pos[2] + sHeight / 2], 1)) @@ -181,24 +212,27 @@ class LODTerrain extends Node { this.material = mat } - createMesh (gl) { - // Ensure only one mesh is generated every tick + // Update terrain meshes + updateLODMesh (gl) { + // Ensure only genPerTick mesh(es) is/are generated every tick let generated = 0 for (let i in this.children) { if (generated >= this.genPerTick) break let child = this.children[i] if (!(child instanceof TerrainNode)) continue - generated += this.children[i].createMesh(gl) + generated += this.children[i].updateLODMesh(gl) } return generated } - update (camera) { + // Divide and merge meshes depending on camera position and lodDistance + update (gl, camera) { + // Ensure only one mesh update per tick let acted = false for (let i in this.children) { if (acted) break let child = this.children[i] - acted = child.update(camera) + acted = child.update(gl, camera) } return acted } diff --git a/src/engine/index.js b/src/engine/index.js index 1729b51..c49c14a 100644 --- a/src/engine/index.js +++ b/src/engine/index.js @@ -3,6 +3,8 @@ import Screen from './screen' import Input from './input' import { ShaderManager } from './shader' +let gl + class Engine { constructor () { this.screen = new Screen() @@ -18,14 +20,22 @@ class Engine { this.frameCount = 0 this.fps = 0 - window.gl = this.screen.gl + gl = this.screen.gl + } + + static get GL () { + return gl + } + + static get gl () { + return gl } get gl () { return this.screen.gl } - render (gl) { + render () { // Set clear color to black, fully opaque gl.clearColor(0.0, 0.7, 1.0, 1.0) diff --git a/src/index.js b/src/index.js index 818f804..36cedf0 100644 --- a/src/index.js +++ b/src/index.js @@ -54,9 +54,9 @@ async function pipeline () { cam.processMouseMove(game.input.mouseOffset) } - // Update LOD meshes - terrain.update(cam) - terrain.createMesh(game.gl) + // Update detail levels + terrain.update(game.gl, cam) + terrain.updateLODMesh(game.gl) // TESTING: Move model forward // t = t + 0.1