start using git cuz this thing is getting pretty big

This commit is contained in:
Evert Prants 2018-11-25 19:44:32 +02:00
commit e4082406c9
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
30 changed files with 47260 additions and 0 deletions

15
.babelrc Normal file
View File

@ -0,0 +1,15 @@
{
"presets": ["@babel/preset-env"],
"plugins": [
[
"@babel/plugin-transform-runtime",
{
"corejs": false,
"helpers": true,
"regenerator": true,
"useESModules": false
}
]
]
}

14
.editorconfig Normal file
View File

@ -0,0 +1,14 @@
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
# Matches multiple files with brace expansion notation
# Set default charset
[*.js]
charset = utf-8
indent_style = space
indent_size = 2

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/node_modules/
/dist/
/package-lock.json

9090
assets/dna.txt Normal file

File diff suppressed because it is too large Load Diff

36461
assets/models/test.json Normal file

File diff suppressed because it is too large Load Diff

9
assets/shaders/basic.fs Normal file
View File

@ -0,0 +1,9 @@
precision mediump float;
varying vec2 uv;
uniform sampler2D texture0;
void main() {
gl_FragColor = texture2D(texture0, uv);
}

16
assets/shaders/basic.vs Normal file
View File

@ -0,0 +1,16 @@
precision mediump float;
attribute vec3 aVertexPosition;
attribute vec2 aTexCoords;
uniform mat4 uModelMatrix;
uniform mat4 uViewMatrix;
uniform mat4 uProjectionMatrix;
varying vec2 uv;
void main() {
vec4 worldPosition = uModelMatrix * vec4(aVertexPosition, 1.0);
gl_Position = uProjectionMatrix * uViewMatrix * worldPosition;
uv = aTexCoords;
}

44
assets/shaders/terrain.fs Normal file
View File

@ -0,0 +1,44 @@
precision highp float;
varying vec2 uv;
varying vec3 toLightVector[8];
varying vec3 surfaceNormal;
varying vec3 toCameraVector;
uniform sampler2D texture0;
uniform vec3 lightColor[8];
uniform vec3 attenuation[8];
void main() {
float reflectivity = 0.0;
float shineDamper = 0.0;
vec3 unitNormal = normalize(surfaceNormal);
vec3 unitVectorToCamera = normalize(toCameraVector);
vec3 totalDiffuse = vec3(0.0);
vec3 totalSpecular = vec3(0.0);
for(int i=0;i<8;i++){
float distance = length(toLightVector[i]);
float attFactor = attenuation[i].x + (attenuation[i].y * distance) + (attenuation[i].z * distance * distance);
vec3 unitLightVector = normalize(toLightVector[i]);
float nDotl = dot(unitNormal,unitLightVector);
float brightness = max(nDotl,0.0);
vec3 lightDirection = -unitLightVector;
vec3 reflectedLightDirection = reflect(lightDirection, unitNormal);
float specularFactor = dot(reflectedLightDirection, unitVectorToCamera);
specularFactor = max(specularFactor,0.0);
float dampedFactor = pow(specularFactor,shineDamper);
totalDiffuse = totalDiffuse + (brightness * lightColor[i]) / attFactor;
totalSpecular = totalSpecular + (dampedFactor * reflectivity * lightColor[i]) / attFactor;
}
totalDiffuse = max(totalDiffuse,0.2);
vec4 textureColor = texture2D(texture0, uv * 40.0);
if(textureColor.a<0.5){
discard;
}
gl_FragColor = vec4(totalDiffuse,1.0) * textureColor + vec4(totalSpecular,1.0);
}

67
assets/shaders/terrain.vs Normal file
View File

@ -0,0 +1,67 @@
precision mediump float;
attribute vec3 aVertexPosition;
attribute vec3 aNormal;
attribute vec2 aTexCoords;
uniform mat4 uModelMatrix;
uniform mat4 uViewMatrix;
uniform mat4 uProjectionMatrix;
uniform vec3 lightPosition[8];
varying vec2 uv;
varying vec3 toLightVector[8];
varying vec3 surfaceNormal;
varying vec3 toCameraVector;
mat4 inverse(mat4 m) {
float
a00 = m[0][0], a01 = m[0][1], a02 = m[0][2], a03 = m[0][3],
a10 = m[1][0], a11 = m[1][1], a12 = m[1][2], a13 = m[1][3],
a20 = m[2][0], a21 = m[2][1], a22 = m[2][2], a23 = m[2][3],
a30 = m[3][0], a31 = m[3][1], a32 = m[3][2], a33 = m[3][3],
b00 = a00 * a11 - a01 * a10,
b01 = a00 * a12 - a02 * a10,
b02 = a00 * a13 - a03 * a10,
b03 = a01 * a12 - a02 * a11,
b04 = a01 * a13 - a03 * a11,
b05 = a02 * a13 - a03 * a12,
b06 = a20 * a31 - a21 * a30,
b07 = a20 * a32 - a22 * a30,
b08 = a20 * a33 - a23 * a30,
b09 = a21 * a32 - a22 * a31,
b10 = a21 * a33 - a23 * a31,
b11 = a22 * a33 - a23 * a32,
det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
return mat4(
a11 * b11 - a12 * b10 + a13 * b09,
a02 * b10 - a01 * b11 - a03 * b09,
a31 * b05 - a32 * b04 + a33 * b03,
a22 * b04 - a21 * b05 - a23 * b03,
a12 * b08 - a10 * b11 - a13 * b07,
a00 * b11 - a02 * b08 + a03 * b07,
a32 * b02 - a30 * b05 - a33 * b01,
a20 * b05 - a22 * b02 + a23 * b01,
a10 * b10 - a11 * b08 + a13 * b06,
a01 * b08 - a00 * b10 - a03 * b06,
a30 * b04 - a31 * b02 + a33 * b00,
a21 * b02 - a20 * b04 - a23 * b00,
a11 * b07 - a10 * b09 - a12 * b06,
a00 * b09 - a01 * b07 + a02 * b06,
a31 * b01 - a30 * b03 - a32 * b00,
a20 * b03 - a21 * b01 + a22 * b00) / det;
}
void main() {
vec4 worldPosition = uModelMatrix * vec4(aVertexPosition, 1.0);
gl_Position = uProjectionMatrix * uViewMatrix * worldPosition;
uv = aTexCoords;
surfaceNormal = (uModelMatrix * vec4(aNormal,0.0)).xyz;
for(int i=0;i<8;i++){
toLightVector[i] = lightPosition[i] - worldPosition.xyz;
}
toCameraVector = (inverse(uViewMatrix) * vec4(0.0,0.0,0.0,1.0)).xyz - worldPosition.xyz;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
assets/textures/noisy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

10
index.html Normal file
View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<style type="text/css">*{margin:0;padding:0;}body{width:100%;height:100%;}body,html{overflow:hidden;}</style>
<title>Trotland</title>
</head>
<body>
</body>
</html>

34
package.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "trotland-game",
"version": "0.0.1",
"description": "Trotland 3D MMORPG",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"serve": "node ./serve.js",
"build": "webpack -p",
"watch": "webpack -w --mode=development"
},
"keywords": [
"game",
"webgl"
],
"private": true,
"author": "Evert \"Diamond\" Prants <evert@lunasqu.ee>",
"license": "LGPL-3.0-or-later",
"devDependencies": {
"@babel/core": "^7.1.6",
"@babel/plugin-transform-runtime": "^7.1.0",
"@babel/preset-env": "^7.1.6",
"babel-loader": "^8.0.4",
"express": "^4.16.4",
"html-webpack-plugin": "^3.2.0",
"standard": "^12.0.1",
"webpack": "^4.26.0",
"webpack-command": "^0.4.2"
},
"dependencies": {
"@babel/runtime": "^7.1.5",
"gl-matrix": "^2.8.1",
"open-simplex-noise": "^1.5.0"
}
}

11
serve.js Executable file
View File

@ -0,0 +1,11 @@
#!/usr/bin/env node
const express = require('express')
const path = require('path')
const app = express()
app.use('/assets', express.static(path.join(__dirname, 'assets')))
app.use('/', express.static(path.join(__dirname, 'dist')))
app.listen(3000, function () {
console.log('server listening on 0.0.0.0:3000')
})

126
src/engine/camera.js Normal file
View File

@ -0,0 +1,126 @@
import { Node } from './mesh'
import { glMatrix, mat4, vec2, vec3 } from 'gl-matrix'
const SPEED = 100.0
const SENSITIVTY = 100.0
const FOV = 45.0
const ZNEAR = 0.1
const ZFAR = 1000.0
class Camera extends Node {
constructor (pos, rotation) {
super(pos, rotation)
this.fov = FOV
this.speed = SPEED
this.sensitivity = SENSITIVTY
// Create an empty projection matrix
this.projection = mat4.create()
// Helping vectors for calculating the view matrix
this.up = vec3.create()
this.front = vec3.fromValues(0.0, 0.0, -1.0)
this.right = vec3.create()
this.worldUp = vec3.fromValues(0.0, 1.0, 0.0)
this.updateTransform()
}
processKeyboard (direction, delta) {
let newSpeed = this.speed * delta
let velocity = vec3.fromValues(newSpeed, newSpeed, newSpeed)
let vec = vec3.create()
if (direction === 0) {
vec3.multiply(vec, this.front, velocity)
vec3.add(this.pos, this.pos, vec)
}
if (direction === 1) {
vec3.multiply(vec, this.front, velocity)
vec3.sub(this.pos, this.pos, vec)
}
if (direction === 2) {
vec3.multiply(vec, this.right, velocity)
vec3.sub(this.pos, this.pos, vec)
}
if (direction === 3) {
vec3.multiply(vec, this.right, velocity)
vec3.add(this.pos, this.pos, vec)
}
}
processMouseMove (offset, constrain = true) {
let fst = vec2.fromValues(offset.x * this.sensitivity, offset.y * this.sensitivity)
this.rotation[0] += glMatrix.toRadian(fst[0])
this.rotation[1] += glMatrix.toRadian(fst[1])
// Make sure that when pitch is out of bounds, screen doesn't get flipped
if (constrain) {
if (this.rotation[1] > glMatrix.toRadian(89.0)) {
this.rotation[1] = glMatrix.toRadian(89.0)
}
if (this.rotation[1] < -glMatrix.toRadian(89.0)) {
this.rotation[1] = -glMatrix.toRadian(89.0)
}
}
this.updateTransform()
}
// Calculate the vertices required for the view matrix
updateTransform () {
// Prevent premature call (from super class)
if (!this.front || !this.worldUp) return
// Calculate the new Front vector
let front = vec3.create()
front[0] = Math.cos(this.rotation[0]) * Math.cos(this.rotation[1])
front[1] = Math.sin(this.rotation[1])
front[2] = Math.sin(this.rotation[0]) * Math.cos(this.rotation[1])
vec3.normalize(this.front, front)
// Also re-calculate the Right and Up vector
// Normalize the vectors, because their length gets closer to 0 the more you look up or down which results in slower movement.
let rightCross = vec3.create()
let upCross = vec3.create()
vec3.cross(rightCross, this.front, this.worldUp)
vec3.normalize(this.right, rightCross)
vec3.cross(upCross, this.right, this.front)
vec3.normalize(this.up, upCross)
}
updateProjection (gl) {
let aspect = gl.canvas.width / gl.canvas.height
mat4.perspective(this.projection, this.fov, aspect, ZNEAR, ZFAR)
}
// Calculate the view matrix on-the-go
// Really no advantage in storing this
get view () {
let mat = mat4.create()
let center = vec3.create()
vec3.add(center, this.pos, this.front)
mat4.lookAt(mat, this.pos, center, this.up)
return mat
}
// Override the default draw method because we don't need to draw the camera,
// instead set the projection and view matrices
draw (gl, shader) {
const projloc = shader.getUniformLocation(gl, 'uProjectionMatrix')
const viewloc = shader.getUniformLocation(gl, 'uViewMatrix')
gl.uniformMatrix4fv(projloc, false, this.projection)
gl.uniformMatrix4fv(viewloc, false, this.view)
}
}
export default Camera

View File

@ -0,0 +1,72 @@
import OpenSimplexNoise from 'open-simplex-noise'
import { vec3 } from 'gl-matrix'
import Resource from '../../resource'
class HeightMap {
constructor (size) {
this.size = size
}
static async fromFile (file, amplitude) {
let img = await Resource.loadImage(file)
if (img.width / img.height !== 1) throw new Error('Height Map needs to be of 1:1 aspect ratio.')
let hmap = new HeightMap(img.width)
let sampler = Resource.imageToSampler(img)
for (let x = 0; x < img.width; x++) {
for (let y = 0; y < img.width; y++) {
hmap['h' + x + ';' + y] = (sampler(x, y)[0] / 255 * 2 - 1) * amplitude
}
}
sampler = null
return hmap
}
getHeight (x, y) {
if (x > this.size || y > this.size || x < 0 || y < 0) return 0
if (!this['h' + x + ';' + y]) return 0
return this['h' + x + ';' + y]
}
getNormal (x, y) {
let hL = this.getHeight(x - 1, y)
let hR = this.getHeight(x + 1, y)
let hD = this.getHeight(x, y - 1)
let hU = this.getHeight(x, y + 1)
let normal = vec3.fromValues(hL - hR, 2.0, hD - hU)
vec3.normalize(normal, normal)
return normal
}
}
class SimplexHeightMap extends HeightMap {
constructor (offsetX, offsetY, size, seed) {
super(size)
this.ix = offsetX
this.iy = offsetY
this.seed = seed
this.osn = new OpenSimplexNoise(seed)
}
getNoise (relX, relY) {
let x = ((this.ix * this.size) + relX) / this.size - 0.5
let y = ((this.iy * this.size) + relY) / this.size - 0.5
let total = this.osn.noise2D(2 * x, 2 * y) +
0.5 * this.osn.noise2D(4 * x, 4 * y) +
0.25 * this.osn.noise2D(2 * x, 2 * y)
total *= 10
return total
}
getHeight (x, y) {
return this.getNoise(x, y)
}
}
export { HeightMap, SimplexHeightMap }

View File

@ -0,0 +1,67 @@
import { Mesh, Node } from '../../mesh'
class Terrain extends Node {
constructor (pos, sWidth, sHeight) {
super(pos)
this.width = sWidth
this.height = sHeight
this.mesh = null
}
createMesh (gl, heightMap) {
let VERTICES = heightMap.size
let count = VERTICES * VERTICES
let vertices = new Array(count * 3)
let normals = new Array(count * 3)
let textureCoords = new Array(count * 2)
let indices = new Array(6 * (VERTICES - 1) * (VERTICES - 1))
let vertexPointer = 0
for (let i = 0; i < VERTICES; i++) {
for (let j = 0; j < VERTICES; j++) {
vertices[vertexPointer * 3] = j / (VERTICES - 1) * this.width
vertices[vertexPointer * 3 + 1] = heightMap.getHeight(j, i)
vertices[vertexPointer * 3 + 2] = i / (VERTICES - 1) * this.height
let normal = heightMap.getNormal(j, i)
normals[vertexPointer * 3] = normal[0]
normals[vertexPointer * 3 + 1] = normal[1]
normals[vertexPointer * 3 + 2] = normal[2]
textureCoords[vertexPointer * 2] = j / (VERTICES - 1)
textureCoords[vertexPointer * 2 + 1] = i / (VERTICES - 1)
vertexPointer++
}
}
let pointer = 0
for (let gz = 0; gz < VERTICES - 1; gz++) {
for (let gx = 0; gx < VERTICES - 1; gx++) {
let topLeft = (gz * VERTICES) + gx
let topRight = topLeft + 1
let bottomLeft = ((gz + 1) * VERTICES) + gx
let bottomRight = bottomLeft + 1
indices[pointer++] = topLeft
indices[pointer++] = bottomLeft
indices[pointer++] = topRight
indices[pointer++] = topRight
indices[pointer++] = bottomLeft
indices[pointer++] = bottomRight
}
}
this.mesh = Mesh.construct(gl, vertices, indices, textureCoords, normals)
}
setMaterial (mat) {
this.mesh.material = mat
}
draw (gl, shader) {
if (!this.mesh) return
super.draw(gl, shader)
this.mesh.draw(gl, shader)
}
}
export { Terrain }

102
src/engine/environment.js Normal file
View File

@ -0,0 +1,102 @@
const ENV_MAX_LIGHTS = 8
class Light {
constructor (pos, color, attenuation = [1.0, 0.0, 0.0]) {
this.pos = pos
this.color = color
this.attenuation = attenuation
}
setPosition (pos) {
this.pos = pos
}
setColor (col) {
this.color = col
}
setAttenuation (attn) {
this.attenuation = attn
}
}
class SpotLight extends Light {
constructor (pos, dir, color, attenuation) {
super(pos, color, attenuation)
this.dir = dir
}
}
class DirectionalLight extends SpotLight {
constructor (pos, dir, color) {
super(pos, dir, color, [1.0, 0.0, 0.0])
}
}
class Environment {
constructor (ambient) {
this.ambient = ambient
this.fogStart = 0
this.fogEnd = 0
this.fogColor = [0.8, 0.8, 0.8]
this.sun = new DirectionalLight([0.0, 1000.0, 2000.0], [-1.0, -1.0, 0.0], [1.0, 1.0, 1.0])
this.lights = [ this.sun ]
this.maxEnvironmentLights = ENV_MAX_LIGHTS
}
addLight (l) {
if (!(l instanceof Light)) return
this.lights.push(l)
}
addSpotLight (pos, dir, color, attenuation) {
this.lights.push(new SpotLight(pos, dir, color, attenuation))
}
addPointLight (pos, color, attenuation) {
this.lights.push(new Light(pos, color, attenuation))
}
setSunlight (pos, dir, color, attenuation) {
this.sun = new DirectionalLight(pos, dir, color, attenuation)
this.lights[0] = this.sun
}
setAmbient (color) {
this.ambient = color
}
setFog (color, start, end) {
this.fogColor = color
this.fogStart = start
this.fogEnd = end
}
setMaxLights (num) {
this.maxEnvironmentLights = num
}
draw (gl, shader) {
for (let i = 0; i < this.maxEnvironmentLights; i++) {
let lightColor = shader.getUniformLocation(gl, 'lightColor[' + i + ']')
let lightPosition = shader.getUniformLocation(gl, 'lightPosition[' + i + ']')
let lightAttn = shader.getUniformLocation(gl, 'attenuation[' + i + ']')
if (this.lights[i]) {
gl.uniform3fv(lightColor, this.lights[i].color)
gl.uniform3fv(lightPosition, this.lights[i].pos)
gl.uniform3fv(lightAttn, this.lights[i].attenuation)
} else {
gl.uniform3fv(lightColor, [0.0, 0.0, 0.0])
gl.uniform3fv(lightPosition, [0.0, 0.0, 0.0])
gl.uniform3fv(lightAttn, [1.0, 0.0, 0.0])
}
}
}
}
export { Environment, Light, SpotLight, DirectionalLight }

89
src/engine/index.js Normal file
View File

@ -0,0 +1,89 @@
/* global performance */
import Screen from './screen'
import Input from './input'
import { ShaderManager } from './shader'
class Engine {
constructor () {
this.screen = new Screen()
this.input = new Input(this.screen.gl.canvas)
this.shaders = new ShaderManager()
this.running = false
// Queues
this.rst = []
this.ust = []
this.frameTime = 0
this.frameCount = 0
this.fps = 0
}
get gl () {
return this.screen.gl
}
render (gl) {
// Set clear color to black, fully opaque
gl.clearColor(0.0, 0.7, 1.0, 1.0)
// Clear the color buffer with specified clear color
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
// Enable depth testing
gl.enable(gl.DEPTH_TEST)
// Enable back-face culling
gl.enable(gl.CULL_FACE)
gl.cullFace(gl.BACK)
// Render functions
for (let i in this.rst) {
this.rst[i](gl)
}
}
update (dt) {
this.input.update()
// Updates
for (let i in this.ust) {
this.ust[i](dt)
}
}
step () {
this.running && window.requestAnimationFrame(() => this.step())
this.render(this.gl)
let ts = performance.now()
let timeDiff = ts - this.frameTime // time difference in milliseconds
this.frameCount++
if (timeDiff > 0) {
this.fps = Math.floor(this.frameCount / timeDiff * 1000)
this.frameCount = 0
this.frameTime = ts
}
this.update(timeDiff / 1000)
}
addRenderFunction (fn) {
this.rst.push(fn)
}
addUpdateFunction (fn) {
this.ust.push(fn)
}
startGameLoop () {
if (this.running) throw new Error('Game Loop is already running!')
this.running = true
this.step()
}
}
export default Engine

222
src/engine/input.js Normal file
View File

@ -0,0 +1,222 @@
const specialKeyMap = {
'backspace': 8,
'tab': 9,
'enter': 13,
'shift': 16,
'ctrl': 17,
'alt': 18,
'pausebreak': 19,
'capslock': 20,
'escape': 27,
'pgup': 33,
'pgdown': 34,
'end': 35,
'home': 36,
'left': 37,
'up': 38,
'right': 39,
'down': 40,
'insert': 45,
'delete': 46,
'left-window': 91,
'right-window': 92,
'select': 93,
'numpad0': 96,
'numpad1': 97,
'numpad2': 98,
'numpad3': 99,
'numpad4': 100,
'numpad5': 101,
'numpad6': 102,
'numpad7': 103,
'numpad8': 104,
'numpad9': 105,
'multiply': 106,
'add': 107,
'subtract': 109,
'decimal': 110,
'divide': 111,
'f1': 112,
'f2': 113,
'f3': 114,
'f4': 115,
'f5': 116,
'f6': 117,
'f7': 118,
'f8': 119,
'f9': 120,
'f10': 121,
'f11': 122,
'f12': 123,
'numlock': 144,
'scrolllock': 145,
'semi-colon': 186,
'equals': 187,
'comma': 188,
'dash': 189,
'period': 190,
'fwdslash': 191,
'grave': 192,
'open-bracket': 219,
'bkslash': 220,
'close-braket': 221,
'single-quote': 222
}
class Input {
constructor (canvas) {
this.keyList = {}
this.previousKeyList = {}
this.canvas = canvas
this.mouse = {
pos: { x: 0, y: 0 },
frame: { x: 0, y: 0 },
previous: { x: 0, y: 0 }
}
window.addEventListener('keydown', (e) => this.keyDown(e), false)
window.addEventListener('keyup', (e) => this.keyUp(e), false)
canvas.addEventListener('mousemove', (e) => {
let x
let y
if (e.changedTouches) {
let touch = e.changedTouches[0]
if (touch) {
e.pageX = touch.pageX
e.pageY = touch.pageY
}
}
if (e.pageX || e.pageY) {
x = e.pageX
y = e.pageY
} else {
x = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft
y = e.clientY + document.body.scrollTop + document.documentElement.scrollTop
}
x -= canvas.offsetLeft
y -= canvas.offsetTop
this.mouse.frame.x = x
this.mouse.frame.y = y
}, false)
canvas.addEventListener('mousedown', (e) => {
e.preventDefault()
this.mouse['btn' + e.button] = true
})
canvas.addEventListener('mouseup', (e) => {
e.preventDefault()
this.mouse['btn' + e.button] = false
})
canvas.addEventListener('contextmenu', (e) => {
e.preventDefault()
})
}
get mousePos () {
return this.mouse.pos
}
get mouseMoved () {
return (this.mouse.pos.x !== this.mouse.previous.x ||
this.mouse.pos.y !== this.mouse.previous.y)
}
get mouseOffset () {
return {
x: this.mouse.previous.x - this.mouse.pos.x,
y: this.mouse.previous.y - this.mouse.pos.y
}
}
toggleKey (keyCode, on) {
// Find key in special key list
let key = null
for (let k in specialKeyMap) {
let val = specialKeyMap[k]
if (keyCode === val) {
key = k
break
}
}
// Use fromCharCode
if (!key) {
key = String.fromCharCode(keyCode).toLowerCase()
}
this.keyList[key] = (on === true)
}
keyDown (e) {
let keycode
if (window.event) {
keycode = window.event.keyCode
} else if (e) {
keycode = e.which
}
this.toggleKey(keycode, true)
}
keyUp (e) {
let keycode
if (window.event) {
keycode = window.event.keyCode
} else if (e) {
keycode = e.which
}
this.toggleKey(keycode, false)
}
down (key) {
return this.keyList[key] != null ? this.keyList[key] : false
}
downLast (key) {
return this.previousKeyList[key] != null ? this.previousKeyList[key] : false
}
isDown (key) {
return this.down(key) && this.downLast(key)
}
isUp (key) {
return !this.isDown(key)
}
isPressed (key) {
return this.down(key) === true && this.downLast(key) === false
}
update () {
this.previousKeyList = {}
for (let k in this.keyList) {
if (this.keyList[k] === true) {
this.previousKeyList[k] = true
}
}
// Mouse positions in the previous frame
this.mouse.previous.x = this.mouse.pos.x
this.mouse.previous.y = this.mouse.pos.y
// Mouse positions in the current frame
// Convert to OpenGL coordinate system
this.mouse.pos.x = this.mouse.frame.x / this.canvas.width * 2 - 1
this.mouse.pos.y = this.mouse.frame.y / this.canvas.height * 2 - 1
}
}
export default Input

View File

@ -0,0 +1,33 @@
import Entity from '../entity'
class AnimatedEntity extends Entity {
constructor (mesh, pos, scale, rotation) {
super(mesh, pos, scale, rotation)
this.animPlayer = null
this.animation = null
this.keyframe = 0
}
setAnimation (name) {
}
playAnimation () {
}
stopAnimation () {
}
update (dt) {
}
draw (gl, shader) {
super.draw(gl, shader)
}
}
export default AnimatedEntity

32
src/engine/mesh/entity.js Normal file
View File

@ -0,0 +1,32 @@
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

287
src/engine/mesh/index.js Normal file
View File

@ -0,0 +1,287 @@
import Resource from '../resource'
import { Texture, Material } from './material'
import { mat4 } from 'gl-matrix'
let meshCache = {}
class Node {
constructor (pos, scale, rotation) {
this.pos = pos || [0.0, 0.0, 0.0]
this.scale = scale || [1.0, 1.0, 1.0]
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()
}
}
// Setters
setPosition (newPos) {
this.pos = newPos
this.updateTransform()
}
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
}
// 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 {
static construct (gl, vertices, indices, uvs, normals) {
// VBO for model vertices
let pos = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, pos)
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW)
// Indices Buffer
let ebo = gl.createBuffer()
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ebo)
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW)
let mesh = new Mesh()
mesh.pos = pos
mesh.ebo = ebo
mesh.indices = indices.length
// VBO for model UVs
if (uvs) {
let uv = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, uv)
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(uvs), gl.STATIC_DRAW)
mesh.uvs = uv
}
// Normals buffer
if (normals) {
let nms = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, nms)
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normals), gl.STATIC_DRAW)
mesh.nms = nms
}
gl.bindBuffer(gl.ARRAY_BUFFER, null)
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
})
}
let finished = []
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)
}
}
finished.push(mesh)
}
// Cache the mesh
meshCache[file] = finished
return finished.length > 1 ? finished : finished[0]
}
bindBuffers (gl, shader) {
gl.bindBuffer(gl.ARRAY_BUFFER, this.pos)
shader.setAttribute(gl, 'aVertexPosition', 3, false, 3 * 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)
}
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)
}
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.ebo)
}
draw (gl, shader) {
// Set model transform uniform
super.draw(gl, shader)
// Bind attrib arrays
this.bindBuffers(gl, shader)
// Give materials to shader
if (this.material) {
this.material.apply(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 }

View File

@ -0,0 +1,50 @@
import Resource from '../resource'
class Texture {
static createTexture2D (gl, img) {
let tex = gl.createTexture()
gl.bindTexture(gl.TEXTURE_2D, tex)
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true)
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img)
gl.generateMipmap(gl.TEXTURE_2D)
gl.bindTexture(gl.TEXTURE_2D, null)
let oTex = new Texture()
oTex.type = gl.TEXTURE_2D
oTex.id = tex
return oTex
}
}
class Material {
async loadTextures (gl) {
if (this.textures) {
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)
this.textures[ti] = result
}
}
}
}
apply (gl, shader) {
// TODO: lighting related things
// Load textures
if (!this.textures || !this.textures.length) return
for (let i in this.textures) {
let tex = this.textures[i]
if (tex && tex instanceof Texture) {
gl.bindTexture(tex.type, tex.id)
gl.activeTexture(gl.TEXTURE0 + parseInt(i))
}
}
}
}
export { Texture, Material }

88
src/engine/resource.js Normal file
View File

@ -0,0 +1,88 @@
/* global XMLHttpRequest, Image */
let imgCache = {}
function GET (url, istext) {
return new Promise((resolve, reject) => {
var xmlHttp = new XMLHttpRequest()
xmlHttp.onreadystatechange = function () {
if (xmlHttp.readyState === 4 && xmlHttp.status === 200) {
resolve(xmlHttp.responseText)
} else if (xmlHttp.readyState === 4 && xmlHttp.status >= 400) {
let err = new Error(xmlHttp.status)
err.request = xmlHttp
reject(err)
}
}
xmlHttp.open('GET', url, true)
istext && (xmlHttp.responseType = 'text')
xmlHttp.send(null)
})
}
function smartGET (data) {
if (typeof data === 'string') {
data = {
url: data,
type: 'text'
}
}
if (!data.type) data.type = 'text'
let istext = (data.type !== 'image' && data.type !== 'file')
let url = data.url
if (!url) throw new Error('URL is required!')
if (data.type === 'json') {
return new Promise((resolve, reject) => {
GET(url).then((dtext) => {
try {
let jsonp = JSON.parse(dtext)
return resolve(jsonp)
} catch (e) {
reject(e)
}
}, reject)
})
}
return GET(data.url, istext)
}
function loadImage (url) {
url = '/assets/textures/' + url
// Ensure we don't load a texture multiple times
if (imgCache[url]) return imgCache[url]
return new Promise((resolve, reject) => {
let img = new Image()
img.onload = function () {
imgCache[url] = img
resolve(img)
}
img.onerror = function (e) {
reject(e)
}
img.src = url
})
}
function imageToSampler (img) {
let canvas = document.createElement('canvas')
let ctx = canvas.getContext('2d')
canvas.width = img.width
canvas.height = img.height
ctx.drawImage(img, 0, 0, img.width, img.height)
return function (x, y) {
return ctx.getImageData(x, y, 1, 1).data
}
}
export default { GET: smartGET, imageToSampler, loadImage }

35
src/engine/screen.js Normal file
View File

@ -0,0 +1,35 @@
/* global alert */
class Screen {
constructor () {
this._el = document.createElement('canvas')
this.resize()
this._gl = this._el.getContext('webgl')
if (!this._gl) {
alert('Your machine or browser does not support WebGL!')
return
}
document.body.appendChild(this._el)
window.addEventListener('resize', (e) => {
this.resize()
}, false)
}
get gl () {
return this._gl
}
get ctx () {
return this._gl
}
resize () {
this._el.width = window.innerWidth
this._el.height = window.innerHeight
}
}
export default Screen

181
src/engine/shader.js Normal file
View File

@ -0,0 +1,181 @@
import Resource from './resource'
class Shader {
constructor (type, source) {
this.type = type
this.source = source
this.id = 0
}
compile (gl) {
const shader = gl.createShader(this.type)
// Send the source to the shader object
gl.shaderSource(shader, this.source)
// Compile the shader program
gl.compileShader(shader)
// See if it compiled successfully
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
let inf = gl.getShaderInfoLog(shader)
gl.deleteShader(shader)
throw new Error('An error occurred compiling the shaders: ' + inf)
}
this.id = shader
return this
}
}
class ShaderProgram {
constructor (name) {
this.name = name
this.id = 0
this.vertexShader = null
this.geometryShader = null
this.fragmentShader = null
this.uniforms = {}
this.attribs = {}
}
link (gl, vs, fs, gs) {
let vsh = [ vs, fs, gs ]
for (let i in vsh) {
vsh[i] && vsh[i].compile(gl)
}
const shaderProgram = gl.createProgram()
// Attach the shaders
gl.attachShader(shaderProgram, vs.id)
gs && gl.attachShader(shaderProgram, gs.id)
gl.attachShader(shaderProgram, fs.id)
// Link the program
gl.linkProgram(shaderProgram)
// If creating the shader program failed, error
if (!gl.getProgramParameter(shaderProgram, gl.LINK_STATUS)) {
throw new Error('Unable to initialize the shader program ' + this.name + ': ' + gl.getProgramInfoLog(shaderProgram))
}
this.id = shaderProgram
this.vertexShader = vs
this.fragmentShader = fs
this.geometryShader = gs
// Detach shaders after use
gl.detachShader(shaderProgram, vs.id)
gs && gl.detachShader(shaderProgram, gs.id)
gl.detachShader(shaderProgram, fs.id)
return this
}
getUniformLocation (gl, name) {
if (this.uniforms[name]) return this.uniforms[name]
let uni = gl.getUniformLocation(this.id, name)
if (uni < 0) return null
this.uniforms[name] = uni
return uni
}
getAttributeLocation (gl, name) {
if (this.attribs[name]) return this.attribs[name]
let pos = gl.getAttribLocation(this.id, name)
if (pos < 0) throw new Error(`No such attribute location ${name} in shader ${this.name}!`)
this.attribs[name] = pos
return pos
}
hasAttribute (gl, name) {
try {
this.getAttributeLocation(gl, name)
return true
} catch (e) {
return false
}
}
setAttribute (gl, name, size, normalized, stride, offset, type) {
let loc = this.getAttributeLocation(gl, name) // throws an error in case the name doesn't exist in shader
gl.enableVertexAttribArray(loc)
gl.vertexAttribPointer(
loc,
size,
type || gl.FLOAT,
normalized,
stride,
offset)
}
use (gl) {
if (this.id === 0) return
gl.useProgram(this.id)
}
}
class ShaderManager {
constructor () {
this.shaders = {}
}
createShader (gl, name, vs, fs, gs) {
if (this.shaders[name]) return this.shaders[name]
let shader = new ShaderProgram(name)
let vert = new Shader(gl.VERTEX_SHADER, vs)
let frag = new Shader(gl.FRAGMENT_SHADER, fs)
let geom = gs ? new Shader(gl.GEOMETRY_SHADER, gs) : null
shader.link(gl, vert, frag, geom)
this.shaders[name] = shader
return shader
}
// Standard shader nomenclature: /assets/shaders/shader-name.vs|fs[|gs]
// shader-name.vs and shader-name.fs are mandatory!
createShaderFromFiles (gl, name, gs) {
let stdloc = '/assets/shaders/' + name
return new Promise((resolve, reject) => {
function finishLink (vs, fs, gss) {
try {
let shader = this.createShader(gl, name, vs, fs, gss)
resolve(shader)
} catch (e) {
reject(e)
}
}
Resource.GET(stdloc + '.vs').then((vs) => {
Resource.GET(stdloc + '.fs').then((fs) => {
if (gs !== false) {
// Try to find the geometry shader if it wasn't explicitly stated to not exist
return Resource.GET(stdloc + '.gs').then((gss) => {
finishLink.apply(this, [vs, fs, gss])
}, () => {
finishLink.apply(this, [vs, fs])
})
}
finishLink.apply(this, [vs, fs, null])
}, reject)
}, reject)
})
}
use (gl, name) {
if (this.shaders[name]) this.shaders[name].use(gl)
}
}
export { Shader, ShaderProgram, ShaderManager }

74
src/index.js Normal file
View File

@ -0,0 +1,74 @@
import Engine from './engine'
import Camera from './engine/camera'
// import Entity from './engine/mesh/entity'
import { Environment } from './engine/environment'
import { Terrain } from './engine/components/terrain'
import { SimplexHeightMap } from './engine/components/terrain/heightmap'
import { Material } from './engine/mesh/material'
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 shader = await game.shaders.createShaderFromFiles(game.gl, 'basic', false)
let terrainShader = await game.shaders.createShaderFromFiles(game.gl, 'terrain', false)
// Create a height map based on OpenSimplex noise
let hmap = new SimplexHeightMap(1, 1, 256, 50)
// Create a terrain
let terrain = new Terrain([0.0, 0.0, 0.0], 256, 256)
terrain.createMesh(game.gl, hmap)
// Terrain material
let material = new Material()
material.textures = ['grass-1024.jpg']
await material.loadTextures(game.gl)
terrain.setMaterial(material)
// Create and initialize the camera
let cam = new Camera([0.0, 1.0, 2.0])
cam.updateProjection(game.gl)
// Update function for camera
game.addUpdateFunction(function (dt) {
if (game.input.isDown('w')) {
cam.processKeyboard(0, dt)
} else if (game.input.isDown('s')) {
cam.processKeyboard(1, dt)
}
if (game.input.isDown('a')) {
cam.processKeyboard(2, dt)
} else if (game.input.isDown('d')) {
cam.processKeyboard(3, dt)
}
if (game.input.mouseMoved && game.input.mouse.btn0) {
cam.processMouseMove(game.input.mouseOffset)
}
})
// Render function for the triangle
game.addRenderFunction(function (gl) {
// Use terrain shader
terrainShader.use(gl)
// Set environment variables in shader
env.draw(gl, terrainShader)
// Set the viewport uniforms
cam.draw(gl, terrainShader)
// Draw terrain
terrain.draw(gl, terrainShader)
})
game.startGameLoop()
}
pipeline().catch(function (e) {
console.error(e)
})

28
webpack.config.js Normal file
View File

@ -0,0 +1,28 @@
const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'app.js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules)/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
]
},
devtool: 'inline-source-map',
plugins: [
new HtmlWebpackPlugin({ template: 'index.html' })
]
}