implement world generator, frustum culling and rebuild queue

This commit is contained in:
LabyStudio
2022-02-01 10:17:03 +01:00
parent ed49c9f776
commit a3c21b3727
16 changed files with 481 additions and 35 deletions
+1 -1
View File
@@ -12,7 +12,7 @@ window.GameWindow = class {
// Stats
this.stats = new Stats()
this.stats.showPanel(1);
this.stats.showPanel(0);
wrapper.appendChild(this.stats.dom);
// Add web renderer canvas to wrapper
+3 -3
View File
@@ -11,6 +11,9 @@ window.Minecraft = class {
this.frames = 0;
this.lastTime = Date.now();
// Create all blocks
Block.create();
// Create world
this.world = new World();
this.worldRenderer.scene.add(this.world.group);
@@ -23,9 +26,6 @@ window.Minecraft = class {
}
init() {
// Create all blocks
Block.create();
// Start render loop
this.running = true;
this.requestNextFrame();
+3 -3
View File
@@ -47,7 +47,7 @@ window.Player = class {
}
resetPos() {
this.setPos(0, 25, 0);
this.setPos(0, 80, 0);
}
setPos(x, y, z) {
@@ -182,7 +182,7 @@ window.Player = class {
this.motionY = 0.42;
if (this.sprinting) {
let radiansYaw = this.yaw * (Math.PI / 180) + Math.PI;
let radiansYaw = MathHelper.toRadians(this.yaw + 180);
this.motionX -= Math.sin(radiansYaw) * 0.2;
this.motionZ += Math.cos(radiansYaw) * 0.2;
}
@@ -281,7 +281,7 @@ window.Player = class {
up = up * distance;
forward = forward * distance;
let yawRadians = this.yaw * (Math.PI / 180) + Math.PI;
let yawRadians = MathHelper.toRadians(this.yaw + 180);
let sin = Math.sin(yawRadians);
let cos = Math.cos(yawRadians);
@@ -14,6 +14,8 @@ window.WorldRenderer = class {
this.camera.rotation.order = 'ZYX';
this.camera.up = new THREE.Vector3(0, 0, 1);
this.frustum = new THREE.Frustum();
// Create scene
this.scene = new THREE.Scene();
this.scene.matrixAutoUpdate = false;
@@ -42,6 +44,8 @@ window.WorldRenderer = class {
// Block Renderer
this.blockRenderer = new BlockRenderer(this);
this.chunkSectionUpdateQueue = [];
}
render(partialTicks) {
@@ -49,7 +53,10 @@ window.WorldRenderer = class {
this.orientCamera(partialTicks);
// Render chunks
this.renderChunks(this, partialTicks);
let player = this.minecraft.player;
let cameraChunkX = Math.floor(player.x >> 4);
let cameraChunkZ = Math.floor(player.z >> 4);
this.renderChunks(cameraChunkX, cameraChunkZ, EnumWorldBlockLayer.SOLID);
// Render window
this.webRenderer.render(this.scene, this.camera);
@@ -59,8 +66,8 @@ window.WorldRenderer = class {
let player = this.minecraft.player;
// Rotation
this.camera.rotation.y = -player.yaw * (Math.PI / 180) + Math.PI;
this.camera.rotation.x = -player.pitch * (Math.PI / 180);
this.camera.rotation.y = -MathHelper.toRadians(player.yaw + 180);
this.camera.rotation.x = -MathHelper.toRadians(player.pitch);
// Position
let x = player.prevX + (player.x - player.prevX) * partialTicks;
@@ -68,6 +75,9 @@ window.WorldRenderer = class {
let z = player.prevZ + (player.z - player.prevZ) * partialTicks;
this.camera.position.set(x, y + player.getEyeHeight(), z);
// Update frustum
this.frustum.setFromProjectionMatrix(new THREE.Matrix4().multiplyMatrices(this.camera.projectionMatrix, this.camera.matrixWorldInverse));
// Update FOV
this.camera.fov = 85 + player.getFOVModifier();
this.camera.updateProjectionMatrix();
@@ -88,17 +98,68 @@ window.WorldRenderer = class {
}
}
renderChunks(renderer, partialTicks) {
renderChunks(cameraChunkX, cameraChunkZ, renderLayer) {
let world = this.minecraft.world;
for(let i in world.chunks) {
for (let i in world.chunks) {
let chunk = world.chunks[i];
for (let y = 0; y < chunk.sections.length; y++) {
let section = chunk.sections[y];
let distanceX = Math.abs(cameraChunkX - chunk.x);
let distanceZ = Math.abs(cameraChunkZ - chunk.z);
if (section.dirty) {
section.rebuild(renderer);
// Is in render distance check
if (distanceX < WorldRenderer.RENDER_DISTANCE && distanceZ < WorldRenderer.RENDER_DISTANCE) {
// Make chunk visible
chunk.group.visible = true;
// For all chunk sections
for (let y in chunk.sections) {
let chunkSection = chunk.sections[y];
// Is in camera view check
if (this.frustum.intersectsBox(chunkSection.boundingBox)) {
// Make section visible
chunkSection.group.visible = true;
// Render chunk section
chunkSection.render(renderLayer);
// Queue for rebuild
if (chunkSection.isQueuedForRebuild() && !this.chunkSectionUpdateQueue.includes(chunkSection)) {
this.chunkSectionUpdateQueue.push(chunkSection);
}
} else {
// Hide section
chunkSection.group.visible = false;
}
}
} else {
// Hide chunk
chunk.group.visible = false;
}
}
// Sort update queue, chunk sections that are closer to the camera get a higher priority
this.chunkSectionUpdateQueue.sort((section1, section2) => {
let distance1 = Math.floor(Math.pow(section1.x - cameraChunkX, 2) + Math.pow(section1.z - cameraChunkZ, 2));
let distance2 = Math.floor(Math.pow(section2.x - cameraChunkX, 2) + Math.pow(section2.z - cameraChunkZ, 2));
return distance1 - distance2;
});
// Rebuild 16 chunk sections per frame (An entire chunk)
for (let i = 0; i < 16; i++) {
if (this.chunkSectionUpdateQueue.length !== 0) {
let chunkSection = this.chunkSectionUpdateQueue.shift();
if (chunkSection != null) {
// Load chunk
let chunk = chunkSection.chunk;
if (!chunk.isLoaded()) {
world.loadChunk(chunk);
}
// Rebuild chunk
chunkSection.rebuild(this);
}
}
}
+15 -1
View File
@@ -8,16 +8,22 @@ window.Chunk = class {
this.group = new THREE.Object3D();
this.group.matrixAutoUpdate = false;
this.loaded = false;
// Initialize sections
this.sections = [];
for (let y = 0; y < 16; y++) {
let section = new ChunkSection(world, x, y, z);
let section = new ChunkSection(world, this, x, y, z);
this.sections[y] = section;
this.group.add(section.group);
}
}
setBlockAt(x, y, z, typeId) {
this.getSection(y >> 4).setBlockAt(x, y & 15, z, typeId);
}
getSection(y) {
return this.sections[y];
}
@@ -34,4 +40,12 @@ window.Chunk = class {
}
}
load() {
this.loaded = true;
}
isLoaded() {
return this.loaded;
}
}
@@ -2,15 +2,24 @@ window.ChunkSection = class {
static SIZE = 16;
constructor(world, x, y, z) {
constructor(world, chunk, x, y, z) {
this.world = world;
this.chunk = chunk;
this.x = x;
this.y = y;
this.z = z;
this.boundingBox = new THREE.Box3();
this.boundingBox.min.x = x * ChunkSection.SIZE;
this.boundingBox.min.y = y * ChunkSection.SIZE;
this.boundingBox.min.z = z * ChunkSection.SIZE;
this.boundingBox.max.x = x * ChunkSection.SIZE + ChunkSection.SIZE;
this.boundingBox.max.y = y * ChunkSection.SIZE + ChunkSection.SIZE;
this.boundingBox.max.z = z * ChunkSection.SIZE + ChunkSection.SIZE;
this.group = new THREE.Object3D();
this.group.matrixAutoUpdate = false;
this.dirty = true;
this.queuedForRebuild = true;
this.blocks = [];
for (let x = 0; x < ChunkSection.SIZE; x++) {
@@ -22,8 +31,12 @@ window.ChunkSection = class {
}
}
render(renderLayer) {
}
rebuild(renderer) {
this.dirty = false;
this.queuedForRebuild = false;
this.group.clear();
// Start drawing chunk section
@@ -62,6 +75,10 @@ window.ChunkSection = class {
}
queueForRebuild() {
this.dirty = true;
this.queuedForRebuild = true;
}
isQueuedForRebuild() {
return this.queuedForRebuild;
}
}
+26 -14
View File
@@ -7,15 +7,19 @@ window.World = class {
this.group.matrixAutoUpdate = false;
this.chunks = [];
// Debug world
for (let x = -16; x < 16; x++) {
for (let y = 0; y < 16; y++) {
for (let z = -16; z < 16; z++) {
this.setBlockAt(x, y, z, y === 15 ? 2 : 3);
}
}
}
this.setBlockAt(0, 16, -2, 17);
// Load world
this.generator = new WorldGenerator(this, Date.now() % 100000);
}
loadChunk(chunk) {
// Load chunk
chunk.load();
// Generate new chunk
this.generator.generateChunk(chunk);
// Populate chunk
this.generator.populateChunk(chunk.x, chunk.z);
}
getChunkAtBlock(x, y, z) {
@@ -56,10 +60,9 @@ window.World = class {
chunkSection.setBlockAt(x & 15, y & 15, z & 15, type);
}
this.blockChanged(x, y, z);
this.onBlockChanged(x, y, z);
}
getBlockAt(x, y, z) {
let chunkSection = this.getChunkAtBlock(x, y, z);
return chunkSection == null ? 0 : chunkSection.getBlockAt(x & 15, y & 15, z & 15);
@@ -79,11 +82,20 @@ window.World = class {
return chunk;
}
blockChanged(x, y, z) {
this.setDirty(x - 1, y - 1, z - 1, x + 1, y + 1, z + 1);
getHighestBlockYAt(x, z) {
for (let y = World.TOTAL_HEIGHT; y > 0; y--) {
if (this.isSolidBlockAt(x, y, z)) {
return y;
}
}
return 0;
}
setDirty(minX, minY, minZ, maxX, maxY, maxZ) {
onBlockChanged(x, y, z) {
this.queueForRebuildInRegion(x - 1, y - 1, z - 1, x + 1, y + 1, z + 1);
}
queueForRebuildInRegion(minX, minY, minZ, maxX, maxY, maxZ) {
// To chunk coordinates
minX = minX >> 4;
maxX = maxX >> 4;
@@ -0,0 +1,5 @@
window.NoiseGenerator = class {
perlin(x, z) {
}
}
@@ -0,0 +1,170 @@
window.WorldGenerator = class {
constructor(world, seed) {
this.world = world;
this.random = new Random(seed);
this.waterLevel = 64;
// Create noise for the ground height
this.groundHeightNoise = new NoiseGeneratorOctaves(this.random, 8);
this.hillNoise = new NoiseGeneratorCombined(new NoiseGeneratorOctaves(this.random, 4),
new NoiseGeneratorCombined(new NoiseGeneratorOctaves(this.random, 4),
new NoiseGeneratorOctaves(this.random, 4)));
// Water noise
this.sandInWaterNoise = new NoiseGeneratorOctaves(this.random, 8);
// Hole in hills and islands
this.holeNoise = new NoiseGeneratorOctaves(this.random, 3);
this.islandNoise = new NoiseGeneratorOctaves(this.random, 3);
// Caves
this.caveNoise = new NoiseGeneratorOctaves(this.random, 8);
// Population
this.forestNoise = new NoiseGeneratorOctaves(this.random, 8);
}
generateChunk(chunk) {
// For each block in the chunk
for (let relX = 0; relX < ChunkSection.SIZE; relX++) {
for (let relZ = 0; relZ < ChunkSection.SIZE; relZ++) {
// Absolute position of the block
let x = chunk.x * ChunkSection.SIZE + relX;
let z = chunk.z * ChunkSection.SIZE + relZ;
// Extract height value of the noise
let heightValue = this.groundHeightNoise.perlin(x, z);
let hillValue = Math.max(0, this.hillNoise.perlin(x / 18, z / 18) * 6);
// Calculate final height for this position
let groundHeightY = Math.floor(heightValue / 10 + this.waterLevel + hillValue);
if (groundHeightY < this.waterLevel) {
// Generate water
for (let y = 0; y <= this.waterLevel; y++) {
// Use noise to place sand in water
let sandInWater = this.sandInWaterNoise.perlin(x, z) < 0;
let block = y > groundHeightY ? Block.WATER : groundHeightY - y < 3 && sandInWater ? Block.SAND : Block.STONE;
// Send water, sand and stone
chunk.setBlockAt(x & 15, y, z & 15, block.getId());
}
} else {
// Generate height, the highest block is grass
for (let y = 0; y <= groundHeightY; y++) {
// Use the height map to determine the start of the water by shifting it
let isBeach = heightValue < 5 && y < this.waterLevel + 2;
let block = y === groundHeightY ? isBeach ? Block.SAND : Block.GRASS : groundHeightY - y < 3 ? Block.DIRT : Block.STONE;
// Set sand, grass, dirt and stone
chunk.setBlockAt(x & 15, y, z & 15, block.getId());
}
}
/*
int holeY = (int) (this.holeNouse.perlin(-x / 20F, -z / 20F) * 3F + this.waterLevel + 10);
int holeHeight = (int) this.holeNouse.perlin(x / 4F, -z / 4F);
if (holeHeight > 0) {
for (int y = holeY - holeHeight; y <= holeY + holeHeight; y++) {
chunk.setBlockAt(x & 15, y, z & 15, 1);
}
}
*/
// Random holes in hills
let holePositionY = Math.floor(this.holeNoise.perlin(-x / 20, -z / 20) * 3 + this.waterLevel + 10);
let holeHeight = Math.floor(this.holeNoise.perlin(x / 4, -z / 4));
if (holeHeight > 0) {
for (let y = holePositionY - holeHeight; y <= holePositionY + holeHeight; y++) {
if (y > this.waterLevel) {
chunk.setBlockAt(x & 15, y, z & 15, 0);
}
}
}
// Floating islands
let islandPositionY = Math.floor(this.islandNoise.perlin(-x / 10, -z / 10) * 3 + this.waterLevel + 10);
let islandHeight = Math.floor(this.islandNoise.perlin(x / 4, -z / 4) * 4);
let islandRarity = Math.floor(this.islandNoise.perlin(x / 40, z / 40) * 4) - 10;
if (islandHeight > 0 && islandRarity > 0) {
for (let y = islandPositionY - islandHeight; y <= islandPositionY + islandHeight; y++) {
let block = y === islandPositionY + islandHeight ? Block.GRASS : (islandPositionY + islandHeight) - y < 2 ? Block.DIRT : Block.STONE;
chunk.setBlockAt(x & 15, y, z & 15, block.getId());
}
}
// Caves
}
}
}
populateChunk(chunkX, chunkZ) {
for (let index = 0; index < 10; index++) {
let x = this.random.nextInt(ChunkSection.SIZE);
let z = this.random.nextInt(ChunkSection.SIZE);
// Absolute position of the block
let absoluteX = chunkX * ChunkSection.SIZE + x;
let absoluteZ = chunkZ * ChunkSection.SIZE + z;
// Use noise for a forest pattern
let perlin = this.forestNoise.perlin(absoluteX * 10, absoluteZ * 10);
if (perlin > 0 && this.random.nextInt(2) === 0) {
// Get highest block at this position
let highestY = this.world.getHighestBlockYAt(absoluteX, absoluteZ);
// Don't place a tree if there is no grass
if (this.world.getBlockAt(absoluteX, highestY, absoluteZ) === Block.GRASS.getId()
&& this.world.getBlockAt(absoluteX, highestY + 1, absoluteZ) === 0) {
let treeHeight = this.random.nextInt(2) + 5;
// Create tree log
for (let i = 0; i < treeHeight; i++) {
this.world.setBlockAt(absoluteX, highestY + i + 1, absoluteZ, Block.LOG.getId());
}
// Create big leave ring
for (let tx = -2; tx <= 2; tx++) {
for (let ty = 0; ty < 2; ty++) {
for (let tz = -2; tz <= 2; tz++) {
let isCorner = Math.abs(tx) === 2 && Math.abs(tz) === 2;
if (isCorner && this.random.nextBoolean()) {
continue;
}
// Place leave if there is no block yet
if (!this.world.isSolidBlockAt(absoluteX + tx, highestY + treeHeight + ty - 2, absoluteZ + tz)) {
this.world.setBlockAt(absoluteX + tx, highestY + treeHeight + ty - 2, absoluteZ + tz, Block.LEAVE.getId());
}
}
}
}
// Create small leave ring on top
for (let tx = -1; tx <= 1; tx++) {
for (let ty = 0; ty < 2; ty++) {
for (let tz = -1; tz <= 1; tz++) {
let isCorner = Math.abs(tx) === 1 && Math.abs(tz) === 1;
if (isCorner && (ty === 1 || this.random.nextBoolean())) {
continue;
}
// Place leave if there is no block yet
if (!this.world.isSolidBlockAt(absoluteX + tx, highestY + treeHeight + ty, absoluteZ + tz)) {
this.world.setBlockAt(absoluteX + tx, highestY + treeHeight + ty, absoluteZ + tz, Block.LEAVE.getId());
}
}
}
}
}
}
}
}
}
@@ -0,0 +1,14 @@
window.NoiseGeneratorCombined = class extends NoiseGenerator {
constructor(firstGenerator, secondGenerator) {
super();
this.firstGenerator = firstGenerator;
this.secondGenerator = secondGenerator;
}
perlin(x, z) {
return this.firstGenerator.perlin(x + this.secondGenerator.perlin(x, z), z);
}
}
@@ -0,0 +1,24 @@
window.NoiseGeneratorOctaves = class extends NoiseGenerator {
constructor(random, octaves) {
super();
this.octaves = octaves;
this.generatorCollection = [];
for (let i = 0; i < octaves; i++) {
this.generatorCollection[i] = new NoiseGeneratorPerlin(random);
}
}
perlin(x, z) {
let total = 0.0;
let frequency = 1.0;
for (let i = 0; i < this.octaves; i++) {
total += this.generatorCollection[i].perlin(x / frequency, z / frequency) * frequency;
frequency *= 2.0;
}
return total;
}
}
@@ -0,0 +1,85 @@
window.NoiseGeneratorPerlin = class extends NoiseGenerator {
constructor(random) {
super();
this.permutations = [];
for (let i = 0; i < 256; i++) {
this.permutations[i] = i;
}
for (let i = 0; i < 256; i++) {
let n = random.nextInt(256 - i) + i;
let n2 = this.permutations[i];
this.permutations[i] = this.permutations[n];
this.permutations[n] = n2;
this.permutations[i + 256] = this.permutations[i];
}
}
fade(t) {
// Fade function as defined by Ken Perlin. This eases coordinate values
// so that they will "ease" towards integral values. This ends up smoothing
// the final output.
return t * t * t * (t * (t * 6 - 15) + 10); // 6t^5 - 15t^4 + 10t^3
}
lerp(x, a, b) {
return a + x * (b - a);
}
grad(hash, x, y, z) {
let h = hash & 15; // Take the hashed value and take the first 4 bits of it (15 == 0b1111)
let u = h < 8 /* 0b1000 */ ? x : y; // If the most significant bit (MSB) of the hash is 0 then set u = x. Otherwise y.
let v; // In Ken Perlin's original implementation this was another conditional operator (?:). I
// expanded it for readability.
if (h < 4 /* 0b0100 */) // If the first and second significant bits are 0 set v = y
v = y;
else if (h === 12 /* 0b1100 */ || h === 14 /* 0b1110*/) // If the first and second significant bits are 1 set v = x
v = x;
else // If the first and second significant bits are not equal (0/1, 1/0) set v = z
v = z;
return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v); // Use the last 2 bits to decide if u and v are positive or negative. Then return their addition.
}
perlin(x, z) {
let y;
let xi = Math.floor(x) & 0xFF;
let zi = Math.floor(z) & 0xFF;
let yi = Math.floor(0.0) & 0xFF;
x -= Math.floor(x);
z -= Math.floor(z);
y = 0.0 - Math.floor(0.0);
let u = this.fade(x);
let w = this.fade(z);
let v = this.fade(y);
let xzi = this.permutations[xi] + zi;
let xzyi = this.permutations[xzi] + yi;
xzi = this.permutations[xzi + 1] + yi;
xi = this.permutations[xi + 1] + zi;
zi = this.permutations[xi] + yi;
xi = this.permutations[xi + 1] + yi;
return this.lerp(v,
this.lerp(w,
this.lerp(u,
this.grad(this.permutations[xzyi], x, z, y),
this.grad(this.permutations[zi], x - 1.0, z, y)),
this.lerp(u,
this.grad(this.permutations[xzi], x, z - 1.0, y),
this.grad(this.permutations[xi], x - 1.0, z - 1.0, y))),
this.lerp(w,
this.lerp(u,
this.grad(this.permutations[xzyi + 1], x, z, y - 1.0),
this.grad(this.permutations[zi + 1], x - 1.0, z, y - 1.0)),
this.lerp(u,
this.grad(this.permutations[xzi + 1], x, z - 1.0, y - 1.0),
this.grad(this.permutations[xi + 1], x - 1.0, z - 1.0, y - 1.0))));
}
}
@@ -0,0 +1,4 @@
window.EnumWorldBlockLayer = class {
static SOLID = 0;
static CUTOUT = 0;
}
+8
View File
@@ -8,4 +8,12 @@ window.MathHelper = class {
return value < i ? i - 1 : i;
}
static toDegrees(angle) {
return angle * (180 / Math.PI);
}
static toRadians(degree) {
return degree * (Math.PI / 180);
};
}
+25
View File
@@ -0,0 +1,25 @@
window.Random = class {
constructor(seed) {
this.mask = 0xffffffff;
this.m_w = (123456789 + seed) & this.mask;
this.m_z = (987654321 - seed) & this.mask;
}
nextBoolean() {
return this.nextFloat() > 0.5;
}
nextInt(max) {
return Math.floor(this.nextFloat() * (max + 1));
}
nextFloat() {
this.m_z = (36969 * (this.m_z & 65535) + (this.m_z >>> 16)) & this.mask;
this.m_w = (18000 * (this.m_w & 65535) + (this.m_w >>> 16)) & this.mask;
let result = ((this.m_z << 16) + (this.m_w & 65535)) >>> 0;
result /= 4294967296;
return result;
}
}
+7
View File
@@ -50,7 +50,9 @@ loadScripts([
// Minecraft Source
"src/js/net/minecraft/util/EnumBlockFace.js",
"src/js/net/minecraft/util/Timer.js",
"src/js/net/minecraft/util/Random.js",
"src/js/net/minecraft/util/Vector3.js",
"src/js/net/minecraft/util/EnumWorldBlockLayer.js",
"src/js/net/minecraft/util/MovingObjectPosition.js",
"src/js/net/minecraft/util/MathHelper.js",
"src/js/net/minecraft/util/BoundingBox.js",
@@ -67,6 +69,11 @@ loadScripts([
"src/js/net/minecraft/client/world/ChunkSection.js",
"src/js/net/minecraft/client/world/Chunk.js",
"src/js/net/minecraft/client/world/World.js",
"src/js/net/minecraft/client/world/generator/NoiseGenerator.js",
"src/js/net/minecraft/client/world/generator/noise/NoiseGeneratorPerlin.js",
"src/js/net/minecraft/client/world/generator/noise/NoiseGeneratorOctaves.js",
"src/js/net/minecraft/client/world/generator/noise/NoiseGeneratorCombined.js",
"src/js/net/minecraft/client/world/generator/WorldGenerator.js",
"src/js/net/minecraft/client/entity/Player.js",
"src/js/net/minecraft/client/Minecraft.js",
"src/js/net/minecraft/client/render/Tessellator.js",