diff --git a/src/minecraft/App.ts b/src/minecraft/App.ts index 004f43a..131dac0 100644 --- a/src/minecraft/App.ts +++ b/src/minecraft/App.ts @@ -16,6 +16,9 @@ export class Config { public static CHUNK_SIZE: number = 64.0; + // How far you can select a cube from the player + public static SELECT_RADIUS: number = 3.0; + // Number of chunks to render outside of the player's chunk // 1 --> 3 x 3, 2 -> 5 x 5, ... n -> 2n+1 x 2n+1 public static BORDER_CHUNKS: number = 1.0; @@ -51,6 +54,13 @@ export class MinecraftAnimation extends CanvasAnimation { private cubeGeometry: Cube; private blankCubeRenderPass: RenderPass; + /* Cube Selection */ + private selectedCubeF32: Float32Array; + private highlightSelected: boolean; + public highlightOn: boolean; + private removeCube: boolean; + private modificationLog: number[][]; + /* Global Rendering Info */ private lightPosition: Vec4; private backgroundColor: Vec4; @@ -91,6 +101,9 @@ export class MinecraftAnimation extends CanvasAnimation { this.lightPosition = new Vec4([-1000, 1000, -1000, 1]); this.backgroundColor = new Vec4([0.0, 0.37254903, 0.37254903, 1.0]); + this.highlightSelected = false; + this.highlightOn = false; + this.modificationLog = []; } private chunkKey(x: number, z: number): string { @@ -126,6 +139,9 @@ export class MinecraftAnimation extends CanvasAnimation { newChunks[key] = this.cache[key]; } else { newChunks[key] = new Chunk(xCoords[i], zCoords[i], Config.CHUNK_SIZE); + + // When loading chunks, we need to update the chunk's state based on modified blocks + newChunks[key].updateFromLog(this.modificationLog); } if (i == Math.floor(((1 + 2 * Config.BORDER_CHUNKS) ** 2) / 2)) { this.chunk = newChunks[key]; @@ -152,7 +168,6 @@ export class MinecraftAnimation extends CanvasAnimation { Math.abs(cameraLocation.x) % Config.CHUNK_SIZE - Config.CHUNK_SIZE / 2); const zMod = Math.abs( Math.abs(cameraLocation.z) % Config.CHUNK_SIZE - Config.CHUNK_SIZE / 2); - // console.log(cameraLocation.x + " " + cameraLocation.z); if (xMod <= 2.0) { candidates.push( this.chunks[this.chunkKey(center.x + Config.CHUNK_SIZE, center.z)]); @@ -247,7 +262,7 @@ export class MinecraftAnimation extends CanvasAnimation { if (!Config.CREATIVE_MODE) { let position: Vec3 = new Vec3(this.playerPosition.xyz); let chunks: Chunk[] = this.collisionChunks(this.playerPosition); - position.add(this.gui.walkDir()); + position.add(this.gui.walkDir().scale(GUI.walkSpeed)); if (!position.equals(this.playerPosition)) { let safe: boolean = true; for (let i = 0; i < chunks.length; i++) { @@ -288,7 +303,7 @@ export class MinecraftAnimation extends CanvasAnimation { } this.gui.getCamera().setPos(this.playerPosition); } else { - this.playerPosition.add(this.gui.walkDir()); + this.playerPosition.add(this.gui.walkDir().scale(GUI.walkSpeed)); this.gui.getCamera().setPos(this.playerPosition); this.gravityTime = Date.now(); } @@ -336,6 +351,12 @@ export class MinecraftAnimation extends CanvasAnimation { 'aOffset', this.chunks[chunk].cubePositions()); this.blankCubeRenderPass.drawInstanced(this.chunks[chunk].numCubes()); } + // We draw a sillouette of the selected cube on top of everything else. + if (this.highlightSelected && this.highlightOn) { + this.blankCubeRenderPass.updateAttributeBuffer( + 'aOffset', this.selectedCubeF32); + this.blankCubeRenderPass.drawInstanced(1); + } } public getGUI(): GUI { @@ -349,6 +370,53 @@ export class MinecraftAnimation extends CanvasAnimation { this.verticalVelocity = new Vec3([0.0, Config.JUMP_VELOCITY, 0.0]); } } + + public updateSelectedCube(selectedCube: Vec3) { + this.selectedCubeF32 = new Float32Array(4 * 1); + this.selectedCubeF32[0] = selectedCube.x; + this.selectedCubeF32[1] = selectedCube.y; + this.selectedCubeF32[2] = selectedCube.z; + this.selectedCubeF32[3] = 2.0; // We use 2.0 to indicate that the cube should be highlighted, as seen in the shader + + // We globally determine the block selected and if it should be removed (ie with this.removeCube) + let isRemovingCube = false; + for (let chunk in this.chunks) { + isRemovingCube = isRemovingCube || this.chunks[chunk].updateSelected(this.highlightOn, selectedCube); + } + this.removeCube = isRemovingCube; + this.highlightSelected = true; + } + + public modifyLandscape(selectedCube: Vec3) { + const x = selectedCube.x; + const y = selectedCube.y; + const z = selectedCube.z; + // See if the selected cube is already in the modification log + let newLog: number[][] = []; + let cubeInLog = false; + for (let i = 0; i < this.modificationLog.length; ++i) { + // Checks if the cube is already in the log + if (this.modificationLog[i][0] == x && + this.modificationLog[i][1] == y && + this.modificationLog[i][2] == z) { + cubeInLog = true; + } else { + newLog.push(this.modificationLog[i]); + } + } + // Remove the cube if it already exists (reverting to original chunk), + // otherwise add it + if (!cubeInLog) { + newLog.push([x, y, z, this.removeCube ? -1 : 1]); + } + + // Update log and all chunks + this.modificationLog = newLog; + for (let chunk in this.chunks) { + this.chunks[chunk].updateLandscape(this.removeCube, selectedCube); + } + this.removeCube = !this.removeCube; + } } export function initializeCanvas(): void { diff --git a/src/minecraft/Chunk.ts b/src/minecraft/Chunk.ts index 002caa3..3fc1da2 100644 --- a/src/minecraft/Chunk.ts +++ b/src/minecraft/Chunk.ts @@ -10,29 +10,43 @@ export class Chunk { // coordinates private x: number; // Center of the chunk private y: number; + private topleftx:number; + private toplefty:number; + private bottomrightx:number; + private bottomrighty:number; private size: number; // Number of cubes along each side of the chunk private heightMap: Float32Array; - private densityMap: Object; + private densityMap: Object; // Used for 3D perlin noise private maxHeight: number = 100; private unitVecs: Object; + private highlightedCubePos: number; + constructor(centerX: number, centerY: number, size: number) { this.x = centerX; this.y = centerY; this.size = size; + this.topleftx = this.x - this.size / 2; + this.toplefty = this.y - this.size / 2; + this.bottomrightx = this.x + this.size / 2; + this.bottomrighty = this.y + this.size / 2; this.cubes = size * size; this.heightMap = new Float32Array(this.size * this.size); this.unitVecs = {}; + // Cube generation logic called on initialization this.generateCubes(); + this.highlightedCubePos = 0; + } + private isInChunkBounds(x: number, y: number): boolean { + return this.topleftx <= x && x < this.bottomrightx && + this.toplefty <= y && y < this.bottomrighty } public verticalCollision(cameraLocation: Vec3, upwards: boolean): number { - const topleftx = this.x - this.size / 2; - const toplefty = this.y - this.size / 2; const base: number = Math.round(cameraLocation.y - Config.PLAYER_HEIGHT); const top: number = Math.round(cameraLocation.y); - const x = Math.round(cameraLocation.x - topleftx); - const y = Math.round(cameraLocation.z - toplefty); + const x = Math.round(cameraLocation.x - this.topleftx); + const y = Math.round(cameraLocation.z - this.toplefty); if (x >= 0 && y >= 0 && x < this.size && y < this.size) { let idx = x * this.size + y; if (upwards) { @@ -55,9 +69,6 @@ export class Chunk { } public sideCollision(cameraLocation: Vec3): boolean { - const topleftx = this.x - this.size / 2; - const toplefty = this.y - this.size / 2; - const base: number = cameraLocation.y - Config.PLAYER_HEIGHT; const top: number = Math.round(cameraLocation.y); for (let i = -1; i <= 1; i++) { for (let j = -1; j <= 1; j++) { @@ -69,8 +80,8 @@ export class Chunk { let distance = Vec2.distance( point, new Vec2([cameraLocation.x, cameraLocation.z])); if (distance < Config.PLAYER_RADIUS) { - const x = Math.round(cameraLocation.x - topleftx) + i; - const y = Math.round(cameraLocation.z - toplefty) + j; + const x = Math.round(cameraLocation.x - this.topleftx) + i; + const y = Math.round(cameraLocation.z - this.toplefty) + j; if (x >= 0 && y >= 0 && x < this.size && y < this.size) { let idx = x * this.size + y; for (let k = 0; k <= Config.PLAYER_HEIGHT; k++) { @@ -259,21 +270,7 @@ export class Chunk { return retArray; } - // Lookup the number of cubes to draw at a given x y coordinate - private numCubesDrawn(arr: Float32Array, i: number, j: number): number { - const idx = this.size * i + j; - // up - - const idxUp = this.size * (i - 1) + j; - const idxDown = this.size * (i + 1) + j; - const idxLeft = this.size * i + j - 1; - const idxRight = this.size * i + j + 1; - const heightNeigh = - [arr[idx], arr[idxUp], arr[idxDown], arr[idxLeft], arr[idxRight]]; - const minNeigh = Math.min(...heightNeigh); - - return Math.floor(arr[idx] - minNeigh + 1); - } + private shouldDrawBasedOnDensity(i: number, j: number, k: number): boolean { // TODO: Should be within bounds let idx = this.size * i + j; @@ -381,12 +378,8 @@ export class Chunk { // Add bias towards lower heights so we dont fall through the ground return c * 0.5; } - + private generateCubes() { - // Coordinate of heightmap's top-left corner - const topleftx = this.x - this.size / 2; - const toplefty = this.y - this.size / 2; - // TODO: The real landscape-generation logic. The example code below shows // you how to use the pseudorandom number generator to create a few cubes. this.cubes = this.size * this.size; @@ -407,8 +400,7 @@ export class Chunk { return value + something[index]; }); } - - + // Generate density map for chunk let densityMap = {}; let totalCubes = 0; @@ -420,7 +412,7 @@ export class Chunk { densityMap[idx] = new Float32Array(height); for (let k = 0; k < height; k++) { // Single octave 3D perlin noise - let curPos = new Vec3([topleftx + i, toplefty + j, k]); + let curPos = new Vec3([this.topleftx + i, this.toplefty + j, k]); densityMap[idx][k] = Config.PERLIN_3D ? this.perlinDensity(32, curPos) : 1; // Add a bias towrds lower heights being less likely to be air @@ -467,9 +459,9 @@ export class Chunk { j !== this.size - 1 && k !== height - 1 && k !== 0) { let shouldDraw = this.shouldDrawBasedOnDensity(i, j, k); if (shouldDraw) { - this.cubePositionsF32[4 * pos] = topleftx + i; + this.cubePositionsF32[4 * pos] = this.topleftx + i; this.cubePositionsF32[4 * pos + 1] = k; - this.cubePositionsF32[4 * pos + 2] = toplefty + j; + this.cubePositionsF32[4 * pos + 2] = this.toplefty + j; this.cubePositionsF32[4 * pos + 3] = 0; pos++; } @@ -477,9 +469,9 @@ export class Chunk { // Only draw if the cube is not air else if (densityMap[idx][k] >= 0) { - this.cubePositionsF32[4 * pos] = topleftx + i; + this.cubePositionsF32[4 * pos] = this.topleftx + i; this.cubePositionsF32[4 * pos + 1] = k; - this.cubePositionsF32[4 * pos + 2] = toplefty + j; + this.cubePositionsF32[4 * pos + 2] = this.toplefty + j; this.cubePositionsF32[4 * pos + 3] = 0; pos++; } @@ -488,6 +480,186 @@ export class Chunk { } } + // Returns if a cube is in the chunk and highlights it if it is + public updateSelected(highlightOn: boolean, selectedCube: Vec3): boolean { + // Reset the previously highlighted box + if (this.highlightedCubePos < this.cubes) { + this.cubePositionsF32[4 * this.highlightedCubePos + 3] = 0; + } + // Do not highlight if highlighting is turned off + if (!highlightOn) { + return false; + } + // See if the selected box is in the current chunk + if (!this.isInChunkBounds(selectedCube.x,selectedCube.z)) { + return false; + } + // Find if the cube is rendered in the current chunk and highlight if so + for (let i = 0; i < this.cubes; ++i) { + if (this.cubePositionsF32[4 * i] == selectedCube.x && + this.cubePositionsF32[4 * i + 1] == selectedCube.y && + this.cubePositionsF32[4 * i + 2] == selectedCube.z) { + this.cubePositionsF32[4 * i + 3] = 3; // Highlight + this.highlightedCubePos = i; + return true; + } + } + return false; + } + + public updateLandscape(removeCube: boolean, selectedCube: Vec3) { + // See if the selected box is in the current chunk + if (!this.isInChunkBounds(selectedCube.x,selectedCube.z)) { + return false; + } + // Update number of cubes + let updatedCubes = this.cubes + (removeCube ? -1 : 1); + + // Copy cube positions into updated array with the selected cube either + // added or removed + let updatedPositionsF32 = new Float32Array(4 * updatedCubes); + let blockIdx = 0; + for (let i = 0; i < this.cubes; ++i) { + // If cube is set to be removed, we skip it + if (removeCube && this.cubePositionsF32[4 * i] == selectedCube.x && + this.cubePositionsF32[4 * i + 1] == selectedCube.y && + this.cubePositionsF32[4 * i + 2] == selectedCube.z) { + // Remove the cube + let idx = (selectedCube.x - this.topleftx) * this.size + (selectedCube.z - this.toplefty); + this.densityMap[idx][selectedCube.y] = -1.0; + continue; + } + // Else we copy the cube position into the updated array + updatedPositionsF32[4 * blockIdx] = this.cubePositionsF32[4 * i]; + updatedPositionsF32[4 * blockIdx + 1] = this.cubePositionsF32[4 * i + 1]; + updatedPositionsF32[4 * blockIdx + 2] = this.cubePositionsF32[4 * i + 2]; + updatedPositionsF32[4 * blockIdx + 3] = this.cubePositionsF32[4 * i + 3]; + ++blockIdx; + } + // We add a new cube + if (!removeCube) { + updatedPositionsF32[4 * blockIdx] = selectedCube.x; + updatedPositionsF32[4 * blockIdx + 1] = selectedCube.y; + updatedPositionsF32[4 * blockIdx + 2] = selectedCube.z; + updatedPositionsF32[4 * blockIdx + 3] = 3; + + this.highlightedCubePos = blockIdx; + // Update height map and density map if we add a cube + let idx = (selectedCube.x - this.topleftx) * this.size + (selectedCube.z - this.toplefty); + let height = this.heightMap[idx]; + // This is the case where we add a cube on top of the current height + if(selectedCube.y >= height) { + // We're building even higher than the current height + let densArr = [...this.densityMap[idx]]; + for(let i = 0; i < selectedCube.y - height; ++i){ + densArr.push(-1.0); + } + densArr.push(1.0); + this.densityMap[idx] = new Float32Array(densArr); + this.heightMap[idx] = densArr.length; + } + else{ + this.densityMap[idx][selectedCube.y] = 1.0; + } + } + + // Update internal data structures + this.cubePositionsF32 = updatedPositionsF32; + this.cubes = updatedCubes; + } + + public updateFromLog(modificationLog: number[][]) { + // Get the modifications for the current the current chunk + let removeCubes = {}; + let addCubes: number[][] = []; + let modification = false; + let updatedCubes = this.cubes; + for (let i = 0; i < modificationLog.length; ++i) { + const x = modificationLog[i][0]; + const y = modificationLog[i][1]; + const z = modificationLog[i][2]; + if (this.isInChunkBounds(x,z)) { + modification = true; + if (modificationLog[i][3] < 0) { + if (!(x in removeCubes)) { + removeCubes[x] = {}; + } + if (!(y in removeCubes[x])) { + removeCubes[x][y] = {}; + } + if (!(z in removeCubes[x][y])) { + removeCubes[x][y][z] = modificationLog[i][3]; + updatedCubes += modificationLog[i][3]; + } + } else { + addCubes.push([x, y, z]); + updatedCubes += modificationLog[i][3]; + } + + } + } + if (!modification) { + return; + } + + // Copy cube positions into updated array with the selected cube either + // added or removed + let updatedPositionsF32 = new Float32Array(4 * updatedCubes); + let blockIdx = 0; + // Render cubes that are not removed + for (let i = 0; i < this.cubes; ++i) { + let x = this.cubePositionsF32[4 * i]; + let y = this.cubePositionsF32[4 * i + 1]; + let z = this.cubePositionsF32[4 * i + 2]; + // Removed Cubes + if (x in removeCubes && y in removeCubes[x] && z in removeCubes[x][y]) { + // Update collision logic to remove cube + let idx = (x - this.topleftx) * this.size + (z - this.toplefty); + this.densityMap[idx][y] = -1.0; + continue; + } + updatedPositionsF32[4 * blockIdx] = this.cubePositionsF32[4 * i]; + updatedPositionsF32[4 * blockIdx + 1] = this.cubePositionsF32[4 * i + 1]; + updatedPositionsF32[4 * blockIdx + 2] = this.cubePositionsF32[4 * i + 2]; + updatedPositionsF32[4 * blockIdx + 3] = this.cubePositionsF32[4 * i + 3]; + ++blockIdx; + } + // Cubes to add + for (let i = 0; i < addCubes.length; ++i) { + updatedPositionsF32[4 * blockIdx] = addCubes[i][0]; + updatedPositionsF32[4 * blockIdx + 1] = addCubes[i][1]; + updatedPositionsF32[4 * blockIdx + 2] = addCubes[i][2]; + updatedPositionsF32[4 * blockIdx + 3] = 0; + ++blockIdx; + + // Update collision logic to add cube + const x = Math.round(addCubes[i][0] - this.topleftx); + const z = Math.round(addCubes[i][2] - this.toplefty); + let idx = x * this.size + z; + + const y = addCubes[i][1]; + let height = this.heightMap[idx]; + // This is the case where we add a cube on top of the current height + if(y >= height) { + // We're building even higher than the current height + let densArr = [...this.densityMap[idx]]; + for(let i = 0; i < y - height; ++i){ + densArr.push(-1.0); + } + densArr.push(1.0); + this.densityMap[idx] = new Float32Array(densArr); + this.heightMap[idx] = densArr.length; + } + else{ + this.densityMap[idx][y] = 1.0; + } + } + // Update internal data structures + this.cubePositionsF32 = updatedPositionsF32; + this.cubes = updatedCubes; + + } + public cubePositions(): Float32Array { return this.cubePositionsF32; } diff --git a/src/minecraft/Gui.ts b/src/minecraft/Gui.ts index 36e8abc..2ca5785 100644 --- a/src/minecraft/Gui.ts +++ b/src/minecraft/Gui.ts @@ -24,7 +24,7 @@ interface IGUI { export class GUI implements IGUI { private static readonly rotationSpeed: number = 0.01; - private static readonly walkSpeed: number = 1; + public static readonly walkSpeed: number = 0.5; private static readonly rollSpeed: number = 0.1; private static readonly panSpeed: number = 0.1; @@ -45,6 +45,8 @@ export class GUI implements IGUI { private SpaceDown: boolean; private ShiftLeftDown: boolean; + private selectedCube: Vec3; + /** * * @param canvas required to get the width and height of the canvas @@ -102,9 +104,14 @@ export class GUI implements IGUI { } public dragStart(mouse: MouseEvent): void { - this.prevX = mouse.screenX; - this.prevY = mouse.screenY; - this.dragging = true; + if (mouse.buttons == 1) { + this.prevX = mouse.screenX; + this.prevY = mouse.screenY; + this.dragging = true; + } else { + // Called whenever mouse 2 is clicked + this.animation.modifyLandscape(this.selectedCube); + } } public dragEnd(mouse: MouseEvent): void { this.dragging = false; @@ -127,6 +134,21 @@ export class GUI implements IGUI { this.camera.rotate(new Vec3([0, 1, 0]), -GUI.rotationSpeed * dx); this.camera.rotate(this.camera.right(), -GUI.rotationSpeed * dy); } + + const mouseNDC = new Vec4([(x/this.width) * 2 - 1, 1 - (y/this.height) * 2, -1, 1]); + const mouseProjection = this.projMatrix().inverse().multiplyVec4(mouseNDC); + let mouseWorld = this.viewMatrix().inverse().multiplyVec4(mouseProjection); + mouseWorld.scale(1 / mouseWorld.w); + + const ray = Vec3.difference(new Vec3(mouseWorld.xyz), this.camera.pos()).normalize(); + const origin = this.camera.pos(); + + // Get the next block from the players current position. + let t = Config.SELECT_RADIUS; + ray.scale(t); + const selectedCube = Vec3.sum(origin, ray); + this.selectedCube = new Vec3([Math.round(selectedCube.x), Math.round(selectedCube.y), Math.round(selectedCube.z)]); + this.animation.updateSelectedCube(this.selectedCube); } public walkDir(): Vec3 { @@ -166,6 +188,10 @@ export class GUI implements IGUI { this.Ddown = true; break; } + case 'KeyH': { + this.animation.highlightOn = !this.animation.highlightOn; + break; + } case 'KeyR': { this.animation.reset(); break; diff --git a/src/minecraft/Shaders.ts b/src/minecraft/Shaders.ts index c54d959..317792f 100644 --- a/src/minecraft/Shaders.ts +++ b/src/minecraft/Shaders.ts @@ -13,10 +13,12 @@ export const blankCubeVSText = ` varying vec4 normal; varying vec4 wsPos; varying vec2 uv; + varying float highlight; void main () { - - gl_Position = uProj * uView * (aVertPos + aOffset); + vec4 offset = vec4(aOffset.x, aOffset.y, aOffset.z, 0.0); + highlight = aOffset.w; + gl_Position = uProj * uView * (aVertPos + offset); wsPos = aVertPos + aOffset; normal = normalize(aNorm); uv = aUV; @@ -32,6 +34,7 @@ export const blankCubeFSText = ` varying vec4 normal; varying vec4 wsPos; varying vec2 uv; + varying float highlight; float smoothmix(float a0, float a1, float w) { return (a1 - a0) * (3.0 - w * 2.0) * w * w + a0; @@ -159,27 +162,40 @@ export const blankCubeFSText = ` float seed = 10.0; vec3 kd = vec3(1.0, 1.0, 1.0); vec3 ka = vec3(0.3, 0.3, 0.3); + float epsilon = 0.1; /* Compute light fall off */ vec4 lightDirection = uLightPos - wsPos; float dot_nl = dot(normalize(lightDirection), normalize(normal)); dot_nl = clamp(dot_nl, 0.0, 1.0); - // Lava/Magma only generates on low locations - if(wsPos.y < 20.5) { - vec3 magma = perlinMagma(uv, seed); - gl_FragColor = vec4(clamp(ka + dot_nl * kd, 0.0, 1.0)* magma, 1.0); - } - // Snow only generates on high locations - else if(wsPos.y > 55.0){ - vec3 snow = perlinSnow(uv, seed); - gl_FragColor = vec4(clamp(ka + dot_nl * kd, 0.0, 1.0)* snow, 1.0); - } - // Stone - else{ - vec3 stone = perlinStone(uv, seed); - gl_FragColor = vec4(clamp(ka + dot_nl * kd, 0.0, 1.0)* stone, 1.0); + // Highlight logic for the block + if (highlight >= 2.0 - epsilon) { + if (highlight >= 2.0 - epsilon && highlight <= 2.0 + epsilon) { + // Green + gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0); + } else { + // Red + gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); + } + } else { + // Lava/Magma only generates on low locations + if(wsPos.y < 20.5) { + vec3 magma = perlinMagma(uv, seed); + gl_FragColor = vec4(clamp(ka + dot_nl * kd, 0.0, 1.0)* magma, 1.0); + } + // Snow only generates on high locations + else if(wsPos.y > 55.0){ + vec3 snow = perlinSnow(uv, seed); + gl_FragColor = vec4(clamp(ka + dot_nl * kd, 0.0, 1.0)* snow, 1.0); + } + // Stone + else{ + vec3 stone = perlinStone(uv, seed); + gl_FragColor = vec4(clamp(ka + dot_nl * kd, 0.0, 1.0)* stone, 1.0); + } } + }