import { Mesh } from '../../mesh' import { BoundingBox } from '../../mesh/aabb' import { mat4, vec3 } from 'gl-matrix' import { subv3, mulv3, addv3, divv3, normalv3, crossv3 } from '../../utility' import Screen from '../../screen' const lodMax = 8 class PlanetGenerator { constructor (radius, noise, waterHeight = 0.3) { this.radius = radius this.noise = noise this.moisture = Object.assign({ seed: noise.seed / 2 }, noise) } getHeight (detail, normal) { return this.noise.getNoise3D(detail, normal[0], normal[1], normal[2]) } getBiome (height, normal) { // const moistureAtPoint = this.moisture.getNoise3D(5, normal) // 0 - hot, 1 - cold const equatorTemp = 1 // 0 - hot, 1 - cold const poleTemp = 0.4 // TODO: pole settings for planet const distanceFromPoles = Math.abs(vec3.dot(normal, [0.0, 1.0, 0.0])) // Calculate temperature for the poles const e = (height * height + poleTemp + (equatorTemp - poleTemp) * distanceFromPoles) if (height < 0.1) return [1 / 255, 56 / 255, 104 / 255] // Water else if (e <= 0.8) return [44 / 255, 50 / 255, 29 / 255] // Grass else if (e < 0.9) return [50 / 255, 50 / 255, 50 / 255] // Stone else return [1, 1, 1] // Snow } } const PlanetIndexBuffers = { generate (sideResolution) { if (sideResolution <= 1 || sideResolution % 2 === 0) { throw new Error('Resolution must be higher than 1 and an odd number.') } PlanetIndexBuffers.resolution = sideResolution PlanetIndexBuffers.base = PlanetIndexBuffers.generateBuffer(sideResolution) PlanetIndexBuffers.fixT = PlanetIndexBuffers.generateBuffer(sideResolution, true, false, false, false) PlanetIndexBuffers.fixTR = PlanetIndexBuffers.generateBuffer(sideResolution, true, true, false, false) PlanetIndexBuffers.fixTL = PlanetIndexBuffers.generateBuffer(sideResolution, true, false, true, false) PlanetIndexBuffers.fixB = PlanetIndexBuffers.generateBuffer(sideResolution, false, false, false, true) PlanetIndexBuffers.fixBR = PlanetIndexBuffers.generateBuffer(sideResolution, false, true, false, true) PlanetIndexBuffers.fixBL = PlanetIndexBuffers.generateBuffer(sideResolution, false, false, true, true) PlanetIndexBuffers.fixR = PlanetIndexBuffers.generateBuffer(sideResolution, false, true, false, false) PlanetIndexBuffers.fixL = PlanetIndexBuffers.generateBuffer(sideResolution, false, false, true, false) }, generateBuffer (sideResolution, fanTop = false, fanRight = false, fanLeft = false, fanBottom = false) { const indices = [] for (let y = 0; y < sideResolution - 1; y++) { let slantLeft = (y % 2) === 0 for (let x = 0; x < sideResolution - 1; x++) { const topLeft = (y * sideResolution) + x const topRight = topLeft + 1 const bottomLeft = ((y + 1) * sideResolution) + x const bottomRight = bottomLeft + 1 let tri1 = slantLeft ? [topLeft, bottomLeft, bottomRight] : [topLeft, bottomLeft, topRight] let tri2 = slantLeft ? [topLeft, bottomRight, topRight] : [bottomLeft, bottomRight, topRight] if (fanTop && y === 0) { if (x % 2 === 0) { tri2 = [topLeft, bottomRight, topRight + 1] } else { tri1 = null } } if (fanRight && x === sideResolution - 2) { if (y % 2 === 0) { tri2 = [topRight, bottomLeft, bottomRight + sideResolution] } else { tri2 = null } } if (fanBottom && y === sideResolution - 2) { if (x % 2 === 0) { tri2 = [bottomLeft, bottomRight + 1, topRight] } else { tri1 = null } } if (fanLeft && x === 0) { if (y % 2 === 0) { tri1 = [topLeft, bottomLeft + sideResolution, bottomRight] } else { tri1 = null } } // faster than concat :p if (tri1) indices.push(tri1[0], tri1[1], tri1[2]) if (tri2) indices.push(tri2[0], tri2[1], tri2[2]) slantLeft = !slantLeft } } return { buffer: Mesh.loadToBuffer(Screen.gl, Screen.gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices)), indices } }, setBuffer (mesh, buffer) { mesh.ebo = buffer.buffer mesh.indices = { length: buffer.indices.length } } } class CubeFace { constructor (index, parent, level, pos, normal, planet) { this.index = index this.parent = parent this.children = [] this.normal = normal this.level = level this.generated = false this.visible = true this.planet = planet this.generator = planet.generator // Calculate left (x) and forward (z) vectors from the normal (y) this.left = [normal[1], normal[2], normal[0]] this.forward = crossv3(normal, this.left) this.position = pos // Center the face if (level === 0) { this.position = subv3(this.position, mulv3(this.left, this.generator.radius / 2)) this.position = subv3(this.position, mulv3(this.forward, this.generator.radius / 2)) } this.generate() } generate () { if (this.generated) return const sideResolution = PlanetIndexBuffers.resolution const vertices = [] const normals = [] const colors = [] const textureCoords = [] const radius = this.generator.radius const divisionLevel = Math.pow(2, this.level) for (let i = 0, vertexPointer = 0; i < sideResolution; i++) { for (let j = 0; j < sideResolution; j++, vertexPointer++) { // Vertex index (0 - 1) const iindex = i / (sideResolution - 1) const jindex = j / (sideResolution - 1) // From the left and forward vectors, we can calculate an oriented vertex const iv = divv3(mulv3(mulv3(this.left, iindex), radius), divisionLevel) const jv = divv3(mulv3(mulv3(this.forward, jindex), radius), divisionLevel) // Add the scaled left and forward to the centered origin const vertex = addv3(this.position, addv3(iv, jv)) // Normalize and multiply by radius to create a spherical mesh const normal = normalv3(vertex) const pointHeight = this.generator.getHeight(this.level + 1, normal) const pos = mulv3(normal, pointHeight * 10 + radius) const pointBiome = this.generator.getBiome(pointHeight, normal) vertices[vertexPointer * 3] = pos[0] vertices[vertexPointer * 3 + 1] = pos[1] vertices[vertexPointer * 3 + 2] = pos[2] normals[vertexPointer * 3] = normal[0] normals[vertexPointer * 3 + 1] = normal[1] normals[vertexPointer * 3 + 2] = normal[2] colors[vertexPointer * 3] = pointBiome[0] colors[vertexPointer * 3 + 1] = pointBiome[1] colors[vertexPointer * 3 + 2] = pointBiome[2] textureCoords[vertexPointer * 2] = i * (1 / sideResolution) textureCoords[vertexPointer * 2 + 1] = j * (1 / sideResolution) if (i === Math.floor(sideResolution / 2) && j === Math.floor(sideResolution / 2)) { this.center = pos } } } // Generate normals for this mesh Mesh.generateNormals(normals, PlanetIndexBuffers.base.indices, vertices) this.mesh = Mesh.construct(Screen.gl, vertices, null, textureCoords, normals) this.mesh.cbuffer = Mesh.loadToBuffer(Screen.gl, Screen.gl.ARRAY_BUFFER, new Float32Array(colors)) this.bounds = BoundingBox.fromMesh(this.mesh) this.generated = true } dispose () { this.generated = false if (this.mesh) { // this.mesh.dispose(Screen.gl) this.mesh = null } } isLeaf () { return !this.children.length } merge () { if (this.isLeaf()) return for (const i in this.children) { const ch = this.children[i] ch.merge() ch.dispose() } this.children = [] this.generate() this.updateNeighbors() } subdivide () { if (this.level === lodMax) return const lv = this.level + 1 const stepLeft = mulv3(this.left, this.generator.radius / Math.pow(2, lv)) const stepForward = mulv3(this.forward, this.generator.radius / Math.pow(2, lv)) this.children = [ // Top left corner new CubeFace(0, this, lv, this.position, this.normal, this.planet), // Top right corner new CubeFace(1, this, lv, addv3(this.position, stepForward), this.normal, this.planet), // Bottom right corner new CubeFace(2, this, lv, addv3(this.position, addv3(stepLeft, stepForward)), this.normal, this.planet), // Bottom left corner new CubeFace(3, this, lv, addv3(this.position, stepLeft), this.normal, this.planet) ] this.updateNeighbors() this.dispose() } draw (gl, shader) { if (!this.visible) return if (!this.mesh) { for (const i in this.children) { this.children[i].draw(gl, shader) } return } CubeFace.determineIndexBuffer(this) this.mesh.prepare(gl, shader) if (this.mesh.cbuffer && shader.hasAttribute(gl, 'aColor')) { gl.bindBuffer(gl.ARRAY_BUFFER, this.mesh.cbuffer) shader.setAttribute(gl, 'aColor', 3, false, 3 * Float32Array.BYTES_PER_ELEMENT, 0) this.mesh._bufferCount++ } this.mesh.draw(gl, shader) } update (camera, dt) { if (!this.center) return this.visible = camera.frustum.containsBox(this.bounds.min, this.bounds.max) const camToOrigin = vec3.distance(camera.pos, this.center) const divisionLevel = Math.pow(2, this.level) const splitDistance = this.generator.radius / divisionLevel if (camToOrigin < splitDistance * 5 && this.children.length === 0) { this.subdivide() return } else if (camToOrigin > splitDistance * 5.5 && this.children.length > 0) { this.merge() return } this.updateNeighbors() if (this.children.length > 0) { for (const i in this.children) { this.children[i].update(camera, dt) } } } get root () { if (this.level === 0) return this if (this.parent.level === 0) return this.parent return this.parent.root } updateNeighbors () { if (this.level === 0) { // The neighbors of root nodes never change if (this.neighborTop) return switch (this.index) { // Front face case 0: this.neighborTop = this.parent.faces[4] this.neighborLeft = this.parent.faces[2] this.neighborRight = this.parent.faces[3] this.neighborBottom = this.parent.faces[5] break // Back face case 1: this.neighborTop = this.parent.faces[4] this.neighborLeft = this.parent.faces[3] this.neighborRight = this.parent.faces[2] this.neighborBottom = this.parent.faces[5] break // Left face case 2: this.neighborTop = this.parent.faces[4] this.neighborLeft = this.parent.faces[1] this.neighborRight = this.parent.faces[0] this.neighborBottom = this.parent.faces[5] break // Right face case 3: this.neighborTop = this.parent.faces[4] this.neighborLeft = this.parent.faces[0] this.neighborRight = this.parent.faces[1] this.neighborBottom = this.parent.faces[5] break // Top face case 4: this.neighborTop = this.parent.faces[1] this.neighborLeft = this.parent.faces[2] this.neighborRight = this.parent.faces[3] this.neighborBottom = this.parent.faces[0] break // Bottom face case 5: this.neighborTop = this.parent.faces[0] this.neighborLeft = this.parent.faces[2] this.neighborRight = this.parent.faces[3] this.neighborBottom = this.parent.faces[1] break } return } switch (this.index) { // Top left corner case 0: if (this.parent.neighborTop != null) { this.neighborTop = this.parent.neighborTop.children[3] } this.neighborRight = this.parent.children[1] this.neighborBottom = this.parent.children[3] if (this.parent.neighborLeft != null) { this.neighborLeft = this.parent.neighborLeft.children[1] } break // Top right corner case 1: if (this.parent.neighborTop != null) { this.neighborTop = this.parent.neighborTop.children[2] } if (this.parent.neighborRight != null) { this.neighborRight = this.parent.neighborRight.children[0] } this.neighborBottom = this.parent.children[2] this.neighborLeft = this.parent.children[0] break // Bottom right corner case 2: this.neighborTop = this.parent.children[1] if (this.parent.neighborRight != null) { this.neighborRight = this.parent.neighborRight.children[3] } if (this.parent.neighborBottom != null) { this.neighborBottom = this.parent.neighborBottom.children[1] } this.neighborLeft = this.parent.children[3] break // Bottom left corner case 3: this.neighborTop = this.parent.children[0] this.neighborRight = this.parent.children[2] if (this.parent.neighborBottom != null) { this.neighborBottom = this.parent.neighborBottom.children[0] } if (this.parent.neighborLeft != null) { this.neighborLeft = this.parent.neighborLeft.children[2] } break } } static determineIndexBuffer (face) { let buffer = PlanetIndexBuffers.base switch (face.index) { // Top left corner case 0: if (face.neighborTop == null && face.neighborLeft == null) { buffer = PlanetIndexBuffers.fixTL } else if (face.neighborTop == null) { buffer = PlanetIndexBuffers.fixT } else if (face.neighborLeft == null) { buffer = PlanetIndexBuffers.fixL } break // Top right corner case 1: if (face.neighborTop == null && face.neighborRight == null) { buffer = PlanetIndexBuffers.fixTR } else if (face.neighborTop == null) { buffer = PlanetIndexBuffers.fixT } else if (face.neighborRight == null) { buffer = PlanetIndexBuffers.fixR } break // Bottom right corner case 2: if (face.neighborBottom == null && face.neighborRight == null) { buffer = PlanetIndexBuffers.fixBR } else if (face.neighborBottom == null) { buffer = PlanetIndexBuffers.fixB } else if (face.neighborRight == null) { buffer = PlanetIndexBuffers.fixR } break // Bottom left corner case 3: if (face.neighborBottom == null && face.neighborLeft == null) { buffer = PlanetIndexBuffers.fixBL } else if (face.neighborBottom == null) { buffer = PlanetIndexBuffers.fixB } else if (face.neighborLeft == null) { buffer = PlanetIndexBuffers.fixL } break } PlanetIndexBuffers.setBuffer(face.mesh, buffer) } } class CubePlanet { constructor (origin, generator) { this.origin = origin this.generator = generator this.transform = mat4.create() mat4.fromTranslation(this.transform, origin) const hs = generator.radius / 2 this.faces = [ new CubeFace(0, this, 0, [0, 0, -hs], [0, 0, -1], this), // front new CubeFace(1, this, 0, [0, 0, hs], [0, 0, 1], this), // back new CubeFace(2, this, 0, [-hs, 0, 0], [-1, 0, 0], this), // left new CubeFace(3, this, 0, [hs, 0, 0], [1, 0, 0], this), // right new CubeFace(4, this, 0, [0, hs, 0], [0, 1, 0], this), // top new CubeFace(5, this, 0, [0, -hs, 0], [0, -1, 0], this) // bottom ] } update (camera, dt) { for (const i in this.faces) { this.faces[i].update(camera, dt) } } draw (gl, shader) { // Set model transform matrix uniform const transformLocation = shader.getUniformLocation(gl, 'uModelMatrix') gl.uniformMatrix4fv(transformLocation, false, this.transform) for (const i in this.faces) { this.faces[i].draw(gl, shader) } } prepare (gl, shader) { if (!this.material) return this.material.apply(gl, shader) } static drawPlanetAtmosphere (gl, planet, atmosphere, camera, sun, atmosShader, planetShader, surfaceShader, surfaceEnvironment) { const height = vec3.length(subv3(camera.pos, planet.origin)) // Render the atmosphere atmosShader.use(gl) camera.draw(gl, atmosShader) atmosphere.draw(gl, atmosShader, camera, sun) if (height > atmosphere.innerRadius + (atmosphere.outerRadius - atmosphere.innerRadius) / 3) { // Draw the planet from space // TODO: maybe separate atmosphere from space and ground shaders? planetShader.use(gl) camera.draw(gl, planetShader) atmosphere.setupPlanetShader(gl, planetShader, camera, sun) planet.prepare(gl, planetShader) planet.draw(gl, planetShader) } else { // Draw the planet within the atmosphere surfaceShader.use(gl) surfaceEnvironment.draw(gl, surfaceShader) camera.draw(gl, surfaceShader) planet.prepare(gl, surfaceShader) planet.draw(gl, surfaceShader) } } } export { CubePlanet, CubeFace, PlanetGenerator, PlanetIndexBuffers }