527 lines
17 KiB
JavaScript
527 lines
17 KiB
JavaScript
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 }
|