diff --git a/assets/shaders/skybox.fs b/assets/shaders/skybox.fs new file mode 100644 index 0000000..94e3bbb --- /dev/null +++ b/assets/shaders/skybox.fs @@ -0,0 +1,9 @@ +precision mediump float; + +varying vec3 uv; + +uniform samplerCube cubeMap; + +void main() { + gl_FragColor = textureCube(cubeMap, uv); +} diff --git a/assets/shaders/skybox.vs b/assets/shaders/skybox.vs new file mode 100644 index 0000000..4b5f170 --- /dev/null +++ b/assets/shaders/skybox.vs @@ -0,0 +1,13 @@ +precision mediump float; + +attribute vec3 aVertexPosition; + +uniform mat4 uViewMatrix; +uniform mat4 uProjectionMatrix; + +varying vec3 uv; + +void main() { + gl_Position = uProjectionMatrix * uViewMatrix * vec4(aVertexPosition, 1.0); + uv = aVertexPosition; +} diff --git a/assets/textures/noisy.png b/assets/textures/noisy.png index 54a89a6..934cbb0 100644 Binary files a/assets/textures/noisy.png and b/assets/textures/noisy.png differ diff --git a/assets/textures/skybox/back.png b/assets/textures/skybox/back.png new file mode 100644 index 0000000..6dea976 Binary files /dev/null and b/assets/textures/skybox/back.png differ diff --git a/assets/textures/skybox/bottom.png b/assets/textures/skybox/bottom.png new file mode 100644 index 0000000..ffd848e Binary files /dev/null and b/assets/textures/skybox/bottom.png differ diff --git a/assets/textures/skybox/front.png b/assets/textures/skybox/front.png new file mode 100644 index 0000000..b4f5fb7 Binary files /dev/null and b/assets/textures/skybox/front.png differ diff --git a/assets/textures/skybox/left.png b/assets/textures/skybox/left.png new file mode 100644 index 0000000..23a1de2 Binary files /dev/null and b/assets/textures/skybox/left.png differ diff --git a/assets/textures/skybox/right.png b/assets/textures/skybox/right.png new file mode 100644 index 0000000..8ce890b Binary files /dev/null and b/assets/textures/skybox/right.png differ diff --git a/assets/textures/skybox/top.png b/assets/textures/skybox/top.png new file mode 100644 index 0000000..dce761f Binary files /dev/null and b/assets/textures/skybox/top.png differ diff --git a/src/engine/camera.js b/src/engine/camera.js index 2d3e8e2..d139c2f 100644 --- a/src/engine/camera.js +++ b/src/engine/camera.js @@ -23,6 +23,9 @@ class Camera extends Node { this.right = vec3.create() this.worldUp = vec3.fromValues(0.0, 1.0, 0.0) + this.nearPlane = ZNEAR + this.farPlane = ZFAR + this.updateTransform() } @@ -99,7 +102,7 @@ class Camera extends Node { updateProjection (gl) { let aspect = gl.canvas.width / gl.canvas.height - mat4.perspective(this.projection, this.fov, aspect, ZNEAR, ZFAR) + mat4.perspective(this.projection, this.fov, aspect, this.nearPlane, this.farPlane) } // Calculate the view matrix on-the-go diff --git a/src/engine/components/index.js b/src/engine/components/index.js index 02c71a1..5fa272c 100644 --- a/src/engine/components/index.js +++ b/src/engine/components/index.js @@ -224,6 +224,7 @@ class MeshInstance extends Node { // Draw the mesh this.mesh.prepare(gl, shader) this.mesh.draw(gl, shader) + this.mesh.postdraw(gl, shader) // Draw children super.draw(gl, shader) diff --git a/src/engine/components/skybox/index.js b/src/engine/components/skybox/index.js new file mode 100644 index 0000000..7fbaf1a --- /dev/null +++ b/src/engine/components/skybox/index.js @@ -0,0 +1,106 @@ +import { Mesh } from '../../mesh' +import { Texture } from '../../mesh/material' + +import { mat4 } from 'gl-matrix' + +import Resource from '../../resource' + +const FNAMES = ['right', 'left', 'top', 'bottom', 'back', 'front'] +const SIZE = 500 + +class Skybox { + constructor (name, size = SIZE) { + this.name = name + this.size = size + } + + async initialize (gl) { + await this.createTexture(gl) + this.createMesh(gl) + } + + async createTexture (gl) { + let ready = [] + for (let i in FNAMES) { + let real = this.name + '/' + FNAMES[i] + '.png' + let loaded = await Resource.loadImage(real) + ready[i] = loaded + } + + let imgCube = Texture.createTextureCubeMap(gl, ready) + this.cubemap = imgCube + } + + createMesh (gl) { + const vertices = [ + -this.size, this.size, -this.size, + -this.size, -this.size, -this.size, + this.size, -this.size, -this.size, + this.size, -this.size, -this.size, + this.size, this.size, -this.size, + -this.size, this.size, -this.size, + + -this.size, -this.size, this.size, + -this.size, -this.size, -this.size, + -this.size, this.size, -this.size, + -this.size, this.size, -this.size, + -this.size, this.size, this.size, + -this.size, -this.size, this.size, + + this.size, -this.size, -this.size, + this.size, -this.size, this.size, + this.size, this.size, this.size, + this.size, this.size, this.size, + this.size, this.size, -this.size, + this.size, -this.size, -this.size, + + -this.size, -this.size, this.size, + -this.size, this.size, this.size, + this.size, this.size, this.size, + this.size, this.size, this.size, + this.size, -this.size, this.size, + -this.size, -this.size, this.size, + + -this.size, this.size, -this.size, + this.size, this.size, -this.size, + this.size, this.size, this.size, + this.size, this.size, this.size, + -this.size, this.size, this.size, + -this.size, this.size, -this.size, + + -this.size, -this.size, -this.size, + -this.size, -this.size, this.size, + this.size, -this.size, -this.size, + this.size, -this.size, -this.size, + -this.size, -this.size, this.size, + this.size, -this.size, this.size + ] + this.mesh = Mesh.constructFromVertices(gl, vertices) + } + + updateCamera (gl, shader, camera) { + const projloc = shader.getUniformLocation(gl, 'uProjectionMatrix') + const viewloc = shader.getUniformLocation(gl, 'uViewMatrix') + + // Set translation to zero to prevent the skybox from moving in relation to the camera + let view = mat4.clone(camera.view) + view[12] = 0 + view[13] = 0 + view[14] = 0 + + gl.uniformMatrix4fv(projloc, false, camera.projection) + gl.uniformMatrix4fv(viewloc, false, view) + } + + draw (gl, shader, camera) { + camera && this.updateCamera(gl, shader, camera) + + gl.activeTexture(gl.TEXTURE0) + gl.bindTexture(this.cubemap.type, this.cubemap.id) + this.mesh.prepare(gl, shader) + this.mesh.draw(gl, shader) + this.mesh.postdraw(gl, shader) + } +} + +export { Skybox } diff --git a/src/engine/components/terrain/basic.js b/src/engine/components/terrain/basic.js index 04765ba..15ec303 100644 --- a/src/engine/components/terrain/basic.js +++ b/src/engine/components/terrain/basic.js @@ -59,6 +59,7 @@ class Terrain extends Node { } draw (gl, shader) { + super.draw(gl, shader) if (!this.mesh) return // Set model transform matrix uniform const transformLocation = shader.getUniformLocation(gl, 'uModelMatrix') @@ -66,8 +67,7 @@ class Terrain extends Node { this.mesh.prepare(gl, shader) this.mesh.draw(gl, shader) - - super.draw(gl, shader) + this.mesh.postdraw(gl, shader) } } diff --git a/src/engine/components/terrain/lod.js b/src/engine/components/terrain/lod.js index 0d13cfd..2781d20 100644 --- a/src/engine/components/terrain/lod.js +++ b/src/engine/components/terrain/lod.js @@ -105,6 +105,7 @@ class TerrainNode extends Node { this.mesh.prepare(gl, shader) this.mesh.draw(gl, shader) + this.mesh.postdraw(gl, shader) } get generated () { diff --git a/src/engine/gui/index.js b/src/engine/gui/index.js index 190b801..0134834 100644 --- a/src/engine/gui/index.js +++ b/src/engine/gui/index.js @@ -1,4 +1,25 @@ -import { clamp } from '../utility' +import { Mesh } from '../mesh' +import { mat4 } from 'gl-matrix' + +const VERTEX_SHADER = ` +precision mediump float; +attribute vec2 aVertexPosition; +varying vec2 uv; +uniform mat4 uTransformationMatrix; +void main() { + gl_Position = uTransformationMatrix * vec4(aVertexPosition, 0.0, 1.0); + uv = vec2((aVertexPosition.x+1.0)/2.0, 1.0 - (aVertexPosition.y+1.0)/2.0); +} +` + +const FRAGMENT_SHADER = ` +precision mediump float; +varying vec2 uv; +uniform sampler2D texture0; +void main() { + gl_FragColor = texture2D(texture0, uv); +} +` class Dim4 { constructor (scX, ofX, scY, ofY) { @@ -67,10 +88,34 @@ class Node2D { this.parent = null this.children = [] + + this.transform = mat4.create() + + this.updateTransform() } updateTransform () { + let matrix = mat4.create() + mat4.translate(matrix, matrix, [this.pos[0], this.pos[2], 0.0]) + mat4.scale(matrix, matrix, [this.size[0], this.size[2], 1.0]) + if (this.rotation !== 0.0) { + mat4.rotate(matrix, matrix, this.rotation * Math.PI / 180, [0.0, 0.0, 1.0]) + } + // Add parent's transform to this + if (this.parent) { + mat4.mul(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 Node2D)) continue + child.updateTransform() + } } // Getters @@ -83,12 +128,12 @@ class Node2D { } // Draw base - draw (gl) { + draw (gl, shader, quad) { // Nothing to draw here, so just draw children for (let i in this.children) { let child = this.children[i] if (!(child instanceof Node2D)) continue - child.draw(gl) + child.draw(gl, shader, quad) } } @@ -118,3 +163,55 @@ class Node2D { this.parent = p } } + +class GUIImage extends Node2D { + constructor (texture, pos, size, rotation) { + super(pos, size, rotation) + this.texture = texture + this.active = true + } + + draw (gl, shader, quad) { + super.draw(gl, shader, quad) + if (!this.active) return + gl.activeTexture(gl.TEXTURE0) + gl.bindTexture(this.texture.type, this.texture.id) + + // Set transformation matrix + const transformLocation = shader.getUniformLocation(gl, 'uTransformationMatrix') + gl.uniformMatrix4fv(transformLocation, false, this.transform) + + // Draw the quad + quad.draw(gl, shader, gl.TRIANGLE_STRIP) + } +} + +class GUIRenderer { + async initialize (game) { + this.shader = await game.shaders.createShader(game.gl, 'gui', VERTEX_SHADER, FRAGMENT_SHADER) + this.createQuad(game.gl) + } + + // Reusable quad mesh for rendering GUIs + createQuad (gl) { + if (this.quad) return this.quad + this.quad = Mesh.constructFromVertices(gl, [-1, 1, -1, -1, 1, 1, 1, -1], 2) + return this.quad + } + + draw (gl, nodes) { + if (typeof nodes !== 'object') nodes = [ nodes ] + this.shader.use(gl) + this.quad.prepare(gl, this.shader) + gl.enable(gl.BLEND) + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) + gl.disable(gl.DEPTH_TEST) + for (let i in nodes) { + nodes[i].draw(gl, this.shader, this.quad) + } + gl.enable(gl.DEPTH_TEST) + gl.disable(gl.BLEND) + } +} + +export { Dim4, GUIRenderer, GUIImage } diff --git a/src/engine/gui/renderer.js b/src/engine/gui/renderer.js deleted file mode 100644 index e69de29..0000000 diff --git a/src/engine/index.js b/src/engine/index.js index c49c14a..539aefa 100644 --- a/src/engine/index.js +++ b/src/engine/index.js @@ -37,7 +37,7 @@ class Engine { render () { // Set clear color to black, fully opaque - gl.clearColor(0.0, 0.7, 1.0, 1.0) + gl.clearColor(0.0, 0.0, 0.0, 1.0) // Clear the color buffer with specified clear color gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT) diff --git a/src/engine/mesh/index.js b/src/engine/mesh/index.js index 573eed5..aa3d5f8 100644 --- a/src/engine/mesh/index.js +++ b/src/engine/mesh/index.js @@ -38,21 +38,38 @@ class Mesh { return mesh } + static constructFromVertices (gl, vertices, dimensions = 3) { + let pos = Mesh.loadToBuffer(gl, gl.ARRAY_BUFFER, new Float32Array(vertices)) + let mesh = new Mesh() + mesh.posBuffer = pos + mesh.vertices = vertices + mesh.vertexCount = vertices.length / dimensions + mesh.vertexLayout = dimensions + gl.bindBuffer(gl.ARRAY_BUFFER, null) + + return mesh + } + bindBuffers (gl, shader) { + this._bufferCount = 1 + + let d = this.vertexLayout || 3 gl.bindBuffer(gl.ARRAY_BUFFER, this.posBuffer) - shader.setAttribute(gl, 'aVertexPosition', 3, false, 3 * Float32Array.BYTES_PER_ELEMENT, 0) + shader.setAttribute(gl, 'aVertexPosition', d, false, d * Float32Array.BYTES_PER_ELEMENT, 0) if (this.nms && shader.hasAttribute(gl, 'aNormal')) { gl.bindBuffer(gl.ARRAY_BUFFER, this.nms) shader.setAttribute(gl, 'aNormal', 3, false, 3 * Float32Array.BYTES_PER_ELEMENT, 0) + this._bufferCount++ } if (this.uvs && shader.hasAttribute(gl, 'aTexCoords')) { gl.bindBuffer(gl.ARRAY_BUFFER, this.uvs) shader.setAttribute(gl, 'aTexCoords', 2, false, 2 * Float32Array.BYTES_PER_ELEMENT, 0) + this._bufferCount++ } - gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.ebo) + this.ebo && gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.ebo) } prepare (gl, shader) { @@ -69,7 +86,14 @@ class Mesh { if (this.indices) { gl.drawElements(mode, this.indices.length, gl.UNSIGNED_SHORT, 0) } else { - gl.drawArrays(mode, 0, this.vertices.length) + gl.drawArrays(mode, 0, this.vertexCount || this.vertices.length / (this.vertexLayout || 3)) + } + } + + // Unbind all attrib arrays after drawing something + postdraw (gl, shader) { + for (let i = 0; i < this._bufferCount; i++) { + gl.disableVertexAttribArray(i) } } diff --git a/src/engine/mesh/material.js b/src/engine/mesh/material.js index 22a6cbb..cae2f35 100644 --- a/src/engine/mesh/material.js +++ b/src/engine/mesh/material.js @@ -1,13 +1,22 @@ import Resource from '../resource' class Texture { - static createTexture2D (gl, img) { + static createTexture2D (gl, img, flip = true, filtering, repeat = gl.REPEAT) { let tex = gl.createTexture() gl.bindTexture(gl.TEXTURE_2D, tex) - gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true) + gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, flip) gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img) - gl.generateMipmap(gl.TEXTURE_2D) + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, repeat) + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, repeat) + + if (filtering) { + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filtering) + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filtering) + } else { + gl.generateMipmap(gl.TEXTURE_2D) + } + gl.bindTexture(gl.TEXTURE_2D, null) let oTex = new Texture() @@ -16,9 +25,39 @@ class Texture { return oTex } + + static createTextureCubeMap (gl, img) { + if (!img || img.length !== 6) throw new Error('createTextureCubeMap() requires six images!') + let tex = gl.createTexture() + gl.bindTexture(gl.TEXTURE_CUBE_MAP, tex) + + gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false) + + gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img[0]) + gl.texImage2D(gl.TEXTURE_CUBE_MAP_NEGATIVE_X, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img[1]) + gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_Y, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img[2]) + gl.texImage2D(gl.TEXTURE_CUBE_MAP_NEGATIVE_Y, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img[3]) + gl.texImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_Z, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img[4]) + gl.texImage2D(gl.TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img[5]) + + gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MAG_FILTER, gl.LINEAR) + gl.texParameteri(gl.TEXTURE_CUBE_MAP, gl.TEXTURE_MIN_FILTER, gl.LINEAR) + + gl.bindTexture(gl.TEXTURE_CUBE_MAP, null) + + let oTex = new Texture() + oTex.type = gl.TEXTURE_CUBE_MAP + oTex.id = tex + + return oTex + } } class Material { + constructor (textures) { + this.textures = textures + } + async loadTextures (gl) { if (this.textures) { for (let ti in this.textures) { diff --git a/src/engine/screen.js b/src/engine/screen.js index a49e8cc..8ff22e3 100644 --- a/src/engine/screen.js +++ b/src/engine/screen.js @@ -4,7 +4,7 @@ class Screen { constructor () { this._el = document.createElement('canvas') this.resize() - this._gl = this._el.getContext('webgl') + this._gl = this._el.getContext('webgl', { alpha: false }) if (!this._gl) { alert('Your machine or browser does not support WebGL!') diff --git a/src/index.js b/src/index.js index 36cedf0..3e2b2a5 100644 --- a/src/index.js +++ b/src/index.js @@ -1,34 +1,50 @@ import Engine from './engine' import Camera from './engine/camera' +import Resource from './engine/resource' import loadMesh from './engine/mesh/loader' import { Environment } from './engine/environment' import { LODTerrain } from './engine/components/terrain/lod' +import { Skybox } from './engine/components/skybox' import { SimplexHeightMap } from './engine/components/terrain/heightmap' -import { Material } from './engine/mesh/material' +import { Material, Texture } from './engine/mesh/material' +import { GUIRenderer, GUIImage, Dim4 } from './engine/gui' let game = new Engine() let env = new Environment() +let gui = new GUIRenderer() -// let t = 0 async function pipeline () { let entity = await loadMesh(game.gl, 'test') let shader = await game.shaders.createShaderFromFiles(game.gl, 'basic', false) let terrainShader = await game.shaders.createShaderFromFiles(game.gl, 'terrain', false) + let skyboxShader = await game.shaders.createShaderFromFiles(game.gl, 'skybox', false) entity.setRotation([0.0, 0.0, -90.0]) + // Initialize GUI + await gui.initialize(game) + let itms = [ + new GUIImage(await Texture.createTexture2D(game.gl, await Resource.loadImage('noisy.png'), false, game.gl.LINEAR), + new Dim4(-0.9, 0.0, 0.9, 0.0), new Dim4(0.1, 0.0, 0.1, 0.0)) + ] + // Create a height map based on OpenSimplex noise let hmap = new SimplexHeightMap(1, 1, 256, 50) - // Create a terrain + // Create a terrain instance let terrain = new LODTerrain([0.0, 0.0, 0.0], 1024, 1024, 850, 4) // Terrain material - let material = new Material() - material.textures = ['grass-1024.jpg'] + let material = new Material(['grass-1024.jpg']) await material.loadTextures(game.gl) + // test code + for (let i in entity.children) { + entity.children[i].mesh.material = material + } + + // Set generator and material for terrain terrain.setGenerator(hmap) terrain.setMaterial(material) @@ -36,7 +52,13 @@ async function pipeline () { let cam = new Camera([-200.0, 1.0, 0.0]) cam.updateProjection(game.gl) - // Update function for camera + // Create skybox + let skybox = new Skybox('skybox', cam.farPlane / 2) + + // Load textures and generate a mesh + await skybox.initialize(game.gl) + + // Update function for camera and terrain game.addUpdateFunction(function (dt) { if (game.input.isDown('w')) { cam.processKeyboard(0, dt) @@ -50,6 +72,7 @@ async function pipeline () { cam.processKeyboard(3, dt) } + // Panning if (game.input.mouseMoved && game.input.mouse.btn0) { cam.processMouseMove(game.input.mouseOffset) } @@ -57,10 +80,6 @@ async function pipeline () { // Update detail levels terrain.update(game.gl, cam) terrain.updateLODMesh(game.gl) - - // TESTING: Move model forward - // t = t + 0.1 - // entity.setPosition([t, 0.0, 0.0]) }) // Render function for the triangle @@ -80,6 +99,13 @@ async function pipeline () { // Draw terrain terrain.draw(gl, terrainShader) + // Draw the skybox + skyboxShader.use(gl) + skybox.draw(gl, skyboxShader, cam) + }) + + game.addRenderFunction(function (gl) { + gui.draw(gl, itms) }) game.startGameLoop()