diff --git a/game.js b/game.js index 7d9dac7..6256b73 100644 --- a/game.js +++ b/game.js @@ -133,6 +133,17 @@ function getObjects(world, z, x, glContext) { } return true; + }) + .sort((a, b) => { + const zz = Math.floor(z / 16); + const xx = Math.floor(x / 16); + const azz = Math.floor(a.position.z / 16) - zz; + const axx = Math.floor(a.position.x / 16) - xx; + const bzz = Math.floor(b.position.z / 16) - zz; + const bxx = Math.floor(b.position.x / 16) - xx; + const da = Math.abs(azz) + Math.abs(axx); + const db = Math.abs(bzz) + Math.abs(bxx); + return db - da; }); const buffers = drawedChunks .map(chunk => chunk.buffer) @@ -203,14 +214,15 @@ function tagABlock(gl, params, objects) { // [x] double jump!! // [x] fullscreen // [ ] a soundrack -// [ ] trees and stuff +// [x] trees and stuff // [ ] different biomes (with different noise stats) +// [ ] water you can swim in (and not walk on) // [ ] flowy water // [ ] save the world (yay) to local storage (bah) // [ ] caves // [ ] crafting // [ ] fix bugs -// [ ] better light +// [ ] better light (esp. cave lighting) // [ ] inventory // [ ] monsters // [ ] multi player diff --git a/terrain.js b/terrain.js index 5090b54..13c7807 100644 --- a/terrain.js +++ b/terrain.js @@ -9,18 +9,23 @@ const sigmoid = (a, b, f = smoothstep) => x => { return f((x - a) / (b - a)); }; +// super ghetto random +function xorshift(x) { + x ^= x << 13; + x ^= x >> 7; + x ^= x << 17; + return x; +} + +export function random(seed, z, x) { + return xorshift(1337 * z + seed + 80085 * x); +} + 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); + const rand = random(seed, x0, y0); return [Math.sin(rand), Math.cos(rand)]; }; @@ -50,8 +55,7 @@ function cliffPerlin(seed, x, y) { return interpolate(-1, 1, 0.5 * (1 + Math.atan2(noise1, noise2) / Math.PI), softerEdge); } -export function makeTerrain(x, y) { - const seed = 1337; +export function makeTerrain(seed, x, y) { const lacunarity = 2.1; const persistence = 0.35; const noiseMap = (x, y) => cliffPerlin(seed, x / 2, y / 2); @@ -119,6 +123,7 @@ function colorInterp(x) { function main() { const canvas = document.querySelector('#canvas'); const ctx = canvas.getContext('2d'); + const seed = 1337; ctx.fillStyle = 'gray'; ctx.fill(); @@ -128,7 +133,7 @@ function main() { const imgdat = ctx.createImageData(16, 16); const dat = imgdat.data; - const chunk = makeTerrain(i - canvas.width / 2, j - canvas.height / 2); + const chunk = makeTerrain(seed, i - canvas.width / 2, j - canvas.height / 2); for (let k = 0; k < 16; k++) { for (let l = 0; l < 16; l++) { const offset = 4 * (16 * (15 - l) + k); diff --git a/texture.png b/texture.png index 4795d44..262de73 100644 Binary files a/texture.png and b/texture.png differ diff --git a/world.js b/world.js index db31e06..c1cbff3 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 } from "./terrain"; +import { makeTerrain, random } from "./terrain"; const VSHADER = ` attribute vec3 aPosition; @@ -56,35 +56,78 @@ export const BlockType = { GRASS: 3, STONE: 4, WATER: 5, + TREE: 6, + LEAVES: 7, }; +function hasATree(seed, z, x) { + const rand = random(seed, z, x); + return (rand % 333 === 123); +} + +function makeATree(data, pos, seed, chunkz, chunkx) { + const height = 3 + random(seed, chunkz + pos[0], chunkx + pos[1]) % 6; + const offset = 256 * (16 * pos[0] + pos[1]); + const firstBlock = pos[2]; + for (let i = 0; i < height; i++) { + data[offset + firstBlock + i] = BlockType.TREE; + } + + function setBlock(i, j, k, type) { + if (i < 0 || j < 0 || k < 0 || i > 15 || j > 15 || k > 255) return; + const offset = 256 * (16 * i + j) + k; + if (data[offset] !== BlockType.AIR) return; + data[offset] = type; + } + + for (let i = pos[0] - 2; i < pos[0] + 3; i++) { + for (let j = pos[1] - 2; j < pos[1] + 3; j++) { + for (let k = firstBlock + height - 2; k < firstBlock + height + 2; k++) { + setBlock(i, j, k, BlockType.LEAVES); + } + } + } + + return firstBlock + height; +} + function makeChunk(z, x) { - const terrain = makeTerrain(z, x); + const seed = 1337; + const terrain = makeTerrain(seed, z, x); const data = new Uint8Array(16 * 16 * 256); + const trees = []; + for (let i = 0; i < 16; i++) { for (let j = 0; j < 16; j++) { const height = 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.max(48, height); const grassHeight = stoneHeight + 8; const waterHeight = 67; + let currentHeight = 0; data.set(Array(stoneHeight).fill(BlockType.STONE), offset); - data.set(Array(grassHeight - 1 - stoneHeight).fill(BlockType.DIRT), offset + stoneHeight); + currentHeight = stoneHeight; + data.set(Array(grassHeight - currentHeight).fill(BlockType.DIRT), offset + currentHeight); + currentHeight = grassHeight; if (grassHeight < waterHeight) { - data.set(Array(waterHeight - grassHeight + 1).fill(BlockType.WATER), offset + grassHeight - 1); + data.set(Array(waterHeight - currentHeight).fill(BlockType.WATER), offset + currentHeight); + currentHeight = waterHeight; } else { data[offset + grassHeight - 1] = BlockType.GRASS; + if (hasATree(seed, z + i, x + j)) { + trees.push([i, j, currentHeight]); + } } - const surfaceHeight = Math.max(waterHeight, grassHeight); - data.set(Array(256 - surfaceHeight).fill(BlockType.AIR), offset + surfaceHeight); + data.set(Array(256 - currentHeight).fill(BlockType.AIR), offset + currentHeight); } } + for (const tree of trees) { + makeATree(data, tree, seed, z, x); + } + return { position: {z, x}, data, @@ -140,6 +183,14 @@ function faceTexture(type, dir) { 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]; default: return [0, 0]; } } @@ -156,6 +207,16 @@ function faceCenter(blockCenter, dir) { } } +function isTransparent(type) { + switch (type) { + case BlockType.WATER: + case BlockType.LEAVES: + case BlockType.AIR: + return true; + default: return false; + } +} + function makeFaceList(data, chunkz, chunkx, blockLookup) { const lookup = (i, j, k) => { if (i < 0 || j < 0 || i > 15 || j > 15) { @@ -176,7 +237,7 @@ function makeFaceList(data, chunkz, chunkx, blockLookup) { ]; const solidFaces = []; - const waterFaces = []; + const transparentFaces = []; for (let i = 0; i < 16; i++) { for (let j = 0; j < 16; j++) { @@ -185,21 +246,28 @@ function makeFaceList(data, chunkz, chunkx, blockLookup) { if (data[bi] === BlockType.AIR) { continue; } - for (const {dir, faceCenter} of neighbors(i, j, k) - .filter(({ block }) => block === BlockType.AIR || (block === BlockType.WATER && data[bi] !== BlockType.WATER))) { - if (data[bi] !== BlockType.WATER) { - solidFaces.push({ - blockIndex: bi, + if (isTransparent(data[bi])) { + for (const { dir, faceCenter } of neighbors(i, j, k) + .filter(({ block }) => block === BlockType.AIR) + ) { + if (data[bi] === BlockType.WATER) { + faceCenter[1] -= 0.15; + } + transparentFaces.push({ dir, face: makeFace(dir, faceTexture(data[bi], dir), faceCenter), + blockIndex: bi, }); - } else { - faceCenter[1] -= 0.15; - waterFaces.push({ - blockIndex: bi, + } + } else { + for (const { dir, faceCenter } of neighbors(i, j, k) + .filter(({ block }) => isTransparent(block)) + ) { + solidFaces.push({ dir, face: makeFace(dir, faceTexture(data[bi], dir), faceCenter), - }) + blockIndex: bi, + }); } } } @@ -208,7 +276,7 @@ function makeFaceList(data, chunkz, chunkx, blockLookup) { return { solidFaces, - waterFaces, + transparentFaces, }; } @@ -292,20 +360,19 @@ function invalidateChunkGeometry(world, i, j) { } function addFace(chunk, block, dir) { - if (block.type === BlockType.WATER) { - const face = createChunkFace(block, dir); - if (dir === '+y') { - face[1] -= 0.15; - } + const face = createChunkFace(block, dir); + if (isTransparent(block.type)) { chunk.transparentFaces.push(face); - } else { - chunk.faces.push(createChunkFace(block, dir)); + chunk.faces.push(face); } } export function createChunkFace(block, dir, center = null) { center = center ?? faceCenter(block.centerPosition, dir); + if (block.type === BlockType.WATER && dir === '+y') { + center[1] -= 0.15; + } return { dir, blockIndex: block.blockIndex, @@ -337,7 +404,7 @@ export function updateWorldGeometry(gl, world, z, x, timeLimit = 10000) { const faces = makeFaceList(chunk.data, chunk.position.z, chunk.position.x, lookup); chunk.faces = faces.solidFaces; - chunk.transparentFaces = faces.waterFaces; + chunk.transparentFaces = faces.transparentFaces; } chunk.buffer = makeBufferFromFaces(gl, chunk.faces.map(f => f.face)); @@ -487,6 +554,7 @@ export function markBlock(world, cameraPosition, direction, maxDistance) { export function destroyBlock(world, block) { const trimFaces = chunk => { chunk.faces = chunk.faces.filter(({blockIndex}) => chunk.data[blockIndex] !== BlockType.AIR); + chunk.transparentFaces = chunk.transparentFaces.filter(({blockIndex}) => chunk.data[blockIndex] !== BlockType.AIR); } block.chunk.data[block.blockIndex] = BlockType.AIR; @@ -511,8 +579,6 @@ export function destroyBlock(world, block) { .filter(({ block }) => block.type !== BlockType.AIR && block.type !== BlockType.UNDEFINED) .forEach(({ block, dir }) => { - // TODO: don't add transparent faces twice - // since we "forget" to trim them addFace(block.chunk, block, dir); trimFaces(block.chunk);