Server-driven block rasterizer with LOD
All checks were successful
studiorailgun/Renderer/pipeline/head This commit looks good
All checks were successful
studiorailgun/Renderer/pipeline/head This commit looks good
This commit is contained in:
parent
749b9c63b4
commit
21e2ec7b06
@ -1149,6 +1149,7 @@ Refactoring server side of terrain management
|
||||
Add server manager for block chunk data & management
|
||||
Add endpoint to request strided block data
|
||||
Add client side handling of block endpoint
|
||||
Add server-driven block rasterizer with LOD
|
||||
|
||||
|
||||
# TODO
|
||||
|
||||
@ -44,6 +44,31 @@ public class BlockChunkData {
|
||||
*/
|
||||
public static final int LOD_FULL_RES = 0;
|
||||
|
||||
/**
|
||||
* Lod value for a half res chunk
|
||||
*/
|
||||
public static final int LOD_HALF_RES = 1;
|
||||
|
||||
/**
|
||||
* Lod value for a quarter res chunk
|
||||
*/
|
||||
public static final int LOD_QUARTER_RES = 2;
|
||||
|
||||
/**
|
||||
* Lod value for a eighth res chunk
|
||||
*/
|
||||
public static final int LOD_EIGHTH_RES = 3;
|
||||
|
||||
/**
|
||||
* Lod value for a sixteenth res chunk
|
||||
*/
|
||||
public static final int LOD_SIXTEENTH_RES = 4;
|
||||
|
||||
/**
|
||||
* Lod value for the lowest resolution possible
|
||||
*/
|
||||
public static final int LOD_LOWEST_RES = LOD_SIXTEENTH_RES;
|
||||
|
||||
|
||||
/**
|
||||
* The type of block at a given position
|
||||
@ -299,4 +324,12 @@ public class BlockChunkData {
|
||||
this.homogenousValue = homogenousValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the homogenous value
|
||||
* @param homogenousValue The homogenous value
|
||||
*/
|
||||
public void setHomogenousValue(int homogenousValue){
|
||||
this.setHomogenousValue((short)homogenousValue);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,315 @@
|
||||
package electrosphere.client.block.cells;
|
||||
|
||||
import org.joml.Quaterniond;
|
||||
import org.joml.Vector3d;
|
||||
import org.joml.Vector3i;
|
||||
|
||||
import electrosphere.client.block.BlockChunkData;
|
||||
import electrosphere.engine.Globals;
|
||||
import electrosphere.entity.ClientEntityUtils;
|
||||
import electrosphere.entity.Entity;
|
||||
import electrosphere.entity.btree.BehaviorTree;
|
||||
import electrosphere.entity.types.terrain.BlockChunkEntity;
|
||||
import electrosphere.server.terrain.manager.ServerTerrainChunk;
|
||||
import electrosphere.util.ds.octree.WorldOctTree.WorldOctTreeNode;
|
||||
import electrosphere.util.math.GeomUtils;
|
||||
|
||||
/**
|
||||
* A single drawcell - contains an entity that has a physics mesh and potentially graphics
|
||||
*/
|
||||
public class BlockDrawCell {
|
||||
|
||||
/**
|
||||
* Number of frames to wait before destroying the chunk entity
|
||||
*/
|
||||
public static final int FRAMES_TO_WAIT_BEFORE_DESTRUCTION = 25;
|
||||
|
||||
/**
|
||||
* Number of child cells per parent cell
|
||||
*/
|
||||
static final int CHILD_CELLS_PER_PARENT = 8;
|
||||
|
||||
//the position of the draw cell in world coordinates
|
||||
Vector3i worldPos;
|
||||
|
||||
/**
|
||||
* The LOD of the draw cell
|
||||
*/
|
||||
int lod;
|
||||
|
||||
//the main entity for the cell
|
||||
Entity modelEntity;
|
||||
|
||||
/**
|
||||
* The data for generating the visuals
|
||||
*/
|
||||
BlockChunkData chunkData;
|
||||
|
||||
/**
|
||||
* Tracks whether the draw cell has requested its chunk data or not
|
||||
*/
|
||||
boolean hasRequested = false;
|
||||
|
||||
/**
|
||||
* Tracks whether the draw cell has generated its entity or not
|
||||
*/
|
||||
boolean hasGenerated = false;
|
||||
|
||||
|
||||
/**
|
||||
* Tracks whether this draw cell is flagged as homogenous from the server or not
|
||||
*/
|
||||
boolean homogenous = false;
|
||||
|
||||
/**
|
||||
* Number of failed generation attempts
|
||||
*/
|
||||
int failedGenerationAttempts = 0;
|
||||
|
||||
/**
|
||||
* Labels an invalid distance cache
|
||||
*/
|
||||
static final int INVALID_DIST_CACHE = -1;
|
||||
|
||||
/**
|
||||
* The cached minimum distance
|
||||
*/
|
||||
double cachedMinDistance = -1;
|
||||
|
||||
/**
|
||||
* Target to notify on generation completion
|
||||
*/
|
||||
BlockDrawCell notifyTarget = null;
|
||||
|
||||
/**
|
||||
* The number of cells that have alerted this one
|
||||
*/
|
||||
int generationAlertCount = 0;
|
||||
|
||||
|
||||
/**
|
||||
* Private constructor
|
||||
*/
|
||||
private BlockDrawCell(){
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Constructs a drawcell object
|
||||
*/
|
||||
public static BlockDrawCell generateBlockCell(
|
||||
Vector3i worldPos,
|
||||
int lod
|
||||
){
|
||||
BlockDrawCell rVal = new BlockDrawCell();
|
||||
rVal.lod = lod;
|
||||
rVal.worldPos = worldPos;
|
||||
return rVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a drawable entity based on this chunk
|
||||
*/
|
||||
public void generateDrawableEntity(BlockTextureAtlas atlas, int lod){
|
||||
boolean success = true;
|
||||
if(chunkData == null){
|
||||
BlockChunkData currentChunk = Globals.clientBlockManager.getChunkDataAtWorldPoint(
|
||||
worldPos.x,
|
||||
worldPos.y,
|
||||
worldPos.z,
|
||||
lod
|
||||
);
|
||||
if(currentChunk == null){
|
||||
success = false;
|
||||
} else {
|
||||
this.homogenous = currentChunk.getHomogenousValue() != BlockChunkData.NOT_HOMOGENOUS;
|
||||
success = true;
|
||||
}
|
||||
if(!success){
|
||||
this.setFailedGenerationAttempts(this.getFailedGenerationAttempts() + 1);
|
||||
return;
|
||||
}
|
||||
this.chunkData = currentChunk;
|
||||
}
|
||||
Entity toDelete = this.modelEntity;
|
||||
modelEntity = BlockChunkEntity.clientCreateBlockChunkEntity(chunkData, notifyTarget, toDelete, lod, atlas, this.hasPolygons());
|
||||
ClientEntityUtils.initiallyPositionEntity(modelEntity, this.getRealPos(), new Quaterniond());
|
||||
this.setHasGenerated(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the real-space position of the draw cell
|
||||
* @return the real-space position
|
||||
*/
|
||||
protected Vector3d getRealPos(){
|
||||
return new Vector3d(
|
||||
worldPos.x * ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET,
|
||||
worldPos.y * ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET,
|
||||
worldPos.z * ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the world-space position of the draw cell
|
||||
* @return the world-space position
|
||||
*/
|
||||
protected Vector3i getWorldPos(){
|
||||
return new Vector3i(worldPos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a target draw cell to notify once this one has completed generating its model
|
||||
* @param notifyTarget The target to notify
|
||||
*/
|
||||
public void registerNotificationTarget(BlockDrawCell notifyTarget){
|
||||
this.notifyTarget = notifyTarget;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alerts this draw cell that a child it is waiting on has generated
|
||||
*/
|
||||
public void alertToGeneration(){
|
||||
this.generationAlertCount++;
|
||||
if(this.generationAlertCount >= CHILD_CELLS_PER_PARENT){
|
||||
this.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys a drawcell including its physics
|
||||
*/
|
||||
public void destroy(){
|
||||
if(modelEntity != null){
|
||||
Globals.clientScene.registerBehaviorTree(new BehaviorTree(){
|
||||
int framesSimulated = 0;
|
||||
public void simulate(float deltaTime) {
|
||||
if(framesSimulated < FRAMES_TO_WAIT_BEFORE_DESTRUCTION){
|
||||
framesSimulated++;
|
||||
} else {
|
||||
ClientEntityUtils.destroyEntity(modelEntity);
|
||||
Globals.clientScene.deregisterBehaviorTree(this);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the entity for the cell
|
||||
* @return The entity if it exists, null otherwise
|
||||
*/
|
||||
public Entity getEntity(){
|
||||
return modelEntity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfers chunk data from the source to this draw cell
|
||||
* @param source The source draw cell
|
||||
*/
|
||||
public void transferChunkData(BlockDrawCell source){
|
||||
this.chunkData = source.chunkData;
|
||||
this.homogenous = source.homogenous;
|
||||
this.hasRequested = source.hasRequested;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether this draw cell has requested its chunk data or not
|
||||
* @return true if has requested, false otherwise
|
||||
*/
|
||||
public boolean hasRequested() {
|
||||
return hasRequested;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether this draw cell has requested its chunk data or not
|
||||
* @param hasRequested true if has requested, false otherwise
|
||||
*/
|
||||
public void setHasRequested(boolean hasRequested) {
|
||||
this.hasRequested = hasRequested;
|
||||
if(!this.hasRequested){
|
||||
this.failedGenerationAttempts = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether this draw cell has generated its entity or not
|
||||
* @return true if has generated, false otherwise
|
||||
*/
|
||||
public boolean hasGenerated() {
|
||||
return hasGenerated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether this draw cell has generated its entity or not
|
||||
* @param hasGenerated true if has generated, false otherwise
|
||||
*/
|
||||
public void setHasGenerated(boolean hasGenerated) {
|
||||
this.hasGenerated = hasGenerated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether this draw cell is homogenous or not
|
||||
* @param hasGenerated true if is homogenous, false otherwise
|
||||
*/
|
||||
public void setHomogenous(boolean homogenous) {
|
||||
this.homogenous = homogenous;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether this draw cell will generate polygons or not
|
||||
* @return true if it has polygons, false otherwise
|
||||
*/
|
||||
private boolean hasPolygons(){
|
||||
return !this.homogenous;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of failed generation attempts
|
||||
* @return The number of failed generation attempts
|
||||
*/
|
||||
public int getFailedGenerationAttempts(){
|
||||
return failedGenerationAttempts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the number of failed generation attempts
|
||||
* @param attempts The number of failed generation attempts
|
||||
*/
|
||||
public void setFailedGenerationAttempts(int attempts){
|
||||
this.failedGenerationAttempts = this.failedGenerationAttempts + attempts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ejects the chunk data
|
||||
*/
|
||||
public void ejectChunkData(){
|
||||
this.chunkData = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether this draw cell is homogenous or not
|
||||
* @return true if it is homogenous, false otherwise
|
||||
*/
|
||||
public boolean isHomogenous(){
|
||||
return homogenous;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the minimum distance from a node to a point
|
||||
* @param pos the position to check against
|
||||
* @param node the node
|
||||
* @param distCache the lod value under which distance caches are invalidated
|
||||
* @return the distance
|
||||
*/
|
||||
public double getMinDistance(Vector3i worldPos, WorldOctTreeNode<BlockDrawCell> node, int distCache){
|
||||
if(cachedMinDistance != INVALID_DIST_CACHE && distCache < lod){
|
||||
return cachedMinDistance;
|
||||
} else {
|
||||
this.cachedMinDistance = GeomUtils.getMinSquaredDistanceAABB(worldPos, node.getMinBound(), node.getMaxBound());
|
||||
return cachedMinDistance;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,80 @@
|
||||
package electrosphere.client.block.cells;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import electrosphere.renderer.texture.Texture;
|
||||
|
||||
/**
|
||||
* An atlas texture and accompanying map of all voxel textures
|
||||
*/
|
||||
public class BlockTextureAtlas {
|
||||
|
||||
//A map of voxel id -> coordinates in the atlas texture for its texture
|
||||
Map<Integer,Integer> typeCoordMap = new HashMap<Integer,Integer>();
|
||||
|
||||
|
||||
//the actual texture
|
||||
Texture specular;
|
||||
|
||||
//the normal texture
|
||||
Texture normal;
|
||||
|
||||
//the width in pixels of a single texture in the atlas
|
||||
public static final int ATLAS_ELEMENT_DIM = 256;
|
||||
//the width in pixels of the whole atlas texture
|
||||
public static final int ATLAS_DIM = 8192;
|
||||
//number of textures per row in the atlas
|
||||
public static final int ELEMENTS_PER_ROW = ATLAS_DIM / ATLAS_ELEMENT_DIM;
|
||||
|
||||
/**
|
||||
* Puts an entry in the type-coord map to map a voxel type to a position
|
||||
* @param type the voxel type
|
||||
* @param coord the coordinate in the map
|
||||
*/
|
||||
public void putTypeCoord(int type, int coord){
|
||||
typeCoordMap.put(type,coord);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the specular
|
||||
* @param specular the specular
|
||||
*/
|
||||
public void setSpecular(Texture specular){
|
||||
this.specular = specular;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the normal
|
||||
* @param normal the normal
|
||||
*/
|
||||
public void setNormal(Texture normal){
|
||||
this.normal = normal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the atlas specular
|
||||
* @return the atlas specular
|
||||
*/
|
||||
public Texture getSpecular(){
|
||||
return specular;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the atlas normal
|
||||
* @return the atlas normal
|
||||
*/
|
||||
public Texture getNormal(){
|
||||
return normal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the index in the atlas of a provided voxel type (the voxel type is provided by its id)
|
||||
* @param voxelTypeId The id of the voxel type
|
||||
* @return the index in the atlas of the texture of the provided voxel type
|
||||
*/
|
||||
public int getVoxelTypeOffset(int voxelTypeId){
|
||||
return typeCoordMap.containsKey(voxelTypeId) ? typeCoordMap.get(voxelTypeId) : -1;
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,838 @@
|
||||
package electrosphere.client.block.cells;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.joml.Vector3d;
|
||||
import org.joml.Vector3i;
|
||||
|
||||
import electrosphere.client.block.BlockChunkData;
|
||||
import electrosphere.collision.PhysicsEntityUtils;
|
||||
import electrosphere.engine.Globals;
|
||||
import electrosphere.entity.EntityUtils;
|
||||
import electrosphere.logger.LoggerInterface;
|
||||
import electrosphere.util.ds.octree.WorldOctTree;
|
||||
import electrosphere.util.ds.octree.WorldOctTree.WorldOctTreeNode;
|
||||
import electrosphere.util.math.GeomUtils;
|
||||
|
||||
/**
|
||||
* Manages draw cells on the client
|
||||
*/
|
||||
public class ClientBlockCellManager {
|
||||
|
||||
/**
|
||||
* Number of times to try updating per frame. Lower this to reduce lag but slow down block mesh generation.
|
||||
*/
|
||||
static final int UPDATE_ATTEMPTS_PER_FRAME = 3;
|
||||
|
||||
/**
|
||||
* The number of generation attempts before a cell is marked as having not requested its data
|
||||
*/
|
||||
static final int FAILED_GENERATION_ATTEMPT_THRESHOLD = 250;
|
||||
|
||||
/**
|
||||
* The distance to draw at full resolution
|
||||
*/
|
||||
public static final double FULL_RES_DIST = 8 * 8;
|
||||
|
||||
/**
|
||||
* The distance for half resolution
|
||||
*/
|
||||
public static final double HALF_RES_DIST = 16 * 16;
|
||||
|
||||
/**
|
||||
* The distance for quarter resolution
|
||||
*/
|
||||
public static final double QUARTER_RES_DIST = 20 * 20;
|
||||
|
||||
/**
|
||||
* The distance for eighth resolution
|
||||
*/
|
||||
public static final double EIGHTH_RES_DIST = 32 * 32;
|
||||
|
||||
/**
|
||||
* The distance for sixteenth resolution
|
||||
*/
|
||||
public static final double SIXTEENTH_RES_DIST = 64 * 64;
|
||||
|
||||
/**
|
||||
* The octree holding all the chunks to evaluate
|
||||
*/
|
||||
WorldOctTree<BlockDrawCell> chunkTree;
|
||||
|
||||
/**
|
||||
* Tracks what nodes have been evaluated this frame -- used to deduplicate evaluation calls
|
||||
*/
|
||||
Map<WorldOctTreeNode<BlockDrawCell>,Boolean> evaluationMap = new HashMap<WorldOctTreeNode<BlockDrawCell>,Boolean>();
|
||||
|
||||
/**
|
||||
* The last recorded player world position
|
||||
*/
|
||||
Vector3i lastPlayerPos = new Vector3i();
|
||||
|
||||
/**
|
||||
* Tracks whether the cell manager updated last frame or not
|
||||
*/
|
||||
boolean updatedLastFrame = true;
|
||||
|
||||
/**
|
||||
* Controls whether the client draw cell manager should update or not
|
||||
*/
|
||||
boolean shouldUpdate = true;
|
||||
|
||||
/**
|
||||
* The voxel texture atlas
|
||||
*/
|
||||
BlockTextureAtlas textureAtlas;
|
||||
|
||||
/**
|
||||
* The dimensions of the world
|
||||
*/
|
||||
int worldDim = 0;
|
||||
|
||||
/**
|
||||
* Tracks the number of currently valid cells (ie didn't require an update this frame)
|
||||
*/
|
||||
int validCellCount = 0;
|
||||
|
||||
/**
|
||||
* The number of maximum resolution chunks
|
||||
*/
|
||||
int maxResCount = 0;
|
||||
|
||||
/**
|
||||
* The number of half resolution chunks
|
||||
*/
|
||||
int halfResCount = 0;
|
||||
|
||||
/**
|
||||
* The number of generated chunks
|
||||
*/
|
||||
int generated = 0;
|
||||
|
||||
/**
|
||||
* Tracks whether the cell manager has initialized or not
|
||||
*/
|
||||
boolean initialized = false;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param voxelTextureAtlas The voxel texture atlas
|
||||
* @param worldDim The size of the world in chunks
|
||||
*/
|
||||
public ClientBlockCellManager(BlockTextureAtlas voxelTextureAtlas, int worldDim){
|
||||
this.chunkTree = new WorldOctTree<BlockDrawCell>(
|
||||
new Vector3i(0,0,0),
|
||||
new Vector3i(worldDim, worldDim, worldDim)
|
||||
);
|
||||
this.chunkTree.getRoot().setData(BlockDrawCell.generateBlockCell(new Vector3i(0,0,0), chunkTree.getMaxLevel()));
|
||||
this.worldDim = worldDim;
|
||||
this.textureAtlas = voxelTextureAtlas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates all cells in the chunk
|
||||
*/
|
||||
public void update(){
|
||||
Globals.profiler.beginCpuSample("ClientDrawCellManager.update");
|
||||
if(shouldUpdate && Globals.playerEntity != null){
|
||||
Vector3d playerPos = EntityUtils.getPosition(Globals.playerEntity);
|
||||
Vector3i playerWorldPos = Globals.clientWorldData.convertRealToWorldSpace(playerPos);
|
||||
int distCache = this.getDistCache(this.lastPlayerPos, playerWorldPos);
|
||||
this.lastPlayerPos.set(playerWorldPos);
|
||||
//the sets to iterate through
|
||||
updatedLastFrame = true;
|
||||
validCellCount = 0;
|
||||
evaluationMap.clear();
|
||||
//update all full res cells
|
||||
WorldOctTreeNode<BlockDrawCell> rootNode = this.chunkTree.getRoot();
|
||||
Globals.profiler.beginCpuSample("ClientDrawCellManager.update - full res cells");
|
||||
updatedLastFrame = this.recursivelyUpdateCells(rootNode, playerWorldPos, evaluationMap, BlockChunkData.LOD_LOWEST_RES, distCache);
|
||||
Globals.profiler.endCpuSample();
|
||||
if(!updatedLastFrame && !this.initialized){
|
||||
this.initialized = true;
|
||||
}
|
||||
}
|
||||
Globals.profiler.endCpuSample();
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively update child nodes
|
||||
* @param node The root node
|
||||
* @param playerPos The player's position
|
||||
* @param minLeafLod The minimum LOD required to evaluate a leaf
|
||||
* @param evaluationMap Map of leaf nodes that have been evaluated this frame
|
||||
* @return true if there is work remaining to be done, false otherwise
|
||||
*/
|
||||
private boolean recursivelyUpdateCells(WorldOctTreeNode<BlockDrawCell> node, Vector3i playerPos, Map<WorldOctTreeNode<BlockDrawCell>,Boolean> evaluationMap, int minLeafLod, int distCache){
|
||||
boolean updated = false;
|
||||
if(evaluationMap.containsKey(node)){
|
||||
return false;
|
||||
}
|
||||
if(node.getData().hasGenerated() && node.getData().isHomogenous()){
|
||||
return false;
|
||||
}
|
||||
if(node.isLeaf()){
|
||||
if(this.shouldSplit(playerPos, node, distCache)){
|
||||
Globals.profiler.beginCpuSample("ClientDrawCellManager.split");
|
||||
//perform op
|
||||
WorldOctTreeNode<BlockDrawCell> container = chunkTree.split(node);
|
||||
BlockDrawCell containerCell = BlockDrawCell.generateBlockCell(container.getMinBound(), this.chunkTree.getMaxLevel() - container.getLevel());
|
||||
container.setData(containerCell);
|
||||
container.getData().transferChunkData(node.getData());
|
||||
|
||||
//do creations
|
||||
container.getChildren().forEach(child -> {
|
||||
Vector3i cellWorldPos = new Vector3i(
|
||||
child.getMinBound().x,
|
||||
child.getMinBound().y,
|
||||
child.getMinBound().z
|
||||
);
|
||||
BlockDrawCell drawCell = BlockDrawCell.generateBlockCell(cellWorldPos,this.chunkTree.getMaxLevel() - child.getLevel());
|
||||
drawCell.registerNotificationTarget(node.getData());
|
||||
child.setLeaf(true);
|
||||
child.setData(drawCell);
|
||||
evaluationMap.put(child,true);
|
||||
});
|
||||
|
||||
//do deletions
|
||||
this.twoLayerDestroy(node);
|
||||
|
||||
//update neighbors
|
||||
this.conditionalUpdateAdjacentNodes(container, container.getChildren().get(0).getLevel());
|
||||
|
||||
Globals.profiler.endCpuSample();
|
||||
updated = true;
|
||||
} else if(this.shouldRequest(playerPos, node, minLeafLod, distCache)){
|
||||
Globals.profiler.beginCpuSample("ClientDrawCellManager.request");
|
||||
|
||||
//calculate what to request
|
||||
BlockDrawCell cell = node.getData();
|
||||
//actually send requests
|
||||
if(this.requestChunks(node)){
|
||||
cell.setHasRequested(true);
|
||||
}
|
||||
evaluationMap.put(node,true);
|
||||
|
||||
Globals.profiler.endCpuSample();
|
||||
updated = true;
|
||||
} else if(this.shouldGenerate(playerPos, node, minLeafLod, distCache)){
|
||||
Globals.profiler.beginCpuSample("ClientDrawCellManager.generate");
|
||||
int lodLevel = this.getLODLevel(node);
|
||||
|
||||
if(this.containsDataToGenerate(node)){
|
||||
node.getData().generateDrawableEntity(textureAtlas, lodLevel);
|
||||
if(node.getData().getFailedGenerationAttempts() > FAILED_GENERATION_ATTEMPT_THRESHOLD){
|
||||
node.getData().setHasRequested(false);
|
||||
}
|
||||
} else if(node.getData() != null){
|
||||
node.getData().setFailedGenerationAttempts(node.getData().getFailedGenerationAttempts() + 1);
|
||||
if(node.getData().getFailedGenerationAttempts() > FAILED_GENERATION_ATTEMPT_THRESHOLD){
|
||||
node.getData().setHasRequested(false);
|
||||
}
|
||||
}
|
||||
evaluationMap.put(node,true);
|
||||
Globals.profiler.endCpuSample();
|
||||
updated = true;
|
||||
}
|
||||
} else {
|
||||
if(this.shouldJoin(playerPos, node, distCache)) {
|
||||
this.join(node);
|
||||
updated = true;
|
||||
} else {
|
||||
this.validCellCount++;
|
||||
List<WorldOctTreeNode<BlockDrawCell>> children = node.getChildren();
|
||||
boolean isHomogenous = true;
|
||||
boolean fullyGenerated = true;
|
||||
for(int i = 0; i < 8; i++){
|
||||
WorldOctTreeNode<BlockDrawCell> child = children.get(i);
|
||||
boolean childUpdate = this.recursivelyUpdateCells(child, playerPos, evaluationMap, minLeafLod, distCache);
|
||||
if(childUpdate == true){
|
||||
updated = true;
|
||||
}
|
||||
if(!child.getData().hasGenerated()){
|
||||
fullyGenerated = false;
|
||||
}
|
||||
if(!child.getData().isHomogenous()){
|
||||
isHomogenous = false;
|
||||
}
|
||||
}
|
||||
WorldOctTreeNode<BlockDrawCell> newNode = null;
|
||||
if(isHomogenous){
|
||||
newNode = this.join(node);
|
||||
newNode.getData().setHomogenous(true);
|
||||
}
|
||||
if(fullyGenerated && newNode != null){
|
||||
newNode.getData().setHasGenerated(true);
|
||||
}
|
||||
if((this.chunkTree.getMaxLevel() - node.getLevel()) < minLeafLod){
|
||||
evaluationMap.put(node,true);
|
||||
}
|
||||
}
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the minimum distance from a node to a point
|
||||
* @param pos the position to check against
|
||||
* @param node the node
|
||||
* @return the distance
|
||||
*/
|
||||
public double getMinDistance(Vector3i worldPos, WorldOctTreeNode<BlockDrawCell> node, int distCache){
|
||||
if(node.getData() == null){
|
||||
return GeomUtils.getMinSquaredDistanceAABB(worldPos, node.getMinBound(), node.getMaxBound());
|
||||
} else {
|
||||
return node.getData().getMinDistance(worldPos, node, distCache);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the distance cache value
|
||||
* @param lastPlayerPos The last player world position
|
||||
* @param currentPlayerPos The current player world position
|
||||
* @return The distance cache value
|
||||
*/
|
||||
private int getDistCache(Vector3i lastPlayerPos, Vector3i currentPlayerPos){
|
||||
if(
|
||||
lastPlayerPos.x / 16 != currentPlayerPos.x / 16 || lastPlayerPos.z / 16 != currentPlayerPos.z / 16 || lastPlayerPos.z / 16 != currentPlayerPos.z / 16
|
||||
){
|
||||
return this.chunkTree.getMaxLevel();
|
||||
}
|
||||
if(
|
||||
lastPlayerPos.x / 16 != currentPlayerPos.x / 16 || lastPlayerPos.z / 16 != currentPlayerPos.z / 16 || lastPlayerPos.z / 16 != currentPlayerPos.z / 16
|
||||
){
|
||||
return BlockChunkData.LOD_SIXTEENTH_RES + 2;
|
||||
}
|
||||
if(
|
||||
lastPlayerPos.x / 8 != currentPlayerPos.x / 8 || lastPlayerPos.z / 8 != currentPlayerPos.z / 8 || lastPlayerPos.z / 8 != currentPlayerPos.z / 8
|
||||
){
|
||||
return BlockChunkData.LOD_SIXTEENTH_RES + 1;
|
||||
}
|
||||
if(
|
||||
lastPlayerPos.x / 4 != currentPlayerPos.x / 4 || lastPlayerPos.z / 4 != currentPlayerPos.z / 4 || lastPlayerPos.z / 4 != currentPlayerPos.z / 4
|
||||
){
|
||||
return BlockChunkData.LOD_SIXTEENTH_RES;
|
||||
}
|
||||
if(
|
||||
lastPlayerPos.x / 2 != currentPlayerPos.x / 2 || lastPlayerPos.z / 2 != currentPlayerPos.z / 2 || lastPlayerPos.z / 2 != currentPlayerPos.z / 2
|
||||
){
|
||||
return BlockChunkData.LOD_EIGHTH_RES;
|
||||
}
|
||||
if(
|
||||
lastPlayerPos.x != currentPlayerPos.x || lastPlayerPos.z != currentPlayerPos.z || lastPlayerPos.z != currentPlayerPos.z
|
||||
){
|
||||
return BlockChunkData.LOD_QUARTER_RES;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether this should be split or not
|
||||
* @param pos the player position
|
||||
* @param node The node
|
||||
* @return true if should split, false otherwise
|
||||
*/
|
||||
public boolean shouldSplit(Vector3i pos, WorldOctTreeNode<BlockDrawCell> node, int distCache){
|
||||
//breaking out into dedicated function so can add case handling ie if we want
|
||||
//to combine fullres nodes into larger nodes to conserve on draw calls
|
||||
return
|
||||
node.canSplit() &&
|
||||
(node.getLevel() != this.chunkTree.getMaxLevel()) &&
|
||||
!node.getData().isHomogenous() &&
|
||||
(node.getParent() != null || node == this.chunkTree.getRoot()) &&
|
||||
(
|
||||
(
|
||||
node.getLevel() < this.chunkTree.getMaxLevel() - BlockChunkData.LOD_SIXTEENTH_RES &&
|
||||
this.getMinDistance(pos, node, distCache) <= SIXTEENTH_RES_DIST
|
||||
)
|
||||
||
|
||||
(
|
||||
node.getLevel() < this.chunkTree.getMaxLevel() - BlockChunkData.LOD_EIGHTH_RES &&
|
||||
this.getMinDistance(pos, node, distCache) <= EIGHTH_RES_DIST
|
||||
)
|
||||
||
|
||||
(
|
||||
node.getLevel() < this.chunkTree.getMaxLevel() - BlockChunkData.LOD_QUARTER_RES &&
|
||||
this.getMinDistance(pos, node, distCache) <= QUARTER_RES_DIST
|
||||
)
|
||||
||
|
||||
(
|
||||
node.getLevel() < this.chunkTree.getMaxLevel() - BlockChunkData.LOD_HALF_RES &&
|
||||
this.getMinDistance(pos, node, distCache) <= HALF_RES_DIST
|
||||
)
|
||||
||
|
||||
(
|
||||
node.getLevel() < this.chunkTree.getMaxLevel() &&
|
||||
this.getMinDistance(pos, node, distCache) <= FULL_RES_DIST
|
||||
)
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the LOD level of the draw cell
|
||||
* @param node The node to consider
|
||||
* @return -1 if outside of render range, -1 if the node is not a valid draw cell leaf, otherwise returns the LOD level
|
||||
*/
|
||||
private int getLODLevel(WorldOctTreeNode<BlockDrawCell> node){
|
||||
return this.chunkTree.getMaxLevel() - node.getLevel();
|
||||
}
|
||||
|
||||
/**
|
||||
* Conditionally updates all adjacent nodes if their level would require transition cells in the voxel rasterization
|
||||
* @param node The node to search from adjacencies from
|
||||
* @param level The level to check against
|
||||
*/
|
||||
private void conditionalUpdateAdjacentNodes(WorldOctTreeNode<BlockDrawCell> node, int level){
|
||||
//don't bother to check if it's a lowest-res chunk
|
||||
if(this.chunkTree.getMaxLevel() - level > BlockChunkData.LOD_FULL_RES){
|
||||
return;
|
||||
}
|
||||
if(node.getMinBound().x - 1 >= 0){
|
||||
WorldOctTreeNode<BlockDrawCell> xNegNode = this.chunkTree.search(new Vector3i(node.getMinBound()).add(-1,0,0), false);
|
||||
if(xNegNode != null && xNegNode.getLevel() < level){
|
||||
xNegNode.getData().setHasGenerated(false);
|
||||
}
|
||||
}
|
||||
if(node.getMinBound().y - 1 >= 0){
|
||||
WorldOctTreeNode<BlockDrawCell> yNegNode = this.chunkTree.search(new Vector3i(node.getMinBound()).add(0,-1,0), false);
|
||||
if(yNegNode != null && yNegNode.getLevel() < level){
|
||||
yNegNode.getData().setHasGenerated(false);
|
||||
}
|
||||
}
|
||||
if(node.getMinBound().z - 1 >= 0){
|
||||
WorldOctTreeNode<BlockDrawCell> zNegNode = this.chunkTree.search(new Vector3i(node.getMinBound()).add(0,0,-1), false);
|
||||
if(zNegNode != null && zNegNode.getLevel() < level){
|
||||
zNegNode.getData().setHasGenerated(false);
|
||||
}
|
||||
}
|
||||
if(node.getMaxBound().x + 1 < this.worldDim){
|
||||
WorldOctTreeNode<BlockDrawCell> xPosNode = this.chunkTree.search(new Vector3i(node.getMaxBound()).add(1,-1,-1), false);
|
||||
if(xPosNode != null && xPosNode.getLevel() < level){
|
||||
xPosNode.getData().setHasGenerated(false);
|
||||
}
|
||||
}
|
||||
if(node.getMaxBound().y + 1 < this.worldDim){
|
||||
WorldOctTreeNode<BlockDrawCell> yPosNode = this.chunkTree.search(new Vector3i(node.getMaxBound()).add(-1,1,-1), false);
|
||||
if(yPosNode != null && yPosNode.getLevel() < level){
|
||||
yPosNode.getData().setHasGenerated(false);
|
||||
}
|
||||
}
|
||||
if(node.getMaxBound().z + 1 < this.worldDim){
|
||||
WorldOctTreeNode<BlockDrawCell> zPosNode = this.chunkTree.search(new Vector3i(node.getMaxBound()).add(-1,-1,1), false);
|
||||
if(zPosNode != null && zPosNode.getLevel() < level){
|
||||
zPosNode.getData().setHasGenerated(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether this should be joined or not
|
||||
* @param pos the player position
|
||||
* @param node The node
|
||||
* @return true if should be joined, false otherwise
|
||||
*/
|
||||
public boolean shouldJoin(Vector3i pos, WorldOctTreeNode<BlockDrawCell> node, int distCache){
|
||||
//breaking out into dedicated function so can add case handling ie if we want
|
||||
//to combine fullres nodes into larger nodes to conserve on draw calls
|
||||
return
|
||||
node.getLevel() > 0 &&
|
||||
(node.getLevel() != this.chunkTree.getMaxLevel()) &&
|
||||
(
|
||||
(
|
||||
node.getLevel() == this.chunkTree.getMaxLevel() - BlockChunkData.LOD_HALF_RES &&
|
||||
this.getMinDistance(pos, node, distCache) > FULL_RES_DIST
|
||||
)
|
||||
||
|
||||
(
|
||||
node.getLevel() == this.chunkTree.getMaxLevel() - BlockChunkData.LOD_QUARTER_RES &&
|
||||
this.getMinDistance(pos, node, distCache) > HALF_RES_DIST
|
||||
)
|
||||
||
|
||||
(
|
||||
node.getLevel() == this.chunkTree.getMaxLevel() - BlockChunkData.LOD_EIGHTH_RES &&
|
||||
this.getMinDistance(pos, node, distCache) > QUARTER_RES_DIST
|
||||
)
|
||||
||
|
||||
(
|
||||
node.getLevel() == this.chunkTree.getMaxLevel() - BlockChunkData.LOD_SIXTEENTH_RES &&
|
||||
this.getMinDistance(pos, node, distCache) > EIGHTH_RES_DIST
|
||||
)
|
||||
||
|
||||
(
|
||||
this.getMinDistance(pos, node, distCache) > SIXTEENTH_RES_DIST
|
||||
)
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins a parent node
|
||||
* @param node The parent node
|
||||
*/
|
||||
private WorldOctTreeNode<BlockDrawCell> join(WorldOctTreeNode<BlockDrawCell> node){
|
||||
Globals.profiler.beginCpuSample("ClientDrawCellManager.join");
|
||||
|
||||
//queue destructions prior to join -- the join operator clears all children on node
|
||||
this.recursivelyDestroy(node);
|
||||
|
||||
//perform op
|
||||
BlockDrawCell newLeafCell = BlockDrawCell.generateBlockCell(node.getMinBound(),node.getData().lod);
|
||||
WorldOctTreeNode<BlockDrawCell> newLeaf = chunkTree.join(node, newLeafCell);
|
||||
newLeaf.getData().transferChunkData(node.getData());
|
||||
|
||||
//update neighbors
|
||||
this.conditionalUpdateAdjacentNodes(newLeaf, newLeaf.getLevel());
|
||||
evaluationMap.put(newLeaf,true);
|
||||
|
||||
Globals.profiler.endCpuSample();
|
||||
return newLeaf;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this cell should request chunk data
|
||||
* @param pos the player's position
|
||||
* @param node the node
|
||||
* @param minLeafLod The minimum LOD required to evaluate a leaf
|
||||
* @return true if should request chunk data, false otherwise
|
||||
*/
|
||||
public boolean shouldRequest(Vector3i pos, WorldOctTreeNode<BlockDrawCell> node, int minLeafLod, int distCache){
|
||||
return
|
||||
node.getData() != null &&
|
||||
!node.getData().hasRequested() &&
|
||||
(this.chunkTree.getMaxLevel() - node.getLevel()) <= minLeafLod &&
|
||||
(
|
||||
(
|
||||
node.getLevel() == this.chunkTree.getMaxLevel()
|
||||
// &&
|
||||
// this.getMinDistance(pos, node) <= FULL_RES_DIST
|
||||
)
|
||||
||
|
||||
(
|
||||
node.getLevel() == this.chunkTree.getMaxLevel() - BlockChunkData.LOD_HALF_RES
|
||||
&&
|
||||
this.getMinDistance(pos, node, distCache) <= QUARTER_RES_DIST
|
||||
)
|
||||
||
|
||||
(
|
||||
node.getLevel() == this.chunkTree.getMaxLevel() - BlockChunkData.LOD_QUARTER_RES
|
||||
&&
|
||||
this.getMinDistance(pos, node, distCache) <= EIGHTH_RES_DIST
|
||||
)
|
||||
||
|
||||
(
|
||||
node.getLevel() == this.chunkTree.getMaxLevel() - BlockChunkData.LOD_EIGHTH_RES
|
||||
&&
|
||||
this.getMinDistance(pos, node, distCache) <= SIXTEENTH_RES_DIST
|
||||
)
|
||||
||
|
||||
(
|
||||
node.getLevel() == this.chunkTree.getMaxLevel() - BlockChunkData.LOD_SIXTEENTH_RES
|
||||
&&
|
||||
this.getMinDistance(pos, node, distCache) <= SIXTEENTH_RES_DIST
|
||||
)
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this cell should generate
|
||||
* @param pos the player's position
|
||||
* @param node the node
|
||||
* @param minLeafLod The minimum LOD required to evaluate a leaf
|
||||
* @return true if should generate, false otherwise
|
||||
*/
|
||||
public boolean shouldGenerate(Vector3i pos, WorldOctTreeNode<BlockDrawCell> node, int minLeafLod, int distCache){
|
||||
return
|
||||
!node.getData().hasGenerated() &&
|
||||
(this.chunkTree.getMaxLevel() - node.getLevel()) <= minLeafLod &&
|
||||
(
|
||||
(
|
||||
node.getLevel() == this.chunkTree.getMaxLevel()
|
||||
// &&
|
||||
// this.getMinDistance(pos, node) <= FULL_RES_DIST
|
||||
)
|
||||
||
|
||||
(
|
||||
node.getLevel() == this.chunkTree.getMaxLevel() - BlockChunkData.LOD_HALF_RES
|
||||
&&
|
||||
this.getMinDistance(pos, node, distCache) <= QUARTER_RES_DIST
|
||||
)
|
||||
||
|
||||
(
|
||||
node.getLevel() == this.chunkTree.getMaxLevel() - BlockChunkData.LOD_QUARTER_RES
|
||||
&&
|
||||
this.getMinDistance(pos, node, distCache) <= EIGHTH_RES_DIST
|
||||
)
|
||||
||
|
||||
(
|
||||
node.getLevel() == this.chunkTree.getMaxLevel() - BlockChunkData.LOD_EIGHTH_RES
|
||||
&&
|
||||
this.getMinDistance(pos, node, distCache) <= SIXTEENTH_RES_DIST
|
||||
)
|
||||
||
|
||||
(
|
||||
node.getLevel() == this.chunkTree.getMaxLevel() - BlockChunkData.LOD_SIXTEENTH_RES
|
||||
&&
|
||||
this.getMinDistance(pos, node, distCache) <= SIXTEENTH_RES_DIST
|
||||
)
|
||||
)
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the node should have destroy called on it
|
||||
* @param node The node
|
||||
* @return true if should destroy, false otherwise
|
||||
*/
|
||||
public boolean shouldDestroy(WorldOctTreeNode<BlockDrawCell> node){
|
||||
return
|
||||
node.getData() != null &&
|
||||
node.getData().getEntity() != null
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys the foliage chunk
|
||||
*/
|
||||
protected void destroy(){
|
||||
this.recursivelyDestroy(this.chunkTree.getRoot());
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively destroy a tree
|
||||
* @param node The root of the tree
|
||||
*/
|
||||
private void recursivelyDestroy(WorldOctTreeNode<BlockDrawCell> node){
|
||||
if(node.getChildren().size() > 0){
|
||||
for(WorldOctTreeNode<BlockDrawCell> child : node.getChildren()){
|
||||
child.getData().destroy();
|
||||
}
|
||||
// node.getChildren().forEach(child -> recursivelyDestroy(child));
|
||||
}
|
||||
if(node.getData() != null){
|
||||
node.getData().destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys two layers of nodes
|
||||
* @param node The top node
|
||||
*/
|
||||
private void twoLayerDestroy(WorldOctTreeNode<BlockDrawCell> node){
|
||||
if(!node.getData().hasGenerated()){
|
||||
for(WorldOctTreeNode<BlockDrawCell> child : node.getChildren()){
|
||||
child.getData().destroy();
|
||||
}
|
||||
} else {
|
||||
node.getData().destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the cell manager made an update last frame or not
|
||||
* @return true if an update occurred, false otherwise
|
||||
*/
|
||||
public boolean updatedLastFrame(){
|
||||
return this.updatedLastFrame;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the position is within the full LOD range
|
||||
* @param worldPos The world position
|
||||
* @return true if within full LOD range, false otherwise
|
||||
*/
|
||||
public boolean isFullLOD(Vector3i worldPos){
|
||||
Vector3d playerRealPos = EntityUtils.getPosition(Globals.playerEntity);
|
||||
Vector3d chunkMin = Globals.clientWorldData.convertWorldToRealSpace(worldPos);
|
||||
Vector3d chunkMax = Globals.clientWorldData.convertWorldToRealSpace(new Vector3i(worldPos).add(1,1,1));
|
||||
return GeomUtils.getMinDistanceAABB(playerRealPos, chunkMin, chunkMax) <= FULL_RES_DIST;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evicts all cells
|
||||
*/
|
||||
public void evictAll(){
|
||||
this.recursivelyDestroy(this.chunkTree.getRoot());
|
||||
this.chunkTree.clear();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Marks a draw cell as updateable
|
||||
* @param worldX The world x position
|
||||
* @param worldY The world y position
|
||||
* @param worldZ The world z position
|
||||
*/
|
||||
public void markUpdateable(int worldX, int worldY, int worldZ){
|
||||
BlockDrawCell drawCell = this.getDrawCell(worldX, worldY, worldZ);
|
||||
drawCell.ejectChunkData();
|
||||
drawCell.setHasGenerated(false);
|
||||
drawCell.setHasRequested(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests all chunks for a given draw cell
|
||||
* @param cell The cell
|
||||
* @return true if all cells were successfully requested, false otherwise
|
||||
*/
|
||||
private boolean requestChunks(WorldOctTree.WorldOctTreeNode<BlockDrawCell> node){
|
||||
int lod = this.chunkTree.getMaxLevel() - node.getLevel();
|
||||
Vector3i worldPos = node.getMinBound();
|
||||
if(
|
||||
worldPos.x >= 0 &&
|
||||
worldPos.x < Globals.clientWorldData.getWorldDiscreteSize() &&
|
||||
worldPos.y >= 0 &&
|
||||
worldPos.y < Globals.clientWorldData.getWorldDiscreteSize() &&
|
||||
worldPos.z >= 0 &&
|
||||
worldPos.z < Globals.clientWorldData.getWorldDiscreteSize() &&
|
||||
!Globals.clientBlockManager.containsChunkDataAtWorldPoint(worldPos.x, worldPos.y, worldPos.z, lod)
|
||||
){
|
||||
//client should request chunk data from server for each chunk necessary to create the model
|
||||
LoggerInterface.loggerNetworking.DEBUG("(Client) Send Request for block data at " + worldPos);
|
||||
if(!Globals.clientBlockManager.requestChunk(worldPos.x, worldPos.y, worldPos.z, lod)){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if all chunk data required to generate this draw cell is present
|
||||
* @param node The node
|
||||
* @return true if all data is available, false otherwise
|
||||
*/
|
||||
private boolean containsDataToGenerate(WorldOctTree.WorldOctTreeNode<BlockDrawCell> node){
|
||||
BlockDrawCell cell = node.getData();
|
||||
int lod = this.chunkTree.getMaxLevel() - node.getLevel();
|
||||
Vector3i worldPos = cell.getWorldPos();
|
||||
if(!Globals.clientBlockManager.containsChunkDataAtWorldPoint(worldPos.x, worldPos.y, worldPos.z, lod)){
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the draw cell manager should update or not
|
||||
* @param shouldUpdate true if should update, false otherwise
|
||||
*/
|
||||
public void setShouldUpdate(boolean shouldUpdate){
|
||||
this.shouldUpdate = shouldUpdate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether the client draw cell manager should update or not
|
||||
* @return true if should update, false otherwise
|
||||
*/
|
||||
public boolean getShouldUpdate(){
|
||||
return this.shouldUpdate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of currently valid cells
|
||||
* @return The number of currently valid cells
|
||||
*/
|
||||
public int getValidCellCount(){
|
||||
return validCellCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the status of the draw cell manager
|
||||
*/
|
||||
public void updateStatus(){
|
||||
maxResCount = 0;
|
||||
halfResCount = 0;
|
||||
generated = 0;
|
||||
this.recursivelyCalculateStatus(this.chunkTree.getRoot());
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively calculates the status of the manager
|
||||
* @param node The root node
|
||||
*/
|
||||
private void recursivelyCalculateStatus(WorldOctTreeNode<BlockDrawCell> node){
|
||||
if(node.getLevel() == this.chunkTree.getMaxLevel() - 1){
|
||||
halfResCount++;
|
||||
}
|
||||
if(node.getLevel() == this.chunkTree.getMaxLevel()){
|
||||
maxResCount++;
|
||||
}
|
||||
if(node.getData() != null && node.getData().hasGenerated()){
|
||||
generated++;
|
||||
}
|
||||
if(node.getChildren() != null && node.getChildren().size() > 0){
|
||||
List<WorldOctTreeNode<BlockDrawCell>> children = new LinkedList<WorldOctTreeNode<BlockDrawCell>>(node.getChildren());
|
||||
for(WorldOctTreeNode<BlockDrawCell> child : children){
|
||||
recursivelyCalculateStatus(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets The number of maximum resolution chunks
|
||||
* @return The number of maximum resolution chunks
|
||||
*/
|
||||
public int getMaxResCount() {
|
||||
return maxResCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets The number of half resolution chunks
|
||||
* @return The number of half resolution chunks
|
||||
*/
|
||||
public int getHalfResCount() {
|
||||
return halfResCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets The number of generated chunks
|
||||
* @return
|
||||
*/
|
||||
public int getGenerated() {
|
||||
return generated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets whether the client draw cell manager has initialized or not
|
||||
* @return true if it has initialized, false otherwise
|
||||
*/
|
||||
public boolean isInitialized(){
|
||||
return this.initialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the draw cell for a given world coordinate if it has been generated
|
||||
* @param worldX The world x coordinate
|
||||
* @param worldY The world y coordinate
|
||||
* @param worldZ The world z coordinate
|
||||
* @return The draw cell if it exists, null otherwise
|
||||
*/
|
||||
public BlockDrawCell getDrawCell(int worldX, int worldY, int worldZ){
|
||||
WorldOctTreeNode<BlockDrawCell> node = this.chunkTree.search(new Vector3i(worldX,worldY,worldZ), false);
|
||||
if(node != null){
|
||||
return node.getData();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if physics has been generated for a given world coordinate
|
||||
* @param worldX The world x coordinate
|
||||
* @param worldY The world y coordinate
|
||||
* @param worldZ The world z coordinate
|
||||
* @return true if physics has been generated, false otherwise
|
||||
*/
|
||||
public boolean hasGeneratedPhysics(int worldX, int worldY, int worldZ){
|
||||
BlockDrawCell cell = this.getDrawCell(worldX, worldY, worldZ);
|
||||
if(cell != null && cell.getEntity() != null){
|
||||
return PhysicsEntityUtils.containsDBody(cell.getEntity());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@ -209,7 +209,9 @@ public class ClientSimulation {
|
||||
* Updates the block cell manager
|
||||
*/
|
||||
private void updateBlockCellManager(){
|
||||
|
||||
if(Globals.clientBlockCellManager != null && Globals.clientWorldData != null){
|
||||
Globals.clientBlockCellManager.update();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -14,6 +14,8 @@ import electrosphere.audio.collision.HitboxAudioService;
|
||||
import electrosphere.audio.movement.MovementAudioService;
|
||||
import electrosphere.auth.AuthenticationManager;
|
||||
import electrosphere.client.block.ClientBlockManager;
|
||||
import electrosphere.client.block.cells.BlockTextureAtlas;
|
||||
import electrosphere.client.block.cells.ClientBlockCellManager;
|
||||
import electrosphere.client.chemistry.ClientChemistryCollisionCallback;
|
||||
import electrosphere.client.entity.particle.ParticleService;
|
||||
import electrosphere.client.fluid.cells.FluidCellManager;
|
||||
@ -362,6 +364,8 @@ public class Globals {
|
||||
//draw cell manager
|
||||
public static ClientDrawCellManager clientDrawCellManager;
|
||||
public static VoxelTextureAtlas voxelTextureAtlas = new VoxelTextureAtlas();
|
||||
public static ClientBlockCellManager clientBlockCellManager;
|
||||
public static BlockTextureAtlas blockTextureAtlas = null;
|
||||
|
||||
//fluid cell manager
|
||||
public static FluidCellManager fluidCellManager;
|
||||
|
||||
@ -5,7 +5,7 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.joml.Vector3f;
|
||||
|
||||
import electrosphere.client.block.BlockChunkData;
|
||||
import electrosphere.client.block.cells.ClientBlockCellManager;
|
||||
import electrosphere.client.entity.camera.CameraEntityUtils;
|
||||
import electrosphere.client.entity.crosshair.Crosshair;
|
||||
import electrosphere.client.fluid.cells.FluidCellManager;
|
||||
@ -19,7 +19,6 @@ import electrosphere.client.ui.menu.mainmenu.MenuCharacterCreation;
|
||||
import electrosphere.controls.ControlHandler;
|
||||
import electrosphere.engine.Globals;
|
||||
import electrosphere.engine.assetmanager.AssetDataStrings;
|
||||
import electrosphere.engine.assetmanager.queue.QueuedModel;
|
||||
import electrosphere.engine.signal.Signal.SignalType;
|
||||
import electrosphere.engine.threads.LabeledThread.ThreadLabel;
|
||||
import electrosphere.entity.DrawableUtils;
|
||||
@ -31,8 +30,6 @@ import electrosphere.net.NetUtils;
|
||||
import electrosphere.net.client.ClientNetworking;
|
||||
import electrosphere.renderer.actor.Actor;
|
||||
import electrosphere.renderer.actor.ActorTextureMask;
|
||||
import electrosphere.renderer.meshgen.BlockMeshgen;
|
||||
import electrosphere.renderer.meshgen.BlockMeshgen.BlockMeshData;
|
||||
|
||||
public class ClientLoading {
|
||||
|
||||
@ -95,12 +92,11 @@ public class ClientLoading {
|
||||
Globals.controlHandler.hintUpdateControlState(ControlHandler.ControlsState.NO_INPUT);
|
||||
//initialize the "real" objects simulation
|
||||
initClientSimulation();
|
||||
//initialize the cell manager (client)
|
||||
//initialize the gridded managers (client)
|
||||
initDrawCellManager(true);
|
||||
//init foliage manager
|
||||
initFoliageManager();
|
||||
//init the fluid cell manager
|
||||
initFluidCellManager(true);
|
||||
initBlockCellManager(true);
|
||||
//initialize the basic graphical entities of the world (skybox, camera)
|
||||
initWorldBaseGraphicalEntities();
|
||||
//init arena specific stuff (ie different skybox colors)
|
||||
@ -141,11 +137,11 @@ public class ClientLoading {
|
||||
Globals.cameraHandler.setUpdate(false);
|
||||
//initialize the "real" objects simulation
|
||||
initClientSimulation();
|
||||
//init foliage manager
|
||||
initFoliageManager();
|
||||
//initialize the cell managers (client)
|
||||
initDrawCellManager(false);
|
||||
initFluidCellManager(false);
|
||||
initBlockCellManager(false);
|
||||
initFoliageManager();
|
||||
|
||||
//sets micro and macro sims to ready if they exist
|
||||
setSimulationsToReady();
|
||||
@ -276,23 +272,6 @@ public class ClientLoading {
|
||||
cursorActor.addTextureMask(new ActorTextureMask("sphere", Arrays.asList(new String[]{"Textures/transparent_red.png"})));
|
||||
DrawableUtils.makeEntityTransparent(Globals.playerCursor);
|
||||
EntityUtils.getScale(Globals.playerCursor).set(0.2f);
|
||||
|
||||
//block test
|
||||
Entity blockEntity = EntityCreationUtils.createClientSpatialEntity();
|
||||
BlockChunkData blockChunkData = BlockChunkData.allocate();
|
||||
blockChunkData.setType(0, 0, 0, 1);
|
||||
blockChunkData.setType(1, 0, 0, 1);
|
||||
blockChunkData.setType(0, 1, 0, 1);
|
||||
blockChunkData.setType(1, 1, 0, 1);
|
||||
blockChunkData.setType(0, 0, 1, 1);
|
||||
blockChunkData.setType(1, 0, 1, 1);
|
||||
blockChunkData.setType(0, 1, 1, 1);
|
||||
blockChunkData.setType(1, 1, 1, 1);
|
||||
BlockMeshData meshData = BlockMeshgen.rasterize(blockChunkData);
|
||||
String modelPath = Globals.assetManager.queuedAsset(new QueuedModel(() -> {
|
||||
return BlockMeshgen.generateBlockModel(meshData);
|
||||
}));
|
||||
EntityCreationUtils.makeEntityDrawablePreexistingModel(blockEntity, modelPath);
|
||||
}
|
||||
|
||||
static final int MAX_DRAW_CELL_WAIT = 1000;
|
||||
@ -386,6 +365,52 @@ public class ClientLoading {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inits the block cell manager
|
||||
* @param blockForInit Blocks the thread until the block cell manager is ready
|
||||
*/
|
||||
static void initBlockCellManager(boolean blockForInit){
|
||||
int iterations = 0;
|
||||
WindowUtils.updateLoadingWindow("WAITING ON WORLD DATA");
|
||||
while(blockForInit && (Globals.clientWorldData == null || InitialAssetLoading.atlasQueuedTexture == null || !InitialAssetLoading.atlasQueuedTexture.hasLoaded()) && Globals.threadManager.shouldKeepRunning()){
|
||||
try {
|
||||
TimeUnit.MILLISECONDS.sleep(10);
|
||||
iterations++;
|
||||
} catch (InterruptedException ex) {
|
||||
LoggerInterface.loggerEngine.ERROR(ex);
|
||||
}
|
||||
if(iterations > MAX_DRAW_CELL_WAIT){
|
||||
String message = "Draw cell took too long to init!\n" +
|
||||
Globals.clientWorldData + "\n" +
|
||||
InitialAssetLoading.atlasQueuedTexture.hasLoaded();
|
||||
throw new IllegalStateException(message);
|
||||
}
|
||||
}
|
||||
Globals.clientBlockCellManager = new ClientBlockCellManager(Globals.blockTextureAtlas, Globals.clientWorldData.getWorldDiscreteSize());
|
||||
//Alerts the client simulation that it should start loading blocks
|
||||
Globals.clientSimulation.setLoadingTerrain(true);
|
||||
//wait for all the block data to arrive
|
||||
int i = 0;
|
||||
while(
|
||||
blockForInit &&
|
||||
!Globals.clientBlockCellManager.isInitialized() &&
|
||||
Globals.threadManager.shouldKeepRunning()
|
||||
){
|
||||
i++;
|
||||
if(i % DRAW_CELL_UPDATE_RATE == 0){
|
||||
WindowUtils.updateLoadingWindow("WAITING ON SERVER TO SEND BLOCKS (" + Globals.clientTerrainManager.getAllChunks().size() + ")");
|
||||
}
|
||||
try {
|
||||
TimeUnit.MILLISECONDS.sleep(10);
|
||||
} catch (InterruptedException ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
if(i < DRAW_CELL_EXPECTED_MINIMUM_FRAMES_TO_INIT){
|
||||
LoggerInterface.loggerEngine.WARNING("Block cell manager loaded exceptionally fast!");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts up the foliage manager
|
||||
*/
|
||||
|
||||
@ -10,6 +10,7 @@ import electrosphere.client.terrain.foliage.FoliageModel;
|
||||
import electrosphere.engine.Globals;
|
||||
import electrosphere.engine.loadingthreads.LoadingThread;
|
||||
import electrosphere.engine.threads.LabeledThread.ThreadLabel;
|
||||
import electrosphere.entity.types.terrain.BlockChunkEntity;
|
||||
import electrosphere.entity.types.terrain.TerrainChunk;
|
||||
import electrosphere.server.datacell.Realm;
|
||||
import electrosphere.util.CodeUtils;
|
||||
@ -118,6 +119,9 @@ public class ThreadManager {
|
||||
if(realm.getServerWorldData() != null && realm.getServerWorldData().getServerTerrainManager() != null){
|
||||
realm.getServerWorldData().getServerTerrainManager().closeThreads();
|
||||
}
|
||||
if(realm.getServerWorldData() != null && realm.getServerWorldData().getServerBlockManager() != null){
|
||||
realm.getServerWorldData().getServerBlockManager().closeThreads();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -126,6 +130,7 @@ public class ThreadManager {
|
||||
*/
|
||||
TerrainChunk.haltThreads();
|
||||
FoliageModel.haltThreads();
|
||||
BlockChunkEntity.haltThreads();
|
||||
|
||||
//
|
||||
//interrupt all threads
|
||||
|
||||
@ -0,0 +1,109 @@
|
||||
package electrosphere.entity.types.terrain;
|
||||
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import org.joml.Quaterniond;
|
||||
import org.joml.Vector3d;
|
||||
|
||||
import electrosphere.client.block.BlockChunkData;
|
||||
import electrosphere.client.block.cells.BlockDrawCell;
|
||||
import electrosphere.client.block.cells.BlockTextureAtlas;
|
||||
import electrosphere.engine.Globals;
|
||||
import electrosphere.engine.assetmanager.queue.QueuedModel;
|
||||
import electrosphere.entity.ClientEntityUtils;
|
||||
import electrosphere.entity.Entity;
|
||||
import electrosphere.entity.EntityCreationUtils;
|
||||
import electrosphere.entity.EntityDataStrings;
|
||||
import electrosphere.entity.EntityUtils;
|
||||
import electrosphere.entity.types.collision.CollisionObjUtils;
|
||||
import electrosphere.logger.LoggerInterface;
|
||||
import electrosphere.renderer.meshgen.BlockMeshgen;
|
||||
import electrosphere.renderer.meshgen.BlockMeshgen.BlockMeshData;
|
||||
|
||||
/**
|
||||
* Generates block chunk entities
|
||||
*/
|
||||
public class BlockChunkEntity {
|
||||
|
||||
/**
|
||||
* Used for generating block chunks
|
||||
*/
|
||||
static final ExecutorService generationService = Executors.newFixedThreadPool(4);
|
||||
|
||||
/**
|
||||
* Creates a client block chunk based on weights and values provided
|
||||
* @param chunkData the chunk data to generate with
|
||||
* @param toDelete The entity to delete on full generation of this entity
|
||||
* @param notifyTarget The target draw cell to notify once this has successfully generated its model
|
||||
* @param levelOfDetail Increasing value that increments level of detail. 0 would be full resolution, 1 would be half resolution and so on. Only generates physics if levelOfDetail is 0
|
||||
* @param hasPolygons true if the chunk has polygons to generate a model with, false otherwise
|
||||
* @return The block chunk entity
|
||||
*/
|
||||
public static Entity clientCreateBlockChunkEntity(
|
||||
BlockChunkData chunkData,
|
||||
BlockDrawCell notifyTarget,
|
||||
Entity toDelete,
|
||||
int levelOfDetail,
|
||||
BlockTextureAtlas atlas,
|
||||
boolean hasPolygons
|
||||
){
|
||||
Globals.profiler.beginAggregateCpuSample("BlockChunk.clientCreateBlockChunkEntity");
|
||||
|
||||
Entity rVal = EntityCreationUtils.createClientSpatialEntity();
|
||||
|
||||
if(hasPolygons && chunkData.getType() != null && chunkData.getMetadata() != null){
|
||||
generationService.submit(() -> {
|
||||
BlockMeshData data;
|
||||
try {
|
||||
data = BlockMeshgen.rasterize(chunkData);
|
||||
if(Globals.clientScene.containsEntity(rVal)){
|
||||
String modelPath = Globals.assetManager.queuedAsset(new QueuedModel(() -> {
|
||||
return BlockMeshgen.generateBlockModel(data);
|
||||
}));
|
||||
EntityCreationUtils.makeEntityDrawablePreexistingModel(rVal, modelPath);
|
||||
if(levelOfDetail == BlockChunkData.LOD_FULL_RES){
|
||||
// PhysicsEntityUtils.clientAttachTerrainChunkRigidBody(rVal, data);
|
||||
CollisionObjUtils.clientPositionCharacter(rVal, new Vector3d(EntityUtils.getPosition(rVal)), new Quaterniond());
|
||||
} else {
|
||||
EntityCreationUtils.bypassShadowPass(rVal);
|
||||
EntityCreationUtils.bypassVolumetics(rVal);
|
||||
}
|
||||
rVal.putData(EntityDataStrings.HAS_UNIQUE_MODEL, true);
|
||||
} else {
|
||||
if(notifyTarget != null){
|
||||
notifyTarget.alertToGeneration();
|
||||
}
|
||||
if(toDelete != null){
|
||||
ClientEntityUtils.destroyEntity(toDelete);
|
||||
}
|
||||
LoggerInterface.loggerEngine.DEBUG("Finished generating block polygons; however, entity has already been deleted.");
|
||||
}
|
||||
} catch (Error e){
|
||||
LoggerInterface.loggerEngine.ERROR(e);
|
||||
} catch(Exception e){
|
||||
LoggerInterface.loggerEngine.ERROR(e);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if(notifyTarget != null){
|
||||
notifyTarget.alertToGeneration();
|
||||
}
|
||||
if(toDelete != null){
|
||||
ClientEntityUtils.destroyEntity(toDelete);
|
||||
}
|
||||
}
|
||||
|
||||
rVal.putData(EntityDataStrings.TERRAIN_IS_TERRAIN, true);
|
||||
Globals.profiler.endCpuSample();
|
||||
return rVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Halts all running generation threads
|
||||
*/
|
||||
public static void haltThreads(){
|
||||
generationService.shutdownNow();
|
||||
}
|
||||
|
||||
}
|
||||
@ -104,6 +104,17 @@ public class ServerBlockChunkGenerationThread implements Runnable {
|
||||
if(chunk == null){
|
||||
//TODO: generate from macro-level data
|
||||
chunk = BlockChunkData.allocate();
|
||||
chunk.setHomogenousValue(0);
|
||||
if(worldX == 0 && worldY == 0 && worldZ == 0){
|
||||
chunk.setHomogenousValue(BlockChunkData.NOT_HOMOGENOUS);
|
||||
for(int x = 0; x < 16; x++){
|
||||
for(int y = 0; y < 16; y++){
|
||||
for(int z = 0; z < 16; z++){
|
||||
chunk.setType(x, y, z, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if(chunk != null){
|
||||
chunkCache.add(worldX, worldY, worldZ, stride, chunk);
|
||||
|
||||
@ -120,6 +120,7 @@ public class ServerBlockManager {
|
||||
returnedChunk.setWorldX(worldX);
|
||||
returnedChunk.setWorldY(worldY);
|
||||
returnedChunk.setWorldZ(worldZ);
|
||||
returnedChunk.setHomogenousValue(0);
|
||||
}
|
||||
this.chunkCache.add(worldX, worldY, worldZ, BlockChunkData.LOD_FULL_RES, returnedChunk);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user