Complete overhaul of foliage management
Some checks failed
studiorailgun/Renderer/pipeline/head There was a failure building this commit

This commit is contained in:
austin 2024-11-21 17:08:41 -05:00
parent dc00a20bb0
commit 4d934873c3
15 changed files with 2081 additions and 16 deletions

View File

@ -1112,6 +1112,7 @@ Remove point lights from skeleton + human
Change grass texture Change grass texture
Fix allocations on FoliageChunk child iterations Fix allocations on FoliageChunk child iterations
Reduce near clip to remove flickering on far chunks Reduce near clip to remove flickering on far chunks
Complete overhaul of foliage management
# TODO # TODO

View File

@ -115,6 +115,45 @@ public class ClientWorldData {
); );
} }
/**
* Converts a real space position to its absolute voxel space equivalent
* @param position The real space position
* @return The absolute voxel space position ie the voxel-aligned position not clamped to the current chunk
*/
public Vector3i convertRealToAbsoluteVoxelSpace(Vector3d position){
return new Vector3i(
(int)Math.floor(position.x),
(int)Math.floor(position.y),
(int)Math.floor(position.z)
);
}
/**
* Converts a absolute voxel position to its relative voxel space equivalent
* @param position The real space position
* @return The relative voxel space position ie the voxel-aligned position not clamped to the current chunk
*/
public Vector3i convertAbsoluteVoxelToRelativeVoxelSpace(Vector3i position){
return new Vector3i(
position.x % ServerTerrainChunk.CHUNK_DIMENSION,
position.y % ServerTerrainChunk.CHUNK_DIMENSION,
position.z % ServerTerrainChunk.CHUNK_DIMENSION
);
}
/**
* Converts a absolute voxel position to its world space equivalent
* @param position The real space position
* @return The world space position ie the voxel-aligned position not clamped to the current chunk
*/
public Vector3i convertAbsoluteVoxelToWorldSpace(Vector3i position){
return new Vector3i(
position.x / ServerTerrainChunk.CHUNK_DIMENSION,
position.y / ServerTerrainChunk.CHUNK_DIMENSION,
position.z / ServerTerrainChunk.CHUNK_DIMENSION
);
}
/** /**
* Converts a world space vector to a real space vector * Converts a world space vector to a real space vector
* @param position The world space vector * @param position The world space vector

View File

@ -87,8 +87,8 @@ public class ClientSimulation {
Globals.profiler.endCpuSample(); Globals.profiler.endCpuSample();
// //
//update foliage //update foliage
if(Globals.clientFoliageManager != null){ if(Globals.foliageCellManager != null){
Globals.clientFoliageManager.update(); Globals.foliageCellManager.update();
} }
// //
//targeting crosshair //targeting crosshair

View File

@ -241,8 +241,8 @@ public class DrawCellManager {
List<DrawCellFace> higherLODFace = null; List<DrawCellFace> higherLODFace = null;
keyCellMap.get(targetKey).generateDrawableEntity(atlas,0,higherLODFace); keyCellMap.get(targetKey).generateDrawableEntity(atlas,0,higherLODFace);
//evaluate for foliage // //evaluate for foliage
Globals.clientFoliageManager.evaluateChunk(worldPos); // Globals.clientFoliageManager.evaluateChunk(worldPos);
} }
} }
} }

View File

@ -0,0 +1,701 @@
package electrosphere.client.terrain.foliage;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import org.joml.Matrix4d;
import org.joml.Quaterniond;
import org.joml.Vector3d;
import org.joml.Vector3f;
import org.joml.Vector3i;
import electrosphere.client.entity.camera.CameraEntityUtils;
import electrosphere.client.terrain.cache.ChunkData;
import electrosphere.engine.Globals;
import electrosphere.entity.ClientEntityUtils;
import electrosphere.entity.Entity;
import electrosphere.entity.EntityCreationUtils;
import electrosphere.entity.EntityUtils;
import electrosphere.entity.btree.BehaviorTree;
import electrosphere.renderer.OpenGLState;
import electrosphere.renderer.RenderPipelineState;
import electrosphere.renderer.actor.instance.TextureInstancedActor;
import electrosphere.renderer.buffer.HomogenousUniformBuffer.HomogenousBufferTypes;
import electrosphere.renderer.buffer.ShaderAttribute;
import electrosphere.renderer.meshgen.TransvoxelModelGeneration.TransvoxelChunkData;
import electrosphere.server.terrain.manager.ServerTerrainChunk;
import electrosphere.util.ds.octree.WorldOctTree.WorldOctTreeNode;
import electrosphere.util.math.GeomUtils;
/**
* A single foliagecell - contains an entity that has a physics mesh and potentially graphics
*/
public class FoliageCell {
/**
* 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;
/**
* Wiggle room in number of entries
*/
static final int BUFFER_WIGGLE_ROOM = 200;
/**
* The interval to space along
*/
static final int TARGET_FOLIAGE_SPACING = 50;
/**
* The target number of foliage to place per cell
*/
static final int TARGET_FOLIAGE_PER_CELL = TARGET_FOLIAGE_SPACING * TARGET_FOLIAGE_SPACING + BUFFER_WIGGLE_ROOM;
/**
* The length of the ray to ground test with
*/
static final float RAY_LENGTH = 1.0f;
/**
* The height above the chunk to start from when sampling downwards
*/
static final float SAMPLE_START_HEIGHT = 0.5f;
/**
* The ID of the air voxel
*/
static final int AIR_VOXEL_ID = 0;
/**
* <p>
* Size of a single item of foliage in the texture buffer
* </p>
* A lot of these are x 4 to account for size of float
* 3 x 4 for position
* 2 x 4 for euler rotation
*
*
* eventually:
* grass type
* color
* wind characteristics?
*/
static final int SINGLE_FOLIAGE_DATA_SIZE_BYTES = 3 * 4 + 2 * 4;
/**
* The map of all attributes for instanced foliage
*/
static final Map<ShaderAttribute,HomogenousBufferTypes> attributes = new HashMap<ShaderAttribute,HomogenousBufferTypes>();
/**
* Model matrix shader attribute
*/
static ShaderAttribute modelMatrixAttribute;
/**
* The list of voxel type ids that should have grass generated on top of them
*/
static final List<Integer> grassGeneratingVoxelIds = new ArrayList<Integer>();
//set attributes
static {
int[] attributeIndices = new int[]{
5,6,7,8
};
modelMatrixAttribute = new ShaderAttribute(attributeIndices);
attributes.put(modelMatrixAttribute,HomogenousBufferTypes.MAT4F);
//set grass generating voxel ids
grassGeneratingVoxelIds.add(2);
}
/**
* Vertex shader path
*/
protected static final String vertexPath = "Shaders/entities/foliage/foliage.vs";
/**
* Fragment shader path
*/
protected static final String fragmentPath = "Shaders/entities/foliage/foliage.fs";
/**
* Random for finding new positions for foliage
*/
Random placementRandomizer = new Random();
/**
* The position of the foliage cell in world coordinates
*/
Vector3i worldPos;
/**
* The position of this cell voxel-wise within its chunk
*/
Vector3i voxelPos;
/**
* The LOD of the foliage cell
*/
int lod;
/**
* The main entity for the cell
*/
Entity modelEntity;
/**
* The data for generating the visuals
*/
TransvoxelChunkData chunkData;
/**
* Tracks whether the foliage cell has requested its chunk data or not
*/
boolean hasRequested = false;
/**
* Tracks whether the foliage cell has generated its entity or not
*/
boolean hasGenerated = false;
/**
* Tracks whether this foliage 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
*/
FoliageCell notifyTarget = null;
/**
* The number of cells that have alerted this one
*/
int generationAlertCount = 0;
/**
* Private constructor
*/
private FoliageCell(){
}
/**
* Constructs a foliagecell object
*/
public static FoliageCell generateTerrainCell(
Vector3i voxelAbsPos,
int lod
){
FoliageCell rVal = new FoliageCell();
rVal.lod = lod;
rVal.worldPos = Globals.clientWorldData.convertAbsoluteVoxelToWorldSpace(voxelAbsPos);
rVal.voxelPos = Globals.clientWorldData.convertAbsoluteVoxelToRelativeVoxelSpace(voxelAbsPos);
return rVal;
}
/**
* Constructs a homogenous foliagecell object
*/
public static FoliageCell generateHomogenousTerrainCell(
Vector3i voxelAbsPos,
int lod
){
FoliageCell rVal = new FoliageCell();
rVal.lod = lod;
rVal.worldPos = Globals.clientWorldData.convertAbsoluteVoxelToWorldSpace(voxelAbsPos);
rVal.voxelPos = Globals.clientWorldData.convertAbsoluteVoxelToRelativeVoxelSpace(voxelAbsPos);
rVal.hasGenerated = true;
rVal.homogenous = true;
return rVal;
}
/**
* Generates a drawable entity based on this chunk
*/
public void generateDrawableEntity(int lod){
boolean success = true;
if(chunkData == null){
ChunkData currentChunk = Globals.clientTerrainManager.getChunkDataAtWorldPoint(
worldPos.x,
worldPos.y,
worldPos.z,
0
);
if(currentChunk == null){
success = false;
} else {
this.homogenous = currentChunk.getHomogenousValue() != ChunkData.NOT_HOMOGENOUS;
success = true;
}
if(!success){
this.setFailedGenerationAttempts(this.getFailedGenerationAttempts() + 1);
return;
}
this.chunkData = new TransvoxelChunkData(currentChunk.getVoxelWeight(), currentChunk.getVoxelType(), 0);
}
this.generate();
this.setHasGenerated(true);
}
/**
* Generates the foliage cell
*/
protected void generate(){
boolean shouldGenerate = false;
if(voxelPos.y + 1 >= ServerTerrainChunk.CHUNK_DIMENSION){
return;
}
ChunkData data = Globals.clientTerrainManager.getChunkDataAtWorldPoint(worldPos,ChunkData.NO_STRIDE);
if(data == null){
return;
}
//get foliage types supported
List<String> foliageTypesSupported = new LinkedList<String>();
boolean airAbove = data.getType(voxelPos.x,voxelPos.y+1,voxelPos.z) == 0;
int scale = (int)Math.pow(2,lod);
for(int x = 0; x < scale; x++){
for(int y = 0; y < scale; y++){
for(int z = 0; z < scale; z++){
List<String> currentList = Globals.gameConfigCurrent.getVoxelData().getTypeFromId(data.getType(voxelPos)).getAmbientFoliage();
if(currentList == null){
continue;
}
foliageTypesSupported.addAll(currentList);
airAbove = data.getType(voxelPos.x,voxelPos.y+1,voxelPos.z) == 0;
if(foliageTypesSupported != null && foliageTypesSupported.size() > 0 && airAbove){
shouldGenerate = true;
}
}
}
}
if(shouldGenerate){
//create entity
this.modelEntity = EntityCreationUtils.createClientSpatialEntity();
FoliageModel.clientCreateFoliageChunkEntity(foliageTypesSupported,scale,this.modelEntity,this.getRealPos(),worldPos,voxelPos,null,null);
//get type
// String foliageTypeName = foliageTypesSupported.get(placementRandomizer.nextInt() % foliageTypesSupported.size());
// FoliageType foliageType = Globals.gameConfigCurrent.getFoliageMap().getFoliage(foliageTypeName);
// //create cell and buffer
// ByteBuffer buffer = BufferUtils.createByteBuffer(TARGET_FOLIAGE_PER_CELL * SINGLE_FOLIAGE_DATA_SIZE_BYTES);
// if(buffer.capacity() < TARGET_FOLIAGE_PER_CELL * SINGLE_FOLIAGE_DATA_SIZE_BYTES){
// LoggerInterface.loggerEngine.WARNING("Failed to allocate data for foliage cell! " + buffer.limit());
// }
// FloatBuffer floatBufferView = buffer.asFloatBuffer();
// int drawCount = 0;
// for(int x = 0; x < scale; x++){
// for(int y = 0; y < scale; y++){
// for(int z = 0; z < scale; z++){
// drawCount = drawCount + this.insertBlades(x, y, z, floatBufferView, data);
// }
// }
// }
// // drawCount = drawCount + this.insertBlades(0, 0, 0, floatBufferView, data);
// if(drawCount > 0){
// buffer.position(0);
// buffer.limit(TARGET_FOLIAGE_PER_CELL * SINGLE_FOLIAGE_DATA_SIZE_BYTES);
// //construct data texture
// Texture dataTexture = new Texture(Globals.renderingEngine.getOpenGLState(),buffer,SINGLE_FOLIAGE_DATA_SIZE_BYTES / 4,TARGET_FOLIAGE_PER_CELL);
// //create entity
// this.modelEntity = EntityCreationUtils.createClientSpatialEntity();
// TextureInstancedActor.attachTextureInstancedActor(this.modelEntity, foliageType.getGraphicsTemplate().getModel().getPath(), vertexPath, fragmentPath, dataTexture, drawCount);
// ClientEntityUtils.initiallyPositionEntity(this.modelEntity, this.getRealPos(), new Quaterniond());
// EntityUtils.getScale(this.modelEntity).set(1,1,1);
// //add ambient foliage behavior tree
// AmbientFoliage.attachAmbientFoliageTree(this.modelEntity, 1.0f, foliageType.getGrowthModel().getGrowthRate());
// }
} else {
this.homogenous = true;
}
this.hasGenerated = true;
}
/**
* Insert blades of grass into the entity
* @param vX the x offset of the voxel
* @param vY the y offset of the voxel
* @param vZ the z offset of the voxel
* @param floatBufferView the gpu data buffer
* @param chunkData the chunk data
* @return the number of blades of grass added
*/
protected int insertBlades(int vX, int vY, int vZ, FloatBuffer floatBufferView, ChunkData chunkData){
int rVal = 0;
//get positions offset
Vector3d voxelRealPos = new Vector3d(this.getRealPos()).add(vX,vY,vZ);
Vector3i currVoxelPos = new Vector3i(this.voxelPos).add(vX,vY,vZ);
int scale = (int)Math.pow(2,lod);
//check that the current voxel even supports foliage
boolean shouldGenerate = false;
List<String> foliageTypesSupported = null;
if(chunkData != null && currVoxelPos.y + 1 < ServerTerrainChunk.CHUNK_DIMENSION){
foliageTypesSupported = Globals.gameConfigCurrent.getVoxelData().getTypeFromId(chunkData.getType(currVoxelPos)).getAmbientFoliage();
boolean airAbove = chunkData.getType(currVoxelPos.x,currVoxelPos.y+1,currVoxelPos.z) == AIR_VOXEL_ID;
if(foliageTypesSupported != null && airAbove){
shouldGenerate = true;
}
}
if(shouldGenerate){
//construct simple grid to place foliage on
Vector3d sample_00 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).add(-0.5,SAMPLE_START_HEIGHT,-0.5), new Vector3d(0,-1,0), RAY_LENGTH);
Vector3d sample_01 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).add(-0.5,SAMPLE_START_HEIGHT, 0), new Vector3d(0,-1,0), RAY_LENGTH);
Vector3d sample_02 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).add(-0.5,SAMPLE_START_HEIGHT, 0.5), new Vector3d(0,-1,0), RAY_LENGTH);
Vector3d sample_10 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).add( 0,SAMPLE_START_HEIGHT,-0.5), new Vector3d(0,-1,0), RAY_LENGTH);
Vector3d sample_11 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).add( 0,SAMPLE_START_HEIGHT, 0), new Vector3d(0,-1,0), RAY_LENGTH);
Vector3d sample_12 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).add( 0,SAMPLE_START_HEIGHT, 0.5), new Vector3d(0,-1,0), RAY_LENGTH);
Vector3d sample_20 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).add( 0.5,SAMPLE_START_HEIGHT,-0.5), new Vector3d(0,-1,0), RAY_LENGTH);
Vector3d sample_21 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).add( 0.5,SAMPLE_START_HEIGHT, 0), new Vector3d(0,-1,0), RAY_LENGTH);
Vector3d sample_22 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).add( 0.5,SAMPLE_START_HEIGHT, 0.5), new Vector3d(0,-1,0), RAY_LENGTH);
//get the heights of each sample
float height_11 = (float)(sample_11 != null ? sample_11.y : 0);
float height_00 = (float)(sample_00 != null ? sample_00.y : height_11);
float height_01 = (float)(sample_01 != null ? sample_01.y : height_11);
float height_02 = (float)(sample_02 != null ? sample_02.y : height_11);
float height_10 = (float)(sample_10 != null ? sample_10.y : height_11);
float height_12 = (float)(sample_12 != null ? sample_12.y : height_11);
float height_20 = (float)(sample_20 != null ? sample_20.y : height_11);
float height_21 = (float)(sample_21 != null ? sample_21.y : height_11);
float height_22 = (float)(sample_22 != null ? sample_22.y : height_11);
//each height is in real world coordinates that are absolute
//when rendering, there's already a y offset for the center of the field of grass (based on the model matrix)
//so when offseting the position of the blade of grass RELATIVE to the overall instance being drawn, need to subtract the real world coordinates of the overall instance
//in other words realPos SPECIFICALLY for the y dimension, for x and z you don't need to worry about it
//if we don't find data for the center sample, can't place grass so don't create entity
if(sample_11 != null){
//generate positions to place
for(int x = 0; x < TARGET_FOLIAGE_SPACING; x=x+scale){
for(int z = 0; z < TARGET_FOLIAGE_SPACING; z=z+scale){
//get position to place
double rand1 = placementRandomizer.nextDouble();
double rand2 = placementRandomizer.nextDouble();
double relativePositionOnGridX = x / (1.0 * TARGET_FOLIAGE_SPACING) + rand1 / TARGET_FOLIAGE_SPACING;
double relativePositionOnGridZ = z / (1.0 * TARGET_FOLIAGE_SPACING) + rand2 / TARGET_FOLIAGE_SPACING;
double offsetX = relativePositionOnGridX - 0.5;
double offsetZ = relativePositionOnGridZ - 0.5;
//determine quadrant we're placing in
double offsetY = 0;
boolean addBlade = false;
if(relativePositionOnGridX >=0.5){
if(relativePositionOnGridZ >= 0.5){
relativePositionOnGridX = relativePositionOnGridX - 0.5;
relativePositionOnGridZ = relativePositionOnGridZ - 0.5;
relativePositionOnGridX /= 0.5;
relativePositionOnGridZ /= 0.5;
// System.out.println(relativePositionOnGridX + " " + relativePositionOnGridZ);
//if we have heights for all four surrounding spots, interpolate for y value
if(sample_11 != null && sample_12 != null && sample_21 != null && sample_22 != null){
offsetY =
height_11 * (1-relativePositionOnGridX) * (1-relativePositionOnGridZ) +
height_12 * (1-relativePositionOnGridX) * ( relativePositionOnGridZ) +
height_21 * ( relativePositionOnGridX) * (1-relativePositionOnGridZ) +
height_22 * ( relativePositionOnGridX) * ( relativePositionOnGridZ);
addBlade = true;
}
} else {
relativePositionOnGridX = relativePositionOnGridX - 0.5;
relativePositionOnGridX /= 0.5;
relativePositionOnGridZ /= 0.5;
//if we have heights for all four surrounding spots, interpolate for y value
if(sample_10 != null && sample_11 != null && sample_20 != null && sample_21 != null){
offsetY =
height_10 * (1-relativePositionOnGridX) * (1-relativePositionOnGridZ) +
height_11 * (1-relativePositionOnGridX) * ( relativePositionOnGridZ) +
height_20 * ( relativePositionOnGridX) * (1-relativePositionOnGridZ) +
height_21 * ( relativePositionOnGridX) * ( relativePositionOnGridZ);
addBlade = true;
}
}
} else {
if(relativePositionOnGridZ >= 0.5){
relativePositionOnGridZ = relativePositionOnGridZ - 0.5;
relativePositionOnGridX /= 0.5;
relativePositionOnGridZ /= 0.5;
//if we have heights for all four surrounding spots, interpolate for y value
if(sample_01 != null && sample_02 != null && sample_11 != null && sample_12 != null){
offsetY =
height_01 * (1-relativePositionOnGridX) * (1-relativePositionOnGridZ) +
height_02 * (1-relativePositionOnGridX) * ( relativePositionOnGridZ) +
height_11 * ( relativePositionOnGridX) * (1-relativePositionOnGridZ) +
height_12 * ( relativePositionOnGridX) * ( relativePositionOnGridZ);
addBlade = true;
}
} else {
relativePositionOnGridX /= 0.5;
relativePositionOnGridZ /= 0.5;
//if we have heights for all four surrounding spots, interpolate for y value
if(sample_00 != null && sample_01 != null && sample_10 != null && sample_11 != null){
offsetY =
height_00 * (1-relativePositionOnGridX) * (1-relativePositionOnGridZ) +
height_01 * (1-relativePositionOnGridX) * ( relativePositionOnGridZ) +
height_10 * ( relativePositionOnGridX) * (1-relativePositionOnGridZ) +
height_11 * ( relativePositionOnGridX) * ( relativePositionOnGridZ);
addBlade = true;
}
}
}
if(addBlade){
//convert y to relative to chunk
offsetY = offsetY - this.getRealPos().y;
double rotVar = placementRandomizer.nextDouble() * Math.PI * 2;
double rotVar2 = placementRandomizer.nextDouble();
if(floatBufferView.limit() >= floatBufferView.position() + SINGLE_FOLIAGE_DATA_SIZE_BYTES / 4){
floatBufferView.put((float)offsetX + vX);
floatBufferView.put((float)offsetY + vY);
floatBufferView.put((float)offsetZ + vZ);
floatBufferView.put((float)rotVar);
floatBufferView.put((float)rotVar2);
rVal++;
}
}
}
}
}
}
return rVal;
}
/**
* Draws all entities in the foliage cell
*/
protected void draw(){
if(this.modelEntity != null){
Matrix4d modelMatrix = new Matrix4d();
Vector3f cameraCenter = CameraEntityUtils.getCameraCenter(Globals.playerCamera);
RenderPipelineState renderPipelineState = Globals.renderingEngine.getRenderPipelineState();
OpenGLState openGLState = Globals.renderingEngine.getOpenGLState();
Vector3d realPosition = this.getRealPos();
Vector3f cameraModifiedPosition = new Vector3f((float)realPosition.x,(float)realPosition.y,(float)realPosition.z).sub(cameraCenter);
//frustum check entire cell
int size = (int)Math.pow(2,this.lod);
boolean shouldRender = renderPipelineState.getFrustumIntersection().testSphere(
(float)(cameraModifiedPosition.x + size / 2.0),
(float)(cameraModifiedPosition.y + size / 2.0),
(float)(cameraModifiedPosition.z + size / 2.0),
(float)(size)
);
if(shouldRender){
//disable frustum check and instead perform at cell level
boolean currentFrustumCheckState = renderPipelineState.shouldFrustumCheck();
renderPipelineState.setFrustumCheck(false);
Vector3d grassPosition = EntityUtils.getPosition(modelEntity);
Quaterniond grassRotation = EntityUtils.getRotation(modelEntity);
TextureInstancedActor actor = TextureInstancedActor.getTextureInstancedActor(modelEntity);
if(actor != null){
modelMatrix = modelMatrix.identity();
modelMatrix.translate(cameraModifiedPosition);
modelMatrix.rotate(new Quaterniond(grassRotation));
modelMatrix.scale(new Vector3d(EntityUtils.getScale(modelEntity)));
actor.applySpatialData(modelMatrix,grassPosition);
//draw
actor.draw(renderPipelineState, openGLState);
renderPipelineState.setFrustumCheck(currentFrustumCheckState);
}
}
}
}
/**
* Gets the real-space position of the foliage cell
* @return the real-space position
*/
protected Vector3d getRealPos(){
return new Vector3d(
worldPos.x * ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET + voxelPos.x,
worldPos.y * ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET + voxelPos.y,
worldPos.z * ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET + voxelPos.z
);
}
/**
* Gets the world-space position of the foliage cell
* @return the world-space position
*/
protected Vector3i getWorldPos(){
return new Vector3i(worldPos);
}
/**
* Registers a target foliage cell to notify once this one has completed generating its model
* @param notifyTarget The target to notify
*/
public void registerNotificationTarget(FoliageCell notifyTarget){
this.notifyTarget = notifyTarget;
}
/**
* Alerts this foliage 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 foliage cell 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 foliage cell
* @param source The source foliage cell
*/
public void transferChunkData(FoliageCell source){
this.chunkData = source.chunkData;
this.homogenous = source.homogenous;
this.hasRequested = source.hasRequested;
}
/**
* Gets whether this foliage cell has requested its chunk data or not
* @return true if has requested, false otherwise
*/
public boolean hasRequested() {
return hasRequested;
}
/**
* Sets whether this foliage 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 foliage cell has generated its entity or not
* @return true if has generated, false otherwise
*/
public boolean hasGenerated() {
return hasGenerated;
}
/**
* Sets whether this foliage 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 foliage cell is homogenous or not
* @param hasGenerated true if is homogenous, false otherwise
*/
public void setHomogenous(boolean homogenous) {
this.homogenous = 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;
}
/**
* Gets whether this foliage 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<FoliageCell> node, int distCache){
if(cachedMinDistance != INVALID_DIST_CACHE && distCache < lod){
return cachedMinDistance;
} else {
this.cachedMinDistance = GeomUtils.getMinSquaredDistanceAABB(worldPos, node.getMinBound(), node.getMaxBound());
return cachedMinDistance;
}
}
}

View File

@ -0,0 +1,876 @@
package electrosphere.client.terrain.foliage;
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.engine.Globals;
import electrosphere.entity.EntityUtils;
import electrosphere.game.data.foliage.type.FoliageType;
import electrosphere.logger.LoggerInterface;
import electrosphere.server.terrain.manager.ServerTerrainChunk;
import electrosphere.util.ds.octree.WorldOctTree;
import electrosphere.util.ds.octree.WorldOctTree.WorldOctTreeNode;
import electrosphere.util.math.GeomUtils;
/**
* Manages foliage cells on the client
*/
public class FoliageCellManager {
/**
* Number of times to try updating per frame. Lower this to reduce lag but slow down terrain 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 foliage at full resolution
*/
public static final double FULL_RES_DIST = 16 * 16;
/**
* The distance for half resolution
*/
public static final double HALF_RES_DIST = 20 * 20;
/**
* The distance for quarter resolution
*/
public static final double QUARTER_RES_DIST = 24 * 24;
/**
* 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 = 48 * 48;
/**
* Lod value for a full res chunk
*/
public static final int FULL_RES_LOD = 0;
/**
* Lod value for a half res chunk
*/
public static final int HALF_RES_LOD = 1;
/**
* Lod value for a quarter res chunk
*/
public static final int QUARTER_RES_LOD = 2;
/**
* Lod value for a eighth res chunk
*/
public static final int EIGHTH_RES_LOD = 3;
/**
* Lod value for a sixteenth res chunk
*/
public static final int SIXTEENTH_RES_LOD = 4;
/**
* Lod value for evaluating all lod levels
*/
public static final int ALL_RES_LOD = 5;
/**
* The octree holding all the chunks to evaluate
*/
WorldOctTree<FoliageCell> chunkTree;
/**
* Tracks what nodes have been evaluated this frame -- used to deduplicate evaluation calls
*/
Map<WorldOctTreeNode<FoliageCell>,Boolean> evaluationMap = new HashMap<WorldOctTreeNode<FoliageCell>,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 foliage cell manager should update or not
*/
boolean shouldUpdate = true;
/**
* 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 worldDim The size of the world in chunks
*/
public FoliageCellManager(int worldDim){
this.chunkTree = new WorldOctTree<FoliageCell>(
new Vector3i(0,0,0),
new Vector3i(worldDim * ServerTerrainChunk.CHUNK_DIMENSION, worldDim * ServerTerrainChunk.CHUNK_DIMENSION, worldDim * ServerTerrainChunk.CHUNK_DIMENSION)
);
this.chunkTree.getRoot().setData(FoliageCell.generateTerrainCell(new Vector3i(0,0,0), chunkTree.getMaxLevel()));
this.worldDim = worldDim;
}
/**
* Inits the foliage cell data
*/
public void init(){
//queue ambient foliage models
for(FoliageType foliageType : Globals.gameConfigCurrent.getFoliageMap().getFoliageList()){
if(foliageType.getTokens().contains(FoliageType.TOKEN_AMBIENT)){
Globals.assetManager.addModelPathToQueue(foliageType.getGraphicsTemplate().getModel().getPath());
Globals.assetManager.addShaderToQueue(FoliageCell.vertexPath, FoliageCell.fragmentPath);
}
}
}
/**
* Updates all cells in the chunk
*/
public void update(){
Globals.profiler.beginCpuSample("FoliageCellManager.update");
if(shouldUpdate && Globals.playerEntity != null){
Vector3d playerPos = EntityUtils.getPosition(Globals.playerEntity);
Vector3i absVoxelPos = Globals.clientWorldData.convertRealToAbsoluteVoxelSpace(playerPos);
int distCache = this.getDistCache(this.lastPlayerPos, absVoxelPos);
this.lastPlayerPos.set(absVoxelPos);
//the sets to iterate through
updatedLastFrame = true;
validCellCount = 0;
evaluationMap.clear();
//update all full res cells
WorldOctTreeNode<FoliageCell> rootNode = this.chunkTree.getRoot();
Globals.profiler.beginCpuSample("FoliageCellManager.update - full res cells");
updatedLastFrame = this.recursivelyUpdateCells(rootNode, absVoxelPos, evaluationMap, SIXTEENTH_RES_LOD, distCache);
Globals.profiler.endCpuSample();
if(!updatedLastFrame && !this.initialized){
this.initialized = true;
}
}
Globals.profiler.endCpuSample();
}
/**
* Recursively update child nodes
* @param node The root node
* @param absVoxelPos 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<FoliageCell> node, Vector3i absVoxelPos, Map<WorldOctTreeNode<FoliageCell>,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(absVoxelPos, node, distCache)){
Globals.profiler.beginCpuSample("FoliageCellManager.split");
//perform op
WorldOctTreeNode<FoliageCell> container = chunkTree.split(node);
FoliageCell containerCell = FoliageCell.generateTerrainCell(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
);
FoliageCell foliageCell = FoliageCell.generateTerrainCell(cellWorldPos,this.chunkTree.getMaxLevel() - child.getLevel());
foliageCell.registerNotificationTarget(node.getData());
child.setLeaf(true);
child.setData(foliageCell);
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(absVoxelPos, node, minLeafLod, distCache)){
Globals.profiler.beginCpuSample("FoliageCellManager.request");
//calculate what to request
FoliageCell 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(absVoxelPos, node, minLeafLod, distCache)){
Globals.profiler.beginCpuSample("FoliageCellManager.generate");
int lodLevel = this.getLODLevel(node);
if(this.containsDataToGenerate(node)){
node.getData().generateDrawableEntity(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(absVoxelPos, node, distCache)) {
this.join(node);
updated = true;
} else {
this.validCellCount++;
List<WorldOctTreeNode<FoliageCell>> children = node.getChildren();
boolean isHomogenous = true;
boolean fullyGenerated = true;
for(int i = 0; i < 8; i++){
WorldOctTreeNode<FoliageCell> child = children.get(i);
boolean childUpdate = this.recursivelyUpdateCells(child, absVoxelPos, evaluationMap, minLeafLod, distCache);
if(childUpdate == true){
updated = true;
}
if(!child.getData().hasGenerated()){
fullyGenerated = false;
}
if(!child.getData().isHomogenous()){
isHomogenous = false;
}
}
WorldOctTreeNode<FoliageCell> 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;
}
/**
* Draw all foliage cells
*/
public void draw(){
this.recursivelyDraw(this.chunkTree.getRoot());
}
/**
* Draws all foliage cells recursively
* @param node The current node
*/
private void recursivelyDraw(WorldOctTreeNode<FoliageCell> node){
if(node.getChildren() != null && node.getChildren().size() > 0){
for(int i = 0; i < 8; i++){
this.recursivelyDraw(node.getChildren().get(i));
}
}
node.getData().draw();
}
/**
* 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<FoliageCell> 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 SIXTEENTH_RES_LOD + 2;
}
if(
lastPlayerPos.x / 8 != currentPlayerPos.x / 8 || lastPlayerPos.z / 8 != currentPlayerPos.z / 8 || lastPlayerPos.z / 8 != currentPlayerPos.z / 8
){
return SIXTEENTH_RES_LOD + 1;
}
if(
lastPlayerPos.x / 4 != currentPlayerPos.x / 4 || lastPlayerPos.z / 4 != currentPlayerPos.z / 4 || lastPlayerPos.z / 4 != currentPlayerPos.z / 4
){
return SIXTEENTH_RES_LOD;
}
if(
lastPlayerPos.x / 2 != currentPlayerPos.x / 2 || lastPlayerPos.z / 2 != currentPlayerPos.z / 2 || lastPlayerPos.z / 2 != currentPlayerPos.z / 2
){
return EIGHTH_RES_LOD;
}
if(
lastPlayerPos.x != currentPlayerPos.x || lastPlayerPos.z != currentPlayerPos.z || lastPlayerPos.z != currentPlayerPos.z
){
return QUARTER_RES_LOD;
}
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<FoliageCell> 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 foliage calls
return
node.canSplit() &&
(node.getLevel() != this.chunkTree.getMaxLevel()) &&
!node.getData().isHomogenous() &&
(node.getParent() != null || node == this.chunkTree.getRoot()) &&
(
(
node.getLevel() < this.chunkTree.getMaxLevel() - SIXTEENTH_RES_LOD &&
this.getMinDistance(pos, node, distCache) <= SIXTEENTH_RES_DIST
)
||
(
node.getLevel() < this.chunkTree.getMaxLevel() - EIGHTH_RES_LOD &&
this.getMinDistance(pos, node, distCache) <= EIGHTH_RES_DIST
)
||
(
node.getLevel() < this.chunkTree.getMaxLevel() - QUARTER_RES_LOD &&
this.getMinDistance(pos, node, distCache) <= QUARTER_RES_DIST
)
// ||
// (
// node.getLevel() < this.chunkTree.getMaxLevel() - HALF_RES_LOD &&
// 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 foliage cell
* @param node The node to consider
* @return -1 if outside of render range, -1 if the node is not a valid foliage cell leaf, otherwise returns the LOD level
*/
private int getLODLevel(WorldOctTreeNode<FoliageCell> 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<FoliageCell> node, int level){
//don't bother to check if it's a lowest-res chunk
if(this.chunkTree.getMaxLevel() - level > FoliageCellManager.FULL_RES_LOD){
return;
}
if(node.getMinBound().x - 1 >= 0){
WorldOctTreeNode<FoliageCell> 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<FoliageCell> 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<FoliageCell> 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<FoliageCell> 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<FoliageCell> 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<FoliageCell> 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<FoliageCell> 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 foliage calls
return
node.getLevel() > 0 &&
(node.getLevel() != this.chunkTree.getMaxLevel()) &&
(
(
node.getLevel() == this.chunkTree.getMaxLevel() - HALF_RES_LOD &&
this.getMinDistance(pos, node, distCache) > FULL_RES_DIST
)
||
(
node.getLevel() == this.chunkTree.getMaxLevel() - QUARTER_RES_LOD &&
this.getMinDistance(pos, node, distCache) > HALF_RES_DIST
)
||
(
node.getLevel() == this.chunkTree.getMaxLevel() - EIGHTH_RES_LOD &&
this.getMinDistance(pos, node, distCache) > QUARTER_RES_DIST
)
||
(
node.getLevel() == this.chunkTree.getMaxLevel() - SIXTEENTH_RES_LOD &&
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<FoliageCell> join(WorldOctTreeNode<FoliageCell> node){
Globals.profiler.beginCpuSample("FoliageCellManager.join");
//queue destructions prior to join -- the join operator clears all children on node
this.recursivelyDestroy(node);
//perform op
FoliageCell newLeafCell = FoliageCell.generateTerrainCell(node.getMinBound(),node.getData().lod);
WorldOctTreeNode<FoliageCell> 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<FoliageCell> 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() - HALF_RES_LOD
&&
this.getMinDistance(pos, node, distCache) <= QUARTER_RES_DIST
)
||
(
node.getLevel() == this.chunkTree.getMaxLevel() - QUARTER_RES_LOD
&&
this.getMinDistance(pos, node, distCache) <= EIGHTH_RES_DIST
)
||
(
node.getLevel() == this.chunkTree.getMaxLevel() - EIGHTH_RES_LOD
&&
this.getMinDistance(pos, node, distCache) <= SIXTEENTH_RES_DIST
)
||
(
node.getLevel() == this.chunkTree.getMaxLevel() - SIXTEENTH_RES_LOD
&&
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<FoliageCell> 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() - HALF_RES_LOD
&&
this.getMinDistance(pos, node, distCache) <= QUARTER_RES_DIST
)
||
(
node.getLevel() == this.chunkTree.getMaxLevel() - QUARTER_RES_LOD
&&
this.getMinDistance(pos, node, distCache) <= EIGHTH_RES_DIST
)
||
(
node.getLevel() == this.chunkTree.getMaxLevel() - EIGHTH_RES_LOD
&&
this.getMinDistance(pos, node, distCache) <= SIXTEENTH_RES_DIST
)
||
(
node.getLevel() == this.chunkTree.getMaxLevel() - SIXTEENTH_RES_LOD
&&
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<FoliageCell> 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<FoliageCell> node){
if(node.getChildren().size() > 0){
for(WorldOctTreeNode<FoliageCell> 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<FoliageCell> node){
if(!node.getData().hasGenerated()){
for(WorldOctTreeNode<FoliageCell> 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 foliage cell as updateable
* @param worldX The world x position
* @param worldY The world y position
* @param worldZ The world z position
*/
public void markUpdateable(float worldX, float worldY, float worldZ){
throw new Error("Unimplemented");
}
/**
* Requests all chunks for a given foliage cell
* @param cell The cell
* @return true if all cells were successfully requested, false otherwise
*/
private boolean requestChunks(WorldOctTree.WorldOctTreeNode<FoliageCell> node){
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.clientTerrainManager.containsChunkDataAtWorldPoint(worldPos.x, worldPos.y, worldPos.z, 0)
){
//client should request chunk data from server for each chunk necessary to create the model
LoggerInterface.loggerNetworking.DEBUG("(Client) Send Request for terrain at " + worldPos);
if(!Globals.clientTerrainManager.requestChunk(worldPos.x, worldPos.y, worldPos.z, 0)){
return false;
}
}
return true;
}
/**
* Checks if all chunk data required to generate this foliage cell is present
* @param node The node
* @param highResFace The higher resolution face of a not-full-resolution chunk. Null if the chunk is max resolution or there is no higher resolution face for the current chunk
* @return true if all data is available, false otherwise
*/
private boolean containsDataToGenerate(WorldOctTree.WorldOctTreeNode<FoliageCell> node){
FoliageCell cell = node.getData();
Vector3i worldPos = cell.getWorldPos();
if(!Globals.clientTerrainManager.containsChunkDataAtWorldPoint(worldPos.x, worldPos.y, worldPos.z, 0)){
return false;
}
return true;
}
/**
* Sets whether the foliage 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 foliage 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 foliage 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<FoliageCell> 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<FoliageCell>> children = new LinkedList<WorldOctTreeNode<FoliageCell>>(node.getChildren());
for(WorldOctTreeNode<FoliageCell> 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 foliage cell manager has initialized or not
* @return true if it has initialized, false otherwise
*/
public boolean isInitialized(){
return this.initialized;
}
/**
* Gets the foliage 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 foliage cell if it exists, null otherwise
*/
public FoliageCell getFoliageCell(int worldX, int worldY, int worldZ){
WorldOctTreeNode<FoliageCell> node = this.chunkTree.search(new Vector3i(worldX,worldY,worldZ), false);
if(node != null){
return node.getData();
}
return null;
}
}

View File

@ -0,0 +1,338 @@
package electrosphere.client.terrain.foliage;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.joml.Quaterniond;
import org.joml.Vector3d;
import org.joml.Vector3i;
import org.lwjgl.BufferUtils;
import electrosphere.client.terrain.cache.ChunkData;
import electrosphere.engine.Globals;
import electrosphere.engine.assetmanager.queue.QueuedTexture;
import electrosphere.entity.ClientEntityUtils;
import electrosphere.entity.Entity;
import electrosphere.entity.EntityCreationUtils;
import electrosphere.entity.EntityUtils;
import electrosphere.entity.state.foliage.AmbientFoliage;
import electrosphere.game.data.foliage.type.FoliageType;
import electrosphere.logger.LoggerInterface;
import electrosphere.renderer.actor.instance.TextureInstancedActor;
import electrosphere.server.terrain.manager.ServerTerrainChunk;
/**
* Generates a foliage model
*/
public class FoliageModel {
/**
* 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;
/**
* Wiggle room in number of entries
*/
static final int BUFFER_WIGGLE_ROOM = 200;
/**
* The interval to space along
*/
static final int TARGET_FOLIAGE_SPACING = 50;
/**
* The target number of foliage to place per cell
*/
static final int TARGET_FOLIAGE_PER_CELL = TARGET_FOLIAGE_SPACING * TARGET_FOLIAGE_SPACING + BUFFER_WIGGLE_ROOM;
/**
* The length of the ray to ground test with
*/
static final float RAY_LENGTH = 1.0f;
/**
* The height above the chunk to start from when sampling downwards
*/
static final float SAMPLE_START_HEIGHT = 0.5f;
/**
* The ID of the air voxel
*/
static final int AIR_VOXEL_ID = 0;
/**
* <p>
* Size of a single item of foliage in the texture buffer
* </p>
* A lot of these are x 4 to account for size of float
* 3 x 4 for position
* 2 x 4 for euler rotation
*
*
* eventually:
* grass type
* color
* wind characteristics?
*/
static final int SINGLE_FOLIAGE_DATA_SIZE_BYTES = 3 * 4 + 2 * 4;
/**
* Vertex shader path
*/
protected static final String vertexPath = "Shaders/entities/foliage/foliage.vs";
/**
* Fragment shader path
*/
protected static final String fragmentPath = "Shaders/entities/foliage/foliage.fs";
/**
* Used for generating foliage cells
*/
static final ExecutorService generationService = Executors.newFixedThreadPool(2);
/**
* Creates a client foliage chunk based on weights and values provided
* @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 hasFoliage true if the chunk has polygons to generate a model with, false otherwise
* @return The terrain chunk entity
*/
public static Entity clientCreateFoliageChunkEntity(
List<String> foliageTypesSupported,
int scale,
Entity modelEntity,
Vector3d realPos,
Vector3i worldPos,
Vector3i voxelPos,
FoliageCell notifyTarget,
Entity toDelete
){
Globals.profiler.beginAggregateCpuSample("FoliageModel.clientCreateFoliageChunkEntity");
Entity rVal = EntityCreationUtils.createClientSpatialEntity();
generationService.submit(() -> {
try {
Random placementRandomizer = new Random();
//get type
String foliageTypeName = foliageTypesSupported.get(0);
FoliageType foliageType = Globals.gameConfigCurrent.getFoliageMap().getFoliage(foliageTypeName);
//create cell and buffer
ByteBuffer buffer = BufferUtils.createByteBuffer(TARGET_FOLIAGE_PER_CELL * SINGLE_FOLIAGE_DATA_SIZE_BYTES);
if(buffer.capacity() < TARGET_FOLIAGE_PER_CELL * SINGLE_FOLIAGE_DATA_SIZE_BYTES){
LoggerInterface.loggerEngine.WARNING("Failed to allocate data for foliage cell! " + buffer.limit());
}
FloatBuffer floatBufferView = buffer.asFloatBuffer();
int drawCount = 0;
for(int x = 0; x < scale; x++){
for(int y = 0; y < scale; y++){
for(int z = 0; z < scale; z++){
ChunkData data = Globals.clientTerrainManager.getChunkDataAtWorldPoint(worldPos,ChunkData.NO_STRIDE);
if(data == null){
continue;
}
drawCount = drawCount + FoliageModel.insertBlades(
realPos, voxelPos,
scale, placementRandomizer,
x, y, z,
floatBufferView, data
);
}
}
}
if(drawCount > 0){
buffer.position(0);
buffer.limit(TARGET_FOLIAGE_PER_CELL * SINGLE_FOLIAGE_DATA_SIZE_BYTES);
//construct data texture
QueuedTexture queuedAsset = new QueuedTexture(buffer,SINGLE_FOLIAGE_DATA_SIZE_BYTES / 4,TARGET_FOLIAGE_PER_CELL);
Globals.assetManager.queuedAsset(queuedAsset);
TextureInstancedActor.attachTextureInstancedActor(modelEntity, foliageType.getGraphicsTemplate().getModel().getPath(), vertexPath, fragmentPath, queuedAsset, drawCount);
ClientEntityUtils.initiallyPositionEntity(modelEntity, realPos, new Quaterniond());
EntityUtils.getScale(modelEntity).set(1,1,1);
//add ambient foliage behavior tree
AmbientFoliage.attachAmbientFoliageTree(modelEntity, 1.0f, foliageType.getGrowthModel().getGrowthRate());
}
} catch (Error e){
LoggerInterface.loggerEngine.ERROR(e);
} catch(Exception e){
LoggerInterface.loggerEngine.ERROR(e);
}
});
Globals.profiler.endCpuSample();
return rVal;
}
/**
* Insert blades of grass into the entity
* @param vX the x offset of the voxel
* @param vY the y offset of the voxel
* @param vZ the z offset of the voxel
* @param floatBufferView the gpu data buffer
* @param chunkData the chunk data
* @return the number of blades of grass added
*/
protected static int insertBlades(
Vector3d realPos, Vector3i voxelPos,
int scale, Random placementRandomizer,
int vX, int vY, int vZ,
FloatBuffer floatBufferView, ChunkData chunkData
){
int rVal = 0;
//get positions offset
Vector3d voxelRealPos = new Vector3d(realPos).add(vX,vY,vZ);
Vector3i currVoxelPos = new Vector3i(voxelPos).add(vX,vY,vZ);
//check that the current voxel even supports foliage
boolean shouldGenerate = false;
List<String> foliageTypesSupported = null;
if(chunkData != null && currVoxelPos.y + 1 < ServerTerrainChunk.CHUNK_DIMENSION){
foliageTypesSupported = Globals.gameConfigCurrent.getVoxelData().getTypeFromId(chunkData.getType(currVoxelPos)).getAmbientFoliage();
boolean airAbove = chunkData.getType(currVoxelPos.x,currVoxelPos.y+1,currVoxelPos.z) == AIR_VOXEL_ID;
if(foliageTypesSupported != null && airAbove){
shouldGenerate = true;
}
}
if(shouldGenerate){
//construct simple grid to place foliage on
Vector3d sample_00 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).add(-0.5,SAMPLE_START_HEIGHT,-0.5), new Vector3d(0,-1,0), RAY_LENGTH);
Vector3d sample_01 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).add(-0.5,SAMPLE_START_HEIGHT, 0), new Vector3d(0,-1,0), RAY_LENGTH);
Vector3d sample_02 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).add(-0.5,SAMPLE_START_HEIGHT, 0.5), new Vector3d(0,-1,0), RAY_LENGTH);
Vector3d sample_10 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).add( 0,SAMPLE_START_HEIGHT,-0.5), new Vector3d(0,-1,0), RAY_LENGTH);
Vector3d sample_11 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).add( 0,SAMPLE_START_HEIGHT, 0), new Vector3d(0,-1,0), RAY_LENGTH);
Vector3d sample_12 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).add( 0,SAMPLE_START_HEIGHT, 0.5), new Vector3d(0,-1,0), RAY_LENGTH);
Vector3d sample_20 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).add( 0.5,SAMPLE_START_HEIGHT,-0.5), new Vector3d(0,-1,0), RAY_LENGTH);
Vector3d sample_21 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).add( 0.5,SAMPLE_START_HEIGHT, 0), new Vector3d(0,-1,0), RAY_LENGTH);
Vector3d sample_22 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(voxelRealPos).add( 0.5,SAMPLE_START_HEIGHT, 0.5), new Vector3d(0,-1,0), RAY_LENGTH);
//get the heights of each sample
float height_11 = (float)(sample_11 != null ? sample_11.y : 0);
float height_00 = (float)(sample_00 != null ? sample_00.y : height_11);
float height_01 = (float)(sample_01 != null ? sample_01.y : height_11);
float height_02 = (float)(sample_02 != null ? sample_02.y : height_11);
float height_10 = (float)(sample_10 != null ? sample_10.y : height_11);
float height_12 = (float)(sample_12 != null ? sample_12.y : height_11);
float height_20 = (float)(sample_20 != null ? sample_20.y : height_11);
float height_21 = (float)(sample_21 != null ? sample_21.y : height_11);
float height_22 = (float)(sample_22 != null ? sample_22.y : height_11);
//each height is in real world coordinates that are absolute
//when rendering, there's already a y offset for the center of the field of grass (based on the model matrix)
//so when offseting the position of the blade of grass RELATIVE to the overall instance being drawn, need to subtract the real world coordinates of the overall instance
//in other words realPos SPECIFICALLY for the y dimension, for x and z you don't need to worry about it
//if we don't find data for the center sample, can't place grass so don't create entity
if(sample_11 != null){
//generate positions to place
for(int x = 0; x < TARGET_FOLIAGE_SPACING; x=x+scale){
for(int z = 0; z < TARGET_FOLIAGE_SPACING; z=z+scale){
//get position to place
double rand1 = placementRandomizer.nextDouble();
double rand2 = placementRandomizer.nextDouble();
double relativePositionOnGridX = x / (1.0 * TARGET_FOLIAGE_SPACING) + rand1 / TARGET_FOLIAGE_SPACING;
double relativePositionOnGridZ = z / (1.0 * TARGET_FOLIAGE_SPACING) + rand2 / TARGET_FOLIAGE_SPACING;
double offsetX = relativePositionOnGridX - 0.5;
double offsetZ = relativePositionOnGridZ - 0.5;
//determine quadrant we're placing in
double offsetY = 0;
boolean addBlade = false;
if(relativePositionOnGridX >=0.5){
if(relativePositionOnGridZ >= 0.5){
relativePositionOnGridX = relativePositionOnGridX - 0.5;
relativePositionOnGridZ = relativePositionOnGridZ - 0.5;
relativePositionOnGridX /= 0.5;
relativePositionOnGridZ /= 0.5;
// System.out.println(relativePositionOnGridX + " " + relativePositionOnGridZ);
//if we have heights for all four surrounding spots, interpolate for y value
if(sample_11 != null && sample_12 != null && sample_21 != null && sample_22 != null){
offsetY =
height_11 * (1-relativePositionOnGridX) * (1-relativePositionOnGridZ) +
height_12 * (1-relativePositionOnGridX) * ( relativePositionOnGridZ) +
height_21 * ( relativePositionOnGridX) * (1-relativePositionOnGridZ) +
height_22 * ( relativePositionOnGridX) * ( relativePositionOnGridZ);
addBlade = true;
}
} else {
relativePositionOnGridX = relativePositionOnGridX - 0.5;
relativePositionOnGridX /= 0.5;
relativePositionOnGridZ /= 0.5;
//if we have heights for all four surrounding spots, interpolate for y value
if(sample_10 != null && sample_11 != null && sample_20 != null && sample_21 != null){
offsetY =
height_10 * (1-relativePositionOnGridX) * (1-relativePositionOnGridZ) +
height_11 * (1-relativePositionOnGridX) * ( relativePositionOnGridZ) +
height_20 * ( relativePositionOnGridX) * (1-relativePositionOnGridZ) +
height_21 * ( relativePositionOnGridX) * ( relativePositionOnGridZ);
addBlade = true;
}
}
} else {
if(relativePositionOnGridZ >= 0.5){
relativePositionOnGridZ = relativePositionOnGridZ - 0.5;
relativePositionOnGridX /= 0.5;
relativePositionOnGridZ /= 0.5;
//if we have heights for all four surrounding spots, interpolate for y value
if(sample_01 != null && sample_02 != null && sample_11 != null && sample_12 != null){
offsetY =
height_01 * (1-relativePositionOnGridX) * (1-relativePositionOnGridZ) +
height_02 * (1-relativePositionOnGridX) * ( relativePositionOnGridZ) +
height_11 * ( relativePositionOnGridX) * (1-relativePositionOnGridZ) +
height_12 * ( relativePositionOnGridX) * ( relativePositionOnGridZ);
addBlade = true;
}
} else {
relativePositionOnGridX /= 0.5;
relativePositionOnGridZ /= 0.5;
//if we have heights for all four surrounding spots, interpolate for y value
if(sample_00 != null && sample_01 != null && sample_10 != null && sample_11 != null){
offsetY =
height_00 * (1-relativePositionOnGridX) * (1-relativePositionOnGridZ) +
height_01 * (1-relativePositionOnGridX) * ( relativePositionOnGridZ) +
height_10 * ( relativePositionOnGridX) * (1-relativePositionOnGridZ) +
height_11 * ( relativePositionOnGridX) * ( relativePositionOnGridZ);
addBlade = true;
}
}
}
if(addBlade){
//convert y to relative to chunk
offsetY = offsetY - realPos.y;
double rotVar = placementRandomizer.nextDouble() * Math.PI * 2;
double rotVar2 = placementRandomizer.nextDouble();
if(floatBufferView.limit() >= floatBufferView.position() + SINGLE_FOLIAGE_DATA_SIZE_BYTES / 4){
floatBufferView.put((float)offsetX + vX);
floatBufferView.put((float)offsetY + vY);
floatBufferView.put((float)offsetZ + vZ);
floatBufferView.put((float)rotVar);
floatBufferView.put((float)rotVar2);
rVal++;
}
}
}
}
}
}
return rVal;
}
/**
* Shuts down the model generation threads
*/
public static void haltThreads(){
generationService.shutdown();
}
}

View File

@ -523,7 +523,8 @@ public class PhysicsEntityUtils {
collisionEngine.getCollidables(); collisionEngine.getCollidables();
throw new Error("Collision engine collidables are null!"); throw new Error("Collision engine collidables are null!");
} }
for(Collidable collidable : collidableList){ for(int i = 0; i < collidableList.size(); i++){
Collidable collidable = collidableList.get(i);
Entity entity = collidable.getParent(); Entity entity = collidable.getParent();
DBody body = PhysicsEntityUtils.getDBody(entity); DBody body = PhysicsEntityUtils.getDBody(entity);
if(body != null && body.isEnabled() && !body.isKinematic()){ if(body != null && body.isEnabled() && !body.isKinematic()){

View File

@ -17,13 +17,13 @@ import electrosphere.client.chemistry.ClientChemistryCollisionCallback;
import electrosphere.client.entity.particle.ParticleService; import electrosphere.client.entity.particle.ParticleService;
import electrosphere.client.fluid.cells.FluidCellManager; import electrosphere.client.fluid.cells.FluidCellManager;
import electrosphere.client.fluid.manager.ClientFluidManager; import electrosphere.client.fluid.manager.ClientFluidManager;
import electrosphere.client.foliagemanager.ClientFoliageManager;
import electrosphere.client.player.ClientPlayerData; import electrosphere.client.player.ClientPlayerData;
import electrosphere.client.scene.ClientSceneWrapper; import electrosphere.client.scene.ClientSceneWrapper;
import electrosphere.client.scene.ClientWorldData; import electrosphere.client.scene.ClientWorldData;
import electrosphere.client.sim.ClientSimulation; import electrosphere.client.sim.ClientSimulation;
import electrosphere.client.terrain.cells.ClientDrawCellManager; import electrosphere.client.terrain.cells.ClientDrawCellManager;
import electrosphere.client.terrain.cells.VoxelTextureAtlas; import electrosphere.client.terrain.cells.VoxelTextureAtlas;
import electrosphere.client.terrain.foliage.FoliageCellManager;
import electrosphere.client.terrain.manager.ClientTerrainManager; import electrosphere.client.terrain.manager.ClientTerrainManager;
import electrosphere.client.ui.menu.WindowUtils; import electrosphere.client.ui.menu.WindowUtils;
import electrosphere.collision.CollisionEngine; import electrosphere.collision.CollisionEngine;
@ -343,7 +343,8 @@ public class Globals {
public static InstanceManager clientInstanceManager = new InstanceManager(); public static InstanceManager clientInstanceManager = new InstanceManager();
//client side foliage manager //client side foliage manager
public static ClientFoliageManager clientFoliageManager; // public static ClientFoliageManager clientFoliageManager;
public static FoliageCellManager foliageCellManager;
//client world data //client world data
public static ClientWorldData clientWorldData; public static ClientWorldData clientWorldData;

View File

@ -1,6 +1,7 @@
package electrosphere.engine.assetmanager.queue; package electrosphere.engine.assetmanager.queue;
import java.awt.image.BufferedImage; import java.awt.image.BufferedImage;
import java.nio.ByteBuffer;
import electrosphere.engine.Globals; import electrosphere.engine.Globals;
import electrosphere.renderer.texture.Texture; import electrosphere.renderer.texture.Texture;
@ -19,6 +20,21 @@ public class QueuedTexture implements QueuedAsset {
//data to be loaded //data to be loaded
BufferedImage data; BufferedImage data;
/**
* The byte buffer
*/
ByteBuffer buffer;
/**
* Width of the image
*/
int width = -1;
/**
* Height of the image
*/
int height = -1;
/** /**
* Creates the queued texture object * Creates the queued texture object
@ -28,9 +44,25 @@ public class QueuedTexture implements QueuedAsset {
this.data = image; this.data = image;
} }
/**
* Creates the queued texture object
* @param buffer The data to buffer
* @param width The width of the buffer
* @param height The height of the buffer
*/
public QueuedTexture(ByteBuffer buffer, int width, int height){
this.buffer = buffer;
this.width = width;
this.height = height;
}
@Override @Override
public void load() { public void load() {
texture = new Texture(Globals.renderingEngine.getOpenGLState(), data); if(data != null){
texture = new Texture(Globals.renderingEngine.getOpenGLState(), data);
} else if(buffer != null){
texture = new Texture(Globals.renderingEngine.getOpenGLState(),buffer,width,height);
}
hasLoaded = true; hasLoaded = true;
} }
@ -46,6 +78,30 @@ public class QueuedTexture implements QueuedAsset {
public Texture getTexture(){ public Texture getTexture(){
return texture; return texture;
} }
/**
* Gets the buffer data
* @return The buffer data
*/
public ByteBuffer getBuffer() {
return buffer;
}
/**
* Gets the width of the buffer
* @return The width
*/
public int getWidth() {
return width;
}
/**
* Gets the height of the buffer
* @return The height
*/
public int getHeight() {
return height;
}

View File

@ -8,9 +8,9 @@ import org.joml.Vector3f;
import electrosphere.client.entity.camera.CameraEntityUtils; import electrosphere.client.entity.camera.CameraEntityUtils;
import electrosphere.client.entity.crosshair.Crosshair; import electrosphere.client.entity.crosshair.Crosshair;
import electrosphere.client.fluid.cells.FluidCellManager; import electrosphere.client.fluid.cells.FluidCellManager;
import electrosphere.client.foliagemanager.ClientFoliageManager;
import electrosphere.client.sim.ClientSimulation; import electrosphere.client.sim.ClientSimulation;
import electrosphere.client.terrain.cells.ClientDrawCellManager; import electrosphere.client.terrain.cells.ClientDrawCellManager;
import electrosphere.client.terrain.foliage.FoliageCellManager;
import electrosphere.client.ui.menu.MenuGenerators; import electrosphere.client.ui.menu.MenuGenerators;
import electrosphere.client.ui.menu.WindowStrings; import electrosphere.client.ui.menu.WindowStrings;
import electrosphere.client.ui.menu.WindowUtils; import electrosphere.client.ui.menu.WindowUtils;
@ -91,10 +91,10 @@ public class ClientLoading {
Globals.controlHandler.hintUpdateControlState(ControlHandler.ControlsState.NO_INPUT); Globals.controlHandler.hintUpdateControlState(ControlHandler.ControlsState.NO_INPUT);
//initialize the "real" objects simulation //initialize the "real" objects simulation
initClientSimulation(); initClientSimulation();
//init foliage manager
initFoliageManager();
//initialize the cell manager (client) //initialize the cell manager (client)
initDrawCellManager(true); initDrawCellManager(true);
//init foliage manager
initFoliageManager();
//init the fluid cell manager //init the fluid cell manager
initFluidCellManager(true); initFluidCellManager(true);
//initialize the basic graphical entities of the world (skybox, camera) //initialize the basic graphical entities of the world (skybox, camera)
@ -369,8 +369,9 @@ public class ClientLoading {
* Starts up the foliage manager * Starts up the foliage manager
*/ */
private static void initFoliageManager(){ private static void initFoliageManager(){
Globals.clientFoliageManager = new ClientFoliageManager(); Globals.foliageCellManager = new FoliageCellManager(Globals.clientWorldData.getWorldDiscreteSize());
Globals.clientFoliageManager.start(); Globals.foliageCellManager.init();
// Globals.foliageCellManager.start();
} }

View File

@ -6,6 +6,7 @@ import java.util.List;
import java.util.concurrent.Semaphore; import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import electrosphere.client.terrain.foliage.FoliageModel;
import electrosphere.engine.Globals; import electrosphere.engine.Globals;
import electrosphere.engine.loadingthreads.LoadingThread; import electrosphere.engine.loadingthreads.LoadingThread;
import electrosphere.engine.threads.LabeledThread.ThreadLabel; import electrosphere.engine.threads.LabeledThread.ThreadLabel;
@ -124,6 +125,7 @@ public class ThreadManager {
* Halds all terrain chunk threads * Halds all terrain chunk threads
*/ */
TerrainChunk.haltThreads(); TerrainChunk.haltThreads();
FoliageModel.haltThreads();
// //
//interrupt all threads //interrupt all threads

View File

@ -113,7 +113,7 @@ public class TerrainProtocol implements ClientProtocolTemplate<TerrainMessage> {
Globals.clientDrawCellManager.markUpdateable(worldPosToUpdate.x, worldPosToUpdate.y, worldPosToUpdate.z); Globals.clientDrawCellManager.markUpdateable(worldPosToUpdate.x, worldPosToUpdate.y, worldPosToUpdate.z);
} }
} }
Globals.clientFoliageManager.evaluateChunk(worldPos); // Globals.clientFoliageManager.evaluateChunk(worldPos);
} }
} break; } break;
case SENDFLUIDDATA: { case SENDFLUIDDATA: {

View File

@ -4,6 +4,7 @@ import org.joml.Matrix4d;
import org.joml.Vector3d; import org.joml.Vector3d;
import electrosphere.engine.Globals; import electrosphere.engine.Globals;
import electrosphere.engine.assetmanager.queue.QueuedTexture;
import electrosphere.entity.Entity; import electrosphere.entity.Entity;
import electrosphere.entity.EntityDataStrings; import electrosphere.entity.EntityDataStrings;
import electrosphere.renderer.OpenGLState; import electrosphere.renderer.OpenGLState;
@ -27,6 +28,16 @@ public class TextureInstancedActor {
//the draw count of the texture instanced actor //the draw count of the texture instanced actor
int drawCount; int drawCount;
/**
* The queued texture
*/
QueuedTexture queuedTexture;
/**
* Set the queued texture pointer to the material
*/
boolean setQueuedTexturePointer = false;
//shader paths //shader paths
String vertexShaderPath; String vertexShaderPath;
String fragmentShaderPath; String fragmentShaderPath;
@ -44,6 +55,19 @@ public class TextureInstancedActor {
this.fragmentShaderPath = fragmentShaderPath; this.fragmentShaderPath = fragmentShaderPath;
} }
/**
* Creates an instanced actor
* @param modelPath The path of the model this actor uses
*/
protected TextureInstancedActor(String modelPath, String vertexShaderPath, String fragmentShaderPath, QueuedTexture dataTexture, int drawCount){
this.modelPath = modelPath;
this.material = new Material();
this.queuedTexture = dataTexture;
this.drawCount = drawCount;
this.vertexShaderPath = vertexShaderPath;
this.fragmentShaderPath = fragmentShaderPath;
}
/** /**
* Attaches a TextureInstancedActor to an entity * Attaches a TextureInstancedActor to an entity
* @param parent The entity * @param parent The entity
@ -54,6 +78,17 @@ public class TextureInstancedActor {
TextureInstancedActor newActor = new TextureInstancedActor(modelPath, vertexShaderPath, fragmentShaderPath, dataTexture, drawCount); TextureInstancedActor newActor = new TextureInstancedActor(modelPath, vertexShaderPath, fragmentShaderPath, dataTexture, drawCount);
parent.putData(EntityDataStrings.TEXTURE_INSTANCED_ACTOR, newActor); parent.putData(EntityDataStrings.TEXTURE_INSTANCED_ACTOR, newActor);
} }
/**
* Attaches a TextureInstancedActor to an entity
* @param parent The entity
* @param modelPath The path to the model for this instanced actor
* @param dataTexture The data texture containing data for this actor
*/
public static void attachTextureInstancedActor(Entity parent, String modelPath, String vertexShaderPath, String fragmentShaderPath, QueuedTexture dataTexture, int drawCount){
TextureInstancedActor newActor = new TextureInstancedActor(modelPath, vertexShaderPath, fragmentShaderPath, dataTexture, drawCount);
parent.putData(EntityDataStrings.TEXTURE_INSTANCED_ACTOR, newActor);
}
/** /**
* Draws the instanced actor. Should be called normally in a loop as if this was a regular actor. * Draws the instanced actor. Should be called normally in a loop as if this was a regular actor.
@ -63,7 +98,21 @@ public class TextureInstancedActor {
public void draw(RenderPipelineState renderPipelineState, OpenGLState openGLState){ public void draw(RenderPipelineState renderPipelineState, OpenGLState openGLState){
Model model = Globals.assetManager.fetchModel(modelPath); Model model = Globals.assetManager.fetchModel(modelPath);
VisualShader shader = Globals.assetManager.fetchShader(vertexShaderPath, fragmentShaderPath); VisualShader shader = Globals.assetManager.fetchShader(vertexShaderPath, fragmentShaderPath);
if(model != null && shader != null){ if(queuedTexture != null && !setQueuedTexturePointer && queuedTexture.getTexture() != null){
this.material.setTexturePointer(queuedTexture.getTexture().getTexturePointer());
setQueuedTexturePointer = true;
}
if(
model != null &&
shader != null &&
(
queuedTexture == null ||
(
queuedTexture != null &&
queuedTexture.getTexture() != null
)
)
){
//setup render pipeline //setup render pipeline
boolean instancedState = renderPipelineState.getInstanced(); boolean instancedState = renderPipelineState.getInstanced();
boolean materialState = renderPipelineState.getUseMaterial(); boolean materialState = renderPipelineState.getUseMaterial();

View File

@ -89,7 +89,7 @@ public class MainContentPipeline implements RenderPipeline {
currentActor.draw(renderPipelineState,openGLState); currentActor.draw(renderPipelineState,openGLState);
} }
} }
Globals.clientFoliageManager.draw(); Globals.foliageCellManager.draw();
for(Entity currentEntity : Globals.clientScene.getEntitiesWithTag(EntityTags.DRAW_INSTANCED)){ for(Entity currentEntity : Globals.clientScene.getEntitiesWithTag(EntityTags.DRAW_INSTANCED)){
Vector3d position = EntityUtils.getPosition(currentEntity); Vector3d position = EntityUtils.getPosition(currentEntity);
if(shouldDrawSolidPass(currentEntity)){ if(shouldDrawSolidPass(currentEntity)){