diff --git a/assets/shaders/water.fs b/assets/shaders/water.fs new file mode 100644 index 0000000..8d41d7d --- /dev/null +++ b/assets/shaders/water.fs @@ -0,0 +1,28 @@ +precision mediump float; + +varying vec4 clipSpace; + +uniform sampler2D reflectionTexture; +uniform sampler2D refractionTexture; +uniform sampler2D dudvMap; + +varying vec2 uv; +const float waveStrength = 0.002; + +void main() { + vec2 ndc = (clipSpace.xy / clipSpace.w) / 2.0 + 0.5; + vec2 refractTexCoords = vec2(ndc.x, ndc.y); + vec2 reflectTexCoords = vec2(ndc.x, 1.0 - ndc.y); + + vec2 distortion1 = texture2D(dudvMap, uv).xy * 2.0 - 1.0 * waveStrength; + refractTexCoords += distortion1; + refractTexCoords = clamp(refractTexCoords, 0.001, 0.999); + reflectTexCoords += distortion1; + reflectTexCoords.x = clamp(reflectTexCoords.x, 0.001, 0.999); + reflectTexCoords.y = clamp(reflectTexCoords.y, -0.999, -0.001); + + vec4 reflectColor = texture2D(reflectionTexture, reflectTexCoords); + vec4 refractColor = texture2D(refractionTexture, refractTexCoords); + + gl_FragColor = mix(reflectColor, refractColor, 0.5); +} diff --git a/assets/shaders/water.vs b/assets/shaders/water.vs new file mode 100644 index 0000000..f675f8d --- /dev/null +++ b/assets/shaders/water.vs @@ -0,0 +1,18 @@ +precision mediump float; + +attribute vec2 aVertexPosition; + +uniform mat4 uModelMatrix; +uniform mat4 uViewMatrix; +uniform mat4 uProjectionMatrix; + +varying vec4 clipSpace; +varying vec2 uv; + +const float tiling = 6.0; + +void main() { + clipSpace = uProjectionMatrix * uViewMatrix * uModelMatrix * vec4(aVertexPosition.x, 0.0, aVertexPosition.y, 1.0); + gl_Position = clipSpace; + uv = vec2(aVertexPosition.x/2.0+0.5,aVertexPosition.y/2.0+0.5) * tiling; +} diff --git a/assets/textures/dudv.png b/assets/textures/dudv.png new file mode 100644 index 0000000..88e313f Binary files /dev/null and b/assets/textures/dudv.png differ diff --git a/src/engine/camera.js b/src/engine/camera.js index d139c2f..97a83af 100644 --- a/src/engine/camera.js +++ b/src/engine/camera.js @@ -1,3 +1,4 @@ +import Screen from './screen' import { Node } from './components' import { glMatrix, mat4, vec2, vec3 } from 'gl-matrix' @@ -101,8 +102,7 @@ class Camera extends Node { } updateProjection (gl) { - let aspect = gl.canvas.width / gl.canvas.height - mat4.perspective(this.projection, this.fov, aspect, this.nearPlane, this.farPlane) + mat4.perspective(this.projection, this.fov, Screen.aspectRatio, this.nearPlane, this.farPlane) } // Calculate the view matrix on-the-go diff --git a/src/engine/components/water/index.js b/src/engine/components/water/index.js new file mode 100644 index 0000000..0c496a7 --- /dev/null +++ b/src/engine/components/water/index.js @@ -0,0 +1,153 @@ +import { Node } from '../' +import { Mesh } from '../../mesh' +import { Texture } from '../../mesh/material' +import Screen from '../../screen' +import Resource from '../../resource' + +class WaterFBOs { + constructor (reflectionWidth = 320, reflectionHeight = 180, refractionWidth = 1280, refractionHeight = 720) { + this.reflectionHeight = reflectionHeight + this.reflectionWidth = reflectionWidth + this.refractionHeight = refractionHeight + this.refractionWidth = refractionWidth + } + + initialize (gl) { + this.initReflectionFrameBuffer(gl) + this.initRefractionFrameBuffer(gl) + } + + initReflectionFrameBuffer (gl) { + this.reflectionFrameBuffer = this.createFrameBuffer(gl) + this.reflectionTexture = this.createTextureAttachment(gl, this.reflectionWidth, this.reflectionHeight) + this.reflectionDepthBuffer = this.createDepthBufferAttachment(gl, this.reflectionWidth, this.reflectionHeight) + this.unbindFrameBuffer(gl) + } + + initRefractionFrameBuffer (gl) { + this.refractionFrameBuffer = this.createFrameBuffer(gl) + this.refractionTexture = this.createTextureAttachment(gl, this.refractionWidth, this.refractionHeight) + this.refractionDepthTexture = this.createDepthTextureAttachment(gl, this.refractionWidth, this.refractionHeight) + this.unbindFrameBuffer(gl) + } + + createFrameBuffer (gl) { + // generate frame buffer + let frameBuffer = gl.createFramebuffer() + // create the framebuffer + gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer) + // indicate that we will always render to color attachment 0 + gl.drawBuffers.drawBuffersWEBGL([gl.COLOR_ATTACHMENT0]) + return frameBuffer + } + + unbindFrameBuffer (gl) { + gl.bindFramebuffer(gl.FRAMEBUFFER, null) + gl.viewport(0, 0, Screen.width, Screen.height) + } + + bindFrameBuffer (gl, frameBuffer, width, height) { + gl.bindTexture(gl.TEXTURE_2D, null) // To make sure the texture isn't bound + gl.bindFramebuffer(gl.FRAMEBUFFER, frameBuffer) + gl.viewport(0, 0, width, height) + } + + // call before rendering to this FBO + bindReflectionFrameBuffer (gl) { + this.bindFrameBuffer(gl, this.reflectionFrameBuffer, this.reflectionWidth, this.reflectionHeight) + } + + // call before rendering to this FBO + bindRefractionFrameBuffer (gl) { + this.bindFrameBuffer(gl, this.refractionFrameBuffer, this.refractionWidth, this.refractionHeight) + } + + createTextureAttachment (gl, width, height) { + let texture = gl.createTexture() + gl.bindTexture(gl.TEXTURE_2D, texture) + gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, width, height, 0, gl.RGB, gl.UNSIGNED_BYTE, null) + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0) + return texture + } + + createDepthTextureAttachment (gl, width, height) { + let texture = gl.createTexture() + gl.bindTexture(gl.TEXTURE_2D, texture) + gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT, width, height, 0, gl.DEPTH_COMPONENT, gl.UNSIGNED_SHORT, null) + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) + gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, texture, 0) + return texture + } + + createDepthBufferAttachment (gl, width, height) { + let depthBuffer = gl.createRenderbuffer() + gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer) + gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, width, height) + gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer) + return depthBuffer + } +} + +class WaterTile extends Node { + constructor (pos, scale, rot) { + super(pos, [scale, 0.0, scale], rot) + this.fbos = new WaterFBOs() + } + + initialize (gl) { + this.mesh = Mesh.constructFromVertices(gl, [-1, -1, -1, 1, 1, -1, 1, -1, -1, 1, 1, 1], 2) + this.fbos.initialize(gl) + } + + async useDUDV (gl, file) { + this.dudv = await Texture.createTexture2D(gl, await Resource.loadImage(file + '.png'), false, gl.LINEAR) + } + + reflect (gl, cam, render) { + this.fbos.bindReflectionFrameBuffer(gl) + let dist = 2 * (cam.pos[1] - this.pos[1]) + cam.pos[1] -= dist + cam.rotation[1] *= -1 + cam.updateTransform() + render(gl) + this.fbos.unbindFrameBuffer(gl) + cam.pos[1] += dist + cam.rotation[1] *= -1 + cam.updateTransform() + } + + refract (gl, cam, render) { + this.fbos.bindRefractionFrameBuffer(gl) + render(gl) + this.fbos.unbindFrameBuffer(gl) + } + + draw (gl, shader) { + super.draw(gl, shader) + + const transformLocation = shader.getUniformLocation(gl, 'uModelMatrix') + gl.uniformMatrix4fv(transformLocation, false, this.transform) + + gl.activeTexture(gl.TEXTURE0) + gl.bindTexture(gl.TEXTURE_2D, this.fbos.reflectionTexture) + gl.activeTexture(gl.TEXTURE1) + gl.bindTexture(gl.TEXTURE_2D, this.fbos.refractionTexture) + if (this.dudv) { + gl.activeTexture(gl.TEXTURE2) + gl.bindTexture(gl.TEXTURE_2D, this.dudv.id) + } + + this.mesh.prepare(gl, shader) + this.mesh.draw(gl, shader) + this.mesh.postdraw(gl, shader) + } +} + +export { WaterTile, WaterFBOs } diff --git a/src/engine/framebuffer.js b/src/engine/framebuffer.js new file mode 100644 index 0000000..e69de29 diff --git a/src/engine/gui/font.js b/src/engine/gui/font.js index 00a2609..364eb3f 100644 --- a/src/engine/gui/font.js +++ b/src/engine/gui/font.js @@ -6,7 +6,6 @@ import { Node2D } from './' import { mat4 } from 'gl-matrix' -const aspectRatio = Screen.width / Screen.height const PAD_TOP = 0 const PAD_LEFT = 1 const PAD_BOTTOM = 2 @@ -46,6 +45,7 @@ class Word { } addCharacter (char) { + if (!char) return this.characters.push(char) this.width += char.xAdvance * this.fontSize } @@ -74,7 +74,6 @@ class Line { class FontFile { constructor (name) { this.name = name - this.values = {} this.metadata = {} } @@ -91,6 +90,7 @@ class FontFile { return result } + // Parse a .fnt file readValues (data) { let lines = data.split('\n') for (let i in lines) { @@ -101,9 +101,7 @@ class FontFile { for (let j in lineSplit) { let valuePairs = lineSplit[j].split('=') if (valuePairs.length === 2) { - let key = valuePairs[0] - let val = valuePairs[1] - lineValues[key] = val + lineValues[valuePairs[0]] = valuePairs[1] } } @@ -114,16 +112,11 @@ class FontFile { } else if (lineSplit[0] === 'common') { let lineHeightPixels = this.getValue(lineValues, 'lineHeight') - this.paddingHeight this.vertPerPixelSize = LINE_HEIGHT / lineHeightPixels - this.horizPixelSize = this.vertPerPixelSize / aspectRatio + this.horizPixelSize = this.vertPerPixelSize / Screen.aspectRatio + this.scaleWidth = this.getValue(lineValues, 'scaleW') } else if (lineSplit[0] === 'char') { - let c = this.loadCharacter(lineValues, this.getValue(this.values, 'scaleW')) + let c = this.loadCharacter(lineValues, this.scaleWidth) if (c) this.metadata[c.id] = c - continue - } else if (lineSplit[0] === 'kernings' || lineSplit[0] === 'kerning' || lineSplit[0] === 'chars') { - continue - } - for (let j in lineValues) { - this.values[j] = lineValues[j] } } } @@ -156,7 +149,6 @@ class FontFile { let load = await Resource.GET('/assets/fonts/' + fontName + '.fnt') let file = new FontFile(fontName) file.readValues(load) - console.log(file) return file } } @@ -168,21 +160,25 @@ class Font { this.texture = null } + // Load font data from a .fnt file and create a Font object with it static async fromFile (name) { let meta = await FontFile.fromFile(name) return new Font(name, meta) } + // Load font texture async loadTextures (gl) { this.texture = await Texture.createTexture2D(gl, await Resource.loadImage('/assets/fonts/' + this.name + '.png'), false, gl.LINEAR) } + // Create a renderable mesh for a text createTextMesh (gl, text) { let lines = this.createStructure(text) let data = this.createQuadVertices(text, lines) return Mesh.constructFromVerticesUVs(gl, data.vertices, data.textureCoords) } + // Create a structure of lines, words and characters in order to generate the vertices properly createStructure (text) { let chars = text.asCharacters let lines = [] @@ -221,6 +217,7 @@ class Font { return lines } + // Add final word completeStructure (lines, currentLine, currentWord, text) { let added = currentLine.attemptToAddWord(currentWord) if (!added) { @@ -232,6 +229,7 @@ class Font { return lines } + // Create text vertices createQuadVertices (text, lines) { text.lines = lines.length let cursorX = 0 @@ -259,6 +257,7 @@ class Font { return { vertices, textureCoords } } + // Create text vertex coordinates for a specific character addVerticesForCharacter (cursorX, cursorY, character, fontSize, vertices) { let x = cursorX + (character.xOffset * fontSize) let y = cursorY + (character.yOffset * fontSize) @@ -271,6 +270,7 @@ class Font { this.addVertices(vertices, properX, properY, properMaxX, properMaxY) } + // Create text vertex coordinates addVertices (vertices, x, y, maxX, maxY) { vertices.push(x) vertices.push(y) @@ -286,6 +286,7 @@ class Font { vertices.push(y) } + // Create text texture coordinates addTexCoords (texCoords, x, y, maxX, maxY) { texCoords.push(x) texCoords.push(y) @@ -312,6 +313,7 @@ class GUIText extends Node2D { this.color = [0.0, 0.0, 0.0] } + // Do not scale the transform like we do with regular GUIs updateTransform () { let matrix = mat4.create() mat4.translate(matrix, matrix, [this.pos[0], this.pos[2], 0.0]) @@ -343,6 +345,7 @@ class GUIText extends Node2D { return this.size[2] } + // Return all characters as their ascii code get asCharacters () { let chars = [] for (let i = 0; i < this.text.length; i++) { @@ -352,6 +355,8 @@ class GUIText extends Node2D { } draw (gl, shader) { + // Let only the font shader be used to render text + if (shader.name !== 'font') return super.draw(gl, shader) const transformLocation = shader.getUniformLocation(gl, 'uTransformation') const colorLocation = shader.getUniformLocation(gl, 'uColor') gl.uniformMatrix4fv(transformLocation, false, this.transform) @@ -369,8 +374,8 @@ class FontRenderer { for (let i in nodes) { let node = nodes[i] if (!(node instanceof GUIText)) { - if (node.children) { - textNodes.concat(this.discoverTextNodes(node.children)) + if (node.children.length) { + textNodes = textNodes.concat(this.discoverTextNodes(node.children)) } continue } @@ -379,7 +384,8 @@ class FontRenderer { return textNodes } - draw (gl, nodes) { + draw (gl, cam, nodes) { + // Discover all nodes in the array that are texts let fontPairs = {} let textNodes = this.discoverTextNodes(nodes) for (let i in textNodes) { @@ -399,6 +405,7 @@ class FontRenderer { } } + // Start rendering individual text arrays this.shader.use(gl) gl.enable(gl.BLEND) gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) @@ -406,8 +413,10 @@ class FontRenderer { for (let i in fontPairs) { let texts = fontPairs[i] let font = this.fonts[i] + // Set font's map as the texture gl.activeTexture(gl.TEXTURE0) gl.bindTexture(font.texture.type, font.texture.id) + // Draw all texts for (let j in texts) { let text = texts[j] text.draw(gl, this.shader) diff --git a/src/engine/gui/index.js b/src/engine/gui/index.js index cc9a0a1..a626621 100644 --- a/src/engine/gui/index.js +++ b/src/engine/gui/index.js @@ -175,7 +175,11 @@ class GUIImage extends Node2D { super.draw(gl, shader, quad) if (!this.active) return gl.activeTexture(gl.TEXTURE0) - gl.bindTexture(this.texture.type, this.texture.id) + if (!this.texture.type) { + gl.bindTexture(gl.TEXTURE_2D, this.texture) + } else { + gl.bindTexture(this.texture.type, this.texture.id) + } // Set transformation matrix const transformLocation = shader.getUniformLocation(gl, 'uTransformationMatrix') @@ -199,7 +203,7 @@ class GUIRenderer { return this.quad } - draw (gl, nodes) { + draw (gl, cam, nodes) { if (typeof nodes !== 'object') nodes = [ nodes ] this.shader.use(gl) this.quad.prepare(gl, this.shader) diff --git a/src/engine/index.js b/src/engine/index.js index 8bc09f5..90a71de 100644 --- a/src/engine/index.js +++ b/src/engine/index.js @@ -35,7 +35,7 @@ class Engine { return this.screen.gl } - render () { + prepare () { // Set clear color to black, fully opaque gl.clearColor(0.0, 0.0, 0.0, 1.0) @@ -48,7 +48,9 @@ class Engine { // Enable back-face culling gl.enable(gl.CULL_FACE) gl.cullFace(gl.BACK) + } + render () { // Render functions for (let i in this.rst) { this.rst[i](gl) @@ -98,4 +100,4 @@ class Engine { } } -export default Engine +export default new Engine() diff --git a/src/engine/screen.js b/src/engine/screen.js index 1941be2..4928241 100644 --- a/src/engine/screen.js +++ b/src/engine/screen.js @@ -11,11 +11,8 @@ class Screen { return } + this.loadExtensions() document.body.appendChild(this._el) - - window.addEventListener('resize', (e) => { - this.resize() - }, false) } get gl () { @@ -31,6 +28,12 @@ class Screen { this._el.height = window.innerHeight } + loadExtensions () { + this._gl.drawBuffers = this._gl.getExtension('WEBGL_draw_buffers') + this._gl.getExtension('WEBGL_depth_texture') + this._gl.getExtension('OES_texture_float') + } + get width () { return this._el.width } @@ -38,6 +41,10 @@ class Screen { get height () { return this._el.height } + + get aspectRatio () { + return this.width / this.height + } } export default new Screen() diff --git a/src/index.js b/src/index.js index b54b90b..322535e 100644 --- a/src/index.js +++ b/src/index.js @@ -6,12 +6,13 @@ 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 { WaterTile } from './engine/components/water' import { SimplexHeightMap } from './engine/components/terrain/heightmap' import { Material, Texture } from './engine/mesh/material' import { GUIRenderer, GUIImage, Dim4 } from './engine/gui' import { FontRenderer, GUIText, Font } from './engine/gui/font' -let game = new Engine() +let game = Engine let env = new Environment() let gui = new GUIRenderer() let fnt = new FontRenderer() @@ -21,9 +22,14 @@ async function pipeline () { 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) + // let waterShader = await game.shaders.createShaderFromFiles(game.gl, 'water', false) entity.setRotation([0.0, 0.0, -90.0]) + // let water = new WaterTile([100.0, 0.0, 100.0], 100.0) + // water.initialize(game.gl) + // await water.useDUDV(game.gl, 'dudv') + let arialFont = await Font.fromFile('arial') await arialFont.loadTextures(game.gl) @@ -33,10 +39,12 @@ async function pipeline () { 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)), - new GUIText('this is example text!\nmulti line!', arialFont, 2, new Dim4(0.1, 0.0, -0.2, 0.0), new Dim4(1.0, 0.0, 0.3, 0.0), true) + new Dim4(-0.9, 0.0, 0.9, 0.0), new Dim4(0.1, 0.0, 0.1, 0.0)) ] - itms[1].color = [1.0, 0.0, 0.2] + // Nesting test + itms[0].addChild(new GUIText('this is example text!\nmulti line!', arialFont, 1.5, new Dim4(0.1, 0.0, -0.1, 0.0), new Dim4(1.0, 0.0, 0.3, 0.0), false)) + itms[0].children[0].color = [1.0, 0.0, 0.2] + itms[0].updateTransform() // Create a height map based on OpenSimplex noise let hmap = new SimplexHeightMap(1, 1, 256, 50) @@ -58,7 +66,7 @@ async function pipeline () { terrain.setMaterial(material) // Create and initialize the camera - let cam = new Camera([-200.0, 1.0, 0.0]) + let cam = new Camera([-32.0, 100.0, -32.0], [0.8, -0.6, 0.0]) cam.updateProjection(game.gl) // Create skybox @@ -67,6 +75,9 @@ async function pipeline () { // Load textures and generate a mesh await skybox.initialize(game.gl) + // itms.push(new GUIImage(water.fbos.reflectionTexture, new Dim4(-0.9, 0.0, 0.9, 0.0), new Dim4(0.1, 0.0, 0.1, 0.0))) + // itms.push(new GUIImage(water.fbos.refractionTexture, new Dim4(-0.3, 0.0, 0.9, 0.0), new Dim4(0.1, 0.0, 0.1, 0.0))) + // Update function for camera and terrain game.addUpdateFunction(function (dt) { if (game.input.isDown('w')) { @@ -91,8 +102,9 @@ async function pipeline () { terrain.updateLODMesh(game.gl) }) - // Render function for the triangle - game.addRenderFunction(function (gl) { + function drawEverything (gl) { + game.prepare() + shader.use(gl) cam.draw(gl, shader) entity.draw(gl, shader) @@ -108,14 +120,26 @@ async function pipeline () { // Draw terrain terrain.draw(gl, terrainShader) + // Draw the skybox skyboxShader.use(gl) skybox.draw(gl, skyboxShader, cam) - }) + } + // Render function for the triangle game.addRenderFunction(function (gl) { - gui.draw(gl, itms) - fnt.draw(gl, itms) + // water.reflect(gl, cam, drawEverything) + // water.refract(gl, cam, drawEverything) + + drawEverything(gl) + + // waterShader.use(gl) + // cam.draw(gl, waterShader) + // water.draw(gl, waterShader) + + // Draw GUIs + gui.draw(gl, cam, itms) + fnt.draw(gl, cam, itms) }) game.startGameLoop()