Skip to content

Commit

Permalink
chore(world): improved chunk rendering
Browse files Browse the repository at this point in the history
  • Loading branch information
PMK744 committed Sep 15, 2024
1 parent 4df5523 commit 75674f4
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 84 deletions.
7 changes: 5 additions & 2 deletions packages/serenity/src/handlers/request-chunk-radius.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class RequestChunkRadius extends SerenityHandler {
const maxViewDistance = player.dimension.viewDistance;

// Get the requested view distance
const viewDistance = packet.radius << 4;
const viewDistance = packet.radius;

// Get the player's chunk rendering component
const component = player.getComponent("minecraft:chunk_rendering");
Expand All @@ -37,9 +37,12 @@ class RequestChunkRadius extends SerenityHandler {
component.viewDistance =
viewDistance > maxViewDistance ? maxViewDistance : viewDistance;

// Clear the player's chunks
component.clear();

// Send the chunk radius updated packet
const update = new ChunkRadiusUpdatePacket();
update.radius = component.viewDistance >> 4;
update.radius = component.viewDistance;

// Send the update to the player
player.session.sendImmediate(update);
Expand Down
4 changes: 2 additions & 2 deletions packages/serenity/src/providers/leveldb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -635,7 +635,7 @@ class LevelDBProvider extends WorldProvider {
const sz = dimension.spawn.z >> 4;

// Get the view distance of the dimension.
const viewDistance = dimension.viewDistance >> 4;
const viewDistance = dimension.viewDistance;

// Calculate the amount of chunks to pregenerate.
const amount = (viewDistance * 2 + 1) ** 2;
Expand All @@ -652,7 +652,7 @@ class LevelDBProvider extends WorldProvider {
const chunk = provider.readChunk(sx + x, sz + z, dimension);

// Serialize the chunk, the will cache the chunk in the provider.
Chunk.serialize(chunk);
chunk.cache = Chunk.serialize(chunk);

// Set the dirty flag to false.
chunk.dirty = false;
Expand Down
191 changes: 114 additions & 77 deletions packages/world/src/components/player/chunk-rendering.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,41 @@ class PlayerChunkRenderingComponent extends PlayerComponent {
* @param chunks The chunks to send to the player.
*/
public send(...chunks: Array<Chunk>): void {
// Iterate over the chunks
for (const chunk of chunks) {
// Check if the chunk is already rendered
if (this.chunks.has(chunk.hash)) continue;

// Add the chunk to the rendered chunks
// Set the chunk to false to indicate that it has been rendered
this.chunks.add(chunk.hash);
// Get the amount of chunks to send
const amount = chunks.length;

// We want to send the chunks in batches of 4
const batches = Math.ceil(amount / 8);

// Iterate over the batches
for (let index = 0; index < batches; index++) {
// Get the start and end index of the batch
const start = index * 8;
const end = Math.min(start + 8, amount);

// Get the chunks to send
const batch = chunks.slice(start, end);

const levelChunks = batch.map((chunk) => {
this.chunks.add(chunk.hash);

// Create a new LevelChunkPacket
const packet = new LevelChunkPacket();

// Assign the chunk data to the packet
packet.x = chunk.x;
packet.z = chunk.z;
packet.dimension = chunk.type;
packet.subChunkCount = chunk.getSubChunkSendCount();
packet.cacheEnabled = false;
packet.data = Chunk.serialize(chunk);

// Return the packet
return packet;
});

// Send the packets to the player
this.player.session.send(...levelChunks);
}
}

Expand All @@ -62,128 +89,138 @@ class PlayerChunkRenderingComponent extends PlayerComponent {
public distance(hash: bigint): number {
// Convert the chunk hash to a position
const { x: cx, z: cz } = ChunkCoords.unhash(hash);
const { bx, bz } = { bx: Math.abs(cx << 4), bz: Math.abs(cz << 4) };

// Get the player's position
const { x, z } = this.player.position.floor();
const { ax, az } = { ax: Math.abs(x), az: Math.abs(z) };
const { x: px, z: pz } = this.player.position.floor();

// Calculate the distance
const dx = Math.abs(ax - bx);
const dz = Math.abs(az - bz);

// Return the distance
return Math.max(dx, dz);
// Calculate the distance between the player and the chunk
return Math.max(Math.abs(cx - (px >> 4)), Math.abs(cz - (pz >> 4)));
}

/**
* Gets the next set of chunk hashes to send to the player.
* @note This method obtains the chunks that are within the player's view distance.
* @param distance The distance to calculate the chunks for.
* @returns An array of chunk hashes to send to the player.
*/
public next(): Array<bigint> {
public next(distance?: number): Array<bigint> {
// Calculate the chunk position of the entity
const cx = this.player.position.x >> 4;
const cz = this.player.position.z >> 4;

// Get the simulation distance of the dimension
const simulationDistance = this.player.dimension.simulationDistance;

// Calculate the distance or use the simulation distance of the dimension
const dx = (this.viewDistance ?? simulationDistance) >> 4;
const dz = (this.viewDistance ?? simulationDistance) >> 4;
const d = distance ?? this.viewDistance;

// Prepare an array to store the chunks that need to be sent to the player.
const hashes: Array<bigint> = [];

// Get the chunks to render.
for (let x = -dx + cx; x <= dx + cx; x++) {
for (let z = -dz + cz; z <= dz + cz; z++) {
// Get the chunks to render, we want to get the chunks from the inside out
for (let dx = -d; dx <= d; dx++) {
for (let dz = -d; dz <= d; dz++) {
// Get the hash of the chunk
const hash = ChunkCoords.hash({ x: cx + dx, z: cz + dz });

// Check if the chunk is already rendered
if (this.chunks.has(ChunkCoords.hash({ x, z }))) continue;
if (this.chunks.has(hash)) continue;

// Calculate the distance between the player and the chunk
const distance = Math.hypot(dx, dz);

// Add the chunk to the array.
hashes.push(ChunkCoords.hash({ x, z }));
// Check if the chunk is within the view distance
if (distance <= d + 0.5) hashes.push(hash);
}
}

// Return the hashes
return hashes;
}

public clear(position?: ChunkCoords): void {
// Convert the hashes to coordinates
const coords = position
? [position]
: [...this.chunks].map((hash) => ChunkCoords.unhash(hash));

// Create an empty chunk
const empty = new Chunk(0, 0, this.player.dimension.type);

// Iterate over the coordinates
for (const coord of coords) {
// Create a new LevelChunkPacket
const packet = new LevelChunkPacket();

// Assign the chunk data to the packet
packet.x = coord.x;
packet.z = coord.z;
packet.dimension = this.player.dimension.type;
packet.subChunkCount = empty.getSubChunkSendCount();
packet.cacheEnabled = false;
packet.data = Chunk.serialize(empty);

// Send the packet to the player
this.player.session.send(packet);

// Remove the chunk from the player's view
this.chunks.delete(ChunkCoords.hash(coord));
}
}

public onTick(): void {
// Check if the player is spawned
if (this.player.status !== PlayerStatus.Spawned) return;

// Get the next chunks to send to the player
const hashes = [...this.chunks.keys(), ...this.next()].filter(
const hashes = [...this.next(), ...this.chunks].filter(
// Filter out the chunks that are already rendered
(hash) => !this.chunks.has(hash) || !this.chunks.has(hash)
(hash) => !this.chunks.has(hash)
);

// Get the coords of the chunks
const coords = hashes.map((hash) => ChunkCoords.unhash(hash));

// Check if there are any chunks to send
if (coords.length > 0) {
// Send the chunks to the player
for (const hash of hashes) {
// Another sanity check to make sure the chunk is not already rendered
if (this.chunks.has(hash)) continue;

// Get the chunk from the dimension
const chunk = this.player.dimension.getChunkFromHash(hash);
// Get the chunks to send
const chunks = coords
.map((coord) => {
// Check if the chunk is already rendered
if (this.chunks.has(ChunkCoords.hash(coord))) return null;

// Check if the chunk is ready
if (!chunk.ready) {
// Remove the coordinates from the list
coords.splice(coords.indexOf({ x: chunk.x, z: chunk.z }), 1);
// Get the chunk from the dimension
const chunk = this.player.dimension.getChunk(coord.x, coord.z);

// Continue to the next chunk
continue;
}
// Check if the chunk is ready
if (!chunk.ready) return null;

// Create a new level chunk packet
const packet = new LevelChunkPacket();
// Return the chunk
return chunk;
})
.filter((chunk) => chunk !== null) as Array<Chunk>;

// Assign the chunk data to the packet
packet.x = chunk.x;
packet.z = chunk.z;
packet.dimension = chunk.type;
packet.subChunkCount = chunk.getSubChunkSendCount();
packet.cacheEnabled = false;
packet.data = Chunk.serialize(chunk);

// Return the packet
this.player.session.send(packet);
// Send the chunks to the player
this.send(...chunks);
} else {
// Check if any chunks need to be removed from the player's view
for (const hash of this.chunks) {
// Get the distance between the player and the chunk
const distance = this.distance(hash);

// Set the chunk to rendered
this.chunks.add(hash);
// Check if the chunk is outside of the player's view distance
if (distance > this.viewDistance) this.clear(ChunkCoords.unhash(hash));
}

// Create a new network chunk publisher update packet
// Create a new NetworkChunkPublisherUpdatePacket
const update = new NetworkChunkPublisherUpdatePacket();

// Set the packet properties
update.radius = this.player.dimension.viewDistance << 4;
// Assign the values to the packet
update.radius = this.viewDistance << 4;
update.coordinate = this.player.position.floor();
update.savedChunks = coords;
update.savedChunks = [...this.chunks].map((hash) =>
ChunkCoords.unhash(hash)
);

// Send the update to the player.
// Send the packets to the player
this.player.session.send(update);
} else {
// Check if any chunks need to be removed from the player's view
for (const hash of this.chunks) {
// Get the distance between the player and the chunk
const distance = this.distance(hash);
const maxDistance = this.viewDistance + this.viewDistance / 4;

// Check if the distance is greater than the view distance
if (maxDistance < distance) {
// Remove the chunk from the player's view
this.chunks.delete(hash);
}
}
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/world/src/player/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,7 @@ class Player extends Entity {
const component = this.getComponent("minecraft:chunk_rendering");

// Clear the chunks
component.chunks.clear();
component.clear();
}

// Spawn the player in the new dimension
Expand Down
4 changes: 2 additions & 2 deletions packages/world/src/world/dimension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,12 @@ class Dimension {
/**
* The view distance of the dimension.
*/
public viewDistance: number = 256;
public viewDistance: number = 16;

/**
* The simulation distance of the dimension.
*/
public simulationDistance: number = 128;
public simulationDistance: number = 8;

/**
* The min-max dimension build limits
Expand Down

0 comments on commit 75674f4

Please sign in to comment.