From 6e47e9cd0a6bc1b4cfe30bb61765fb9008ef3917 Mon Sep 17 00:00:00 2001 From: Paul Mathieu Date: Sun, 12 Dec 2021 00:46:24 -0800 Subject: [PATCH] Inifinite world!! (almost) --- geometry.js | 173 +++++++++++++++++++++---------- index.js | 291 +++++++++++++++++++++++++++++++++++++--------------- 2 files changed, 326 insertions(+), 138 deletions(-) diff --git a/geometry.js b/geometry.js index ac1d8de..c1aa0ab 100644 --- a/geometry.js +++ b/geometry.js @@ -37,79 +37,142 @@ function makeSquare() { }; } -function makeZFace(normal, texture, transform) { - const textMul = 0.0625; - const textOff = texture.map(x => x * textMul); +function memoize(f) { + const memo = {}; - const vertices = []; - const textures = []; - - const sq = makeSquare(); - - for (let i = 0; i < 6; i++) { - vertices.push( - se3.product( - transform, - sq.vertices.slice(3 * i, 3 * (i + 1)).concat([1.0]) - ).slice(0, 3)); - textures.push(sq.textures.slice(2 * i, 2 * (i + 1)).map((v, j) => textMul * v + textOff[j])); + function g(...args) { + if (!(args in memo)) { + memo[args] = f(...args); + } + return memo[args]; } - const normals = [].concat(...Array(6).fill(normal)); + return g; +} +function _makeTextureFace(texture) { + const textMul = 0.0625; + const textOff = texture.map(x => x * textMul); + const textuv = [ + [0.99, 0.99], + [0.01, 0.99], + [0.01, 0.01], + [0.99, 0.99], + [0.01, 0.01], + [0.99, 0.01], + ]; + + const textures = textuv.map(uv => uv.map((x, j) => textMul * x + textOff[j])); + + return textures; +} + +const makeTextureFace = memoize(_makeTextureFace); + +function makePZFace(texture) { return { - vertices: [].concat(...vertices), - normals, - textures: [].concat(...textures), + vertices: [[0.5, 0.5, 0], [-0.5, 0.5, 0], [-0.5, -0.5, 0], [0.5, 0.5, 0], [-0.5, -0.5, 0], [0.5, -0.5, 0]], + normals: Array(6).fill([0.0, 0.0, 1.0]), + textures: makeTextureFace(texture), }; +} +function makeNZFace(texture) { + return { + vertices: [[-0.5, 0.5, 0.0], [0.5, 0.5, 0.0], [0.5, -0.5, 0.0], [-0.5, 0.5, 0.0], [0.5, -0.5, 0.0], [-0.5, -0.5, 0.0]], + normals: Array(6).fill([0.0, 0.0, -1.0]), + textures: makeTextureFace(texture), + }; +} +function makePXFace(texture) { + return { + vertices: [[0, 0.5, -0.5], [0, 0.5, 0.5], [0, -0.5, 0.5], [0, 0.5, -0.5], [0, -0.5, 0.5], [0, -0.5, -0.5]], + normals: Array(6).fill([1.0, 0.0, 0.0]), + textures: makeTextureFace(texture), + }; +} +function makeNXFace(texture) { + return { + vertices: [[0, 0.5, 0.5], [0, 0.5, -0.5], [0, -0.5, -0.5], [0, 0.5, 0.5], [0, -0.5, -0.5], [0, -0.5, 0.5]], + normals: Array(6).fill([-1.0, 0.0, 0.0]), + textures: makeTextureFace(texture), + }; +} +function makePYFace(texture) { + return { + vertices: [[0.5, 0, -0.5], [-0.5, 0, -0.5], [-0.5, 0, 0.5], [0.5, 0, -0.5], [-0.5, 0, 0.5], [0.5, 0, 0.5]], + normals: Array(6).fill([0.0, 1.0, 0.0]), + textures: makeTextureFace(texture), + }; +} +function makeNYFace(texture) { + return { + vertices: [[0.5, 0, 0.5], [-0.5, 0, 0.5], [-0.5, 0, -0.5], [0.5, 0, 0.5], [-0.5, 0, -0.5], [0.5, 0, -0.5]], + normals: Array(6).fill([0.0, -1.0, 0.0]), + textures: makeTextureFace(texture), + }; +} +function translateFace(face, x, y, z) { + return { + normals: face.normals, + textures: face.textures, + vertices: face.vertices.map(([vx, vy, vz]) => [vx + x, vy + y, vz + z]), + }; } export function makeFace(which, texture, centerPos) { - const rot = { - '-x': se3.roty(-Math.PI / 2), - '+x': se3.roty(Math.PI / 2), - '-y': se3.rotx(Math.PI / 2), - '+y': se3.rotx(-Math.PI / 2), - '-z': se3.roty(Math.PI), - '+z': se3.identity(), - }[which]; + switch(which) { + case '-x': return translateFace(makeNXFace(texture), ...centerPos); + case '+x': return translateFace(makePXFace(texture), ...centerPos); + case '-y': return translateFace(makeNYFace(texture), ...centerPos); + case '+y': return translateFace(makePYFace(texture), ...centerPos); + case '-z': return translateFace(makeNZFace(texture), ...centerPos); + case '+z': return translateFace(makePZFace(texture), ...centerPos); + } - const normal = { - '+x': [1.0, 0.0, 0.0], - '-x': [-1.0, 0.0, 0.0], - '+y': [0.0, 1.0, 0.0], - '-y': [0.0, -1.0, 0.0], - '+z': [0.0, 0.0, 1.0], - '-z': [0.0, 0.0, -1.0], - }[which]; - - const tf = se3.product(se3.translation(...centerPos), rot); - return makeZFace(normal, texture, tf); + throw Error('unknown face'); } -export function makeBuffersFromFraces(gl, faces) { - vertices = [].concat(...faces.map(f => f.vertices)); - normals = [].concat(...faces.map(f => f.normals)); - textures = [].concat(...faces.map(f => f.textures)); +/** Packs all those faces into one big buffer. */ +export function makeBufferFromFaces(gl, faces) { + const numVertices = faces.map(f => f.vertices).reduce((count, vertices) => count + vertices.length, 0); - const vertexBuffer = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); - gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW); + // 3 * float32 + 3 * byte (padded to 4) + 2 * short + // see https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/vertexAttribPointer#examples + const vertexSize = 3 * 4 + 4 + 4; - const normalBuffer = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, normalBuffer); - gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(normals), gl.STATIC_DRAW); + const glBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, glBuffer); - const textureBuffer = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, textureBuffer); - gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textures), gl.STATIC_DRAW); + const buffer = new Uint8Array(numVertices * vertexSize); + const dv = new DataView(buffer.buffer); + let offset = 0; + + faces.forEach(face => { + for (let i = 0; i < face.vertices.length; i++) { + const position = face.vertices[i]; + dv.setFloat32(offset + 0, position[0], true); + dv.setFloat32(offset + 4, position[1], true); + dv.setFloat32(offset + 8, position[2], true); + offset += 12; + const normal = face.normals[i]; + dv.setInt8(offset + 0, normal[0] * 0x7f); + dv.setInt8(offset + 1, normal[1] * 0x7f); + dv.setInt8(offset + 2, normal[2] * 0x7f); + offset += 4; + const texture = face.textures[i]; + dv.setUint16(offset + 0, texture[0] * 0xffff, true); + dv.setUint16(offset + 2, texture[1] * 0xffff, true); + offset += 4; + } + }); + + gl.bufferData(gl.ARRAY_BUFFER, buffer, gl.STATIC_DRAW); return { - vertexBuffer, - normalBuffer, - textureBuffer, - numVertices: vertices.length / 3, + glBuffer, + numVertices, + delete: () => gl.deleteBuffer(glBuffer), }; } diff --git a/index.js b/index.js index 209c69a..e5c6aeb 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,4 @@ -import { makeBuffersFromFraces, makeFace, makeGrassCube } from "./geometry"; +import { makeBufferFromFaces, makeFace, makeGrassCube } from "./geometry"; import { loadTexture, makeProgram } from "./gl"; import * as se3 from './se3'; @@ -70,6 +70,7 @@ function draw(gl, params, objects) { ); for (const {program, texture, position, orientation, geometry} of objects) { + // load those ahead of time const viewLoc = gl.getUniformLocation(program, 'uView'); const modelLoc = gl.getUniformLocation(program, 'uModel'); const projLoc = gl.getUniformLocation(program, 'uProjection'); @@ -80,7 +81,8 @@ function draw(gl, params, objects) { const normalLoc = gl.getAttribLocation(program, 'aNormal'); const textureLoc = gl.getAttribLocation(program, 'aTextureCoord'); - const mvMatrix = se3.product(se3.translation(...position), se3.rotxyz(...orientation)); + //const mvMatrix = se3.product(se3.translation(...position), se3.rotxyz(...orientation)); + const mvMatrix = se3.identity(); gl.useProgram(program); @@ -89,16 +91,15 @@ function draw(gl, params, objects) { 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.bindBuffer(gl.ARRAY_BUFFER, geometry.glBuffer); + + gl.vertexAttribPointer(positionLoc, 3, gl.FLOAT, false, 20, 0); gl.enableVertexAttribArray(positionLoc); - gl.bindBuffer(gl.ARRAY_BUFFER, geometry.normalBuffer); - gl.vertexAttribPointer(normalLoc, 3, gl.FLOAT, false, 0, 0); + gl.vertexAttribPointer(normalLoc, 3, gl.BYTE, true, 20, 12); gl.enableVertexAttribArray(normalLoc); - gl.bindBuffer(gl.ARRAY_BUFFER, geometry.textureBuffer); - gl.vertexAttribPointer(textureLoc, 2, gl.FLOAT, false, 0, 0); + gl.vertexAttribPointer(textureLoc, 2, gl.UNSIGNED_SHORT, true, 20, 16); gl.enableVertexAttribArray(textureLoc); gl.activeTexture(gl.TEXTURE0); @@ -149,7 +150,47 @@ function handleKeys(params) { }); } -function tick(time, gl, params, objects) { +function getObjects(world, z, x, program, texture) { + return world.chunks + .filter(chunk => { + if (chunk.position.z < z - 8 * 16) return false; + if (chunk.position.z > z + 7 * 16) return false; + if (chunk.position.x < x - 8 * 16) return false; + if (chunk.position.x > x + 7 * 16) return false; + + if (chunk.buffer === undefined) { + return false; + } + + return true; + }) + .map(chunk => ({ + position: [0.0, 0.0, 0.0], + orientation: [0.0, 0.0, 0.0], + program, + geometry: chunk.buffer, + texture, + })); +} + +function tick(time, gl, params) { + const campos = params.camera.position; + // expensive stuff, can take several cycles + try { + let timeLeft = 10; + const start = performance.now(); + generateMissingChunks(params.world, campos[2], campos[0], timeLeft); + + timeLeft -= performance.now() - start; + updateWorldGeometry(gl, params.world, campos[2], campos[0], timeLeft); + } + catch (ex) { + if (ex !== 'timesup') { + throw ex; + } + } + + const objects = getObjects(params.world, campos[2], campos[0], params.program, params.texture); draw(gl, params, objects); handleKeys(params); @@ -159,7 +200,7 @@ function tick(time, gl, params, objects) { document.querySelector('#fps').textContent = `${1.0 / dt} fps`; - requestAnimationFrame(time => tick(time, gl, params, objects)); + requestAnimationFrame(time => tick(time, gl, params)); } const BlockType = { @@ -209,7 +250,7 @@ function makeTerrain(x, y) { + ghettoPerlinNoise(seed, x, y, 64) * 0.5 ); - const terrain = Array(16); + const terrain = Array(16 * 16); for (let i = 0; i < 16; i++) { for (let j = 0; j < 16; j++) { @@ -220,8 +261,8 @@ function makeTerrain(x, y) { return terrain; } -function makeChunk(x, y) { - const terrain = makeTerrain(x, y); +function makeChunk(z, x) { + const terrain = makeTerrain(z, x); const data = new Uint8Array(16 * 16 * 256); @@ -232,7 +273,7 @@ function makeChunk(x, y) { // that block is grass // everything below is dirt const offset = i * (16 * 256) + j * 256; - const stoneHeight = Math.min(62, height); + const stoneHeight = Math.min(52, height); data.set(Array(stoneHeight).fill(BlockType.STONE), offset); if (stoneHeight < height) { data.set(Array(height - 1 - stoneHeight).fill(BlockType.DIRT), offset + stoneHeight); @@ -243,50 +284,51 @@ function makeChunk(x, y) { } return { - position: {x, y}, + position: {z, x}, data, }; } -function makeChunkMesh(chunk) { - const {x: z0, y: x0} = chunk.position; - const {data} = chunk; +function blockLookup(world, x, y, z) { + const chunki = Math.floor(z / 16); + const chunkj = Math.floor(x / 16); - 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); + const chunk = world.chunkMap.get(chunki, chunkj); + if (chunk === undefined) { + return BlockType.STONE; + } - return [x, y, z]; - }; + const i = Math.floor(z - chunki * 16); + const j = Math.floor(x - chunkj * 16); + const k = Math.floor(y); - const blockIndex = (i, j, k) => 256 * (i*16 +j) + k; + return chunk.data[256 * (16*i + j) + k]; +} - const sideNeighbors = (i, j) => { +function makeChunkBuffer(gl, data, z0, x0, lookup) { + const sideNeighbors = (bi, i, j, k) => { + if (i === 0 || j === 0 || i === 15 || j === 15) { + return [ + { block: lookup(i - 1, j, k), dir: '-z', faceCenter: [x0 + j, k, z0 + i - 0.5] }, + { block: lookup(i + 1, j, k), dir: '+z', faceCenter: [x0 + j, k, z0 + i + 0.5] }, + { block: lookup(i, j - 1, k), dir: '-x', faceCenter: [x0 + j - 0.5, k, z0 + i] }, + { block: lookup(i, j + 1, k), dir: '+x', faceCenter: [x0 + j + 0.5, k, z0 + i] }, + ]; + } 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]; + { block: data[bi - 256 * 16], dir: '-z', faceCenter: [x0 + j, k, z0 + i - 0.5] }, + { block: data[bi + 256 * 16], dir: '+z', faceCenter: [x0 + j, k, z0 + i + 0.5] }, + { block: data[bi - 256], dir: '-x', faceCenter: [x0 + j - 0.5, k, z0 + i] }, + { block: data[bi + 256], dir: '+x', faceCenter: [x0 + j + 0.5, k, z0 + i] }, + ]; }; 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); - + let bi = i * 16 * 256 + j * 256; + for (let k = 0; k < 256; k++, bi++) { const upTexture = (() => { switch (data[bi - 1]) { case BlockType.GRASS: return [0, 15]; @@ -308,41 +350,131 @@ function makeChunkMesh(chunk) { } })(); - for ({ pos, dir } of sideNeighbors(i, j)) { - if (data[blockIndex(...pos, k)] == BlockType.AIR) { - faces.push(makeFace(dir, sideTexture, faceDir(dir, x0 + j, k, z0 + i))); + for (let {block, dir, faceCenter} of sideNeighbors(bi, i, j, k)) { + if (block === BlockType.AIR) { + faces.push(makeFace(dir, sideTexture, faceCenter)); } } - 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; + return makeBufferFromFaces(gl, faces); } -function makeWorldBuffers(gl) { - const chunkFaces = [].concat( - makeChunkMesh(makeChunk(0, -16)), - makeChunkMesh(makeChunk(16, -16)), - makeChunkMesh(makeChunk(0, 0)), - makeChunkMesh(makeChunk(16, 0)), - makeChunkMesh(makeChunk(16, 16)), - makeChunkMesh(makeChunk(0, 16)), - ); +// - data <-- need to generate the first time, update when m&p +// - faces <-- need to generate once when chunk enters view +// - buffers <-- need to generate every time geometry changes? +// --> could also render chunks 1 by 1 - return makeBuffersFromFraces(gl, chunkFaces); +class ChunkMap { + map = {}; + + key(i, j) { + // meh, this limits us to 65536 chunks in the x direction :/ + return (i << 16) + j; + } + + get(i, j) { + return this.map[this.key(i, j)]; + } + + set(i, j, x) { + this.map[this.key(i, j)] = x; + } + + has(i, j) { + return this.key(i, j) in this.map; + } +} + +/** Makes a brave new world, centered at (0, 0). */ +function makeWorld() { + return generateMissingChunks({chunks: [], chunkMap: new ChunkMap()}, 0, 0); +} + +/** Update the world, generating missing chunks if necessary. */ +function generateMissingChunks(world, z, x, timeLimit = 10000) { + const ic = Math.floor(z / 16); + const jc = Math.floor(x / 16); + + const start = performance.now(); + + for (let i = ic - 8; i < ic + 8; i++) { + for (let j = jc - 8; j < jc + 8; j++) { + if (world.chunkMap.has(i, j)) { + continue; + } + const chunk = makeChunk(i * 16, j * 16); + world.chunks.push(chunk); + world.chunkMap.set(i, j, chunk); + + invalidateChunkGeometry(world, i - 1, j); + invalidateChunkGeometry(world, i + 1, j); + invalidateChunkGeometry(world, i, j - 1); + invalidateChunkGeometry(world, i, j + 1); + + if (performance.now() - start > timeLimit) { + throw 'timesup'; + } + } + } + + return world; +} + +function invalidateChunkGeometry(world, i, j) { + const chunk = world.chunkMap.get(i, j); + if (chunk === undefined) { + return; + } + if (chunk.buffer === undefined) { + return; + } + chunk.buffer.delete(); + delete chunk.buffer; +} + +/** Generates geometry for all visible chunks. */ +function updateWorldGeometry(gl, world, z, x, timeLimit = 10000) { + const ic = Math.floor(z / 16); + const jc = Math.floor(x / 16); + + const blockIndex = (i, j, k) => 256 * (i*16 +j) + k; + + const start = performance.now(); + + // k. Now, generate buffers for all chunks + for (let i = ic - 8; i < ic + 8; i++) { + for (let j = jc - 8; j < jc + 8; j++) { + const chunk = world.chunkMap.get(i, j); + + if (chunk.buffer !== undefined) { + continue; + } + + const chunkz = 16 * i; + const chunkx = 16 * j; + const lookup = (i, j, k) => blockLookup(world, j + chunkx, k, i + chunkz); + + chunk.buffer = makeChunkBuffer(gl, chunk.data, chunk.position.z, chunk.position.x, lookup); + + // throttle this for fluidity + if (performance.now() - start > timeLimit) { + throw 'timesup'; + } + } + } +} + +function checkCollision(curPos, newPos, world) { + // [ ] get a BB for the player at newPos + // [ ] check it against world + // [ ] need a struct to access world @ x, y, z + // [ ] need to check all 8 corners of the BB + // [ ] if it collides, figure out by how much and return a safe newPos + + return safeNewPos; } // Stuff I need to do: @@ -359,6 +491,7 @@ function makeWorldBuffers(gl) { // [ ] flowy water // [ ] ALIGN CHUNK WITH WORLD COORDS // [x] better controls +// [ ] save the world (yay) to local storage (bah) async function main() { const canvas = document.querySelector('#game'); @@ -370,6 +503,7 @@ async function main() { } const program = makeProgram(gl, TEST_VSHADER, TEST_FSHADER); + const texture = await loadTexture(gl, 'texture.png'); const params = { projMatrix: se3.perspective(Math.PI / 3, canvas.clientWidth / canvas.clientHeight, 0.1, 100.0), @@ -377,21 +511,12 @@ async function main() { position: [0.0, 70.5, 0.0], orientation: [0.0, Math.PI, 0.0], }, - keys: new Set() + keys: new Set(), + world: makeWorld(), + texture, + program, } - 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 => { @@ -419,7 +544,7 @@ async function main() { document.addEventListener('keydown', keyListener); document.addEventListener('keyup', keyListener); }; - requestAnimationFrame(time => tick(time, gl, params, objects)); + requestAnimationFrame(time => tick(time, gl, params)); } window.onload = main; \ No newline at end of file