Renderer/src/main/java/electrosphere/client/terrain/manager/ClientTerrainManager.java
austin 1b5d6c1795
Some checks failed
studiorailgun/Renderer/pipeline/head There was a failure building this commit
Terrain optimizations
2024-11-11 10:23:49 -05:00

334 lines
14 KiB
Java

package electrosphere.client.terrain.manager;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.Semaphore;
import org.joml.Vector3i;
import electrosphere.client.scene.ClientWorldData;
import electrosphere.client.terrain.cache.ChunkData;
import electrosphere.client.terrain.cache.ClientTerrainCache;
import electrosphere.client.terrain.cells.ClientDrawCellManager;
import electrosphere.client.terrain.cells.VoxelTextureAtlas;
import electrosphere.engine.Globals;
import electrosphere.entity.types.terrain.TerrainChunkData;
import electrosphere.logger.LoggerInterface;
import electrosphere.net.parser.net.message.TerrainMessage;
import electrosphere.renderer.meshgen.TerrainChunkModelGeneration;
import electrosphere.renderer.model.Model;
import electrosphere.server.terrain.manager.ServerTerrainChunk;
import electrosphere.server.terrain.manager.ServerTerrainManager;
import io.github.studiorailgun.HashUtils;
/**
* Manages terrain storage and access on the client
*/
public class ClientTerrainManager {
//queues messages from server
List<TerrainMessage> messageQueue = new LinkedList<TerrainMessage>();
/**
* Locks the terrain manager (eg if adding message from network)
*/
static Semaphore lock = new Semaphore(1);
/**
* Maximum concurrent terrain requests
*/
public static final int MAX_CONCURRENT_REQUESTS = 500;
/**
* Number of frames to wait before flagging a request as failed
*/
public static final int FAILED_REQUEST_THRESHOLD = 500;
//The interpolation ratio of terrain
public static final int INTERPOLATION_RATIO = ServerTerrainManager.SERVER_TERRAIN_MANAGER_INTERPOLATION_RATIO;
//caches chunks from server
static final int CACHE_SIZE = 5000 + (int)(ClientDrawCellManager.FULL_RES_DIST * 10) + (int)(ClientDrawCellManager.HALF_RES_DIST * 10);
/**
* Size of the cache in bytes
*/
static final int CACHE_SIZE_IN_MB = (CACHE_SIZE * ServerTerrainChunk.CHUNK_DIMENSION * ServerTerrainChunk.CHUNK_DIMENSION * ServerTerrainChunk.CHUNK_DIMENSION * 2 * 4) / 1024 / 1024;
//used for caching the macro values
ClientTerrainCache terrainCache;
//The world data for the client
ClientWorldData clientWorldData;
//The queue of terrain chunk data to be buffered to gpu
static List<TerrainChunkGenQueueItem> terrainChunkGenerationQueue = new LinkedList<TerrainChunkGenQueueItem>();
/**
* Tracks what outgoing requests are currently active
*/
Map<Long,Integer> requestedMap = new HashMap<Long,Integer>();
/**
* Constructor
*/
public ClientTerrainManager(){
terrainCache = new ClientTerrainCache(CACHE_SIZE);
}
/**
* Handles messages that have been received from the server
*/
public void handleMessages(){
Globals.profiler.beginCpuSample("ClientTerrainManager.handleMessages");
lock.acquireUninterruptibly();
List<TerrainMessage> bouncedMessages = new LinkedList<TerrainMessage>();
for(TerrainMessage message : messageQueue){
switch(message.getMessageSubtype()){
case SENDCHUNKDATA: {
int[][][] values = new int[ChunkData.CHUNK_SIZE][ChunkData.CHUNK_SIZE][ChunkData.CHUNK_SIZE];
float[][][] weights = new float[ChunkData.CHUNK_SIZE][ChunkData.CHUNK_SIZE][ChunkData.CHUNK_SIZE];
ByteBuffer buffer = ByteBuffer.wrap(message.getchunkData());
FloatBuffer floatBuffer = buffer.asFloatBuffer();
for(int x = 0; x < ChunkData.CHUNK_SIZE; x++){
for(int y = 0; y < ChunkData.CHUNK_SIZE; y++){
for(int z = 0; z < ChunkData.CHUNK_SIZE; z++){
weights[x][y][z] = floatBuffer.get();
}
}
}
IntBuffer intView = buffer.asIntBuffer();
intView.position(floatBuffer.position());
for(int x = 0; x < ChunkData.CHUNK_SIZE; x++){
for(int y = 0; y < ChunkData.CHUNK_SIZE; y++){
for(int z = 0; z < ChunkData.CHUNK_SIZE; z++){
values[x][y][z] = intView.get();
}
}
}
ChunkData data = new ChunkData(message.getworldX(), message.getworldY(), message.getworldZ(), ChunkData.NO_STRIDE, ChunkData.NOT_HOMOGENOUS);
data.setVoxelType(values);
data.setVoxelWeight(weights);
terrainCache.addChunkDataToCache(
message.getworldX(), message.getworldY(), message.getworldZ(),
data
);
} break;
case SENDREDUCEDCHUNKDATA: {
int[][][] values = new int[ChunkData.CHUNK_SIZE][ChunkData.CHUNK_SIZE][ChunkData.CHUNK_SIZE];
float[][][] weights = new float[ChunkData.CHUNK_SIZE][ChunkData.CHUNK_SIZE][ChunkData.CHUNK_SIZE];
ByteBuffer buffer = ByteBuffer.wrap(message.getchunkData());
FloatBuffer floatBuffer = buffer.asFloatBuffer();
for(int x = 0; x < ChunkData.CHUNK_SIZE; x++){
for(int y = 0; y < ChunkData.CHUNK_SIZE; y++){
for(int z = 0; z < ChunkData.CHUNK_SIZE; z++){
weights[x][y][z] = floatBuffer.get();
}
}
}
IntBuffer intView = buffer.asIntBuffer();
intView.position(floatBuffer.position());
int firstType = -1;
boolean homogenous = true;
for(int x = 0; x < ChunkData.CHUNK_SIZE; x++){
for(int y = 0; y < ChunkData.CHUNK_SIZE; y++){
for(int z = 0; z < ChunkData.CHUNK_SIZE; z++){
values[x][y][z] = intView.get();
if(firstType == -1){
firstType = values[x][y][z];
} else if(homogenous && firstType == values[x][y][z]){
homogenous = false;
}
}
}
}
ChunkData data = new ChunkData(message.getworldX(), message.getworldY(), message.getworldZ(), message.getchunkResolution(),homogenous ? firstType : ChunkData.NOT_HOMOGENOUS);
data.setVoxelType(values);
data.setVoxelWeight(weights);
terrainCache.addChunkDataToCache(
message.getworldX(), message.getworldY(), message.getworldZ(),
data
);
//remove from request map
this.requestedMap.remove(this.getRequestKey(message.getworldX(), message.getworldY(), message.getworldZ(), message.getchunkResolution()));
} break;
default:
LoggerInterface.loggerEngine.WARNING("ClientTerrainManager: unhandled network message of type" + message.getMessageSubtype());
break;
}
}
messageQueue.clear();
for(TerrainMessage message : bouncedMessages){
messageQueue.add(message);
}
//evaluate if any chunks have failed to request
for(Long key : this.requestedMap.keySet()){
int duration = this.requestedMap.get(key);
if(duration > FAILED_REQUEST_THRESHOLD){
this.requestedMap.remove(key);
} else {
this.requestedMap.put(key,duration + 1);
}
}
lock.release();
Globals.profiler.endCpuSample();
}
/**
* Evicts all cached terrain
*/
public void evictAll(){
this.terrainCache.evictAll();
}
/**
* Attaches a terrain message to the queue of messages that this manager needs to process
* @param message The message
*/
public void attachTerrainMessage(TerrainMessage message){
lock.acquireUninterruptibly();
messageQueue.add(message);
lock.release();
}
/**
* Checks if the terrain cache contains chunk data at a given world position
* @param worldX the x position
* @param worldY the y position
* @param worldZ the z position
* @param stride The stride of the data
* @return true if the data exists, false otherwise
*/
public boolean containsChunkDataAtWorldPoint(int worldX, int worldY, int worldZ, int stride){
return terrainCache.containsChunkDataAtWorldPoint(worldX, worldY, worldZ, stride);
}
/**
* Checks if the terrain cache contains chunk data at a given world position
* @param worldPos The vector containing the world-space position
* @param stride The stride of the data
* @return true if the data exists, false otherwise
*/
public boolean containsChunkDataAtWorldPoint(Vector3i worldPos, int stride){
return this.containsChunkDataAtWorldPoint(worldPos.x, worldPos.y, worldPos.z, stride);
}
/**
* Requests a chunk from the server
* @param worldX the world x coordinate of the chunk
* @param worldY the world y coordinate of the chunk
* @param worldZ the world z coordinate of the chunk
* @param stride The stride of the data
* @return true if the request was successfully sent, false otherwise
*/
public boolean requestChunk(int worldX, int worldY, int worldZ, int stride){
boolean rVal = false;
lock.acquireUninterruptibly();
if(this.requestedMap.size() < MAX_CONCURRENT_REQUESTS && !this.requestedMap.containsKey(this.getRequestKey(worldX, worldY, worldZ, stride))){
Globals.clientConnection.queueOutgoingMessage(TerrainMessage.constructRequestReducedChunkDataMessage(
worldX,
worldY,
worldZ,
stride
));
this.requestedMap.put(this.getRequestKey(worldX, worldY, worldZ, stride), 0);
rVal = true;
}
lock.release();
return rVal;
}
/**
* Gets the chunk data at a given world position
* @param worldX The x component of the world coordinate
* @param worldY The y component of the world coordinate
* @param worldZ The z component of the world coordinate
* @param stride The stride of the data
* @return The chunk data if it exists, otherwise null
*/
public ChunkData getChunkDataAtWorldPoint(int worldX, int worldY, int worldZ, int stride){
return terrainCache.getSubChunkDataAtPoint(worldX, worldY, worldZ, stride);
}
/**
* Gets the chunk data at a given world position
* @param worldPos The world position as a joml vector
* @param stride The stride of the data
* @return The chunk data if it exists, otherwise null
*/
public ChunkData getChunkDataAtWorldPoint(Vector3i worldPos, int stride){
return this.getChunkDataAtWorldPoint(worldPos.x, worldPos.y, worldPos.z, stride);
}
/**
* Queues a terrain chunk to be pushed to GPU based on chunk data
* @param data The chunk data (triangles, normals, etc)
* @return The model path that is promised to eventually reflect the terrain model when it makes it to gpu
*/
public static String queueTerrainGridGeneration(TerrainChunkData data, VoxelTextureAtlas atlas){
String promisedHash = "";
UUID newUUID = UUID.randomUUID();
promisedHash = newUUID.toString();
TerrainChunkGenQueueItem queueItem = new TerrainChunkGenQueueItem(data, promisedHash, atlas);
lock.acquireUninterruptibly();
terrainChunkGenerationQueue.add(queueItem);
lock.release();
return promisedHash;
}
/**
* Pushes all terrain data in queue to the gpu and registers the resulting models
*/
public static void generateTerrainChunkGeometry(){
Globals.profiler.beginCpuSample("ClientTerrainManager.generateTerrainChunkGeometry");
lock.acquireUninterruptibly();
for(TerrainChunkGenQueueItem queueItem : terrainChunkGenerationQueue){
Model terrainModel = TerrainChunkModelGeneration.generateTerrainModel(queueItem.getData(), queueItem.getAtlas());
Globals.assetManager.registerModelToSpecificString(terrainModel, queueItem.getPromisedHash());
}
terrainChunkGenerationQueue.clear();
lock.release();
Globals.profiler.endCpuSample();
}
/**
* Gets all chunks in the terrain cache
* @return The collection of all chunk data objects
*/
public Collection<ChunkData> getAllChunks(){
return terrainCache.getAllChunks();
}
/**
* Gets the world position of a given chunk
* @param chunk The chunk
* @return The world position of the chunk
*/
public Vector3i getPositionOfChunk(ChunkData chunk){
return terrainCache.getChunkPosition(chunk);
}
/**
* Gets the key for a given request
* @param worldX The world x coordinate
* @param worldY The world y coordinate
* @param worldZ The world z coordinate
* @param stride The stride of the data
* @return The key
*/
private Long getRequestKey(int worldX, int worldY, int worldZ, int stride){
return (long)HashUtils.cantorHash(worldY, worldZ, worldZ);
}
}