From a523c588ba3de4e04472bbcf2aa995973509ddf2 Mon Sep 17 00:00:00 2001 From: Paul Mathieu Date: Tue, 14 Dec 2021 14:41:33 -0800 Subject: [PATCH] Moving code around --- index.js | 289 ++----------------------------------------------------- world.js | 280 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 289 insertions(+), 280 deletions(-) create mode 100644 world.js diff --git a/index.js b/index.js index 9159e91..4d076de 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,5 @@ -import { makeBufferFromFaces, makeFace} from "./geometry"; import { loadTexture, makeProgram } from "./gl"; +import { blockLookup, BlockType, generateMissingChunks, makeWorld, updateWorldGeometry } from './world'; import * as se3 from './se3'; const TEST_VSHADER = ` @@ -251,285 +251,6 @@ function tick(time, gl, params) { requestAnimationFrame(time => tick(time, gl, params)); } -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 * 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(z, x) { - const terrain = makeTerrain(z, x); - - const data = 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(52, height); - data.set(Array(stoneHeight).fill(BlockType.STONE), offset); - if (stoneHeight < height) { - data.set(Array(height - 1 - stoneHeight).fill(BlockType.DIRT), offset + stoneHeight); - data[offset + height - 1] = BlockType.GRASS; - } - data.set(Array(256 - height).fill(BlockType.AIR), offset + height); - } - } - - return { - position: {z, x}, - data, - }; -} - -function blockLookup(world, x, y, z) { - const midx = x + 0.5; - const midy = y + 0.5; - const midz = z + 0.5; - - const chunki = Math.floor(midz / 16); - const chunkj = Math.floor(midx / 16); - - const chunk = world.chunkMap.get(chunki, chunkj); - if (chunk === undefined) { - return { - type: BlockType.STONE, - x, y, z, - }; - } - - const i = Math.floor(midz - chunki * 16); - const j = Math.floor(midx - chunkj * 16); - const k = Math.floor(midy); - - return { - type: chunk.data[256 * (16*i + j) + k], - x: j, - y: k, - z: i, - }; -} - -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 [ - { 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++) { - 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]; - case BlockType.DIRT: return [2, 15]; - case BlockType.STONE: return [3, 15]; - } - })(); - - if (data[bi] == BlockType.AIR) { - faces.push(makeFace('+y', upTexture, [x0 + j, k - 0.5, z0 + i])); - break; - } - - const sideTexture = (() => { - switch (data[bi]) { - case BlockType.GRASS: return [1, 15]; - case BlockType.DIRT: return [2, 15]; - case BlockType.STONE: return [3, 15]; - } - })(); - - for (let {block, dir, faceCenter} of sideNeighbors(bi, i, j, k)) { - if (block === BlockType.AIR) { - faces.push(makeFace(dir, sideTexture, faceCenter)); - } - } - } - } - } - - return makeBufferFromFaces(gl, faces); -} - -// - 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 - -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 radius = 0; radius < 8; radius++) { - for (let i = ic - radius; i < ic + radius; i++) { - for (let j = jc - radius; j < jc + radius; 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).type; - - 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) { // I guess Steve is about 1.7 m tall? // he also has a 60x60 cm axis-aligned square section '^_^ @@ -579,6 +300,14 @@ function checkCollision(curPos, newPos, world) { }; } +// Mine & place +// ------------ +// [ ] ray casting +// [ ] block outline +// [ ] crosshair +// [ ] dynamic terrain re-rendering +// [ ] should use a linked list of air contact blocks + // Stuff I need to do: // [x] a skybox // [x] a movable camera diff --git a/world.js b/world.js new file mode 100644 index 0000000..5cae85c --- /dev/null +++ b/world.js @@ -0,0 +1,280 @@ +import { makeBufferFromFaces, makeFace} from "./geometry"; + +export 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 * 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(z, x) { + const terrain = makeTerrain(z, x); + + const data = 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(52, height); + data.set(Array(stoneHeight).fill(BlockType.STONE), offset); + if (stoneHeight < height) { + data.set(Array(height - 1 - stoneHeight).fill(BlockType.DIRT), offset + stoneHeight); + data[offset + height - 1] = BlockType.GRASS; + } + data.set(Array(256 - height).fill(BlockType.AIR), offset + height); + } + } + + return { + position: {z, x}, + data, + }; +} + +export function blockLookup(world, x, y, z) { + const midx = x + 0.5; + const midy = y + 0.5; + const midz = z + 0.5; + + const chunki = Math.floor(midz / 16); + const chunkj = Math.floor(midx / 16); + + const chunk = world.chunkMap.get(chunki, chunkj); + if (chunk === undefined) { + return { + type: BlockType.STONE, + x, y, z, + }; + } + + const i = Math.floor(midz - chunki * 16); + const j = Math.floor(midx - chunkj * 16); + const k = Math.floor(midy); + + return { + type: chunk.data[256 * (16*i + j) + k], + x: j, + y: k, + z: i, + }; +} + +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 [ + { 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++) { + 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]; + case BlockType.DIRT: return [2, 15]; + case BlockType.STONE: return [3, 15]; + } + })(); + + if (data[bi] == BlockType.AIR) { + faces.push(makeFace('+y', upTexture, [x0 + j, k - 0.5, z0 + i])); + break; + } + + const sideTexture = (() => { + switch (data[bi]) { + case BlockType.GRASS: return [1, 15]; + case BlockType.DIRT: return [2, 15]; + case BlockType.STONE: return [3, 15]; + } + })(); + + for (let {block, dir, faceCenter} of sideNeighbors(bi, i, j, k)) { + if (block === BlockType.AIR) { + faces.push(makeFace(dir, sideTexture, faceCenter)); + } + } + } + } + } + + return makeBufferFromFaces(gl, faces); +} + +// - 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 + +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 (empty) world */ +export function makeWorld() { + return {chunks: [], chunkMap: new ChunkMap()}; +} + +/** Update the world, generating missing chunks if necessary. */ +export 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. */ +export 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 radius = 0; radius < 8; radius++) { + for (let i = ic - radius; i < ic + radius; i++) { + for (let j = jc - radius; j < jc + radius; 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).type; + + chunk.buffer = makeChunkBuffer(gl, chunk.data, chunk.position.z, chunk.position.x, lookup); + + // throttle this for fluidity + if (performance.now() - start > timeLimit) { + throw 'timesup'; + } + } + } + + } +} \ No newline at end of file