import { makeFace } from 'wmc-common/geometry'; import * as linalg from './linalg'; import {memoize} from 'wmc-common/memoize'; type direction = ('-x' | '+x' | '-y' | '+y' | '-z' | '+z'); export const BlockType = { UNDEFINED: 0, AIR: 1, DIRT: 2, GRASS: 3, STONE: 4, WATER: 5, TREE: 6, LEAVES: 7, SUN: 8, }; const CHUNKSIZE = 32; /** seed: some kind of number uniquely defining the body * x, y, z: space coordinates in the body's frame * * returns: a chunk */ function makeDirtBlock(seed: number) { // XXX: for now, return a premade chunk: a 24x24x24 cube of dirt // surrounded by 4 blocks of air all around const cs = CHUNKSIZE; if (seed !== 1337) { return {}; } // if (Math.abs const blocks = new Array(cs * cs * cs); blocks.fill(BlockType.AIR); const dirt = new Array(24).fill(BlockType.DIRT); for (let i = 0; i < 24; i++) { for (let j = 4; j < 28; j++) { const offset = cs * cs * (i + 4) + cs * j; blocks.splice(offset + 4, 24, ...dirt); } } const half = cs / 2; return { position: [-half, -half, -half], blocks, underground: false, seed, }; } function makeSunChunk(seed: number, i: number, j: number, k: number) { const cs = CHUNKSIZE; const radius = 42; if (Math.abs(cs * i) > radius || Math.abs(cs * j) > radius || Math.abs(cs * k) > radius) { return undefined; } const half = cs / 2; const blocks = new Array(cs**3); blocks.fill(BlockType.SUN); let underground = true; for (let x = 0; x < cs; x++) { for (let y = 0; y < cs; y++) { for (let z = 0; z < cs; z++) { const pos = [ x + i * cs - half, y + j * cs - half, z + k * cs - half, ]; const idx = ( z * cs * cs + y * cs + x ); if (pos[0]**2 + pos[1]**2 + pos[2]**2 > radius**2) { blocks[idx] = BlockType.AIR; underground = false; } } } } return { position: [i * cs - half, j * cs - half, k * cs - half], layout: [i, j, k], blocks, seed, underground, }; } function _getChunk(seed: number, chunkX: number, chunkY: number, chunkZ: number) { if (seed === 0) { return makeSunChunk(seed, chunkX, chunkY, chunkZ); } if (chunkX === 0 && chunkY === 0 && chunkZ === 0) { return makeDirtBlock(seed); // x, y, z unused right now } return undefined; } const getChunk = memoize(_getChunk); function faceTexture(type: number, dir: direction) { switch (type) { case BlockType.GRASS: switch (dir) { case '+y': return [0, 15]; case '-y': return [2, 15]; default: return [1, 15]; } case BlockType.DIRT: return [2, 15]; case BlockType.STONE: return [3, 15]; case BlockType.WATER: return [4, 15]; case BlockType.TREE: switch (dir) { case '+y': case '-y': return [5, 15]; default: return [6, 15]; } case BlockType.LEAVES: return [7, 15]; case BlockType.SUN: return [0, 4]; default: return [0, 0]; } } function* makeChunkFaces(chunk) { const cs = CHUNKSIZE; function faceCenter(pos: linalg.Vec3, dir: direction) { switch (dir) { case '-x': return [pos[0] - 0.5, pos[1], pos[2]]; case '+x': return [pos[0] + 0.5, pos[1], pos[2]]; case '-y': return [pos[0], pos[1] - 0.5, pos[2]]; case '+y': return [pos[0], pos[1] + 0.5, pos[2]]; case '-z': return [pos[0], pos[1], pos[2] - 0.5]; case '+z': return [pos[0], pos[1], pos[2] + 0.5]; } } function neighborChunk(dir: direction) { const [chunkX, chunkY, chunkZ] = chunk.layout; if (chunk.neighbors === undefined) { chunk.neighbors = {}; } if (!(dir in chunk.neighbors)) { switch (dir) { case '-x': chunk.neighbors[dir] = getChunk(chunk.seed, chunkX - 1, chunkY, chunkZ); break; case '+x': chunk.neighbors[dir] = getChunk(chunk.seed, chunkX + 1, chunkY, chunkZ); break; case '-y': chunk.neighbors[dir] = getChunk(chunk.seed, chunkX, chunkY - 1, chunkZ); break; case '+y': chunk.neighbors[dir] = getChunk(chunk.seed, chunkX, chunkY + 1, chunkZ); break; case '-z': chunk.neighbors[dir] = getChunk(chunk.seed, chunkX, chunkY, chunkZ - 1); break; case '+z': chunk.neighbors[dir] = getChunk(chunk.seed, chunkX, chunkY, chunkZ + 1); break; } } return chunk.neighbors[dir]; } const neighborIndices = { '-x': (x, y, z) => z * cs * cs + y * cs + (cs - 1), '+x': (x, y, z) => z * cs * cs + y * cs + 0, '-y': (x, y, z) => z * cs * cs + (cs - 1) * cs + x, '+y': (x, y, z) => z * cs * cs + 0 * cs + x, '-z': (x, y, z) => (cs - 1) * cs * cs + y * cs + x, '+z': (x, y, z) => 0 * cs * cs + y * cs + x, }; function neighborBlock(dir: direction, x, y, z) { const neighbor = neighborChunk(dir); let block; if (neighbor === undefined) { block = BlockType.AIR; } else { block = neighbor.blocks[neighborIndices[dir](x, y, z)]; } return { dir, block }; } function* neighbors(x, y, z) { const idx = ( z * cs * cs + y * cs + x ); if (x > 0) { yield { block: chunk.blocks[idx - 1], dir: '-x', }; } else { yield neighborBlock('-x', x, y, z); } if (x < cs - 1) { yield { block: chunk.blocks[idx + 1], dir: '+x', }; } else { yield neighborBlock('+x', x, y, z); } if (y > 0) { yield { block: chunk.blocks[idx - cs], dir: '-y', }; } else { yield neighborBlock('-y', x, y, z); } if (y < cs - 1) { yield { block: chunk.blocks[idx + cs], dir: '+y', }; } else { yield neighborBlock('+y', x, y, z); } if (z > 0) { yield { block: chunk.blocks[idx - cs * cs], dir: '-z', }; } else { yield neighborBlock('-z', x, y, z); } if (z < cs - 1) { yield { block: chunk.blocks[idx + cs * cs], dir: '+z', }; } else { yield neighborBlock('+z', x, y, z); } } for (let x = 0; x < cs; x++) { for (let y = 0; y < cs; y++) { for (let z = 0; z < cs; z++) { const idx = ( z * cs * cs + y * cs + x ); const chpos = chunk.position; const bkpos = [ chpos[0] + x, chpos[1] + y, chpos[2] + z, ]; const bt = chunk.blocks[idx]; if (bt === BlockType.AIR) { continue; } for (const { block, dir } of neighbors(x, y, z)) { if (block !== BlockType.AIR) { continue; } yield makeFace(dir, faceTexture(bt, dir), faceCenter(bkpos, dir)); } } } } } function getBodyChunks(seed: number) { const chunks = []; const toCheck = [[0, 0, 0]]; while (toCheck.length > 0) { const [chunkX, chunkY, chunkZ] = toCheck.pop(); const thisChunk = getChunk(seed, chunkX, chunkY, chunkZ); if (thisChunk === undefined || chunks.includes(thisChunk)) { continue; } chunks.push(thisChunk); toCheck.push([chunkX - 1, chunkY, chunkZ]); toCheck.push([chunkX + 1, chunkY, chunkZ]); toCheck.push([chunkX, chunkY - 1, chunkZ]); toCheck.push([chunkX, chunkY + 1, chunkZ]); toCheck.push([chunkX, chunkY, chunkZ - 1]); toCheck.push([chunkX, chunkY, chunkZ + 1]); } return chunks; } /** fake chunk map re-using the memoize */ class ChunkMap { constructor(seed) { this.seed = seed; } get(i, j, k) { return getChunk(this.seed, i, j, k); } has(i, j, k) { return getChunk(this.seed, i, j, k) !== undefined; } } export function getBodyGeometry(seed: number) { const faces = getBodyChunks(seed) .filter(chunk => !chunk.underground) .map(chunk => [...makeChunkFaces(chunk)]); return { faces: faces.reduce((a, b) => a.concat(b)), chunkMap: new ChunkMap(seed), }; } function blockLookup(chunkMap, x, y, z) { const chunki = Math.floor((x + CHUNKSIZE / 2) / CHUNKSIZE); const chunkj = Math.floor((y + CHUNKSIZE / 2) / CHUNKSIZE); const chunkk = Math.floor((z + CHUNKSIZE / 2) / CHUNKSIZE); const chunk = chunkMap.get(chunki, chunkj, chunkk); if (chunk === undefined) { return { type: BlockType.UNDEFINED, }; } const i = Math.floor(x - chunk.position[0] + 0.5); const j = Math.floor(y - chunk.position[1] + 0.5); const k = Math.floor(z - chunk.position[2] + 0.5); const blockIndex = CHUNKSIZE * (CHUNKSIZE * i + j) + k; return { type: chunk.blocks[blockIndex], centerPosition: [ chunk.position[0] + i, chunk.position[1] + j, chunk.position[2] + k, ], chunk, blockIndex, }; } function movePoint(p, s, u) { return linalg.add(p, linalg.scale(u, s)); } function minIndex(arr) { return arr.reduce((min, val, i) => val >= arr[min] ? min : i, -1); } /** Imported from wmc, looks like it calculates the distance to the next grid block */ function rayThroughGrid(origin, direction, maxDistance) { const range = i => [...Array(i).keys()]; const nextGrid = range(3).map(i => direction[i] > 0 ? Math.floor(origin[i] + 0.5) + 0.5 : Math.floor(origin[i] + 0.5) - 0.5); const distanceToGrid = range(3).map(i => (nextGrid[i] - origin[i]) / direction[i]) .map(v => v === 0.0 ? Number.POSITIVE_INFINITY : v); const axis = minIndex(distanceToGrid); const rayLength = distanceToGrid[axis]; if (rayLength > maxDistance) { return {}; } const position = movePoint(origin, distanceToGrid[axis], direction); const normal = range(3).map(i => i === axis ? -Math.sign(direction[i]) : 0); return {position, normal, distance: rayLength}; } /** needs a blockLookup function, finds the first non-air block along a ray */ export function castRay(chunkMap, origin, direction, maxDistance=CHUNKSIZE) { let currentPoint = origin; while (maxDistance > 0) { const {position, normal, distance} = rayThroughGrid(currentPoint, direction, maxDistance); if (position === undefined) { return; } maxDistance -= distance; currentPoint = movePoint(position, 0.01, direction); const blockCenter = movePoint(position, -0.5, normal); const block = blockLookup(chunkMap, ...blockCenter); if (block.type === BlockType.AIR) { continue; } return { block, normal, }; } }