From 4d0f4066698d2d01344283e36207cce09fdfd7b1 Mon Sep 17 00:00:00 2001 From: Paul Mathieu Date: Sat, 18 Dec 2021 09:57:00 -0800 Subject: [PATCH] Split code out of index.js into game.js and world.js --- game.js | 325 +++++++++++++++++++++++++++++ index.js | 618 +------------------------------------------------------ world.js | 300 ++++++++++++++++++++++++++- 3 files changed, 628 insertions(+), 615 deletions(-) create mode 100644 game.js diff --git a/game.js b/game.js new file mode 100644 index 0000000..2e78d5a --- /dev/null +++ b/game.js @@ -0,0 +1,325 @@ +import { makeBufferFromFaces } from "./geometry"; +import * as se3 from './se3'; +import { checkCollision, destroySelectedBlock, generateMissingChunks, markBlock, updateWorldGeometry } from './world'; + +/** Draw. + * + * @param {WebGLRenderingContext} gl + */ +function draw(gl, params, objects) { + const skyColor = [0.6, 0.8, 1.0]; + + gl.clearColor(...skyColor, 1.0); + gl.clearDepth(1.0); + gl.enable(gl.DEPTH_TEST); + gl.depthFunc(gl.LEQUAL); + + gl.enable(gl.CULL_FACE); + gl.cullFace(gl.BACK); + gl.enable(gl.BLEND); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + + gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); + + const camrot = params.camera.orientation; + const campos = params.camera.position; + const viewMatrix = se3.product( + se3.rotxyz(-camrot[0], -camrot[1], -camrot[2]), + se3.translation(-campos[0], -campos[1], -campos[2]) + ); + + let lastGlContext; + + for (const {glContext, position, orientation, geometry} of objects) { + if (glContext !== lastGlContext) { + glContext.setupScene({ + projectionMatrix: params.projMatrix, + viewMatrix, + fogColor: skyColor, + lightDirection: params.lightDirection, + ambiantLightAmount: params.ambiantLight, + }); + } + + lastGlContext = glContext; + + glContext.drawObject({ + position, + orientation, + glBuffer: geometry.glBuffer, + numVertices: geometry.numVertices, + }); + } +} + +const stuff = { + lastFrameTime: 0, +}; + +function handleKeys(params) { + const move = (forward, right) => { + const dir = [right, 0, -forward, 1.0]; + const ori = se3.roty(params.camera.orientation[1]); + const tf = se3.apply(ori, dir); + + if (params.flying) { + params.camera.position[0] += tf[0]; + params.camera.position[2] += tf[2]; + } + if (params.isOnGround) { + params.camera.velocity[0] = tf[0]; + params.camera.velocity[2] = tf[2]; + } else { + params.camera.velocity[0] += tf[0] / 60; + params.camera.velocity[2] += tf[2] / 60; + + if (Math.abs(params.camera.velocity[0]) > Math.abs(tf[0])) { + params.camera.velocity[0] = tf[0]; + } + if (Math.abs(params.camera.velocity[2]) > Math.abs(tf[2])) { + params.camera.velocity[2] = tf[2]; + } + } + }; + + params.keys.forEach(key => { + switch (key) { + case 'KeyW': + move(0.1, 0.0); + return; + case 'KeyA': + move(0.0, -0.1); + return; + case 'KeyS': + move(-0.1, 0.0); + return; + case 'KeyD': + move(0.0, 0.1); + return; + + case 'Space': + if(params.flying) { + params.camera.position[1] += 0.1; + } else { + if (params.jumpAmount > 0) { + const amount = 0.4 * params.jumpAmount; + params.camera.velocity[1] += amount / 60; + params.jumpAmount -= amount; + } + } + return; + case 'ControlLeft': + params.camera.position[1] -= 0.1; + return; + } + }); +} + +function getObjects(world, z, x, glContext) { + 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], + geometry: chunk.buffer, + glContext, + })); +} + +function updatePhysics(params) { + params.camera.velocity[1] -= 9.8 / 60 / 60; + + const oldPos = params.camera.position; + const targetPos = params.flying ? oldPos : params.camera.position.map((v, i) => v + params.camera.velocity[i]); + const {isOnGround, newPos} = checkCollision(oldPos, targetPos, params.world); + params.camera.position = newPos; + params.camera.velocity = newPos.map((v, i) => v - oldPos[i]); + if (isOnGround) { + params.jumpAmount = 6; + params.camera.velocity = params.camera.velocity.map(v => v * 0.7); + } + params.isOnGround = isOnGround; +} + +function tagABlock(gl, params, objects) { + const dir = [0, 0, -1, 1.0]; + const camori = params.camera.orientation; + const ori = se3.inverse(se3.rotxyz(-camori[0], -camori[1], -camori[2])); + const viewDirection = se3.apply(ori, dir).slice(0, 3); + + const face = markBlock(params.world, params.camera.position, viewDirection, params.blockSelectDistance); + if (face === undefined) { + return; + } + + if (params.tagBuffer !== undefined) { + gl.deleteBuffer(params.tagBuffer.glBuffer); + delete params.tagBuffer; + } + const buffer = makeBufferFromFaces(gl, [face]); + params.tagBuffer = buffer; + + const obj = { + position: [0.0, 0.0, 0.0], + orientation: [0.0, 0.0, 0.0], + geometry: buffer, + glContext: params.worldGl, + }; + + objects.push(obj); +} + +// Mine & place +// ------------ +// [x] ray casting +// [x] block outline +// [ ] crosshair +// [ ] dynamic terrain re-rendering +// [ ] should use a linked list of air contact blocks +// --> might not be needed. Only need to re-render a single chunk, +// should be fast enough. We render 16 of them every time we +// walk 16 blocks in any direction. + +// Stuff I need to do: +// [x] a skybox +// [x] a movable camera +// [x] some kind of gravity +// [x] collision detection +// [x] more blocks +// [ ] ability to mine & place +// [x] generating & loading of more chunks +// [x] distance fog +// [ ] different biomes (with different noise stats) +// [ ] non-flowy water +// [ ] flowy water +// [ ] ALIGN CHUNK WITH WORLD COORDS +// [x] better controls +// [ ] save the world (yay) to local storage (bah) + +export function setupParamPanel(params) { + document.querySelector('#lightx').oninput = e => { + params.lightDirection[0] = e.target.value / 100; + }; + document.querySelector('#lighty').oninput = e => { + params.lightDirection[1] = e.target.value / 100; + }; + document.querySelector('#lightz').oninput = e => { + params.lightDirection[2] = e.target.value / 100; + }; + document.querySelector('#ambiant').oninput = e => { + params.ambiantLight = e.target.value / 100; + }; + + const collapsibles = document.getElementsByClassName("collapsible"); + for (const collapsible of collapsibles) { + collapsible.onclick = () => { + const content = collapsible.nextElementSibling; + if (content.style.height === 'fit-content') { + content.style.height = '0px'; + } else { + content.style.height = 'fit-content'; + } + }; + } +} + +export function initUiListeners(params, canvas) { + const canvasClickHandler = () => { + canvas.requestPointerLock(); + canvas.onclick = null; + const clickListener = e => { + switch(e.button) { + case 0: // left click + destroySelectedBlock(params); + break; + case 2: // right click + break; + } + }; + const keyListener = e => { + if (e.type === 'keydown') { + params.keys.add(e.code); + + switch (e.code) { + case 'KeyF': + params.flying = !params.flying; + } + } else { + params.keys.delete(e.code); + } + }; + const moveListener = e => { + params.camera.orientation[1] -= e.movementX / 500; + params.camera.orientation[0] -= e.movementY / 500; + + params.camera.orientation[0] = Math.min(Math.max( + params.camera.orientation[0], -Math.PI / 2 + ), Math.PI / 2); + }; + const changeListener = () => { + if (document.pointerLockElement === canvas) { + return; + } + document.removeEventListener('pointerdown', clickListener); + document.removeEventListener('pointerlockchange', changeListener); + document.removeEventListener('pointermove', moveListener); + document.removeEventListener('keydown', keyListener); + document.removeEventListener('keyup', keyListener); + canvas.onclick = canvasClickHandler; + }; + document.addEventListener('pointerdown', clickListener); + document.addEventListener('pointerlockchange', changeListener); + document.addEventListener('pointermove', moveListener); + document.addEventListener('keydown', keyListener); + document.addEventListener('keyup', keyListener); + }; + canvas.onclick = canvasClickHandler; +} + +export function tick(time, gl, params) { + handleKeys(params); + updatePhysics(params); + + const campos = params.camera.position; + + // expensive stuff, can take several cycles + try { + // frame time is typically 16.7ms, so this may lag a bit + let timeLeft = 30; + 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.worldGl); + + tagABlock(gl, params, objects); + + draw(gl, params, objects); + + const dt = (time - stuff.lastFrameTime) * 0.001; + stuff.lastFrameTime = time; + + document.querySelector('#fps').textContent = `${1.0 / dt} fps`; + document.querySelector('#lightDirVec').textContent = JSON.stringify(params.lightDirection); + + requestAnimationFrame(time => tick(time, gl, params)); +} \ No newline at end of file diff --git a/index.js b/index.js index 559f455..6f2b895 100644 --- a/index.js +++ b/index.js @@ -1,544 +1,6 @@ -import { loadTexture, makeProgram } from "./gl"; -import { blockLookup, BlockType, createChunkFace, generateMissingChunks, makeWorld, updateWorldGeometry } from './world'; +import { initUiListeners, setupParamPanel, tick } from './game'; +import { initWorldGl, makeWorld } from './world'; import * as se3 from './se3'; -import { makeBufferFromFaces, makeFace } from "./geometry"; - -const TEST_VSHADER = ` -attribute vec3 aPosition; -attribute vec3 aNormal; -attribute vec2 aTextureCoord; - -uniform mat4 uProjection; -uniform mat4 uModel; -uniform mat4 uView; -uniform vec3 uLightDirection; -uniform float uAmbiantLight; - -varying highp vec2 vTextureCoord; -varying lowp vec3 vLighting; -varying lowp float vDistance; - -void main() { - highp mat4 modelview = uView * uModel; - - gl_Position = uProjection * modelview * vec4(aPosition, 1.0); - - lowp vec3 normal = mat3(uModel) * aNormal; - lowp float diffuseAmount = max(dot(-uLightDirection, normal), 0.0); - lowp vec3 ambiant = uAmbiantLight * vec3(1.0, 1.0, 0.9); - vLighting = ambiant + vec3(1.0, 1.0, 1.0) * diffuseAmount; - - vTextureCoord = aTextureCoord; - - vDistance = length(modelview * vec4(aPosition, 1.0)); -} -`; - -const TEST_FSHADER = ` -uniform sampler2D uSampler; -uniform lowp vec3 uFogColor; - -varying highp vec2 vTextureCoord; -varying lowp vec3 vLighting; -varying lowp float vDistance; - -void main() { - highp vec4 color = texture2D(uSampler, vTextureCoord); - lowp float fogamount = smoothstep(30.0, 90.0, vDistance); - gl_FragColor = vec4(mix(vLighting * color.rgb, uFogColor, fogamount), color.a); -} -`; - -/** Draw. - * - * @param {WebGLRenderingContext} gl - */ -function draw(gl, params, objects) { - const skyColor = [0.6, 0.8, 1.0]; - - gl.clearColor(...skyColor, 1.0); - gl.clearDepth(1.0); - gl.enable(gl.DEPTH_TEST); - gl.depthFunc(gl.LEQUAL); - - gl.enable(gl.CULL_FACE); - gl.cullFace(gl.BACK); - gl.enable(gl.BLEND); - gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); - - gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT); - - const camrot = params.camera.orientation; - const campos = params.camera.position; - const viewMatrix = se3.product( - se3.rotxyz(-camrot[0], -camrot[1], -camrot[2]), - se3.translation(-campos[0], -campos[1], -campos[2]) - ); - - let lastGlContext; - - for (const {glContext, position, orientation, geometry} of objects) { - if (glContext !== lastGlContext) { - glContext.setupScene({ - projectionMatrix: params.projMatrix, - viewMatrix, - fogColor: skyColor, - lightDirection: params.lightDirection, - ambiantLightAmount: params.ambiantLight, - }); - } - - lastGlContext = glContext; - - glContext.drawObject({ - position, - orientation, - glBuffer: geometry.glBuffer, - numVertices: geometry.numVertices, - }); - } -} - -const stuff = { - lastFrameTime: 0, -}; - -function handleKeys(params) { - const move = (forward, right) => { - const dir = [right, 0, -forward, 1.0]; - const ori = se3.roty(params.camera.orientation[1]); - const tf = se3.apply(ori, dir); - - if (params.flying) { - params.camera.position[0] += tf[0]; - params.camera.position[2] += tf[2]; - } - if (params.isOnGround) { - params.camera.velocity[0] = tf[0]; - params.camera.velocity[2] = tf[2]; - } else { - params.camera.velocity[0] += tf[0] / 60; - params.camera.velocity[2] += tf[2] / 60; - - if (Math.abs(params.camera.velocity[0]) > Math.abs(tf[0])) { - params.camera.velocity[0] = tf[0]; - } - if (Math.abs(params.camera.velocity[2]) > Math.abs(tf[2])) { - params.camera.velocity[2] = tf[2]; - } - } - }; - - params.keys.forEach(key => { - switch (key) { - case 'KeyW': - move(0.1, 0.0); - return; - case 'KeyA': - move(0.0, -0.1); - return; - case 'KeyS': - move(-0.1, 0.0); - return; - case 'KeyD': - move(0.0, 0.1); - return; - - case 'Space': - if(params.flying) { - params.camera.position[1] += 0.1; - } else { - if (params.jumpAmount > 0) { - const amount = 0.4 * params.jumpAmount; - params.camera.velocity[1] += amount / 60; - params.jumpAmount -= amount; - } - } - return; - case 'ControlLeft': - params.camera.position[1] -= 0.1; - return; - } - }); -} - -function getObjects(world, z, x, glContext) { - 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], - geometry: chunk.buffer, - glContext, - })); -} - -function updatePhysics(params) { - params.camera.velocity[1] -= 9.8 / 60 / 60; - - const oldPos = params.camera.position; - const targetPos = params.flying ? oldPos : params.camera.position.map((v, i) => v + params.camera.velocity[i]); - const {isOnGround, newPos} = checkCollision(oldPos, targetPos, params.world); - params.camera.position = newPos; - params.camera.velocity = newPos.map((v, i) => v - oldPos[i]); - if (isOnGround) { - params.jumpAmount = 6; - params.camera.velocity = params.camera.velocity.map(v => v * 0.7); - } - params.isOnGround = isOnGround; -} - -function tagABlock(gl, params, objects) { - const dir = [0, 0, -1, 1.0]; - const camori = params.camera.orientation; - const ori = se3.inverse(se3.rotxyz(-camori[0], -camori[1], -camori[2])); - const viewDirection = se3.apply(ori, dir).slice(0, 3); - - const face = markBlock(params.world, params.camera.position, viewDirection, params.blockSelectDistance); - if (face === undefined) { - return; - } - - if (params.tagBuffer !== undefined) { - gl.deleteBuffer(params.tagBuffer.glBuffer); - delete params.tagBuffer; - } - const buffer = makeBufferFromFaces(gl, [face]); - params.tagBuffer = buffer; - - const obj = { - position: [0.0, 0.0, 0.0], - orientation: [0.0, 0.0, 0.0], - geometry: buffer, - glContext: params.worldGl, - }; - - objects.push(obj); -} - -function tick(time, gl, params) { - handleKeys(params); - updatePhysics(params); - - const campos = params.camera.position; - - // expensive stuff, can take several cycles - try { - // frame time is typically 16.7ms, so this may lag a bit - let timeLeft = 30; - 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.worldGl); - - tagABlock(gl, params, objects); - - draw(gl, params, objects); - - const dt = (time - stuff.lastFrameTime) * 0.001; - stuff.lastFrameTime = time; - - document.querySelector('#fps').textContent = `${1.0 / dt} fps`; - document.querySelector('#lightDirVec').textContent = JSON.stringify(params.lightDirection); - - requestAnimationFrame(time => tick(time, gl, params)); -} - -function checkCollision(curPos, newPos, world) { - // I guess Steve is about 1.7 m tall? - // he also has a 60x60 cm axis-aligned square section '^_^ - // box is centered around the camera - const steveBB = [ - [-0.3, 0.2, -0.3], - [-0.3, 0.2, 0.3], - [0.3, 0.2, -0.3], - [0.3, 0.2, 0.3], - - [-0.3, -1.5, -0.3], - [-0.3, -1.5, 0.3], - [0.3, -1.5, -0.3], - [0.3, -1.5, 0.3], - ]; - - const translate = (v, pos) => v.map((el, i) => el + pos[i]); - - let dp = newPos.map((x, i) => x - curPos[i]); - let isOnGround = false; - - for (let i = 0; i < 3; i++) { - const newSteve = v => v.map((x, j) => i === j ? x + newPos[j] : x + curPos[j]); - for (const point of steveBB.map(newSteve)) { - const block = blockLookup(world, ...point); - if (block.type !== BlockType.AIR) { - if (i === 1 && dp[i] < 0) { - isOnGround = true; - } - dp[i] = 0; - } - } - } - for (let i = 0; i < 3; i++) { - const newSteve = v => v.map((x, j) => curPos[j] + x + dp[j]); - for (const point of steveBB.map(newSteve)) { - const block = blockLookup(world, ...point); - if (block.type !== BlockType.AIR) { - dp[i] = 0; - } - } - } - - return { - newPos: translate(curPos, dp), - isOnGround, - }; -} - -function minIndex(arr) { - return arr.reduce((min, val, i) => val >= arr[min] ? min : i, -1); -} - -function movePoint(p, s, u) { - return [p[0] + s * u[0], p[1] + s * u[1], p[2] + s * u[2]]; -} - -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.499) - 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}; -} - -function castRay(world, origin, direction, maxDistance) { - let currentPoint = origin; - - while (maxDistance > 0) { - const {position, normal, distance} = rayThroughGrid(currentPoint, direction, maxDistance); - - if (position === undefined) { - return; - } - - maxDistance -= distance; - currentPoint = position; - const blockCenter = movePoint(position, -0.5, normal); - const block = blockLookup(world, ...blockCenter); - - if (block.type === BlockType.AIR) { - continue; - } - - return { - block, - normal, - }; - } -} - -function viewDirection(params) { - const dir = [0, 0, -1, 1.0]; - const camori = params.camera.orientation; - const ori = se3.inverse(se3.rotxyz(-camori[0], -camori[1], -camori[2])); - return se3.apply(ori, dir).slice(0, 3); -} - -function destroySelectedBlock(params) { - const hit = castRay(params.world, params.camera.position, viewDirection(params), params.blockSelectDistance); - if (hit === undefined || hit.block.type === BlockType.UNDEFINED) { - return; - } - - const trimFaces = chunk => { - chunk.faces = chunk.faces.filter(({blockIndex}) => chunk.data[blockIndex] !== BlockType.AIR); - } - - hit.block.chunk.data[hit.block.blockIndex] = BlockType.AIR; - if (hit.block.chunk.buffer !== undefined) { - hit.block.chunk.buffer.delete(); - delete hit.block.chunk.buffer; - } - trimFaces(hit.block.chunk); - - const [bx, by, bz] = hit.block.centerPosition; - - const neighbors = [ - { block: blockLookup(params.world, bx - 1, by, bz), dir: '+x' }, - { block: blockLookup(params.world, bx + 1, by, bz), dir: '-x' }, - { block: blockLookup(params.world, bx, by - 1, bz), dir: '+y' }, - { block: blockLookup(params.world, bx, by + 1, bz), dir: '-y' }, - { block: blockLookup(params.world, bx, by, bz - 1), dir: '+z' }, - { block: blockLookup(params.world, bx, by, bz + 1), dir: '-z' }, - ]; - - neighbors - .filter(({ block }) => block.type !== BlockType.AIR && - block.type !== BlockType.UNDEFINED) - .forEach(({ block, dir }) => { - const blocki = Math.floor(block.blockIndex / (16 * 256)); - const blockj = Math.floor(block.blockIndex / 256) - 16 * blocki; - const blockk = block.blockIndex % 256; - - block.chunk.faces.push(createChunkFace(block, dir)); - trimFaces(block.chunk); - - if (block.chunk.buffer !== undefined) { - block.chunk.buffer.delete(); - delete block.chunk.buffer; - } - }); -} - -function markBlock(world, cameraPosition, direction, maxDistance) { - const hit = castRay(world, cameraPosition, direction, maxDistance); - - if (hit === undefined || hit.block.type === BlockType.UNDEFINED) { - return; - } - - const texture = [0, 14]; - const {normal, block} = hit; - const faceCenter = movePoint(block.centerPosition, 0.51, normal); - - if (normal[0] > 0) { - return makeFace('+x', texture, faceCenter); - } else if (normal[0] < 0) { - return makeFace('-x', texture, faceCenter); - } else if (normal[1] > 0) { - return makeFace('+y', texture, faceCenter); - } else if (normal[1] < 0) { - return makeFace('-y', texture, faceCenter); - } else if (normal[2] > 0) { - return makeFace('+z', texture, faceCenter); - } else if (normal[2] < 0) { - return makeFace('-z', texture, faceCenter); - } -} - -// Mine & place -// ------------ -// [x] ray casting -// [x] block outline -// [ ] crosshair -// [ ] dynamic terrain re-rendering -// [ ] should use a linked list of air contact blocks -// --> might not be needed. Only need to re-render a single chunk, -// should be fast enough. We render 16 of them every time we -// walk 16 blocks in any direction. - -// Stuff I need to do: -// [x] a skybox -// [x] a movable camera -// [x] some kind of gravity -// [x] collision detection -// [x] more blocks -// [ ] ability to mine & place -// [x] generating & loading of more chunks -// [x] distance fog -// [ ] different biomes (with different noise stats) -// [ ] non-flowy water -// [ ] flowy water -// [ ] ALIGN CHUNK WITH WORLD COORDS -// [x] better controls -// [ ] save the world (yay) to local storage (bah) - -async function initWorldGl(gl) { - const program = makeProgram(gl, TEST_VSHADER, TEST_FSHADER); - const texture = await loadTexture(gl, 'texture.png'); - - // load those ahead of time - const viewLoc = gl.getUniformLocation(program, 'uView'); - const modelLoc = gl.getUniformLocation(program, 'uModel'); - const projLoc = gl.getUniformLocation(program, 'uProjection'); - const samplerLoc = gl.getUniformLocation(program, 'uSampler'); - const fogColorLoc = gl.getUniformLocation(program, 'uFogColor'); - const lightDirectionLoc = gl.getUniformLocation(program, 'uLightDirection'); - const ambiantLoc = gl.getUniformLocation(program, 'uAmbiantLight'); - - const positionLoc = gl.getAttribLocation(program, 'aPosition'); - const normalLoc = gl.getAttribLocation(program, 'aNormal'); - const textureLoc = gl.getAttribLocation(program, 'aTextureCoord'); - - const setupScene = (sceneParams) => { - const { - projectionMatrix, - viewMatrix, - fogColor, - lightDirection, - ambiantLightAmount, - } = sceneParams; - - gl.useProgram(program); - - gl.uniformMatrix4fv(projLoc, false, new Float32Array(projectionMatrix)); - gl.uniformMatrix4fv(viewLoc, false, new Float32Array(viewMatrix)); - gl.uniform3fv(fogColorLoc, fogColor); - gl.uniform3fv(lightDirectionLoc, lightDirection); - gl.uniform1f(ambiantLoc, ambiantLightAmount); - - // doing this here because it's the same for all world stuff - gl.uniformMatrix4fv(modelLoc, false, new Float32Array(se3.identity())); - gl.uniform1i(samplerLoc, 0); - - gl.enableVertexAttribArray(positionLoc); - gl.enableVertexAttribArray(normalLoc); - gl.enableVertexAttribArray(textureLoc); - gl.activeTexture(gl.TEXTURE0); - gl.bindTexture(gl.TEXTURE_2D, texture); - }; - - const drawObject = (objectParams) => { - const { - glBuffer, - numVertices, - } = objectParams; - - gl.bindBuffer(gl.ARRAY_BUFFER, glBuffer); - - gl.vertexAttribPointer(positionLoc, 3, gl.FLOAT, false, 20, 0); - gl.vertexAttribPointer(normalLoc, 3, gl.BYTE, true, 20, 12); - gl.vertexAttribPointer(textureLoc, 2, gl.UNSIGNED_SHORT, true, 20, 16); - - gl.drawArrays(gl.TRIANGLES, 0, numVertices); - }; - - return { - setupScene, - drawObject, - }; -} async function main() { const canvas = document.querySelector('#game'); @@ -566,81 +28,9 @@ async function main() { worldGl: await initWorldGl(gl), } - document.querySelector('#lightx').oninput = e => { - params.lightDirection[0] = e.target.value / 100; - }; - document.querySelector('#lighty').oninput = e => { - params.lightDirection[1] = e.target.value / 100; - }; - document.querySelector('#lightz').oninput = e => { - params.lightDirection[2] = e.target.value / 100; - }; - document.querySelector('#ambiant').oninput = e => { - params.ambiantLight = e.target.value / 100; - }; + setupParamPanel(params); + initUiListeners(params, canvas); - const collapsibles = document.getElementsByClassName("collapsible"); - for (const collapsible of collapsibles) { - collapsible.onclick = e => { - const content = collapsible.nextElementSibling; - if (content.style.height === 'fit-content') { - content.style.height = '0px'; - } else { - content.style.height = 'fit-content'; - } - }; - } - - const canvasClickHandler = e => { - canvas.requestPointerLock(); - canvas.onclick = null; - const clickListener = e => { - switch(e.button) { - case 0: // left click - destroySelectedBlock(params); - break; - case 2: // right click - break; - } - }; - const keyListener = e => { - if (e.type === 'keydown') { - params.keys.add(e.code); - - switch (e.code) { - case 'KeyF': - params.flying = !params.flying; - } - } else { - params.keys.delete(e.code); - } - }; - const moveListener = e => { - params.camera.orientation[1] -= e.movementX / 500; - params.camera.orientation[0] -= e.movementY / 500; - - params.camera.orientation[0] = Math.min(Math.max( - params.camera.orientation[0], -Math.PI / 2 - ), Math.PI / 2); - }; - const changeListener = () => { - if (document.pointerLockElement === canvas) { - return; - } - document.removeEventListener('pointerdown', clickListener); - document.removeEventListener('pointerlockchange', changeListener); - document.removeEventListener('pointermove', moveListener); - document.removeEventListener('keydown', keyListener); - document.removeEventListener('keyup', keyListener); - canvas.onclick = canvasClickHandler; - }; - document.addEventListener('pointerdown', clickListener); - document.addEventListener('pointerlockchange', changeListener); - document.addEventListener('pointermove', moveListener); - document.addEventListener('keydown', keyListener); - document.addEventListener('keyup', keyListener); - }; - canvas.onclick = canvasClickHandler; requestAnimationFrame(time => tick(time, gl, params)); } diff --git a/world.js b/world.js index 3291f1f..d2a6d7c 100644 --- a/world.js +++ b/world.js @@ -1,4 +1,52 @@ import { makeBufferFromFaces, makeFace} from "./geometry"; +import { loadTexture, makeProgram } from "./gl"; +import * as se3 from './se3'; + +const VSHADER = ` +attribute vec3 aPosition; +attribute vec3 aNormal; +attribute vec2 aTextureCoord; + +uniform mat4 uProjection; +uniform mat4 uModel; +uniform mat4 uView; +uniform vec3 uLightDirection; +uniform float uAmbiantLight; + +varying highp vec2 vTextureCoord; +varying lowp vec3 vLighting; +varying lowp float vDistance; + +void main() { + highp mat4 modelview = uView * uModel; + + gl_Position = uProjection * modelview * vec4(aPosition, 1.0); + + lowp vec3 normal = mat3(uModel) * aNormal; + lowp float diffuseAmount = max(dot(-uLightDirection, normal), 0.0); + lowp vec3 ambiant = uAmbiantLight * vec3(1.0, 1.0, 0.9); + vLighting = ambiant + vec3(1.0, 1.0, 1.0) * diffuseAmount; + + vTextureCoord = aTextureCoord; + + vDistance = length(modelview * vec4(aPosition, 1.0)); +} +`; + +const FSHADER = ` +uniform sampler2D uSampler; +uniform lowp vec3 uFogColor; + +varying highp vec2 vTextureCoord; +varying lowp vec3 vLighting; +varying lowp float vDistance; + +void main() { + highp vec4 color = texture2D(uSampler, vTextureCoord); + lowp float fogamount = smoothstep(30.0, 90.0, vDistance); + gl_FragColor = vec4(mix(vLighting * color.rgb, uFogColor, fogamount), color.a); +} +`; export const BlockType = { UNDEFINED: 0, @@ -307,4 +355,254 @@ export function updateWorldGeometry(gl, world, z, x, timeLimit = 10000) { } } -} \ No newline at end of file +} + +export function checkCollision(curPos, newPos, world) { + // I guess Steve is about 1.7 m tall? + // he also has a 60x60 cm axis-aligned square section '^_^ + // box is centered around the camera + const steveBB = [ + [-0.3, 0.2, -0.3], + [-0.3, 0.2, 0.3], + [0.3, 0.2, -0.3], + [0.3, 0.2, 0.3], + + [-0.3, -1.5, -0.3], + [-0.3, -1.5, 0.3], + [0.3, -1.5, -0.3], + [0.3, -1.5, 0.3], + ]; + + const translate = (v, pos) => v.map((el, i) => el + pos[i]); + + let dp = newPos.map((x, i) => x - curPos[i]); + let isOnGround = false; + + for (let i = 0; i < 3; i++) { + const newSteve = v => v.map((x, j) => i === j ? x + newPos[j] : x + curPos[j]); + for (const point of steveBB.map(newSteve)) { + const block = blockLookup(world, ...point); + if (block.type !== BlockType.AIR) { + if (i === 1 && dp[i] < 0) { + isOnGround = true; + } + dp[i] = 0; + } + } + } + for (let i = 0; i < 3; i++) { + const newSteve = v => v.map((x, j) => curPos[j] + x + dp[j]); + for (const point of steveBB.map(newSteve)) { + const block = blockLookup(world, ...point); + if (block.type !== BlockType.AIR) { + dp[i] = 0; + } + } + } + + return { + newPos: translate(curPos, dp), + isOnGround, + }; +} + +function minIndex(arr) { + return arr.reduce((min, val, i) => val >= arr[min] ? min : i, -1); +} + +function movePoint(p, s, u) { + return [p[0] + s * u[0], p[1] + s * u[1], p[2] + s * u[2]]; +} + +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.499) - 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}; +} + +function castRay(world, origin, direction, maxDistance) { + let currentPoint = origin; + + while (maxDistance > 0) { + const {position, normal, distance} = rayThroughGrid(currentPoint, direction, maxDistance); + + if (position === undefined) { + return; + } + + maxDistance -= distance; + currentPoint = position; + const blockCenter = movePoint(position, -0.5, normal); + const block = blockLookup(world, ...blockCenter); + + if (block.type === BlockType.AIR) { + continue; + } + + return { + block, + normal, + }; + } +} + +export function markBlock(world, cameraPosition, direction, maxDistance) { + const hit = castRay(world, cameraPosition, direction, maxDistance); + + if (hit === undefined || hit.block.type === BlockType.UNDEFINED) { + return; + } + + const texture = [0, 14]; + const {normal, block} = hit; + const faceCenter = movePoint(block.centerPosition, 0.51, normal); + + if (normal[0] > 0) { + return makeFace('+x', texture, faceCenter); + } else if (normal[0] < 0) { + return makeFace('-x', texture, faceCenter); + } else if (normal[1] > 0) { + return makeFace('+y', texture, faceCenter); + } else if (normal[1] < 0) { + return makeFace('-y', texture, faceCenter); + } else if (normal[2] > 0) { + return makeFace('+z', texture, faceCenter); + } else if (normal[2] < 0) { + return makeFace('-z', texture, faceCenter); + } +} + + +function viewDirection(params) { + const dir = [0, 0, -1, 1.0]; + const camori = params.camera.orientation; + const ori = se3.inverse(se3.rotxyz(-camori[0], -camori[1], -camori[2])); + return se3.apply(ori, dir).slice(0, 3); +} + +export function destroySelectedBlock(params) { + const hit = castRay(params.world, params.camera.position, viewDirection(params), params.blockSelectDistance); + if (hit === undefined || hit.block.type === BlockType.UNDEFINED) { + return; + } + + const trimFaces = chunk => { + chunk.faces = chunk.faces.filter(({blockIndex}) => chunk.data[blockIndex] !== BlockType.AIR); + } + + hit.block.chunk.data[hit.block.blockIndex] = BlockType.AIR; + if (hit.block.chunk.buffer !== undefined) { + hit.block.chunk.buffer.delete(); + delete hit.block.chunk.buffer; + } + trimFaces(hit.block.chunk); + + const [bx, by, bz] = hit.block.centerPosition; + + const neighbors = [ + { block: blockLookup(params.world, bx - 1, by, bz), dir: '+x' }, + { block: blockLookup(params.world, bx + 1, by, bz), dir: '-x' }, + { block: blockLookup(params.world, bx, by - 1, bz), dir: '+y' }, + { block: blockLookup(params.world, bx, by + 1, bz), dir: '-y' }, + { block: blockLookup(params.world, bx, by, bz - 1), dir: '+z' }, + { block: blockLookup(params.world, bx, by, bz + 1), dir: '-z' }, + ]; + + neighbors + .filter(({ block }) => block.type !== BlockType.AIR && + block.type !== BlockType.UNDEFINED) + .forEach(({ block, dir }) => { + const blocki = Math.floor(block.blockIndex / (16 * 256)); + const blockj = Math.floor(block.blockIndex / 256) - 16 * blocki; + const blockk = block.blockIndex % 256; + + block.chunk.faces.push(createChunkFace(block, dir)); + trimFaces(block.chunk); + + if (block.chunk.buffer !== undefined) { + block.chunk.buffer.delete(); + delete block.chunk.buffer; + } + }); +} + +export async function initWorldGl(gl) { + const program = makeProgram(gl, VSHADER, FSHADER); + const texture = await loadTexture(gl, 'texture.png'); + + // load those ahead of time + const viewLoc = gl.getUniformLocation(program, 'uView'); + const modelLoc = gl.getUniformLocation(program, 'uModel'); + const projLoc = gl.getUniformLocation(program, 'uProjection'); + const samplerLoc = gl.getUniformLocation(program, 'uSampler'); + const fogColorLoc = gl.getUniformLocation(program, 'uFogColor'); + const lightDirectionLoc = gl.getUniformLocation(program, 'uLightDirection'); + const ambiantLoc = gl.getUniformLocation(program, 'uAmbiantLight'); + + const positionLoc = gl.getAttribLocation(program, 'aPosition'); + const normalLoc = gl.getAttribLocation(program, 'aNormal'); + const textureLoc = gl.getAttribLocation(program, 'aTextureCoord'); + + const setupScene = (sceneParams) => { + const { + projectionMatrix, + viewMatrix, + fogColor, + lightDirection, + ambiantLightAmount, + } = sceneParams; + + gl.useProgram(program); + + gl.uniformMatrix4fv(projLoc, false, new Float32Array(projectionMatrix)); + gl.uniformMatrix4fv(viewLoc, false, new Float32Array(viewMatrix)); + gl.uniform3fv(fogColorLoc, fogColor); + gl.uniform3fv(lightDirectionLoc, lightDirection); + gl.uniform1f(ambiantLoc, ambiantLightAmount); + + // doing this here because it's the same for all world stuff + gl.uniformMatrix4fv(modelLoc, false, new Float32Array(se3.identity())); + gl.uniform1i(samplerLoc, 0); + + gl.enableVertexAttribArray(positionLoc); + gl.enableVertexAttribArray(normalLoc); + gl.enableVertexAttribArray(textureLoc); + gl.activeTexture(gl.TEXTURE0); + gl.bindTexture(gl.TEXTURE_2D, texture); + }; + + const drawObject = (objectParams) => { + const { + glBuffer, + numVertices, + } = objectParams; + + gl.bindBuffer(gl.ARRAY_BUFFER, glBuffer); + + gl.vertexAttribPointer(positionLoc, 3, gl.FLOAT, false, 20, 0); + gl.vertexAttribPointer(normalLoc, 3, gl.BYTE, true, 20, 12); + gl.vertexAttribPointer(textureLoc, 2, gl.UNSIGNED_SHORT, true, 20, 16); + + gl.drawArrays(gl.TRIANGLES, 0, numVertices); + }; + + return { + setupScene, + drawObject, + }; +}