import { makeBuffersFromFraces, makeFace, makeGrassCube } from "./geometry"; import { loadTexture, makeProgram } from "./gl"; import * as se3 from './se3'; const TEST_VSHADER = ` attribute vec3 aPosition; attribute vec3 aNormal; attribute vec2 aTextureCoord; uniform mat4 uProjection; uniform mat4 uModel; uniform mat4 uView; varying highp vec2 vTextureCoord; varying lowp vec3 vLighting; varying lowp float vDistance; void main() { highp mat4 modelview = uView * uModel; gl_Position = uProjection * modelview * vec4(aPosition, 1.0); lowp vec3 normal = mat3(modelview) * aNormal; lowp vec3 lightDirection = - normalize(mat3(uView) * vec3(1.0, -0.3, -0.8)); lowp float diffuseAmount = max(dot(lightDirection, normal), 0.0); lowp vec3 ambiant = vec3(0.7, 0.7, 0.2); vLighting = ambiant + vec3(1.0, 1.0, 1.0) * diffuseAmount; vTextureCoord = aTextureCoord; vDistance = length(modelview * vec4(aPosition, 1.0)); } `; const TEST_FSHADER = ` uniform sampler2D uSampler; uniform lowp vec3 uFogColor; varying highp vec2 vTextureCoord; varying lowp vec3 vLighting; varying lowp float vDistance; void main() { highp vec4 color = texture2D(uSampler, vTextureCoord); lowp float fogamount = smoothstep(30.0, 90.0, vDistance); gl_FragColor = vec4(mix(vLighting * color.rgb, uFogColor, fogamount), color.a); } `; /** Draw. * * @param {WebGLRenderingContext} gl */ function draw(gl, params, objects) { gl.clearColor(0.6, 0.8, 1.0, 1.0); gl.clearDepth(1.0); gl.enable(gl.DEPTH_TEST); gl.depthFunc(gl.LEQUAL); gl.enable(gl.CULL_FACE); gl.cullFace(gl.BACK); gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); const camrot = params.camera.orientation; const campos = params.camera.position; const viewMat = se3.product( se3.rotxyz(-camrot[0], -camrot[1], -camrot[2]), se3.translation(-campos[0], -campos[1], -campos[2]) ); for (const {program, texture, position, orientation, geometry} of objects) { const viewLoc = gl.getUniformLocation(program, 'uView'); const modelLoc = gl.getUniformLocation(program, 'uModel'); const projLoc = gl.getUniformLocation(program, 'uProjection'); const samplerLoc = gl.getUniformLocation(program, 'uSampler'); const fogColorLoc = gl.getUniformLocation(program, 'uFogColor'); const positionLoc = gl.getAttribLocation(program, 'aPosition'); const normalLoc = gl.getAttribLocation(program, 'aNormal'); const textureLoc = gl.getAttribLocation(program, 'aTextureCoord'); const mvMatrix = se3.product(se3.translation(...position), se3.rotxyz(...orientation)); gl.useProgram(program); gl.uniformMatrix4fv(viewLoc, false, new Float32Array(viewMat)); gl.uniformMatrix4fv(modelLoc, false, new Float32Array(mvMatrix)); gl.uniformMatrix4fv(projLoc, false, new Float32Array(params.projMatrix)); gl.uniform3f(fogColorLoc, 0.6, 0.8, 1.0); gl.bindBuffer(gl.ARRAY_BUFFER, geometry.vertexBuffer); gl.vertexAttribPointer(positionLoc, 3, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(positionLoc); gl.bindBuffer(gl.ARRAY_BUFFER, geometry.normalBuffer); gl.vertexAttribPointer(normalLoc, 3, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(normalLoc); gl.bindBuffer(gl.ARRAY_BUFFER, geometry.textureBuffer); gl.vertexAttribPointer(textureLoc, 2, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(textureLoc); gl.activeTexture(gl.TEXTURE0); gl.bindTexture(gl.TEXTURE_2D, texture); gl.uniform1i(samplerLoc, 0); gl.drawArrays(gl.TRIANGLES, 0, geometry.numVertices); } } const stuff = { lastFrameTime: 0, }; function handleKeys(params) { const move = (forward, right) => { const dir = [right, 0, -forward, 1.0]; const camori = params.camera.orientation; const ori = se3.rotxyz(...camori); const tf = se3.apply(ori, dir); params.camera.position[0] += tf[0]; params.camera.position[2] += tf[2]; }; params.keys.forEach(key => { switch (key) { case 'KeyW': move(0.1, 0.0); return; case 'KeyA': move(0.0, -0.1); return; case 'KeyS': move(-0.1, 0.0); return; case 'KeyD': move(0.0, 0.1); return; case 'Space': params.camera.position[1] += 0.1; return; case 'ControlLeft': params.camera.position[1] -= 0.1; return; } }); } function tick(time, gl, params, objects) { draw(gl, params, objects); handleKeys(params); const dt = (time - stuff.lastFrameTime) * 0.001; stuff.lastFrameTime = time; document.querySelector('#fps').textContent = `${1.0 / dt} fps`; requestAnimationFrame(time => tick(time, gl, params, objects)); } const BlockType = { AIR: 1, DIRT: 2, GRASS: 3, STONE: 4, }; function ghettoPerlinNoise(seed, x, y, gridSize = 16) { const dot = (vx, vy) => vx[0] * vy[0] + vx[1] * vy[1]; // super ghetto random const xorshift = (x) => { x ^= x << 13; x ^= x >> 7; x ^= x << 17; return x; }; const randGrad = (x0, y0) => { const rand = xorshift(1337 * x0 + seed + 80085 * y0); return [Math.sin(rand), Math.cos(rand)]; }; const interpol = (a, b, x) => a + (x*x*(3 - 2*x)) * (b - a); const x0 = Math.floor(x / gridSize); const y0 = Math.floor(y / gridSize); const sx = x / gridSize - x0; const sy = y / gridSize - y0; const n0 = dot(randGrad(x0, y0), [sx, sy]); const n1 = dot(randGrad(x0 + 1, y0), [sx - 1, sy]); const n2 = dot(randGrad(x0, y0 + 1), [sx, sy - 1]); const n3 = dot(randGrad(x0 + 1, y0 + 1), [sx - 1, sy - 1]); return interpol(interpol(n0, n1, sx), interpol(n2, n3, sx), sy); } function makeTerrain(x, y) { const seed = 1337; const fractalNoise = (x, y) => ( ghettoPerlinNoise(seed, x, y, 8) * 0.06 + ghettoPerlinNoise(seed, x, y, 16) * 0.125 + ghettoPerlinNoise(seed, x, y, 32) * 0.25 + ghettoPerlinNoise(seed, x, y, 64) * 0.5 ); const terrain = Array(16); for (let i = 0; i < 16; i++) { for (let j = 0; j < 16; j++) { terrain[i * 16 + j] = fractalNoise(x + i, y + j); } } return terrain; } function makeChunk(x, y) { const terrain = makeTerrain(x, y); const chunk = new Uint8Array(16 * 16 * 256); for (let i = 0; i < 16; i++) { for (let j = 0; j < 16; j++) { const height = Math.floor(64 + 64 * terrain[i * 16 + j]); // everything above is air // that block is grass // everything below is dirt const offset = i * (16 * 256) + j * 256; const stoneHeight = Math.min(62, height); chunk.set(Array(stoneHeight).fill(BlockType.STONE), offset); if (stoneHeight < height) { chunk.set(Array(height - 1 - stoneHeight).fill(BlockType.DIRT), offset + stoneHeight); chunk[offset + height - 1] = BlockType.GRASS; } chunk.set(Array(256 - height).fill(BlockType.AIR), offset + height); } } return chunk; } function makeChunkMesh(chunk, z0, x0) { const blockPos = i => { const x = Math.floor(i / (16 * 256)); const y = Math.floor((i - 16 * 258 * x) / 256); const z = i - 256 * (16*x + y); return [x, y, z]; }; const blockIndex = (i, j, k) => 256 * (i*16 +j) + k; const sideNeighbors = (i, j) => { return [ {pos: [i - 1, j], dir: '-z'}, {pos: [i + 1, j], dir: '+z'}, {pos: [i, j - 1], dir: '-x'}, {pos: [i, j + 1], dir: '+x'}, ].filter(v => v.pos.every(i => i >= 0 && i < 16)); }; const faceDir = (dir, x, y, z) => { return { '-x': [x - 0.5, y, z], '+x': [x + 0.5, y, z], '-z': [x, y, z - 0.5], '+z': [x, y, z + 0.5], }[dir]; }; const faces = []; for (let i = 0; i < 16; i++) { for (let j = 0; j < 16; j++) { for (let k = 0; k < 256; k++) { const bi = blockIndex(i, j, k); const upTexture = (() => { switch (chunk[bi - 1]) { case BlockType.GRASS: return [0, 15]; case BlockType.DIRT: return [2, 15]; case BlockType.STONE: return [3, 15]; } })(); if (chunk[bi] == BlockType.AIR) { faces.push(makeFace('+y', upTexture, [x0 + j, k - 0.5, z0 + i])); break; } const sideTexture = (() => { switch (chunk[bi]) { case BlockType.GRASS: return [1, 15]; case BlockType.DIRT: return [2, 15]; case BlockType.STONE: return [3, 15]; } })(); for ({ pos, dir } of sideNeighbors(i, j)) { if (chunk[blockIndex(...pos, k)] == BlockType.AIR) { faces.push(makeFace(dir, sideTexture, faceDir(dir, x0 + j, k, z0 + i))); } } if (i == 0) { faces.push(makeFace('-z', sideTexture, faceDir('-z', x0 + j, k, z0 + i))); } if (i == 15) { faces.push(makeFace('+z', sideTexture, faceDir('+z', x0 + j, k, z0 + i))); } if (j == 0) { faces.push(makeFace('-x', sideTexture, faceDir('-x', x0 + j, k, z0 + i))); } if (j == 15) { faces.push(makeFace('+x', sideTexture, faceDir('+x', x0 + j, k, z0 + i))); } } } } return faces; } function makeWorldBuffers(gl) { const chunkFaces = [].concat( makeChunkMesh(makeChunk(0, -16), 0, -16), makeChunkMesh(makeChunk(16, -16), 16, -16), makeChunkMesh(makeChunk(0, 0), 0, 0), makeChunkMesh(makeChunk(16, 0), 16, 0), makeChunkMesh(makeChunk(16, 16), 16, 16), makeChunkMesh(makeChunk(0, 16), 0, 16), ); return makeBuffersFromFraces(gl, chunkFaces); } // Stuff I need to do: // [x] a skybox // [x] a movable camera // [ ] some kind of gravity // [ ] collision detection // [x] more blocks // [ ] ability to mine & place // [ ] generating & loading of more chunks // [x] distance fog // [ ] different biomes (with different noise stats) // [ ] non-flowy water // [ ] flowy water // [ ] ALIGN CHUNK WITH WORLD COORDS // [x] better controls async function main() { const canvas = document.querySelector('#game'); const gl = canvas.getContext('webgl'); if (gl === null) { console.error('webgl not available') return; } const program = makeProgram(gl, TEST_VSHADER, TEST_FSHADER); const params = { projMatrix: se3.perspective(Math.PI / 3, canvas.clientWidth / canvas.clientHeight, 0.1, 100.0), camera: { position: [0.0, 70.5, 0.0], orientation: [0.0, Math.PI, 0.0], }, keys: new Set() } const texture = await loadTexture(gl, 'texture.png'); const objects = [ { program, position: [0.0, 0.0, 0.0], orientation: [0.0, 0.0, 0.0], geometry: makeWorldBuffers(gl), texture, }, ]; canvas.onclick = e => { canvas.requestPointerLock(); const keyListener = e => { if (e.type === 'keydown') { params.keys.add(e.code); } else { params.keys.delete(e.code); } }; const moveListener = e => { params.camera.orientation[1] -= e.movementX / 500; params.camera.orientation[0] -= e.movementY / 500; }; const changeListener = () => { if (document.pointerLockElement === canvas) { return; } document.removeEventListener('pointerlockchange', changeListener); document.removeEventListener('pointermove', moveListener); document.removeEventListener('keydown', keyListener); document.removeEventListener('keyup', keyListener); }; document.addEventListener('pointerlockchange', changeListener); document.addEventListener('pointermove', moveListener); document.addEventListener('keydown', keyListener); document.addEventListener('keyup', keyListener); }; requestAnimationFrame(time => tick(time, gl, params, objects)); } window.onload = main;