From 1e10dda0e246ed802c71b079b312ae3611f18029 Mon Sep 17 00:00:00 2001 From: LabyStudio Date: Sun, 15 May 2022 21:05:11 +0200 Subject: [PATCH] implement block break particles, version 1.0.2 --- README.md | 1 + src/js/net/minecraft/client/Minecraft.js | 11 +- src/js/net/minecraft/client/entity/Entity.js | 128 ++++++++++++++++++ .../minecraft/client/entity/PlayerEntity.js | 115 +--------------- .../minecraft/client/render/WorldRenderer.js | 5 +- .../render/entity/EntityRenderManager.js | 3 + .../client/render/particle/Particle.js | 123 +++++++++++++++++ .../render/particle/ParticleRenderer.js | 79 +++++++++++ .../particle/particle/ParticleDigging.js | 23 ++++ .../net/minecraft/client/world/block/Block.js | 8 ++ .../client/world/block/type/BlockGrass.js | 4 + 11 files changed, 386 insertions(+), 114 deletions(-) create mode 100644 src/js/net/minecraft/client/render/particle/Particle.js create mode 100644 src/js/net/minecraft/client/render/particle/ParticleRenderer.js create mode 100644 src/js/net/minecraft/client/render/particle/particle/ParticleDigging.js diff --git a/README.md b/README.md index 39ccbd2..3255f8c 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Click [here](https://labystudio.de/page/minecraft/) for a demo! - Arm swing animation - Walking animation - Crouch animation + - Block break particles - World - 16x16x16 Chunks - Block type, data, sky & block lightning diff --git a/src/js/net/minecraft/client/Minecraft.js b/src/js/net/minecraft/client/Minecraft.js index 0f2f7ab..f03f058 100644 --- a/src/js/net/minecraft/client/Minecraft.js +++ b/src/js/net/minecraft/client/Minecraft.js @@ -15,10 +15,11 @@ import GrassColorizer from "./render/GrassColorizer.js"; import GuiMainMenu from "./gui/screens/GuiMainMenu.js"; import GuiLoadingScreen from "./gui/screens/GuiLoadingScreen.js"; import * as THREE from "../../../../../libraries/three.module.js"; +import ParticleRenderer from "./render/particle/ParticleRenderer.js"; export default class Minecraft { - static VERSION = "1.0.1" + static VERSION = "1.0.2" static URL_GITHUB = "https://github.com/labystudio/js-minecraft"; /** @@ -65,6 +66,8 @@ export default class Minecraft { // Grass colorizer this.grassColorizer = new GrassColorizer(this); + this.particleRenderer = new ParticleRenderer(this); + // Update window size this.window.updateWindowSize(); @@ -219,6 +222,9 @@ export default class Minecraft { // Tick the player this.player.onUpdate(); + + // Tick particle renderer + this.particleRenderer.onTick(); } // Tick the screen @@ -293,6 +299,9 @@ export default class Minecraft { 1.0 ); + // Spawn particle + this.particleRenderer.spawnBlockBreakParticle(this.world, hitResult.x, hitResult.y, hitResult.z); + // Destroy block this.world.setBlockAt(hitResult.x, hitResult.y, hitResult.z, 0); } diff --git a/src/js/net/minecraft/client/entity/Entity.js b/src/js/net/minecraft/client/entity/Entity.js index e1a484f..e68008b 100644 --- a/src/js/net/minecraft/client/entity/Entity.js +++ b/src/js/net/minecraft/client/entity/Entity.js @@ -1,11 +1,13 @@ import BoundingBox from "../../util/BoundingBox.js"; import MathHelper from "../../util/MathHelper.js"; +import Random from "../../util/Random.js"; export default class Entity { constructor(minecraft, world) { this.minecraft = minecraft; this.world = world; + this.random = new Random(); this.renderer = this.minecraft.worldRenderer.entityRenderManager.createEntityRendererByEntity(this); @@ -13,10 +15,15 @@ export default class Entity { this.y = 0; this.z = 0; + this.width = 0.0; + this.height = 0.0; + this.motionX = 0; this.motionY = 0; this.motionZ = 0; + this.stepHeight = 0.0; + this.onGround = false; this.sneaking = false; @@ -35,8 +42,36 @@ export default class Entity { this.nextStepDistance = 1; this.ticksExisted = 0; + this.isDead = false; this.boundingBox = new BoundingBox(); + this.setPosition(this.x, this.y, this.z); + } + + setPosition(x, y, z) { + // Update position + this.x = x; + this.y = y; + this.z = z; + + // Update bounding box + let width = this.width / 2; + this.boundingBox = new BoundingBox( + x - width, + y, + z - width, + x + width, + y + this.height, + z + width + ); + + this.motionX = 0; + this.motionY = 0; + this.motionZ = 0; + + this.prevX = this.x; + this.prevY = this.y; + this.prevZ = this.z; } onUpdate() { @@ -67,4 +102,97 @@ export default class Entity { return this.boundingBox.height() * 0.8; } + moveCollide(targetX, targetY, targetZ) { + // Target position + let originalTargetX = targetX; + let originalTargetY = targetY; + let originalTargetZ = targetZ; + + if (this.onGround && this.sneaking) { + for (; targetX !== 0.0 && this.world.getCollisionBoxes(this.boundingBox.offset(targetX, -this.stepHeight, 0.0)).length === 0; originalTargetX = targetX) { + if (targetX < 0.05 && targetX >= -0.05) { + targetX = 0.0; + } else if (targetX > 0.0) { + targetX -= 0.05; + } else { + targetX += 0.05; + } + } + + for (; targetZ !== 0.0 && this.world.getCollisionBoxes(this.boundingBox.offset(0.0, -this.stepHeight, targetZ)).length === 0; originalTargetZ = targetZ) { + if (targetZ < 0.05 && targetZ >= -0.05) { + targetZ = 0.0; + } else if (targetZ > 0.0) { + targetZ -= 0.05; + } else { + targetZ += 0.05; + } + } + + for (; targetX !== 0.0 && targetZ !== 0.0 && this.world.getCollisionBoxes(this.boundingBox.offset(targetX, -this.stepHeight, targetZ)).length === 0; originalTargetZ = targetZ) { + if (targetX < 0.05 && targetX >= -0.05) { + targetX = 0.0; + } else if (targetX > 0.0) { + targetX -= 0.05; + } else { + targetX += 0.05; + } + + originalTargetX = targetX; + + if (targetZ < 0.05 && targetZ >= -0.05) { + targetZ = 0.0; + } else if (targetZ > 0.0) { + targetZ -= 0.05; + } else { + targetZ += 0.05; + } + } + } + + // Get level tiles as bounding boxes + let boundingBoxList = this.world.getCollisionBoxes(this.boundingBox.expand(targetX, targetY, targetZ)); + + // Move bounding box + for (let aABB in boundingBoxList) { + targetY = boundingBoxList[aABB].clipYCollide(this.boundingBox, targetY); + } + this.boundingBox.move(0.0, targetY, 0.0); + + for (let aABB in boundingBoxList) { + targetX = boundingBoxList[aABB].clipXCollide(this.boundingBox, targetX); + } + this.boundingBox.move(targetX, 0.0, 0.0); + + for (let aABB in boundingBoxList) { + targetZ = boundingBoxList[aABB].clipZCollide(this.boundingBox, targetZ); + } + this.boundingBox.move(0.0, 0.0, targetZ); + + this.onGround = originalTargetY !== targetY && originalTargetY < 0.0; + + // Stop motion on collision + if (originalTargetX !== targetX) { + this.motionX = 0.0; + } + if (originalTargetY !== targetY) { + this.motionY = 0.0; + } + if (originalTargetZ !== targetZ) { + this.motionZ = 0.0; + } + + // Update position + this.x = (this.boundingBox.minX + this.boundingBox.maxX) / 2.0; + this.y = this.boundingBox.minY; + this.z = (this.boundingBox.minZ + this.boundingBox.maxZ) / 2.0; + + // Horizontal collision? + return originalTargetX !== targetX || originalTargetZ !== targetZ; + } + + kill() { + this.isDead = true; + } + } \ No newline at end of file diff --git a/src/js/net/minecraft/client/entity/PlayerEntity.js b/src/js/net/minecraft/client/entity/PlayerEntity.js index f975440..81bcb2d 100644 --- a/src/js/net/minecraft/client/entity/PlayerEntity.js +++ b/src/js/net/minecraft/client/entity/PlayerEntity.js @@ -1,6 +1,5 @@ import Inventory from "../inventory/Inventory.js"; import EntityLiving from "./EntityLiving.js"; -import BoundingBox from "../../util/BoundingBox.js"; import Block from "../world/block/Block.js"; import MathHelper from "../../util/MathHelper.js"; import Keyboard from "../../util/Keyboard.js"; @@ -44,6 +43,9 @@ export default class PlayerEntity extends EntityLiving { this.cameraPitch = 0; this.prevCameraYaw = 0; this.prevCameraPitch = 0; + + this.width = 0.6; + this.height = 1.8; } respawn() { @@ -51,28 +53,6 @@ export default class PlayerEntity extends EntityLiving { this.setPosition(spawn.x, spawn.y, spawn.z); } - setPosition(x, y, z) { - let width = 0.3; - let height = 0.9; - this.boundingBox = new BoundingBox( - x - width, - y - height, - z - width, - x + width, - y + height, - z + width - ); - - this.motionX = 0; - this.motionY = 0; - this.motionZ = 0; - - // Update position - this.x = (this.boundingBox.minX + this.boundingBox.maxX) / 2.0; - this.y = this.boundingBox.minY; - this.z = (this.boundingBox.minZ + this.boundingBox.maxZ) / 2.0; - } - turn(motionX, motionY) { let sensitivity = this.minecraft.settings.sensitivity / 500; this.rotationYaw = this.rotationYaw + motionX * sensitivity; @@ -366,95 +346,6 @@ export default class PlayerEntity extends EntityLiving { this.sneaking = sneaking; } - moveCollide(targetX, targetY, targetZ) { - // Target position - let originalTargetX = targetX; - let originalTargetY = targetY; - let originalTargetZ = targetZ; - - if (this.onGround && this.sneaking) { - for (; targetX !== 0.0 && this.world.getCollisionBoxes(this.boundingBox.offset(targetX, -this.stepHeight, 0.0)).length === 0; originalTargetX = targetX) { - if (targetX < 0.05 && targetX >= -0.05) { - targetX = 0.0; - } else if (targetX > 0.0) { - targetX -= 0.05; - } else { - targetX += 0.05; - } - } - - for (; targetZ !== 0.0 && this.world.getCollisionBoxes(this.boundingBox.offset(0.0, -this.stepHeight, targetZ)).length === 0; originalTargetZ = targetZ) { - if (targetZ < 0.05 && targetZ >= -0.05) { - targetZ = 0.0; - } else if (targetZ > 0.0) { - targetZ -= 0.05; - } else { - targetZ += 0.05; - } - } - - for (; targetX !== 0.0 && targetZ !== 0.0 && this.world.getCollisionBoxes(this.boundingBox.offset(targetX, -this.stepHeight, targetZ)).length === 0; originalTargetZ = targetZ) { - if (targetX < 0.05 && targetX >= -0.05) { - targetX = 0.0; - } else if (targetX > 0.0) { - targetX -= 0.05; - } else { - targetX += 0.05; - } - - originalTargetX = targetX; - - if (targetZ < 0.05 && targetZ >= -0.05) { - targetZ = 0.0; - } else if (targetZ > 0.0) { - targetZ -= 0.05; - } else { - targetZ += 0.05; - } - } - } - - // Get level tiles as bounding boxes - let boundingBoxList = this.world.getCollisionBoxes(this.boundingBox.expand(targetX, targetY, targetZ)); - - // Move bounding box - for (let aABB in boundingBoxList) { - targetY = boundingBoxList[aABB].clipYCollide(this.boundingBox, targetY); - } - this.boundingBox.move(0.0, targetY, 0.0); - - for (let aABB in boundingBoxList) { - targetX = boundingBoxList[aABB].clipXCollide(this.boundingBox, targetX); - } - this.boundingBox.move(targetX, 0.0, 0.0); - - for (let aABB in boundingBoxList) { - targetZ = boundingBoxList[aABB].clipZCollide(this.boundingBox, targetZ); - } - this.boundingBox.move(0.0, 0.0, targetZ); - - this.onGround = originalTargetY !== targetY && originalTargetY < 0.0; - - // Stop motion on collision - if (originalTargetX !== targetX) { - this.motionX = 0.0; - } - if (originalTargetY !== targetY) { - this.motionY = 0.0; - } - if (originalTargetZ !== targetZ) { - this.motionZ = 0.0; - } - - // Update position - this.x = (this.boundingBox.minX + this.boundingBox.maxX) / 2.0; - this.y = this.boundingBox.minY; - this.z = (this.boundingBox.minZ + this.boundingBox.maxZ) / 2.0; - - // Horizontal collision? - return originalTargetX !== targetX || originalTargetZ !== targetZ; - } - getEyeHeight() { return this.sneaking ? 1.50 : 1.62; } diff --git a/src/js/net/minecraft/client/render/WorldRenderer.js b/src/js/net/minecraft/client/render/WorldRenderer.js index 461d506..0d42dce 100644 --- a/src/js/net/minecraft/client/render/WorldRenderer.js +++ b/src/js/net/minecraft/client/render/WorldRenderer.js @@ -117,6 +117,9 @@ export default class WorldRenderer { // Render target block this.renderBlockHitBox(player, partialTicks); + // Render particles + this.minecraft.particleRenderer.renderParticles(player, partialTicks); + // Hide all entities and make them visible during rendering for (let entity of this.minecraft.world.entities) { entity.renderer.group.visible = false; @@ -615,7 +618,7 @@ export default class WorldRenderer { }); // Flush by rebuilding 8 chunk sections - if(this.flushRebuild) { + if (this.flushRebuild) { this.flushRebuild = false; for (let i = 0; i < 8; i++) { diff --git a/src/js/net/minecraft/client/render/entity/EntityRenderManager.js b/src/js/net/minecraft/client/render/entity/EntityRenderManager.js index f1d0c85..dd9e0dc 100644 --- a/src/js/net/minecraft/client/render/entity/EntityRenderManager.js +++ b/src/js/net/minecraft/client/render/entity/EntityRenderManager.js @@ -15,6 +15,9 @@ export default class EntityRenderManager { } createEntityRendererByEntity(entity) { + if (!(entity.constructor.name in this.renderers)) { + return null; + } return new this.renderers[entity.constructor.name].prototype.constructor(this.worldRenderer); } } \ No newline at end of file diff --git a/src/js/net/minecraft/client/render/particle/Particle.js b/src/js/net/minecraft/client/render/particle/Particle.js new file mode 100644 index 0000000..b27ba43 --- /dev/null +++ b/src/js/net/minecraft/client/render/particle/Particle.js @@ -0,0 +1,123 @@ +import Entity from "../../entity/Entity.js"; +import * as THREE from "../../../../../../../libraries/three.module.js"; +import Tessellator from "../Tessellator.js"; + +export default class Particle extends Entity { + + constructor(minecraft, world, x, y, z, motionX, motionY, motionZ) { + super(minecraft, world); + + this.setPosition(x, y, z); + + this.textureIndex = 0; + + this.randomX = this.random.nextFloat() * 3; + this.randomY = this.random.nextFloat() * 3; + this.randomZ = (this.random.nextFloat() * 0.5 + 0.5) * 2.0; + + this.motionX = motionX + (Math.random() * 2 - 1.0) * 0.4; + this.motionY = motionY + (Math.random() * 2 - 1.0) * 0.4; + this.motionZ = motionZ + (Math.random() * 2 - 1.0) * 0.4; + + let strength = (Math.random() + Math.random() + 1.0) * 0.15; + let length = Math.sqrt(this.motionX * this.motionX + this.motionY * this.motionY + this.motionZ * this.motionZ); + this.motionX = (this.motionX / length) * strength * 0.4; + this.motionY = (this.motionY / length) * strength * 0.4 + 0.1; + this.motionZ = (this.motionZ / length) * strength * 0.4; + + this.maxTicksExisted = Math.floor(4 / (this.random.nextFloat() * 0.9 + 0.1)) + this.color = -1; + + this.group = null; + } + + onUpdate() { + super.onUpdate(); + + if (this.ticksExisted >= this.maxTicksExisted) { + this.kill(); + } + + this.motionY -= 0.04; + + this.moveCollide(this.motionX, this.motionY, this.motionZ); + + this.motionX *= 0.98; + this.motionY *= 0.98; + this.motionZ *= 0.98; + + if (this.onGround) { + this.motionX *= 0.7; + this.motionZ *= 0.7; + } + } + + render(rotationX, rotationY, rotationZ, partialTicks) { + let x = ((this.prevX + (this.x - this.prevX) * partialTicks)); + let y = ((this.prevY + (this.y - this.prevY) * partialTicks)); + let z = ((this.prevZ + (this.z - this.prevZ) * partialTicks)); + + // Create mesh + if (this.group === null) { + this.rebuild(); + } + + let factor = 0.1 * this.randomZ; + this.group.scale.x = factor; + this.group.scale.y = factor; + this.group.scale.z = factor; + + this.group.rotation.x = rotationX; + this.group.rotation.y = rotationY; + this.group.rotation.z = rotationZ; + + this.group.position.x = x; + this.group.position.y = y; + this.group.position.z = z; + this.group.updateMatrix(); + } + + rebuild() { + this.group = new THREE.Object3D(); + this.group.rotation.order = 'ZYX'; + + let tessellator = new Tessellator(); + tessellator.bindTexture(this.minecraft.worldRenderer.textureTerrain); + + let minU = ((this.textureIndex % 16) + this.randomX / 4) / 16.0; + let maxU = minU + (16 / 256 / 4); + let minV = (Math.floor(this.textureIndex / 16) + this.randomX / 4) / 16.0; + let maxV = minV + (16 / 256 / 4); + + // Flip V + minV = 1 - minV; + maxV = 1 - maxV; + + let red = (this.color >> 16 & 255) / 255.0; + let green = (this.color >> 8 & 255) / 255.0; + let blue = (this.color & 255) / 255.0; + + let brightness = this.getEntityBrightness(); + + // Render particle + tessellator.startDrawing(); + tessellator.setColor(red * brightness, green * brightness, blue * brightness); + tessellator.addVertexWithUV(0, 1, 0, minU, minV); + tessellator.addVertexWithUV(0, 0, 0, minU, maxV); + tessellator.addVertexWithUV(1, 0, 0, maxU, maxV); + tessellator.addVertexWithUV(1, 1, 0, maxU, minV); + let mesh = tessellator.draw(this.group); + mesh.geometry.center(); + + this.minecraft.worldRenderer.scene.add(this.group); + } + + kill() { + super.kill(); + + if (this.group !== null) { + this.minecraft.worldRenderer.scene.remove(this.group); + } + } + +} \ No newline at end of file diff --git a/src/js/net/minecraft/client/render/particle/ParticleRenderer.js b/src/js/net/minecraft/client/render/particle/ParticleRenderer.js new file mode 100644 index 0000000..dcb54f9 --- /dev/null +++ b/src/js/net/minecraft/client/render/particle/ParticleRenderer.js @@ -0,0 +1,79 @@ +import MathHelper from "../../../util/MathHelper.js"; +import Block from "../../world/block/Block.js"; +import ParticleDigging from "./particle/ParticleDigging.js"; + +export default class ParticleRenderer { + + constructor(minecraft) { + this.minecraft = minecraft; + this.particles = []; + } + + spawnParticle(particle) { + this.particles.push(particle); + } + + onTick() { + for (let i = 0; i < this.particles.length; i++) { + const particle = this.particles[i]; + particle.onUpdate(); + + if (particle.isDead) { + this.particles.splice(i, 1); + i--; + } + } + } + + renderParticles(cameraEntity, partialTicks) { + let yaw = cameraEntity.prevRotationYaw + (cameraEntity.rotationYaw - cameraEntity.prevRotationYaw) * partialTicks; + let pitch = cameraEntity.prevRotationPitch + (cameraEntity.rotationPitch - cameraEntity.prevRotationPitch) * partialTicks; + + let rotationX = MathHelper.toRadians(pitch); + let rotationY = -MathHelper.toRadians(yaw); + + for (let i = 0; i < this.particles.length; i++) { + const particle = this.particles[i]; + + particle.render(rotationX, rotationY, 0, partialTicks); + } + } + + spawnBlockBreakParticle(world, x, y, z) { + let typeId = world.getBlockAt(x, y, z); + if (typeId === 0) { + return; + } + + let block = Block.getById(typeId); + let range = 4; + for (let offsetX = 0; offsetX < range; offsetX++) { + for (let offsetY = 0; offsetY < range; offsetY++) { + for (let offsetZ = 0; offsetZ < range; offsetZ++) { + + let targetX = x + (offsetX + 0.5) / range; + let targetY = y + (offsetY + 0.5) / range; + let targetZ = z + (offsetZ + 0.5) / range; + + let motionX = targetX - x - 0.5; + let motionY = targetY - y - 0.5; + let motionZ = targetZ - z - 0.5; + + this.spawnParticle(new ParticleDigging( + this.minecraft, + world, + targetX, + targetY, + targetZ, + motionX, + motionY, + motionZ, + block + )); + } + } + } + } + + +} \ No newline at end of file diff --git a/src/js/net/minecraft/client/render/particle/particle/ParticleDigging.js b/src/js/net/minecraft/client/render/particle/particle/ParticleDigging.js new file mode 100644 index 0000000..612ae17 --- /dev/null +++ b/src/js/net/minecraft/client/render/particle/particle/ParticleDigging.js @@ -0,0 +1,23 @@ +import Particle from "../Particle.js"; + +export default class ParticleDigging extends Particle { + + constructor(minecraft, world, x, y, z, motionX, motionY, motionZ, block) { + super(minecraft, world, x, y, z, motionX, motionY, motionZ); + + // Get color multiplier + let color = block.getParticleColor(world, x, y, z); + let red = color >> 16 & 0xFF; + let green = color >> 8 & 0xFF; + let blue = color & 0xFF; + + red *= 0.6; + green *= 0.6; + blue *= 0.6; + + this.color = red << 16 | green << 8 | blue; + + this.textureIndex = block.getTextureForFace(block.getParticleTextureFace()); + } + +} \ No newline at end of file diff --git a/src/js/net/minecraft/client/world/block/Block.js b/src/js/net/minecraft/client/world/block/Block.js index cfa1a5e..dfe5775 100644 --- a/src/js/net/minecraft/client/world/block/Block.js +++ b/src/js/net/minecraft/client/world/block/Block.js @@ -31,6 +31,10 @@ export default class Block { return BlockRenderType.BLOCK; } + getParticleTextureFace() { + return EnumBlockFace.TOP; + } + getTextureForFace(face) { return this.textureSlotId; } @@ -52,6 +56,10 @@ export default class Block { return 0xffffff; } + getParticleColor(world, x, y, z) { + return this.getColor(world, x, y, z, this.getParticleTextureFace()); + } + getLightValue() { return 0; } diff --git a/src/js/net/minecraft/client/world/block/type/BlockGrass.js b/src/js/net/minecraft/client/world/block/type/BlockGrass.js index c099c03..baff80a 100644 --- a/src/js/net/minecraft/client/world/block/type/BlockGrass.js +++ b/src/js/net/minecraft/client/world/block/type/BlockGrass.js @@ -26,6 +26,10 @@ export default class BlockGrass extends Block { return world.minecraft.grassColorizer.getColor(temperature, humidity); } + getParticleTextureFace() { + return EnumBlockFace.NORTH; + } + getTextureForFace(face) { switch (face) { case EnumBlockFace.TOP: