diff --git a/assets/shaders/particles.fs b/assets/shaders/particles.fs new file mode 100644 index 0000000..d654f80 --- /dev/null +++ b/assets/shaders/particles.fs @@ -0,0 +1,13 @@ +precision mediump float; + +varying vec2 uv1; +varying vec2 uv2; +varying float blend; + +uniform sampler2D texture0; + +void main() { + vec4 color1 = texture2D(texture0, uv1); + vec4 color2 = texture2D(texture0, uv2); + gl_FragColor = mix(color1, color2, blend); +} diff --git a/assets/shaders/particles.vs b/assets/shaders/particles.vs new file mode 100644 index 0000000..30f7882 --- /dev/null +++ b/assets/shaders/particles.vs @@ -0,0 +1,28 @@ +precision mediump float; + +attribute vec2 aVertexPosition; + +uniform mat4 uModelMatrix; +uniform mat4 uViewMatrix; +uniform mat4 uProjectionMatrix; + +varying vec2 uv1; +varying vec2 uv2; +varying float blend; + +uniform vec2 uAtlasOffset1; +uniform vec2 uAtlasOffset2; +uniform vec2 uAtlasInfo; + +void main() { + + vec2 uv = aVertexPosition + vec2(0.5, 0.5); + uv.y = 1.0 - uv.y; + uv /= uAtlasInfo.x; + uv1 = uv + uAtlasOffset1; + uv2 = uv + uAtlasOffset2; + blend = uAtlasInfo.y; + + vec4 worldPosition = uModelMatrix * vec4(aVertexPosition, 0.0, 1.0); + gl_Position = uProjectionMatrix * uViewMatrix * worldPosition; +} diff --git a/assets/textures/particleAtlas.png b/assets/textures/particleAtlas.png new file mode 100644 index 0000000..c202947 Binary files /dev/null and b/assets/textures/particleAtlas.png differ diff --git a/src/engine/components/particles/index.js b/src/engine/components/particles/index.js new file mode 100644 index 0000000..ee5db7a --- /dev/null +++ b/src/engine/components/particles/index.js @@ -0,0 +1,159 @@ +import { Mesh } from '../../mesh' +import { mat4 } from 'gl-matrix' + +class ParticleTexture { + constructor (texture, rows) { + this.texture = texture + this.rows = rows + } + + get id () { + return this.texture.id + } +} + +class Particle { + constructor (texture, pos, vel, gravity, life, rot, scale) { + this.texture = texture + this.pos = pos + this.vel = vel + this.gravity = gravity + this.life = life + this.rot = rot + this.scale = scale + this.elapsed = 0 + + this.texOffset1 = [0.0, 0.0] + this.texOffset2 = [0.0, 0.0] + this.texBlend = 0.0 + + this.distance = 0 + } + + _createOffset (index) { + let col = Math.floor(index % this.texture.rows) + let row = Math.floor(index / this.texture.rows) + return [ col / this.texture.rows, row / this.texture.rows ] + } + + _updateTexture () { + let lifeFactor = this.elapsed / this.life + let stages = this.texture.rows * this.texture.rows + let progression = lifeFactor * stages + let index1 = Math.floor(progression) + let index2 = (index1 < stages - 1) ? index1 + 1 : index1 + this.texBlend = progression % 1 + this.texOffset1 = this._createOffset(index1) + this.texOffset2 = this._createOffset(index2) + } + + update (dt, gravity) { + this.vel[1] += gravity * this.gravity * dt + let change = [this.vel[0] * dt, this.vel[1] * dt, this.vel[2] * dt] + this.pos = [this.pos[0] + change[0], this.pos[1] + change[1], this.pos[2] + change[2]] + this._updateTexture() + this.elapsed += dt + return this.elapsed < this.life + } +} + +class ParticleSystem { + constructor (renderer) { + this.renderer = renderer + this.particles = [] + } + + add (particle) { + this.particles.push(particle) + } + + update (dt, gravity) { + let alive = [] + for (let i in this.particles) { + let particle = this.particles[i] + let stillAlive = particle.update(dt, gravity) + if (!stillAlive) continue + alive.push(particle) + } + this.particles = alive + } + + draw (gl, cam) { + if (!this.renderer) return + this.renderer.draw(gl, this, cam) + } +} + +class ParticleRenderer { + async initialize (game) { + this.shader = await game.shaders.createShaderFromFiles(game.gl, 'particles', false) + this.createMesh(game.gl) + } + + createMesh (gl) { + this.mesh = Mesh.constructFromVertices(gl, [-0.5, 0.5, -0.5, -0.5, 0.5, 0.5, 0.5, -0.5], 2) + } + + createModelMatrix (shader, viewMatrix, particle) { + let modelMatrix = mat4.create() + mat4.translate(modelMatrix, modelMatrix, particle.pos) + modelMatrix[0] = viewMatrix[0] + modelMatrix[1] = viewMatrix[4] + modelMatrix[2] = viewMatrix[8] + modelMatrix[4] = viewMatrix[1] + modelMatrix[5] = viewMatrix[5] + modelMatrix[6] = viewMatrix[9] + modelMatrix[8] = viewMatrix[2] + modelMatrix[10] = viewMatrix[10] + mat4.rotate(modelMatrix, modelMatrix, particle.rot * Math.PI / 180, [0.0, 0.0, 1.0]) + mat4.scale(modelMatrix, modelMatrix, [particle.scale, particle.scale, particle.scale]) + return modelMatrix + } + + draw (gl, particles, cam) { + particles = (particles instanceof ParticleSystem) ? particles.particles : particles + let textures = [] + for (let i in particles) { + let particle = particles[i] + if (textures.indexOf(particle.texture) !== -1) continue + textures.push(particle.texture) + } + + this.shader.use(gl) + const modelLoc = this.shader.getUniformLocation(gl, 'uModelMatrix') + const off1Loc = this.shader.getUniformLocation(gl, 'uAtlasOffset1') + const off2Loc = this.shader.getUniformLocation(gl, 'uAtlasOffset2') + const atlasLoc = this.shader.getUniformLocation(gl, 'uAtlasInfo') + + cam.draw(gl, this.shader) + + gl.enable(gl.BLEND) + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA) + gl.depthMask(false) + + this.mesh.prepare(gl, this.shader) + for (let i in textures) { + let texture = textures[i] + gl.activeTexture(gl.TEXTURE0) + gl.bindTexture(gl.TEXTURE_2D, texture.id) + for (let j in particles) { + let particle = particles[j] + if (particle.texture !== texture) continue + + let mat = this.createModelMatrix(this.shader, cam.view, particle) + gl.uniformMatrix4fv(modelLoc, false, mat) + gl.uniform2fv(off1Loc, particle.texOffset1) + gl.uniform2fv(off2Loc, particle.texOffset2) + gl.uniform2fv(atlasLoc, [texture.rows, particle.texBlend]) + + this.mesh.draw(gl, this.shader, gl.TRIANGLE_STRIP) + } + } + this.mesh.postdraw(gl, this.shader) + + gl.disable(gl.BLEND) + gl.depthMask(true) + } +} + +export { ParticleTexture, Particle, ParticleSystem, ParticleRenderer } diff --git a/src/engine/components/water/index.js b/src/engine/components/water/index.js index 25305cd..636cd57 100644 --- a/src/engine/components/water/index.js +++ b/src/engine/components/water/index.js @@ -115,11 +115,11 @@ class WaterTile extends Node { } async useDUDVMap (gl, file) { - this.dudvMap = await Texture.createTexture2D(gl, await Resource.loadImage(file + '.png'), false, gl.LINEAR) + this.dudvMap = await Texture.fromFile(gl, file + '.png', false, gl.LINEAR) } async useNormalMap (gl, file) { - this.normalMap = await Texture.createTexture2D(gl, await Resource.loadImage(file + '.png'), false, gl.LINEAR) + this.normalMap = await Texture.fromFile(gl, file + '.png', false, gl.LINEAR) } reflect (gl, cam, render) { diff --git a/src/engine/index.js b/src/engine/index.js index 90a71de..f4d8d40 100644 --- a/src/engine/index.js +++ b/src/engine/index.js @@ -58,12 +58,12 @@ class Engine { } update (dt) { - this.input.update() - // Updates for (let i in this.ust) { this.ust[i](dt) } + + this.input.update() } step () { diff --git a/src/engine/mesh/material.js b/src/engine/mesh/material.js index cae2f35..eecb952 100644 --- a/src/engine/mesh/material.js +++ b/src/engine/mesh/material.js @@ -1,7 +1,12 @@ import Resource from '../resource' class Texture { - static createTexture2D (gl, img, flip = true, filtering, repeat = gl.REPEAT) { + static async fromFile (gl, file, flip, filtering, repeat) { + let image = await Resource.loadImage(file) + return Texture.createTexture2D(gl, image, flip, filtering, repeat) + } + + static createTexture2D (gl, img, flip = false, filtering, repeat = gl.REPEAT) { let tex = gl.createTexture() gl.bindTexture(gl.TEXTURE_2D, tex) gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, flip) @@ -63,8 +68,7 @@ class Material { for (let ti in this.textures) { let tex = this.textures[ti] if (!(tex instanceof Texture)) { - let teximg = await Resource.loadImage(tex) - let result = Texture.createTexture2D(gl, teximg) + let result = await Texture.fromFile(gl, tex, true) this.textures[ti] = result } } diff --git a/src/engine/utility.js b/src/engine/utility.js index b7df9d1..000fe23 100644 --- a/src/engine/utility.js +++ b/src/engine/utility.js @@ -1,5 +1,9 @@ import { vec3 } from 'gl-matrix' +export function randomInt (min, max) { + return Math.floor(Math.random() * (max - min + 1) + min) +} + export function clamp (num, min, max) { return Math.min(Math.max(num, min), max) } diff --git a/src/index.js b/src/index.js index 1297983..9f10d6f 100644 --- a/src/index.js +++ b/src/index.js @@ -1,12 +1,14 @@ import Engine from './engine' import Camera from './engine/camera' -import Resource from './engine/resource' import loadMesh from './engine/mesh/loader' +import { randomInt } from './engine/utility' + 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 { Particle, ParticleTexture, ParticleSystem, ParticleRenderer } from './engine/components/particles' import { SimplexHeightMap } from './engine/components/terrain/heightmap' import { Material, Texture } from './engine/mesh/material' import { GUIRenderer, GUIImage, Dim4 } from './engine/gui' @@ -16,6 +18,7 @@ let game = Engine let env = new Environment() let gui = new GUIRenderer() let fnt = new FontRenderer() +let prt = new ParticleRenderer() async function pipeline () { let entity = await loadMesh(game.gl, 'test') @@ -38,8 +41,15 @@ async function pipeline () { await gui.initialize(game) await fnt.initialize(game) + // Initialize particles + await prt.initialize(game) + let particleSystem = new ParticleSystem(prt) + + // Particle texture atlas + let particleTexture = new ParticleTexture(await Texture.fromFile(game.gl, 'particleAtlas.png'), 4) + let itms = [ - new GUIImage(await Texture.createTexture2D(game.gl, await Resource.loadImage('noisy.png'), false, game.gl.LINEAR), + new GUIImage(await Texture.fromFile(game.gl, '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)) ] // Nesting test @@ -94,6 +104,19 @@ async function pipeline () { cam.processMouseMove(game.input.mouseOffset) } + // Particles + particleSystem.update(dt, -50) + + if (game.input.isDown('y')) { + let velocity = 20 + for (let i = 0; i < 360; i += 15) { + let rad1 = i * Math.PI / 180 + let x1 = (Math.cos(rad1) * velocity) + randomInt(-5, 5) + let y1 = (Math.sin(rad1) * velocity) + randomInt(-5, 5) + particleSystem.add(new Particle(particleTexture, [0.0, 0.0, 0.0], [x1, randomInt(-velocity, velocity), y1], 0.2, 2, randomInt(0, 360), randomInt(0.1, 1))) + } + } + // Update detail levels terrain.update(game.gl, cam) terrain.updateLODMesh(game.gl) @@ -136,6 +159,9 @@ async function pipeline () { cam.draw(gl, waterShader) water.draw(gl, waterShader, cam, env.sun) + // Draw particles + particleSystem.draw(gl, cam) + // Draw GUIs gui.draw(gl, cam, itms) fnt.draw(gl, cam, itms)