diff --git a/src/engine/camera.js b/src/engine/camera.js index 24b276a..f17d298 100644 --- a/src/engine/camera.js +++ b/src/engine/camera.js @@ -1,4 +1,4 @@ -import { Node } from './mesh' +import { Node } from './components' import { glMatrix, mat4, vec2, vec3 } from 'gl-matrix' const SPEED = 100.0 diff --git a/src/engine/components/index.js b/src/engine/components/index.js new file mode 100644 index 0000000..3adbd6c --- /dev/null +++ b/src/engine/components/index.js @@ -0,0 +1,334 @@ +import Resource from '../resource' +import { mat4, quat, vec3 } from 'gl-matrix' +import { Mesh } from '../mesh' +import { Material } from '../mesh/material' + +let meshCache = {} + +/** + * Returns an euler angle representation of a quaternion + * @param {vec3} out Euler angles, pitch-yaw-roll + * @param {quat} mat Quaternion + * @return {vec3} out + */ +quat.getEuler = function (out, quat) { + let x = quat[0] + let y = quat[1] + let z = quat[2] + let w = quat[3] + let x2 = x * x + let y2 = y * y + let z2 = z * z + let w2 = w * w + let unit = x2 + y2 + z2 + w2 + let test = x * w - y * z + if (test > 0.499995 * unit) { + // singularity at the north pole + out[0] = Math.PI / 2 + out[1] = 2 * Math.atan2(y, x) + out[2] = 0 + } else if (test < -0.499995 * unit) { + // singularity at the south pole + out[0] = -Math.PI / 2 + out[1] = 2 * Math.atan2(y, x) + out[2] = 0 + } else { + out[0] = Math.asin(2 * (x * z - w * y)) + out[1] = Math.atan2(2 * (x * w + y * z), 1 - 2 * (z2 + w2)) + out[2] = Math.atan2(2 * (x * y + z * w), 1 - 2 * (y2 + z2)) + } + return out +} + +class Node { + constructor (pos, scale, rotation) { + // Translation + this.pos = pos || [0.0, 0.0, 0.0] + + // Scaling + this.scale = scale || [1.0, 1.0, 1.0] + + // Rotation in Euler angles (yaw, pitch, roll) in radians + this.rotation = rotation || [0.0, 0.0, 0.0] + + this.transform = mat4.create() + this.updateTransform() + + this.parent = null + this.children = [] + } + + updateTransform () { + let matrix = mat4.create() + + // Set translation + mat4.translate(matrix, matrix, this.pos) + + // Set scale + mat4.scale(matrix, matrix, this.scale) + + // Set rotation + if (this.rotation[0] !== 0) { + mat4.rotateX(matrix, matrix, this.rotation[0]) + } + + if (this.rotation[1] !== 0) { + mat4.rotateY(matrix, matrix, this.rotation[1]) + } + + if (this.rotation[2] !== 0) { + mat4.rotateZ(matrix, matrix, this.rotation[2]) + } + + // Add parent node's transform + if (this.parent) { + mat4.add(matrix, this.parent.transform, matrix) + } + + // Set the matrix + this.transform = matrix + + // Update children's transforms + for (let i in this.children) { + let child = this.children[i] + if (!(child instanceof Node)) continue + child.updateTransform() + } + } + + setTransformation (transform) { + let quaternion = quat.create() + let translation = vec3.create() + let rotation = vec3.create() + let scale = vec3.create() + + mat4.getTranslation(translation, transform) + mat4.getScaling(scale, transform) + mat4.getRotation(quaternion, transform) + + quat.getEuler(rotation, quaternion) + + this.rotation = rotation + this.pos = translation + this.scale = scale + + this.updateTransform() + } + + // Setters + setPosition (newPos) { + this.pos = newPos + this.updateTransform() + } + + setTranslation (newTrans) { + this.setPosition(newTrans) + } + + setScale (newScale) { + this.scale = newScale + this.updateTransform() + } + + setRotation (newRotation) { + this.rotation = newRotation + this.updateTransform() + } + + // Transforms + translate (pos) { + mat4.translate(this.transform, this.transform, pos) + } + + scale (scale) { + mat4.scale(this.transform, this.transform, scale) + } + + rotate (rot) { + if (rot[0] !== 0) { + mat4.rotateX(this.transform, this.transform, rot[0]) + } + + if (rot[1] !== 0) { + mat4.rotateY(this.transform, this.transform, rot[1]) + } + + if (rot[2] !== 0) { + mat4.rotateZ(this.transform, this.transform, rot[2]) + } + } + + // Getters + get position () { + return this.pos + } + + get translation () { + return this.pos + } + + // Draw base + draw (gl, shader) { + // Set model transform matrix uniform + const transformLocation = shader.getUniformLocation(gl, 'uModelMatrix') + gl.uniformMatrix4fv(transformLocation, false, this.transform) + } + + addChild (ch) { + if (!(ch instanceof Node)) return + this.children.push(ch) + this.updateTransform() + } + + setParent (p) { + if (!(p instanceof Node)) return + this.parent = p + this.updateTransform() + } +} + +class MeshInstance extends Node { + constructor (mesh, pos, scale, rot) { + super(pos, scale, rot) + this.mesh = mesh + } + + static async loadFile (gl, file, pos) { + file = '/assets/models/' + file + '.json' + + // Ensure each mesh file is loaded only once + if (meshCache[file]) return meshCache[file].length > 1 ? meshCache[file] : meshCache[file][0] + + let dat = await Resource.GET({ type: 'json', url: file }) + if (!dat.meshes) throw new Error('No meshes defined in file.') + + let cleaned = [] + let materials = [] + for (let mi in dat.meshes) { + let mesh = dat.meshes[mi] + let material + + if (mesh.materialindex != null && dat.materials && dat.materials.length) { + // Ensure we don't re-create materials with the same index + if (materials[mesh.materialindex]) { + material = materials[mesh.materialindex] + } else { + // Load a new material + material = new Material() + let matdata = dat.materials[mesh.materialindex].properties + + // Parse material information + for (let pi in matdata) { + let property = matdata[pi] + if (!property || !property.key) continue + if (property.key === '?mat.name') material.name = property.value + else if (property.key.indexOf('$clr.') === 0) { + let dproperty = property.key.substr(5) + switch (dproperty) { + case 'specular': + case 'diffuse': + case 'shininess': + case 'ambient': + case 'reflective': + material[dproperty] = property.value + break + } + } else if (property.key.indexOf('$tex.file') === 0) { + if (!material.textures) { + material.textures = [] + } + + material.textures.push(property.value) + } + } + + materials[mesh.materialindex] = material + } + } + + cleaned.push({ + vertices: mesh.vertices, + indices: [].concat.apply([], mesh.faces), + uv: mesh.texturecoords ? mesh.texturecoords[0] : null, + normals: mesh.normals ? mesh.normals : null, + material + }) + } + + // Load everything + let loadComplete = [] + for (let i in cleaned) { + let meshdata = cleaned[i] + let mesh = Mesh.construct(gl, meshdata.vertices, meshdata.indices, + meshdata.uv, meshdata.normals) + + // Initialize the material's texture if present + if (meshdata.material) { + mesh.material = meshdata.material + + // Ensure all textures get loaded before finishing + if (meshdata.material.textures) { + await meshdata.material.loadTextures(gl) + } + } + + loadComplete.push(mesh) + } + + // Now, we need to give the meshes the appropriate parents and transforms. + let finished = [] + function setChildren (parent, chMeshes) { + let meshIndex = chMeshes.meshes[0] + let mesh = loadComplete[meshIndex] + let meshInstance = new MeshInstance(mesh, parent == null ? pos : null) + + meshInstance.mesh = mesh + + if (chMeshes.children) { + for (let i in chMeshes.children) { + if (!chMeshes.children[i].meshes) continue + setChildren(meshInstance, chMeshes.children[i]) + } + } + + meshInstance.name = chMeshes.name + + if (parent == null) { + finished.push(meshInstance) + } else { + parent.children.push(meshInstance) + meshInstance.parent = parent + meshInstance.setTransformation(chMeshes.transformation) + } + } + + let rootchildren = dat.rootnode.children + for (let j in rootchildren) { + if (!rootchildren[j].meshes) continue + setChildren(null, rootchildren[j]) + } + + // Cache the mesh + meshCache[file] = finished + + return finished.length > 1 ? finished : finished[0] + } + + draw (gl, shader) { + // Set model transform uniform + super.draw(gl, shader) + + // Draw the mesh + this.mesh.prepare(gl, shader) + this.mesh.draw(gl, shader) + + // Invoke children's draw methods + for (let i in this.children) { + let child = this.children[i] + if (!(child instanceof Mesh)) continue + child.draw(gl, shader) + } + } +} + +export { Node, MeshInstance } diff --git a/src/engine/components/terrain/index.js b/src/engine/components/terrain/index.js index 48a6765..2cc9346 100644 --- a/src/engine/components/terrain/index.js +++ b/src/engine/components/terrain/index.js @@ -1,4 +1,5 @@ -import { Mesh, Node } from '../../mesh' +import { Node } from '../' +import { Mesh } from '../../mesh' class Terrain extends Node { constructor (pos, sWidth, sHeight) { @@ -60,6 +61,8 @@ class Terrain extends Node { draw (gl, shader) { if (!this.mesh) return super.draw(gl, shader) + + this.mesh.prepare(gl, shader) this.mesh.draw(gl, shader) } } diff --git a/src/engine/mesh/entity.js b/src/engine/mesh/entity.js deleted file mode 100644 index ca132db..0000000 --- a/src/engine/mesh/entity.js +++ /dev/null @@ -1,32 +0,0 @@ -import { Mesh, Node } from './index' - -// Entity is just a Mesh with extra functionality. -class Entity extends Node { - constructor (mesh, pos, scale, rotation) { - super(pos, scale, rotation) - this.mesh = mesh - } - - // Drawing related - update (dt) { - // Override this! - } - - draw (gl, shader) { - // Set model transform matrix uniform - const modelloc = shader.getUniformLocation(gl, 'uModelMatrix') - gl.uniformMatrix4fv(modelloc, false, this.transform) - - // Draw the mesh - this.mesh.draw(gl, shader) - } - - // Generators - static async createEntity (gl, meshName, pos) { - let mesh = await Mesh.loadFile(gl, meshName) - let entity = new Entity(mesh, pos) - return entity - } -} - -export default Entity diff --git a/src/engine/mesh/index.js b/src/engine/mesh/index.js index 26e446f..6e898bf 100644 --- a/src/engine/mesh/index.js +++ b/src/engine/mesh/index.js @@ -1,192 +1,5 @@ -import Resource from '../resource' -import { Texture, Material } from './material' -import { mat4, quat, vec3 } from 'gl-matrix' -let meshCache = {} - -/** - * Returns an euler angle representation of a quaternion - * @param {vec3} out Euler angles, pitch-yaw-roll - * @param {quat} mat Quaternion - * @return {vec3} out - */ -quat.getEuler = function (out, quat) { - let x = quat[0] - let y = quat[1] - let z = quat[2] - let w = quat[3] - let x2 = x * x - let y2 = y * y - let z2 = z * z - let w2 = w * w - let unit = x2 + y2 + z2 + w2 - let test = x * w - y * z - if (test > 0.499995 * unit) { - // singularity at the north pole - out[0] = Math.PI / 2 - out[1] = 2 * Math.atan2(y, x) - out[2] = 0 - } else if (test < -0.499995 * unit) { - // singularity at the south pole - out[0] = -Math.PI / 2 - out[1] = 2 * Math.atan2(y, x) - out[2] = 0 - } else { - out[0] = Math.asin(2 * (x * z - w * y)) - out[1] = Math.atan2(2 * (x * w + y * z), 1 - 2 * (z2 + w2)) - out[2] = Math.atan2(2 * (x * y + z * w), 1 - 2 * (y2 + z2)) - } - return out -} - -class Node { - constructor (pos, scale, rotation) { - // Translation - this.pos = pos || [0.0, 0.0, 0.0] - - // Scaling - this.scale = scale || [1.0, 1.0, 1.0] - - // Rotation in Euler angles (yaw, pitch, roll) in radians - this.rotation = rotation || [0.0, 0.0, 0.0] - - this.transform = mat4.create() - this.updateTransform() - - this.parent = null - this.children = [] - } - - updateTransform () { - let matrix = mat4.create() - - // Set translation - mat4.translate(matrix, matrix, this.pos) - - // Set scale - mat4.scale(matrix, matrix, this.scale) - - // Set rotation - if (this.rotation[0] !== 0) { - mat4.rotateX(matrix, matrix, this.rotation[0]) - } - - if (this.rotation[1] !== 0) { - mat4.rotateY(matrix, matrix, this.rotation[1]) - } - - if (this.rotation[2] !== 0) { - mat4.rotateZ(matrix, matrix, this.rotation[2]) - } - - // Add parent node's transform - if (this.parent) { - mat4.add(matrix, this.parent.transform, matrix) - } - - // Set the matrix - this.transform = matrix - - // Update children's transforms - for (let i in this.children) { - let child = this.children[i] - if (!(child instanceof Node)) continue - child.updateTransform() - } - } - - setTransformation (transform) { - let quaternion = quat.create() - let translation = vec3.create() - let rotation = vec3.create() - let scale = vec3.create() - - mat4.getTranslation(translation, transform) - mat4.getScaling(scale, transform) - mat4.getRotation(quaternion, transform) - - quat.getEuler(rotation, quaternion) - - this.rotation = rotation - this.pos = translation - this.scale = scale - - this.updateTransform() - } - - // Setters - setPosition (newPos) { - this.pos = newPos - this.updateTransform() - } - - setTranslation (newTrans) { - this.setPosition(newTrans) - } - - setScale (newScale) { - this.scale = newScale - this.updateTransform() - } - - setRotation (newRotation) { - this.rotation = newRotation - this.updateTransform() - } - - // Transforms - translate (pos) { - mat4.translate(this.transform, this.transform, pos) - } - - scale (scale) { - mat4.scale(this.transform, this.transform, scale) - } - - rotate (rot) { - if (rot[0] !== 0) { - mat4.rotateX(this.transform, this.transform, rot[0]) - } - - if (rot[1] !== 0) { - mat4.rotateY(this.transform, this.transform, rot[1]) - } - - if (rot[2] !== 0) { - mat4.rotateZ(this.transform, this.transform, rot[2]) - } - } - - // Getters - get position () { - return this.pos - } - - get translation () { - return this.pos - } - - // Draw base - draw (gl, shader) { - // Set model transform matrix uniform - const transformLocation = shader.getUniformLocation(gl, 'uModelMatrix') - gl.uniformMatrix4fv(transformLocation, false, this.transform) - } - - addChild (ch) { - if (!(ch instanceof Node)) return - this.children.push(ch) - this.updateTransform() - } - - setParent (p) { - if (!(p instanceof Node)) return - this.parent = p - this.updateTransform() - } -} - -class Mesh extends Node { +class Mesh { static construct (gl, vertices, indices, uvs, normals) { // VBO for model vertices let pos = gl.createBuffer() @@ -224,124 +37,6 @@ class Mesh extends Node { return mesh } - static async loadFile (gl, file) { - file = '/assets/models/' + file + '.json' - - // Ensure each mesh file is loaded only once - if (meshCache[file]) return meshCache[file].length > 1 ? meshCache[file] : meshCache[file][0] - - let dat = await Resource.GET({ type: 'json', url: file }) - if (!dat.meshes) throw new Error('No meshes defined in file.') - - let cleaned = [] - let materials = [] - for (let mi in dat.meshes) { - let mesh = dat.meshes[mi] - let material - - if (mesh.materialindex != null && dat.materials && dat.materials.length) { - // Ensure we don't re-create materials with the same index - if (materials[mesh.materialindex]) { - material = materials[mesh.materialindex] - } else { - // Load a new material - material = new Material() - let matdata = dat.materials[mesh.materialindex].properties - - // Parse material information - for (let pi in matdata) { - let property = matdata[pi] - if (!property || !property.key) continue - if (property.key === '?mat.name') material.name = property.value - else if (property.key.indexOf('$clr.') === 0) { - let dproperty = property.key.substr(5) - switch (dproperty) { - case 'specular': - case 'diffuse': - case 'shininess': - case 'ambient': - case 'reflective': - material[dproperty] = property.value - break - } - } else if (property.key.indexOf('$tex.file') === 0) { - if (!material.textures) { - material.textures = [] - } - - material.textures.push(property.value) - } - } - - materials[mesh.materialindex] = material - } - } - - cleaned.push({ - vertices: mesh.vertices, - indices: [].concat.apply([], mesh.faces), - uv: mesh.texturecoords ? mesh.texturecoords[0] : null, - normals: mesh.normals ? mesh.normals : null, - material - }) - } - - // Load everything - let loadComplete = [] - for (let i in cleaned) { - let meshdata = cleaned[i] - let mesh = Mesh.construct(gl, meshdata.vertices, meshdata.indices, - meshdata.uv, meshdata.normals) - - // Initialize the material's texture if present - if (meshdata.material) { - mesh.material = meshdata.material - - // Ensure all textures get loaded before finishing - if (meshdata.material.textures) { - await meshdata.material.loadTextures(gl) - } - } - - loadComplete.push(mesh) - } - - // Now, we need to give the meshes the appropriate parents and transforms. - let finished = [] - function setChildren (parent, chMeshes) { - let meshIndex = chMeshes.meshes[0] - let mesh = loadComplete[meshIndex] - - if (chMeshes.children) { - for (let i in chMeshes.children) { - if (!chMeshes.children[i].meshes) continue - setChildren(mesh, chMeshes.children[i]) - } - } - - mesh.name = chMeshes.name - - if (parent == null) { - finished.push(mesh) - } else { - parent.children.push(mesh) - mesh.parent = parent - mesh.setTransformation(chMeshes.transformation) - } - } - - let rootchildren = dat.rootnode.children - for (let j in rootchildren) { - if (!rootchildren[j].meshes) continue - setChildren(null, rootchildren[j]) - } - - // Cache the mesh - meshCache[file] = finished - - return finished.length > 1 ? finished : finished[0] - } - bindBuffers (gl, shader) { gl.bindBuffer(gl.ARRAY_BUFFER, this.posBuffer) shader.setAttribute(gl, 'aVertexPosition', 3, false, 3 * Float32Array.BYTES_PER_ELEMENT, 0) @@ -359,10 +54,7 @@ class Mesh extends Node { gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.ebo) } - draw (gl, shader) { - // Set model transform uniform - super.draw(gl, shader) - + prepare (gl, shader) { // Bind attrib arrays this.bindBuffers(gl, shader) @@ -370,16 +62,11 @@ class Mesh extends Node { if (this.material) { this.material.apply(gl, shader) } + } + draw (gl, shader) { gl.drawElements(gl.TRIANGLES, this.indices, gl.UNSIGNED_SHORT, 0) - - // Invoke children's draw methods - for (let i in this.children) { - let child = this.children[i] - if (!(child instanceof Mesh)) continue - child.draw(gl, shader) - } } } -export { Node, Mesh, Texture, Material } +export { Mesh } diff --git a/src/index.js b/src/index.js index 3697c91..6fdf673 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,6 @@ import Engine from './engine' import Camera from './engine/camera' -import Entity from './engine/mesh/entity' +import { MeshInstance } from './engine/components' import { Environment } from './engine/environment' import { Terrain } from './engine/components/terrain' @@ -11,12 +11,10 @@ let game = new Engine() let env = new Environment() async function pipeline () { - let entity = await Entity.createEntity(game.gl, 'test', [0.0, 0.0, -6.0]) + let entity = await MeshInstance.loadFile(game.gl, 'test', [0.0, 0.0, 0.0]) let shader = await game.shaders.createShaderFromFiles(game.gl, 'basic', false) let terrainShader = await game.shaders.createShaderFromFiles(game.gl, 'terrain', false) - console.log(entity.mesh) - // Create a height map based on OpenSimplex noise let hmap = new SimplexHeightMap(1, 1, 256, 50)