diff --git a/game.js b/game.js index 7914d56..fc29adb 100644 --- a/game.js +++ b/game.js @@ -1,6 +1,8 @@ import { makeBufferFromFaces } from "./geometry"; +import { memoize } from "./memoize"; import * as se3 from './se3'; import { + blockLookup, BlockType, castRay, checkCollision, @@ -123,19 +125,6 @@ function handleKeys(params) { }); } -function memoize(f) { - const memo = {}; - - function g(...args) { - if (!(args in memo)) { - memo[args] = f(...args); - } - return memo[args]; - } - - return g; -} - /** From far to near. */ function _getChunkOrder(chunki, chunkj) { return [...function* () { @@ -193,6 +182,10 @@ function tagABlock(gl, params, objects) { const ori = se3.inverse(se3.rotxyz(-camori[0], -camori[1], -camori[2])); const viewDirection = se3.apply(ori, dir).slice(0, 3); + if (blockLookup(params.world, ...params.camera.position).type !== BlockType.AIR) { + return; + } + const face = markBlock(params.world, params.camera.position, viewDirection, params.blockSelectDistance); if (face === undefined) { return; @@ -242,6 +235,7 @@ function tagABlock(gl, params, objects) { // [ ] inventory // [ ] monsters // [ ] multi player +// [ ] chat export function setupParamPanel(params) { document.querySelector('#lightx').oninput = e => { diff --git a/geometry.js b/geometry.js index 45c66b6..d930c2a 100644 --- a/geometry.js +++ b/geometry.js @@ -1,15 +1,4 @@ -function memoize(f) { - const memo = {}; - - function g(...args) { - if (!(args in memo)) { - memo[args] = f(...args); - } - return memo[args]; - } - - return g; -} +import { memoize } from "./memoize"; function _makeTextureFace(texture) { const textMul = 0.0625; diff --git a/memoize.js b/memoize.js new file mode 100644 index 0000000..3d5aefc --- /dev/null +++ b/memoize.js @@ -0,0 +1,12 @@ +export function memoize(f) { + const memo = {}; + + function g(...args) { + if (!(args in memo)) { + memo[args] = f(...args); + } + return memo[args]; + } + + return g; +} \ No newline at end of file diff --git a/terrain.js b/terrain.js index 13c7807..f70a1f5 100644 --- a/terrain.js +++ b/terrain.js @@ -1,3 +1,5 @@ +import { memoize } from "./memoize"; + const smoothstep = x => x*x*(3 - 2*x); function interpolate(a, b, x, f = smoothstep) { const val = a + f(x) * (b - a); @@ -21,6 +23,10 @@ export function random(seed, z, x) { return xorshift(1337 * z + seed + 80085 * x); } +export function random3d(seed, z, x, y) { + return xorshift(1337 * z + seed + 80085 * x + 13 * y); +} + function ghettoPerlinNoise(seed, x, y, gridSize = 16) { const dot = (vx, vy) => vx[0] * vy[0] + vx[1] * vy[1]; @@ -42,6 +48,39 @@ function ghettoPerlinNoise(seed, x, y, gridSize = 16) { return 1.5 * interpolate(interpolate(n0, n1, sx), interpolate(n2, n3, sx), sy); } +function gpn3d(seed, x, y, z, gridSize = 16) { + const dot = (u, v) => u[0] * v[0] + u[1] * v[1] + u[2] * v[2]; + + const randGrad = (x0, y0, z0) => { + const rand0 = random3d(seed, x0, y0, z0); + const rand1 = random3d(seed+1, x0, y0, z0); + return [Math.cos(rand0), Math.sin(rand1) * Math.sin(rand0), Math.cos(rand1) * Math.sin(rand0)]; + }; + + const x0 = Math.floor(x / gridSize); + const y0 = Math.floor(y / gridSize); + const z0 = Math.floor(z / gridSize); + const sx = x / gridSize - x0; + const sy = y / gridSize - y0; + const sz = z / gridSize - z0; + + const n0 = dot(randGrad(x0, y0, z0), [sx, sy, sz]); + const n1 = dot(randGrad(x0 + 1, y0, z0), [sx - 1, sy, sz]); + const n2 = dot(randGrad(x0, y0 + 1, z0), [sx, sy - 1, sz]); + const n3 = dot(randGrad(x0 + 1, y0 + 1, z0), [sx - 1, sy - 1, sz]); + + const n4 = dot(randGrad(x0, y0, z0 + 1), [sx, sy, sz - 1]); + const n5 = dot(randGrad(x0 + 1, y0, z0 + 1), [sx - 1, sy, sz - 1]); + const n6 = dot(randGrad(x0, y0 + 1, z0 + 1), [sx, sy - 1, sz - 1]); + const n7 = dot(randGrad(x0 + 1, y0 + 1, z0 + 1), [sx - 1, sy - 1, sz - 1]); + + return 1.5 * interpolate( + interpolate(interpolate(n0, n1, sx), interpolate(n2, n3, sx), sy), + interpolate(interpolate(n4, n5, sx), interpolate(n6, n7, sx), sy), + sz, + ); +} + function cliffPerlin(seed, x, y) { const noise1 = ghettoPerlinNoise(seed, x, y); const noise2 = ghettoPerlinNoise(seed+1, x, y); @@ -55,6 +94,56 @@ function cliffPerlin(seed, x, y) { return interpolate(-1, 1, 0.5 * (1 + Math.atan2(noise1, noise2) / Math.PI), softerEdge); } +function _fractalGpn3d(seed, x, y, z) { + const lacunarity = 3.4; + const persistence = 0.28; + + let value = 0; + let power = 0.6; + let scale = 0.7; + const noises = [gpn3d, gpn3d, gpn3d]; + + for (const noiseFun of noises) { + const noise = noiseFun(seed, scale * x, scale * y, scale * z); + + value += noise * power; + + power *= persistence; + scale *= lacunarity; + } + + return value; +} +const fractalGpn3d = memoize(_fractalGpn3d); + +export function checkCave(seed, x, y, z) { + const nx = Math.floor(x / 8) * 8; + const ny = Math.floor(y / 4) * 4; + const nz = Math.floor(z / 8) * 8; + + const sx = (x - nx) / 8; + const sy = (y - ny) / 4; + const sz = (z - nz) / 8; + + const itp = interpolate; + const n = (x, y, z) => fractalGpn3d(seed, x / 3, y, z / 3); + + const noise = itp( + itp(itp(n(nx, ny, nz), n(nx + 8, ny, nz), sx), + itp(n(nx, ny + 4, nz), n(nx + 8, ny + 4, nz), sx), + sy + ), + itp(itp(n(nx, ny, nz + 8), n(nx + 8, ny, nz + 8), sx), + itp(n(nx, ny + 4, nz + 8), n(nx + 8, ny + 4, nz + 8), sx), + sy + ), + sz); + + //if (n(nx, ny, nz) > 0.3) throw Error('break!'); + + return noise > 0.28 && noise < 0.55; +} + export function makeTerrain(seed, x, y) { const lacunarity = 2.1; const persistence = 0.35; diff --git a/world.js b/world.js index 41d6f6d..ce70867 100644 --- a/world.js +++ b/world.js @@ -1,7 +1,7 @@ import { makeBufferFromFaces, makeFace } from "./geometry"; import { loadTexture, makeProgram } from "./gl"; import * as se3 from './se3'; -import { makeTerrain, random } from "./terrain"; +import { checkCave, makeTerrain, random } from "./terrain"; const VSHADER = ` attribute vec3 aPosition; @@ -132,6 +132,54 @@ function makeChunk(z, x) { makeATree(data, tree, seed, z, x); } + // caves + // [ ] 3d perlin noise up to 64 + // [ ] sample 4x4x4 to check for caves + // [ ] fill with air where appropriate + + function propagateCave(i, j, k) { + const neighbors = (i, j, k) => [ + [i - 1, j, k], + [i + 1, j, k], + [i, j - 1, k], + [i, j + 1, k], + [i, j, k - 1], + [i, j, k + 1], + ]; + + const queue = neighbors(i, j, k); + + while (queue.length > 0) { + const [ni, nj, nk] = queue.pop(); + if (ni < 0 || ni > 15 || nj < 0 || nj > 15 || nk < 0 || nk > 255) { + continue; + } + const bi = 256 * (16 * ni + nj) + nk; + if (data[bi] === BlockType.AIR || data[bi] === BlockType.WATER) { + continue; + } + if (checkCave(seed, x + nj, nk, z + ni)) { + data[bi] = BlockType.AIR; + queue.push(...neighbors(ni, nj, nk)); + } + } + } + + for (let i = 0; i < 16; i += 4) { + for (let j = 0; j < 16; j += 4) { + let bi = 256 * (16 * i + j); + for (let k = 0; k < 58; k += 4, bi += 4) { + if (data[bi] === BlockType.AIR) { + continue; + } + if (checkCave(seed, x + j, k, z + i)) { + data[bi] = BlockType.AIR; + propagateCave(i, j, k); + } + } + } + } + return { position: {z, x}, data, @@ -302,7 +350,7 @@ function makeFaces(chunk, blocks, neighbors) { .filter(({ block }) => isTransparent(block) && block !== BlockType.WATER) ) { if (chunk.data[bi] === BlockType.WATER) { - faceCenter[1] -= 0.15; + faceCenter[1] -= 0.15; // TODO: lower face should be normal } chunk.transparentFaces.push({ dir,