octree foliage work

This commit is contained in:
austin 2024-09-11 17:33:21 -04:00
parent f234c6423c
commit ce8261aa13
9 changed files with 1157 additions and 652 deletions

View File

@ -1,3 +1,3 @@
#maven.buildNumber.plugin properties file
#Tue Sep 10 20:21:08 EDT 2024
buildNumber=328
#Wed Sep 11 17:27:47 EDT 2024
buildNumber=329

View File

@ -1,124 +1,42 @@
package electrosphere.client.foliagemanager;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.joml.Quaterniond;
import org.joml.Vector3d;
import org.joml.Vector3f;
import org.joml.Vector3i;
import org.lwjgl.BufferUtils;
import electrosphere.client.terrain.cache.ChunkData;
import electrosphere.engine.Globals;
import electrosphere.entity.Entity;
import electrosphere.entity.EntityCreationUtils;
import electrosphere.entity.EntityDataStrings;
import electrosphere.entity.EntityTags;
import electrosphere.entity.EntityUtils;
import electrosphere.entity.state.foliage.AmbientFoliage;
import electrosphere.game.data.foliage.type.FoliageType;
import electrosphere.renderer.actor.instance.TextureInstancedActor;
import electrosphere.renderer.buffer.ShaderAttribute;
import electrosphere.renderer.buffer.HomogenousUniformBuffer.HomogenousBufferTypes;
import electrosphere.renderer.texture.Texture;
import electrosphere.util.ds.Octree;
import electrosphere.util.ds.Octree.OctreeNode;
/**
* Manages ambient foliage (grass, small plants, etc) that should be shown, typically instanced
*/
public class ClientFoliageManager {
//Random for finding new positions for foliage
Random placementRandomizer = new Random();
//Used to prevent concurrent usage of grassEntities set
boolean ready = false;
//The list of voxel type ids that should have grass generated on top of them
static final List<Integer> grassGeneratingVoxelIds = new ArrayList<Integer>();
//FoliageCells that are active and have foliage that is being drawn
Set<FoliageCell> activeCells = new HashSet<FoliageCell>();
//map of position-based key to foliage cell at the position
Map<String,FoliageCell> locationCellMap = new HashMap<String,FoliageCell>();
//The maximum distance a cell can be away from the player before being destroyed
static final float CELL_DISTANCE_MAX = 15f;
//The maximum number of foliage cells
static final int CELL_COUNT_MAX = 1000;
//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;
//size of a single item of foliage in the texture buffer
/*
* 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?
/**
* The target density for placing foliage
*/
static final int SINGLE_FOLIAGE_DATA_SIZE_BYTES = 3 * 4 + 2 * 4;
//Stores a list of all locations that are currently invalid which map to
//the amount of frames that must pass before they are considered valid to evaluate
Map<String,Integer> locationEvaluationCooldownMap = new ConcurrentHashMap<String,Integer>();
//The number of frames that must pass before a cell can be reevaluated for foliage placement
static final int EVALUATION_COOLDOWN = 100;
float targetDensity = 1.0f;
/**
* Number of chunks to check
*/
int chunkRadius = 1;
/**
* The octree holding all the chunks to evaluate
* The list of all chunks with foliage, currently
*/
Octree<FoliageChunk> chunkTree;
//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;
//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);
}
//shader paths
static final String vertexPath = "Shaders/entities/foliage/foliage.vs";
static final String fragmentPath = "Shaders/entities/foliage/foliage.fs";
List<FoliageChunk> chunks = null;
/**
* Cache used to transfer still-valid chunks to the next frame
*/
List<FoliageChunk> chunkUpdateCache = null;
@ -127,14 +45,9 @@ public class ClientFoliageManager {
* Starts up the foliage manager
*/
public void start(){
//queue ambient foliage models
for(FoliageType foliageType : Globals.gameConfigCurrent.getFoliageMap().getFoliageList()){
if(foliageType.getTokens().contains(FoliageType.TOKEN_AMBIENT)){
Globals.assetManager.addModelPathToQueue(foliageType.getModelPath());
Globals.assetManager.addShaderToQueue(vertexPath, fragmentPath);
}
}
this.chunkTree = new Octree<FoliageChunk>(new Vector3d(0), new Vector3d(1000000000l));
FoliageCell.init();
chunks = new LinkedList<FoliageChunk>();
chunkUpdateCache = new LinkedList<FoliageChunk>();
ready = true;
}
@ -144,101 +57,56 @@ public class ClientFoliageManager {
public void update(){
Globals.profiler.beginCpuSample("ClientFoliageManager.update");
if(ready && this.dependenciesAreReady()){
Vector3d playerPosition = null;
if(Globals.playerEntity != null){
playerPosition = EntityUtils.getPosition(Globals.playerEntity);
}
Vector3i worldPos = Globals.clientWorldData.convertRealToWorldSpace(playerPosition);
if(!chunkTree.containsLeaf(new Vector3d(worldPos))){
this.createFoliageChunk(worldPos);
}
//
//evaluate what cells should be updated
Globals.profiler.beginCpuSample("ClientFoliageManager.update (evaluate foliage chunks)");
this.evaluateChunks(playerPosition, this.chunkTree.getRoot());
Globals.profiler.endCpuSample();
//
//flip through all valid cells and see if you can invalidate any
Globals.profiler.beginCpuSample("ClientFoliageManager.update (talley invalid cells)");
List<FoliageCell> invalidationList = new LinkedList<FoliageCell>();
for(FoliageCell cell : activeCells){
if(
playerPosition != null &&
playerPosition.distance(cell.realPosition) > CELL_DISTANCE_MAX
){
invalidationList.add(cell);
this.flipUpdateCache();
for(int x = -chunkRadius; x < chunkRadius+1; x++){
for(int y = -chunkRadius; y < chunkRadius+1; y++){
for(int z = -chunkRadius; z < chunkRadius+1; z++){
Vector3i worldPos = Globals.clientWorldData.convertRealToWorldSpace(EntityUtils.getPosition(Globals.playerEntity));
updatePosition(worldPos);
}
}
Globals.profiler.endCpuSample();
//
//actually invalidate cells
Globals.profiler.beginCpuSample("ClientFoliageManager.update (actually invalidate cells)");
for(FoliageCell cell : invalidationList){
invalidateCell(cell);
}
Globals.profiler.endCpuSample();
invalidationList.clear();
//for each invalid cell, see if can be revalidated
Globals.profiler.beginCpuSample("ClientFoliageManager.update (revalidate cells)");
for(String key : locationEvaluationCooldownMap.keySet()){
int cooldownTime = locationEvaluationCooldownMap.get(key);
cooldownTime--;
if(cooldownTime <= 0){
String split[] = key.split("_");
worldPos = new Vector3i(Integer.parseInt(split[0]),Integer.parseInt(split[1]),Integer.parseInt(split[2]));
Vector3i voxelPos = new Vector3i(Integer.parseInt(split[3]),Integer.parseInt(split[4]),Integer.parseInt(split[5]));
Vector3d realPos = Globals.clientWorldData.convertWorldToRealSpace(worldPos).add(voxelPos.x,voxelPos.y,voxelPos.z);
ChunkData data = Globals.clientTerrainManager.getChunkDataAtWorldPoint(worldPos);
//evaluate
if(
data != null &&
data.getWeight(voxelPos) > 0 &&
data.getWeight(new Vector3i(voxelPos.x,voxelPos.y + 1,voxelPos.z)) < 0 &&
typeSupportsFoliage(data.getType(voxelPos)) &&
playerPosition.distance(realPos) < CELL_DISTANCE_MAX
){
//create foliage cell
createFoliageCell(worldPos,voxelPos,1,targetDensity);
locationEvaluationCooldownMap.remove(key);
} else {
locationEvaluationCooldownMap.put(key, EVALUATION_COOLDOWN);
for(FoliageChunk chunk : this.chunkUpdateCache){
chunk.destroy();
}
} else {
locationEvaluationCooldownMap.put(key, cooldownTime);
}
}
Globals.profiler.endCpuSample();
//invalidate foliage cells that have had their voxel changed
invalidateModifiedPositions();
this.chunkUpdateCache.clear();
}
Globals.profiler.endCpuSample();
}
/**
* Gets a good position to put a new blade of grass
* @param centerPosition The player's position
* @return The new position for the blade of grass
* Evaluates a position in the world
* @param worldPos The world position
*/
protected Vector3d getNewPosition(Vector3d centerPosition){
double angle = placementRandomizer.nextDouble() * Math.PI * 2;
double radius = placementRandomizer.nextDouble();
return new Vector3d(
centerPosition.x + Math.cos(angle) * radius,
centerPosition.y,
centerPosition.z + Math.sin(angle) * radius
);
private void updatePosition(Vector3i worldPos){
FoliageChunk foundChunk = null;
for(FoliageChunk chunk : chunkUpdateCache){
if(chunk.getWorldPos().equals(worldPos)){
this.chunks.add(chunk);
foundChunk = chunk;
break;
}
}
if(foundChunk == null){
foundChunk = new FoliageChunk(worldPos);
this.chunks.add(foundChunk);
foundChunk.initCells();
} else {
chunkUpdateCache.remove(foundChunk);
}
foundChunk.updateCells();
}
/**
* Gets a new rotation for a blade of grass
* @return The rotation
* Flips references for the chunk list and chunk update cache
*/
protected Quaterniond getNewRotation(){
return new Quaterniond().rotationX(-Math.PI / 2.0f).rotateLocalY(Math.PI * placementRandomizer.nextFloat()).normalize();
private void flipUpdateCache(){
List<FoliageChunk> list1 = this.chunks;
this.chunks = this.chunkUpdateCache;
this.chunkUpdateCache = list1;
if(this.chunks.size() > 0){
throw new IllegalStateException("Update cache should have be empty and it isn't!");
}
}
/**
@ -247,306 +115,10 @@ public class ClientFoliageManager {
* @param voxelPosition The voxel position of the cell
* @return The key for the cell
*/
private String getFoliageCellKey(Vector3i worldPosition, Vector3i voxelPosition){
protected static String getFoliageCellKey(Vector3i worldPosition, Vector3i voxelPosition){
return worldPosition.x + "_" + worldPosition.y + "_" + worldPosition.z + "_" + voxelPosition.x + "_" + voxelPosition.y + "_" + voxelPosition.z;
}
/**
* Makes an already created entity a drawable, instanced entity (client only) by backing it with an InstancedActor
* @param entity The entity
* @param modelPath The model path for the model to back the instanced actor
* @param capacity The capacity of the instanced actor to draw
*/
public static void makeEntityTextureInstancedFoliage(Entity entity, String modelPath, int capacity){
entity.putData(EntityDataStrings.INSTANCED_ACTOR, Globals.clientInstanceManager.createInstancedActor(modelPath, vertexPath, fragmentPath, attributes, capacity));
entity.putData(EntityDataStrings.DATA_STRING_POSITION, new Vector3d(0,0,0));
entity.putData(EntityDataStrings.DATA_STRING_ROTATION, new Quaterniond().identity());
entity.putData(EntityDataStrings.DATA_STRING_SCALE, new Vector3f(1,1,1));
entity.putData(EntityDataStrings.DRAW_SOLID_PASS, true);
Globals.clientScene.registerEntity(entity);
Globals.clientScene.registerEntityToTag(entity, EntityTags.DRAW_INSTANCED_MANAGED);
}
/**
* Evaluates a chunk to see where foliage cells should be created or updated
* @param worldPos The world position of the chunk
*/
public void evaluateChunk(Vector3i worldPos){
ChunkData data = Globals.clientTerrainManager.getChunkDataAtWorldPoint(worldPos);
for(int x = 0; x < ChunkData.CHUNK_SIZE; x++){
//can't go to very top 'cause otherwise there would be no room to put grass
for(int y = 0; y < ChunkData.CHUNK_SIZE - 1; y++){
for(int z = 0; z < ChunkData.CHUNK_SIZE; z++){
Vector3i currentPos = new Vector3i(x,y,z);
String key = getFoliageCellKey(worldPos, currentPos);
if(locationCellMap.get(key) != null){
//destroy if there's no longer ground or
//if the cell above is now occupied or
//if the lower cell is no longer supporting foliage
if(
data.getWeight(currentPos) <= 0 ||
data.getWeight(new Vector3i(x,y + 1,z)) > 0 ||
!typeSupportsFoliage(data.getType(currentPos))
){
//destroy
FoliageCell toDestroy = locationCellMap.get(key);
toDestroy.destroy();
activeCells.remove(toDestroy);
locationCellMap.remove(key);
} else {
//TODO: evaluate if foliage is placed well
}
} else {
//create if current is ground and above is air
if(
!locationEvaluationCooldownMap.containsKey(key) &&
data.getWeight(currentPos) > 0 &&
data.getWeight(new Vector3i(x,y + 1,z)) < 0 &&
typeSupportsFoliage(data.getType(currentPos)) &&
activeCells.size() < CELL_COUNT_MAX
){
//create foliage cell
createFoliageCell(worldPos,currentPos,1,targetDensity);
}
}
}
}
}
//evaluate top cells if chunk above this one exists
ChunkData aboveData = Globals.clientTerrainManager.getChunkDataAtWorldPoint(new Vector3i(worldPos).add(0,1,0));
if(aboveData != null){
for(int x = 0; x < ChunkData.CHUNK_SIZE; x++){
for(int z = 0; z < ChunkData.CHUNK_SIZE; z++){
Vector3i currentPos = new Vector3i(x,ChunkData.CHUNK_SIZE-1,z);
String key = getFoliageCellKey(worldPos, currentPos);
if(locationCellMap.get(key) != null){
//destroy if there's no longer ground or
//if the cell above is now occupied or
//if the lower cell is no longer supporting foliage
if(
data.getWeight(currentPos) <= 0 ||
aboveData.getWeight(new Vector3i(x,0,z)) > 0 ||
!typeSupportsFoliage(data.getType(currentPos))
){
//destroy
FoliageCell toDestroy = locationCellMap.get(key);
toDestroy.destroy();
activeCells.remove(toDestroy);
locationCellMap.remove(key);
} else {
//TODO: evaluate if foliage is placed well
}
} else {
//create if current is ground and above is air
if(
data.getWeight(currentPos) > 0 &&
aboveData.getWeight(new Vector3i(x,0,z)) < 0 &&
typeSupportsFoliage(data.getType(currentPos)) &&
activeCells.size() < CELL_COUNT_MAX
){
//create foliage cell
createFoliageCell(worldPos,currentPos,1,targetDensity);
}
}
}
}
}
}
//the length of the ray to ground test with
static final float RAY_LENGTH = 2.0f;
//the height above the chunk to start from when sampling downwards
static final float SAMPLE_START_HEIGHT = 1.0f;
/**
* Creates a foliage cell at a given position
* @param worldPos The world position
* @param voxelPos The voxel position
*/
private void createFoliageCell(Vector3i worldPos, Vector3i voxelPos, float initialGrowthLevel, float density){
//get foliage types supported
ChunkData data = Globals.clientTerrainManager.getChunkDataAtWorldPoint(worldPos);
List<String> foliageTypesSupported = Globals.gameConfigCurrent.getVoxelData().getTypeFromId(data.getType(voxelPos)).getAmbientFoliage();
if(foliageTypesSupported != null){
Vector3d realPos = new Vector3d(
worldPos.x * ChunkData.CHUNK_SIZE + voxelPos.x,
worldPos.y * ChunkData.CHUNK_SIZE + voxelPos.y,
worldPos.z * ChunkData.CHUNK_SIZE + voxelPos.z
);
//get type
String foliageTypeName = foliageTypesSupported.get(placementRandomizer.nextInt() % foliageTypesSupported.size());
FoliageType foliageType = Globals.gameConfigCurrent.getFoliageMap().getFoliage(foliageTypeName);
//create cell and buffer
FoliageCell cell = new FoliageCell(worldPos, voxelPos, realPos, density);
int FINAL_SPACING = (int)(TARGET_FOLIAGE_SPACING * density);
ByteBuffer buffer = BufferUtils.createByteBuffer(FINAL_SPACING * FINAL_SPACING * SINGLE_FOLIAGE_DATA_SIZE_BYTES);
FloatBuffer floatBufferView = buffer.asFloatBuffer();
//construct simple grid to place foliage on
Vector3d sample_00 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPos).add(-0.5,SAMPLE_START_HEIGHT,-0.5), new Vector3d(0,-1,0), RAY_LENGTH);
Vector3d sample_01 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPos).add(-0.5,SAMPLE_START_HEIGHT, 0), new Vector3d(0,-1,0), RAY_LENGTH);
Vector3d sample_02 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPos).add(-0.5,SAMPLE_START_HEIGHT, 0.5), new Vector3d(0,-1,0), RAY_LENGTH);
Vector3d sample_10 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPos).add( 0,SAMPLE_START_HEIGHT,-0.5), new Vector3d(0,-1,0), RAY_LENGTH);
Vector3d sample_11 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPos).add( 0,SAMPLE_START_HEIGHT, 0), new Vector3d(0,-1,0), RAY_LENGTH);
Vector3d sample_12 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPos).add( 0,SAMPLE_START_HEIGHT, 0.5), new Vector3d(0,-1,0), RAY_LENGTH);
Vector3d sample_20 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPos).add( 0.5,SAMPLE_START_HEIGHT,-0.5), new Vector3d(0,-1,0), RAY_LENGTH);
Vector3d sample_21 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPos).add( 0.5,SAMPLE_START_HEIGHT, 0), new Vector3d(0,-1,0), RAY_LENGTH);
Vector3d sample_22 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPos).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
int drawCount = 0;
for(int x = 0; x < FINAL_SPACING; x++){
for(int z = 0; z < FINAL_SPACING; z++){
//get position to place
double rand1 = placementRandomizer.nextDouble();
double rand2 = placementRandomizer.nextDouble();
double relativePositionOnGridX = x / (1.0 * FINAL_SPACING) + rand1 / FINAL_SPACING;
double relativePositionOnGridZ = z / (1.0 * FINAL_SPACING) + rand2 / FINAL_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();
floatBufferView.put((float)offsetX);
floatBufferView.put((float)offsetY);
floatBufferView.put((float)offsetZ);
floatBufferView.put((float)rotVar);
floatBufferView.put((float)rotVar2);
drawCount++;
}
}
}
buffer.position(0);
buffer.limit(FINAL_SPACING * FINAL_SPACING * SINGLE_FOLIAGE_DATA_SIZE_BYTES);
//construct data texture
Texture dataTexture = new Texture(Globals.renderingEngine.getOpenGLState(),buffer,SINGLE_FOLIAGE_DATA_SIZE_BYTES / 4,FINAL_SPACING * FINAL_SPACING);
//create entity
Entity grassEntity = EntityCreationUtils.createClientSpatialEntity();
TextureInstancedActor.attachTextureInstancedActor(grassEntity, foliageType.getModelPath(), vertexPath, fragmentPath, dataTexture, drawCount);
EntityUtils.getPosition(grassEntity).set(realPos);
EntityUtils.getRotation(grassEntity).set(0,0,0,1);
EntityUtils.getScale(grassEntity).set(1,1,1);
//add ambient foliage behavior tree
AmbientFoliage.attachAmbientFoliageTree(grassEntity, initialGrowthLevel, foliageType.getGrowthModel().getGrowthRate());
cell.addEntity(grassEntity);
activeCells.add(cell);
locationCellMap.put(getFoliageCellKey(worldPos, voxelPos),cell);
}
}
}
/**
* Creates a foliage chunk
* @param worldPosition The world position of the chunk
*/
private void createFoliageChunk(Vector3i worldPosition){
FoliageChunk chunk = new FoliageChunk(worldPosition);
chunkTree.addLeaf(new Vector3d(worldPosition), chunk);
}
/**
* Evaluates all chunk nodes
* @param chunkNode The node to evaluate
*/
private void evaluateChunks(Vector3d playerPos, OctreeNode<FoliageChunk> chunkNode){
if(chunkNode.isLeaf()){
FoliageChunk chunk = chunkNode.getData();
Vector3d chunkPos = Globals.clientWorldData.convertWorldToRealSpace(chunk.getWorldPos());
} else {
for(OctreeNode<FoliageChunk> child : chunkNode.getChildren()){
if(child != null){
evaluateChunks(playerPos, child);
}
}
}
}
/**
* Checks that all dependencies of this manager are ready
* @return true if all are ready, false otherwise
@ -556,64 +128,14 @@ public class ClientFoliageManager {
}
/**
* Gets whether the voxel type supports foliage or not
* @param type
* @return
* Evaluates the foliage chunk at the position
* @param worldPos The position
*/
private boolean typeSupportsFoliage(int type){
if(Globals.gameConfigCurrent.getVoxelData().getTypeFromId(type) != null){
return Globals.gameConfigCurrent.getVoxelData().getTypeFromId(type).getAmbientFoliage() != null;
}
return false;
}
/**
* Invalidates a foliage cell at a position, destroying all foliage in the cell and unregistering it.
* Furthermore, it adds it to a cooldown queue to wait until it can recreate foliage
* @param worldPosition The world position of the cell
* @param voxelPosition The voxel position of the cell
*/
private void invalidateCell(Vector3i worldPosition, Vector3i voxelPosition){
String key = getFoliageCellKey(worldPosition, voxelPosition);
if(!locationEvaluationCooldownMap.containsKey(key)){
locationEvaluationCooldownMap.put(key,EVALUATION_COOLDOWN);
FoliageCell cell = locationCellMap.get(key);
if(cell != null){
//destroy
FoliageCell toDestroy = locationCellMap.get(key);
toDestroy.destroy();
activeCells.remove(toDestroy);
locationCellMap.remove(key);
}
}
}
/**
* Invalidates a cell by direct reference
* @param cell The cell to invalidate
*/
private void invalidateCell(FoliageCell cell){
String key = getFoliageCellKey(cell.worldPosition, cell.voxelPosition);
if(!locationEvaluationCooldownMap.containsKey(key)){
locationEvaluationCooldownMap.put(key,EVALUATION_COOLDOWN);
}
//destroy
FoliageCell toDestroy = locationCellMap.get(key);
toDestroy.destroy();
activeCells.remove(toDestroy);
locationCellMap.remove(key);
}
/**
* Invalidates the foliage cells for all modified chunks
*/
private void invalidateModifiedPositions(){
for(ChunkData chunk : Globals.clientTerrainManager.getAllChunks()){
if(chunk.getModifiedPositions().size() > 0){
for(Vector3i position : chunk.getModifiedPositions()){
Globals.clientFoliageManager.invalidateCell(Globals.clientTerrainManager.getPositionOfChunk(chunk), position);
}
chunk.resetModifiedPositions();
public void evaluateChunk(Vector3i worldPos){
for(FoliageChunk chunk : chunkUpdateCache){
if(chunk.getWorldPos().equals(worldPos)){
chunk.updateCells();
break;
}
}
}
@ -622,8 +144,8 @@ public class ClientFoliageManager {
* Draws all foliage in the foliage manager
*/
public void draw(){
for(FoliageCell cell : activeCells){
cell.draw(modelMatrixAttribute);
for(FoliageChunk chunk : chunks){
chunk.draw();
}
}

View File

@ -1,6 +1,13 @@
package electrosphere.client.foliagemanager;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import org.joml.Matrix4d;
@ -9,20 +16,114 @@ import org.joml.Sphered;
import org.joml.Vector3d;
import org.joml.Vector3f;
import org.joml.Vector3i;
import org.lwjgl.BufferUtils;
import electrosphere.client.terrain.cache.ChunkData;
import electrosphere.engine.Globals;
import electrosphere.entity.Entity;
import electrosphere.entity.EntityCreationUtils;
import electrosphere.entity.EntityDataStrings;
import electrosphere.entity.EntityTags;
import electrosphere.entity.EntityUtils;
import electrosphere.entity.state.foliage.AmbientFoliage;
import electrosphere.entity.types.camera.CameraEntityUtils;
import electrosphere.game.data.foliage.type.FoliageType;
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.texture.Texture;
import electrosphere.server.terrain.manager.ServerTerrainChunk;
/**
* Contains a set of foliage entities and groups them together.
*/
public class FoliageCell {
/**
* 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;
/**
* the length of the ray to ground test with
*/
static final float RAY_LENGTH = 2.0f;
/**
* the height above the chunk to start from when sampling downwards
*/
static final float SAMPLE_START_HEIGHT = 1.0f;
/**
* <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
*/
static final String vertexPath = "Shaders/entities/foliage/foliage.vs";
/**
* fragment shader path
*/
static final String fragmentPath = "Shaders/entities/foliage/foliage.fs";
/**
* Random for finding new positions for foliage
*/
Random placementRandomizer = new Random();
/**
* The scale of the chunk
*/
int scale;
//position of the foliage cell in world coordinates
protected Vector3i worldPosition;
//position of the foliage cell in local coordinates
@ -40,15 +141,29 @@ public class FoliageCell {
*/
protected float density;
/**
* Inits the foliage cell data
*/
static void init(){
//queue ambient foliage models
for(FoliageType foliageType : Globals.gameConfigCurrent.getFoliageMap().getFoliageList()){
if(foliageType.getTokens().contains(FoliageType.TOKEN_AMBIENT)){
Globals.assetManager.addModelPathToQueue(foliageType.getModelPath());
Globals.assetManager.addShaderToQueue(vertexPath, fragmentPath);
}
}
}
/**
* Constructor
* @param worldPos The position of the foliage cell in world coordinates
* @param voxelPos The position of the foliage cell in voxel coordinates
*/
protected FoliageCell(Vector3i worldPos, Vector3i voxelPos, Vector3d realPos, float density){
protected FoliageCell(Vector3i worldPos, Vector3i voxelPos, Vector3d realPos, int scale, float density){
this.worldPosition = worldPos;
this.voxelPosition = voxelPos;
this.realPosition = realPos;
this.scale = scale;
this.density = density;
this.containedEntities = new HashSet<Entity>();
}
@ -79,10 +194,206 @@ public class FoliageCell {
}
/**
* Draws all entities in the foliage cell
* @param modelMatrixAttribute The model matrix attribute to draw with
* Generates the foliage cell
*/
protected void draw(ShaderAttribute modelMatrixAttribute){
protected void generate(){
boolean shouldGenerate = false;
//get foliage types supported
ChunkData data = Globals.clientTerrainManager.getChunkDataAtWorldPoint(worldPosition);
List<String> foliageTypesSupported = null;
if(data != null && voxelPosition.y + 1 < ServerTerrainChunk.CHUNK_DIMENSION){
foliageTypesSupported = Globals.gameConfigCurrent.getVoxelData().getTypeFromId(data.getType(voxelPosition)).getAmbientFoliage();
boolean airAbove = data.getType(voxelPosition.x,voxelPosition.y+1,voxelPosition.z) == 0;
if(foliageTypesSupported != null && airAbove && scale < 2){
shouldGenerate = true;
}
}
if(shouldGenerate){
//get type
String foliageTypeName = foliageTypesSupported.get(placementRandomizer.nextInt() % foliageTypesSupported.size());
FoliageType foliageType = Globals.gameConfigCurrent.getFoliageMap().getFoliage(foliageTypeName);
//create cell and buffer
int FINAL_SPACING = (int)(TARGET_FOLIAGE_SPACING * density);
ByteBuffer buffer = BufferUtils.createByteBuffer(FINAL_SPACING * FINAL_SPACING * SINGLE_FOLIAGE_DATA_SIZE_BYTES);
FloatBuffer floatBufferView = buffer.asFloatBuffer();
//construct simple grid to place foliage on
Vector3d sample_00 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPosition).add(-0.5,SAMPLE_START_HEIGHT,-0.5), new Vector3d(0,-1,0), RAY_LENGTH);
Vector3d sample_01 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPosition).add(-0.5,SAMPLE_START_HEIGHT, 0), new Vector3d(0,-1,0), RAY_LENGTH);
Vector3d sample_02 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPosition).add(-0.5,SAMPLE_START_HEIGHT, 0.5), new Vector3d(0,-1,0), RAY_LENGTH);
Vector3d sample_10 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPosition).add( 0,SAMPLE_START_HEIGHT,-0.5), new Vector3d(0,-1,0), RAY_LENGTH);
Vector3d sample_11 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPosition).add( 0,SAMPLE_START_HEIGHT, 0), new Vector3d(0,-1,0), RAY_LENGTH);
Vector3d sample_12 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPosition).add( 0,SAMPLE_START_HEIGHT, 0.5), new Vector3d(0,-1,0), RAY_LENGTH);
Vector3d sample_20 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPosition).add( 0.5,SAMPLE_START_HEIGHT,-0.5), new Vector3d(0,-1,0), RAY_LENGTH);
Vector3d sample_21 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPosition).add( 0.5,SAMPLE_START_HEIGHT, 0), new Vector3d(0,-1,0), RAY_LENGTH);
Vector3d sample_22 = Globals.clientSceneWrapper.getCollisionEngine().rayCastPosition(new Vector3d(realPosition).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
int drawCount = 0;
for(int x = 0; x < FINAL_SPACING; x++){
for(int z = 0; z < FINAL_SPACING; z++){
//get position to place
double rand1 = placementRandomizer.nextDouble();
double rand2 = placementRandomizer.nextDouble();
double relativePositionOnGridX = x / (1.0 * FINAL_SPACING) + rand1 / FINAL_SPACING;
double relativePositionOnGridZ = z / (1.0 * FINAL_SPACING) + rand2 / FINAL_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 - realPosition.y;
double rotVar = placementRandomizer.nextDouble() * Math.PI * 2;
double rotVar2 = placementRandomizer.nextDouble();
floatBufferView.put((float)offsetX);
floatBufferView.put((float)offsetY);
floatBufferView.put((float)offsetZ);
floatBufferView.put((float)rotVar);
floatBufferView.put((float)rotVar2);
drawCount++;
}
}
}
buffer.position(0);
buffer.limit(FINAL_SPACING * FINAL_SPACING * SINGLE_FOLIAGE_DATA_SIZE_BYTES);
//construct data texture
Texture dataTexture = new Texture(Globals.renderingEngine.getOpenGLState(),buffer,SINGLE_FOLIAGE_DATA_SIZE_BYTES / 4,FINAL_SPACING * FINAL_SPACING);
//create entity
Entity grassEntity = EntityCreationUtils.createClientSpatialEntity();
TextureInstancedActor.attachTextureInstancedActor(grassEntity, foliageType.getModelPath(), vertexPath, fragmentPath, dataTexture, drawCount);
EntityUtils.getPosition(grassEntity).set(realPosition);
EntityUtils.getRotation(grassEntity).set(0,0,0,1);
EntityUtils.getScale(grassEntity).set(1,1,1);
//add ambient foliage behavior tree
AmbientFoliage.attachAmbientFoliageTree(grassEntity, 0.0f, foliageType.getGrowthModel().getGrowthRate());
this.addEntity(grassEntity);
}
}
}
/**
* Gets a good position to put a new blade of grass
* @param centerPosition The player's position
* @return The new position for the blade of grass
*/
protected Vector3d getNewPosition(Vector3d centerPosition){
double angle = placementRandomizer.nextDouble() * Math.PI * 2;
double radius = placementRandomizer.nextDouble();
return new Vector3d(
centerPosition.x + Math.cos(angle) * radius,
centerPosition.y,
centerPosition.z + Math.sin(angle) * radius
);
}
/**
* Gets a new rotation for a blade of grass
* @return The rotation
*/
protected Quaterniond getNewRotation(){
return new Quaterniond().rotationX(-Math.PI / 2.0f).rotateLocalY(Math.PI * placementRandomizer.nextFloat()).normalize();
}
/**
* Makes an already created entity a drawable, instanced entity (client only) by backing it with an InstancedActor
* @param entity The entity
* @param modelPath The model path for the model to back the instanced actor
* @param capacity The capacity of the instanced actor to draw
*/
public static void makeEntityTextureInstancedFoliage(Entity entity, String modelPath, int capacity){
entity.putData(EntityDataStrings.INSTANCED_ACTOR, Globals.clientInstanceManager.createInstancedActor(modelPath, vertexPath, fragmentPath, attributes, capacity));
entity.putData(EntityDataStrings.DATA_STRING_POSITION, new Vector3d(0,0,0));
entity.putData(EntityDataStrings.DATA_STRING_ROTATION, new Quaterniond().identity());
entity.putData(EntityDataStrings.DATA_STRING_SCALE, new Vector3f(1,1,1));
entity.putData(EntityDataStrings.DRAW_SOLID_PASS, true);
Globals.clientScene.registerEntity(entity);
Globals.clientScene.registerEntityToTag(entity, EntityTags.DRAW_INSTANCED_MANAGED);
}
/**
* Draws all entities in the foliage cell
*/
protected void draw(){
if(this.containedEntities.size() > 0){
Matrix4d modelMatrix = new Matrix4d();
Vector3f cameraCenter = CameraEntityUtils.getCameraCenter(Globals.playerCamera);
@ -91,7 +402,7 @@ public class FoliageCell {
Vector3f cameraModifiedPosition = new Vector3f((float)realPosition.x,(float)realPosition.y,(float)realPosition.z).sub(CameraEntityUtils.getCameraCenter(Globals.playerCamera));
//frustum check entire cell
boolean shouldRender = renderPipelineState.getFrustumIntersection().testSphere((float)(cameraModifiedPosition.x + boundingSphere.x), (float)(cameraModifiedPosition.y + boundingSphere.y), (float)(cameraModifiedPosition.z + boundingSphere.z), (float)(boundingSphere.r));
boolean shouldRender = true;//renderPipelineState.getFrustumIntersection().testSphere((float)(cameraModifiedPosition.x + boundingSphere.x), (float)(cameraModifiedPosition.y + boundingSphere.y), (float)(cameraModifiedPosition.z + boundingSphere.z), (float)(boundingSphere.r));
if(shouldRender){
//disable frustum check and instead perform at cell level
boolean currentFrustumCheckState = renderPipelineState.shouldFrustumCheck();
@ -116,4 +427,107 @@ public class FoliageCell {
renderPipelineState.setFrustumCheck(currentFrustumCheckState);
}
}
}
/**
* SCAFFOLDING FOR BUILDING SCALE>1 CELLS AND ALSO FOR TOP LEVEL CELL CHECKING
*/
/**
* Evaluates a chunk to see where foliage cells should be created or updated
* @param worldPos The world position of the chunk
*/
// public void evaluateChunk(Vector3i worldPos){
// ChunkData data = Globals.clientTerrainManager.getChunkDataAtWorldPoint(worldPos);
// for(int x = 0; x < ChunkData.CHUNK_SIZE; x++){
// //can't go to very top 'cause otherwise there would be no room to put grass
// for(int y = 0; y < ChunkData.CHUNK_SIZE - 1; y++){
// for(int z = 0; z < ChunkData.CHUNK_SIZE; z++){
// Vector3i currentPos = new Vector3i(x,y,z);
// String key = getFoliageCellKey(worldPos, currentPos);
// if(locationCellMap.get(key) != null){
// //destroy if there's no longer ground or
// //if the cell above is now occupied or
// //if the lower cell is no longer supporting foliage
// if(
// data.getWeight(currentPos) <= 0 ||
// data.getWeight(new Vector3i(x,y + 1,z)) > 0 ||
// !typeSupportsFoliage(data.getType(currentPos))
// ){
// //destroy
// FoliageCell toDestroy = locationCellMap.get(key);
// toDestroy.destroy();
// activeCells.remove(toDestroy);
// locationCellMap.remove(key);
// } else {
// //TODO: evaluate if foliage is placed well
// }
// } else {
// //create if current is ground and above is air
// if(
// !locationEvaluationCooldownMap.containsKey(key) &&
// data.getWeight(currentPos) > 0 &&
// data.getWeight(new Vector3i(x,y + 1,z)) < 0 &&
// typeSupportsFoliage(data.getType(currentPos)) &&
// activeCells.size() < CELL_COUNT_MAX
// ){
// //create foliage cell
// createFoliageCell(worldPos,currentPos,1,targetDensity);
// }
// }
// }
// }
// }
// //evaluate top cells if chunk above this one exists
// ChunkData aboveData = Globals.clientTerrainManager.getChunkDataAtWorldPoint(new Vector3i(worldPos).add(0,1,0));
// if(aboveData != null){
// for(int x = 0; x < ChunkData.CHUNK_SIZE; x++){
// for(int z = 0; z < ChunkData.CHUNK_SIZE; z++){
// Vector3i currentPos = new Vector3i(x,ChunkData.CHUNK_SIZE-1,z);
// String key = getFoliageCellKey(worldPos, currentPos);
// if(locationCellMap.get(key) != null){
// //destroy if there's no longer ground or
// //if the cell above is now occupied or
// //if the lower cell is no longer supporting foliage
// if(
// data.getWeight(currentPos) <= 0 ||
// aboveData.getWeight(new Vector3i(x,0,z)) > 0 ||
// !typeSupportsFoliage(data.getType(currentPos))
// ){
// //destroy
// FoliageCell toDestroy = locationCellMap.get(key);
// toDestroy.destroy();
// activeCells.remove(toDestroy);
// locationCellMap.remove(key);
// } else {
// //TODO: evaluate if foliage is placed well
// }
// } else {
// //create if current is ground and above is air
// if(
// data.getWeight(currentPos) > 0 &&
// aboveData.getWeight(new Vector3i(x,0,z)) < 0 &&
// typeSupportsFoliage(data.getType(currentPos)) &&
// activeCells.size() < CELL_COUNT_MAX
// ){
// //create foliage cell
// createFoliageCell(worldPos,currentPos,1,targetDensity);
// }
// }
// }
// }
// }
// }
/**
* Gets whether the voxel type supports foliage or not
* @param type
* @return
*/
private boolean typeSupportsFoliage(int type){
if(Globals.gameConfigCurrent.getVoxelData().getTypeFromId(type) != null){
return Globals.gameConfigCurrent.getVoxelData().getTypeFromId(type).getAmbientFoliage() != null;
}
return false;
}
}

View File

@ -1,10 +1,17 @@
package electrosphere.client.foliagemanager;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import org.joml.Vector3d;
import org.joml.Vector3i;
import electrosphere.client.terrain.cache.ChunkData;
import electrosphere.engine.Globals;
import electrosphere.entity.EntityUtils;
import electrosphere.util.ds.octree.ChunkTree;
import electrosphere.util.ds.octree.ChunkTree.ChunkTreeNode;
/**
* A whole chunk of foliage
*/
@ -14,11 +21,27 @@ public class FoliageChunk {
* The world position of this chunk
*/
Vector3i worldPos;
Vector3d realPos;
/**
* A list of all cells that are currently generated
* The distance to draw at full resolution
*/
List<Vector3i> currentlyGeneratedCellPositions;
static final double FULL_RES_DIST = 15;
/**
* The octree holding all the chunks to evaluate
*/
ChunkTree<FoliageCell> chunkTree;
/**
* Data for the current chunk
*/
ChunkData currentChunkData;
/**
* Data for the above chunk
*/
ChunkData aboveChunkData;
/**
* Constructor
@ -26,7 +49,8 @@ public class FoliageChunk {
*/
public FoliageChunk(Vector3i worldPos){
this.worldPos = worldPos;
this.currentlyGeneratedCellPositions = new ArrayList<Vector3i>();
this.realPos = Globals.clientWorldData.convertWorldToRealSpace(worldPos);
this.chunkTree = new ChunkTree<FoliageCell>();
}
/**
@ -37,4 +61,178 @@ public class FoliageChunk {
return worldPos;
}
/**
* Initializes all cells in the chunk
*/
public void initCells(){
Vector3d playerPos = EntityUtils.getPosition(Globals.playerEntity);
this.currentChunkData = Globals.clientTerrainManager.getChunkDataAtWorldPoint(worldPos);
// //evaluate top cells if chunk above this one exists
this.aboveChunkData = Globals.clientTerrainManager.getChunkDataAtWorldPoint(new Vector3i(worldPos).add(0,1,0));
//the sets to iterate through
ChunkTreeNode<FoliageCell> rootNode = this.chunkTree.getRoot();
List<ChunkTreeNode<FoliageCell>> openSet = new LinkedList<ChunkTreeNode<FoliageCell>>();
List<ChunkTreeNode<FoliageCell>> toInit = new LinkedList<ChunkTreeNode<FoliageCell>>();
openSet.add(rootNode);
//split into nodes
while(openSet.size() > 0){
ChunkTreeNode<FoliageCell> current = openSet.remove(0);
if(this.shouldSplit(playerPos, current)){
//add children for this one
ChunkTreeNode<FoliageCell> container = chunkTree.split(current);
openSet.addAll(container.getChildren());
} else {
//do nothing
toInit.add(current);
}
}
//init end-nodes as leaves
for(ChunkTreeNode<FoliageCell> current : toInit){
Vector3d realPos = Globals.clientWorldData.convertWorldToRealSpace(worldPos).add(new Vector3d(current.getMinBound()));
current.convertToLeaf(new FoliageCell(worldPos, current.getMinBound(), realPos, 4 - current.getLevel(), 1.0f));
current.getData().generate();
}
}
/**
* Updates all cells in the chunk
*/
public void updateCells(){
Vector3d playerPos = EntityUtils.getPosition(Globals.playerEntity);
//the sets to iterate through
ChunkTreeNode<FoliageCell> rootNode = this.chunkTree.getRoot();
List<ChunkTreeNode<FoliageCell>> openSet = new LinkedList<ChunkTreeNode<FoliageCell>>();
List<ChunkTreeNode<FoliageCell>> toInit = new LinkedList<ChunkTreeNode<FoliageCell>>();
List<ChunkTreeNode<FoliageCell>> toCleanup = new LinkedList<ChunkTreeNode<FoliageCell>>();
openSet.add(rootNode);
//split into nodes
while(openSet.size() > 0){
ChunkTreeNode<FoliageCell> current = openSet.remove(0);
if(this.shouldSplit(playerPos, current)){
//add children for this one
ChunkTreeNode<FoliageCell> container = chunkTree.split(current);
toInit.addAll(container.getChildren());
toCleanup.add(current);
} else if(this.shouldJoin(playerPos, current)) {
//add children for this one
ChunkTreeNode<FoliageCell> newLeaf = chunkTree.join(current);
toCleanup.addAll(current.getChildren());
toCleanup.add(current);
toInit.add(newLeaf);
} else if(!current.isLeaf()){
openSet.addAll(current.getChildren());
}
}
//init end-nodes as leaves
for(ChunkTreeNode<FoliageCell> current : toInit){
Vector3d realPos = Globals.clientWorldData.convertWorldToRealSpace(worldPos).add(new Vector3d(current.getMinBound()));
current.convertToLeaf(new FoliageCell(worldPos, current.getMinBound(), realPos, 5 - current.getLevel(), 1.0f));
current.getData().generate();
}
//destroy orphan nodes
for(ChunkTreeNode<FoliageCell> current : toCleanup){
if(current.getData() != null){
current.getData().destroy();
}
}
}
/**
* 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(Vector3d pos, ChunkTreeNode<FoliageCell> node){
Vector3i min = node.getMinBound();
Vector3i max = node.getMaxBound();
double minX = min.x + realPos.x;
double minY = min.y + realPos.y;
double minZ = min.z + realPos.z;
double maxX = max.x + realPos.x;
double maxY = max.y + realPos.y;
double maxZ = max.z + realPos.z;
if(Math.abs(pos.x - minX) < Math.abs(pos.x - maxX)){
if(Math.abs(pos.y - minY) < Math.abs(pos.y - maxY)){
if(Math.abs(pos.z - minZ) < Math.abs(pos.z - maxZ)){
return pos.distance(minX,minY,minZ);
} else {
return pos.distance(minX,minY,maxZ);
}
} else {
if(Math.abs(pos.z - minZ) < Math.abs(pos.z - maxZ)){
return pos.distance(minX,maxY,minZ);
} else {
return pos.distance(minX,maxY,maxZ);
}
}
} else {
if(Math.abs(pos.y - minY) < Math.abs(pos.y - maxY)){
if(Math.abs(pos.z - minZ) < Math.abs(pos.z - maxZ)){
return pos.distance(maxX,minY,minZ);
} else {
return pos.distance(maxX,minY,maxZ);
}
} else {
if(Math.abs(pos.z - minZ) < Math.abs(pos.z - maxZ)){
return pos.distance(maxX,maxY,minZ);
} else {
return pos.distance(maxX,maxY,maxZ);
}
}
}
}
/**
* 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(Vector3d pos, ChunkTreeNode<FoliageCell> node){
//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 this.getMinDistance(pos, node) <= FULL_RES_DIST &&
node.isLeaf() &&
node.canSplit()
;
}
/**
* 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(Vector3d pos, ChunkTreeNode<FoliageCell> node){
//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 this.getMinDistance(pos, node) > FULL_RES_DIST &&
!node.isLeaf()
;
}
/**
* Destroys the foliage chunk
*/
protected void destroy(){
for(ChunkTreeNode<FoliageCell> leaf : this.chunkTree.getLeaves()){
leaf.getData().destroy();
}
}
/**
* Draws all cells in the chunk
*/
protected void draw(){
for(ChunkTreeNode<FoliageCell> leaf : this.chunkTree.getLeaves()){
leaf.getData().draw();
}
}
}

View File

@ -0,0 +1,284 @@
package electrosphere.util.ds.octree;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;
import org.joml.Vector3i;
/**
* An octree that has a guaranteed subdivision strategy that makes it appealing to use for chunk-based operations
*/
public class ChunkTree<T> {
/**
* The dimension of the tree
*/
static final int DIMENSION = 16;
/**
* The maximum level
*/
public static final int MAX_LEVEL = 4;
//The root node
private ChunkTreeNode<T> root = null;
/**
* The list of all nodes in the tree
*/
List<ChunkTreeNode<T>> nodes = null;
/**
* Constructor
*/
public ChunkTree(){
this.nodes = new ArrayList<ChunkTreeNode<T>>();
this.root = new ChunkTreeNode<T>(0, new Vector3i(0,0,0), new Vector3i(16,16,16));
this.root.isLeaf = true;
this.nodes.add(this.root);
}
/**
* Splits a parent into child nodes
* @param parent The parent
* @return The new non-leaf node
*/
public ChunkTreeNode<T> split(ChunkTreeNode<T> existing){
if(!existing.isLeaf()){
throw new IllegalArgumentException("Tried to split non-leaf!");
}
Vector3i min = existing.getMinBound();
Vector3i max = existing.getMaxBound();
int midX = (max.x - min.x) / 2 + min.x;
int midY = (max.y - min.y) / 2 + min.y;
int midZ = (max.z - min.z) / 2 + min.z;
int currentLevel = existing.getLevel();
ChunkTreeNode<T> newContainer = new ChunkTreeNode<>(currentLevel, min, max);
//add children
newContainer.addChild(new ChunkTreeNode<T>(currentLevel + 1, new Vector3i(min.x,min.y,min.z), new Vector3i(midX,midY,midZ)));
newContainer.addChild(new ChunkTreeNode<T>(currentLevel + 1, new Vector3i(midX,min.y,min.z), new Vector3i(max.x,midY,midZ)));
newContainer.addChild(new ChunkTreeNode<T>(currentLevel + 1, new Vector3i(min.x,midY,min.z), new Vector3i(midX,max.y,midZ)));
newContainer.addChild(new ChunkTreeNode<T>(currentLevel + 1, new Vector3i(midX,midY,min.z), new Vector3i(max.x,max.y,midZ)));
//
newContainer.addChild(new ChunkTreeNode<T>(currentLevel + 1, new Vector3i(min.x,min.y,midZ), new Vector3i(midX,midY,max.z)));
newContainer.addChild(new ChunkTreeNode<T>(currentLevel + 1, new Vector3i(midX,min.y,midZ), new Vector3i(max.x,midY,max.z)));
newContainer.addChild(new ChunkTreeNode<T>(currentLevel + 1, new Vector3i(min.x,midY,midZ), new Vector3i(midX,max.y,max.z)));
newContainer.addChild(new ChunkTreeNode<T>(currentLevel + 1, new Vector3i(midX,midY,midZ), new Vector3i(max.x,max.y,max.z)));
//replace existing node
replaceNode(existing,newContainer);
//update tracking
this.nodes.remove(existing);
this.nodes.add(newContainer);
this.nodes.addAll(newContainer.getChildren());
return newContainer;
}
/**
* Joins a non-leaf node's children into a single node
* @param parent The non-leaf
* @return The new leaf node
*/
public ChunkTreeNode<T> join(ChunkTreeNode<T> existing){
if(existing.isLeaf()){
throw new IllegalArgumentException("Tried to split non-leaf!");
}
Vector3i min = existing.getMinBound();
Vector3i max = existing.getMaxBound();
int currentLevel = existing.getLevel();
ChunkTreeNode<T> newContainer = new ChunkTreeNode<>(currentLevel, min, max);
//replace existing node
replaceNode(existing,newContainer);
//update tracking
this.nodes.remove(existing);
this.nodes.removeAll(existing.getChildren());
this.nodes.add(newContainer);
return newContainer;
}
/**
* Replaces an existing node with a new node
* @param existing the existing node
* @param newNode the new node
*/
private void replaceNode(ChunkTreeNode<T> existing, ChunkTreeNode<T> newNode){
if(existing == this.root){
this.root = newNode;
} else {
ChunkTreeNode<T> parent = existing.getParent();
parent.removeChild(existing);
parent.addChild(newNode);
}
}
/**
* Gets the root node of the tree
*/
public ChunkTreeNode<T> getRoot() {
return this.root;
}
/**
* Gets the number of leaves in the tree
*/
public int getNumLeaves() {
return this.getLeaves().size();
}
/**
* Gets all leaf nodes
* @return All leaf nodes
*/
public List<ChunkTreeNode<T>> getLeaves(){
return this.nodes.stream().filter(node -> node.isLeaf()).collect(Collectors.toList());
}
/**
* A node in a chunk tree
*/
public static class ChunkTreeNode<T> {
//True if this is a leaf node, false otherwise
private boolean isLeaf;
//the parent node
private ChunkTreeNode<T> parent;
//the children of this node
private List<ChunkTreeNode<T>> children = new LinkedList<ChunkTreeNode<T>>();
//The data at the node
private T data;
/**
* The min bound
*/
private Vector3i min;
/**
* The max bound
*/
private Vector3i max;
/**
* The level of the chunk tree node
*/
int level;
/**
* Constructor for non-leaf node
*/
private ChunkTreeNode(int level, Vector3i min, Vector3i max){
if(min.x == 16 || min.y == 16 || min.z == 16){
throw new IllegalArgumentException("Invalid minimum! " + min);
}
if(level < 0 || level > MAX_LEVEL){
throw new IllegalArgumentException("Invalid level! " + level);
}
this.isLeaf = false;
this.level = level;
this.min = min;
this.max = max;
}
/**
* Converts this node to a leaf
* @param data The data to put in the leaf
*/
public void convertToLeaf(T data){
this.isLeaf = true;
this.data = data;
}
/**
* Gets the data associated with this node
*/
public T getData() {
return data;
}
/**
* Gets the parent of this node
*/
public ChunkTreeNode<T> getParent() {
return parent;
}
/**
* Gets the children of this node
*/
public List<ChunkTreeNode<T>> getChildren() {
return Collections.unmodifiableList(this.children);
}
/**
* Checks if this node is a leaf
* @return true if it is a leaf, false otherwise
*/
public boolean isLeaf() {
return isLeaf;
}
/**
* Checks if the node can split
* @return true if can split, false otherwise
*/
public boolean canSplit(){
return isLeaf && level < MAX_LEVEL;
}
/**
* Gets the level of the node
* @return The level of the node
*/
public int getLevel(){
return level;
}
/**
* Gets the min bound of this node
* @return The min bound
*/
public Vector3i getMinBound(){
return min;
}
/**
* Gets the max bound of this node
* @return The max bound
*/
public Vector3i getMaxBound(){
return max;
}
/**
* Adds a child to this node
* @param child The child
*/
private void addChild(ChunkTreeNode<T> child){
this.children.add(child);
child.parent = this;
}
/**
* Removes a child node
* @param child the child
*/
private void removeChild(ChunkTreeNode<T> child){
this.children.remove(child);
child.parent = null;
}
}
}

View File

@ -0,0 +1,51 @@
package electrosphere.util.ds.octree;
import org.joml.Vector3d;
/**
* An octree implementation
*/
public interface Octree<T> {
/**
* Adds a leaf to the octree
* @param location The location of the leaf
* @param data The data associated with the leaf
* @throws IllegalArgumentException Thrown if a location is provided that already has an exact match
*/
public void addLeaf(Vector3d location, T data) throws IllegalArgumentException;
/**
* Checks if the octree contains a leaf at an exact location
* @param location The location
* @return true if a leaf exists for that exact location, false otherwise
*/
public boolean containsLeaf(Vector3d location);
/**
* Gets the leaf at an exact location
* @param location The location
* @return The leaf
* @throws ArrayIndexOutOfBoundsException Thrown if a location is queries that does not have an associated leaf in the octree
*/
public OctreeNode<T> getLeaf(Vector3d location) throws ArrayIndexOutOfBoundsException;
/**
* Removes a node from the octree
* @param node The node
*/
public void removeNode(OctreeNode<T> node);
/**
* Gets the root node
* @return The root node
*/
public OctreeNode<T> getRoot();
/**
* Gets the number of leaves under this tree
* @return The number of leaves
*/
public int getNumLeaves();
}

View File

@ -0,0 +1,34 @@
package electrosphere.util.ds.octree;
import java.util.List;
/**
* An octree node
*/
public interface OctreeNode<T> {
/**
* Gets the data in the node
* @return The data
*/
public T getData();
/**
* Gets the parent node of this node
* @return The parent node
*/
public OctreeNode<T> getParent();
/**
* Gets the children of this node
* @return The children
*/
public List<OctreeNode<T>> getChildren();
/**
* Checks if this is a leaf node or not
* @return true if it is a leaf, false otherwise
*/
public boolean isLeaf();
}

View File

@ -1,4 +1,4 @@
package electrosphere.util.ds;
package electrosphere.util.ds.octree;
import java.util.HashMap;
import java.util.LinkedList;
@ -11,7 +11,7 @@ import org.joml.Vector3d;
/**
* An octree implementation
*/
public class Octree<T> {
public class RealOctree<T> {
/**
* The number of children in a full, non-leaf node
@ -20,20 +20,20 @@ public class Octree<T> {
//The root node
private OctreeNode<T> root = null;
private RealOctreeNode<T> root = null;
/**
* Table used for looking up the presence of nodes
* Maps a position key to a corresponding Octree node
*/
Map<String,OctreeNode<T>> lookupTable = new HashMap<String,OctreeNode<T>>();
Map<String,RealOctreeNode<T>> lookupTable = new HashMap<String,RealOctreeNode<T>>();
/**
* Creates an octree
* @param center The center of the root node of the octree
*/
public Octree(Vector3d boundLower, Vector3d boundUpper){
root = new OctreeNode<T>(boundLower, boundUpper);
public RealOctree(Vector3d boundLower, Vector3d boundUpper){
root = new RealOctreeNode<T>(boundLower, boundUpper);
}
/**
@ -47,18 +47,18 @@ public class Octree<T> {
if(this.containsLeaf(location)){
throw new IllegalArgumentException("Tried adding leaf that is already occupied!");
}
OctreeNode<T> node = new OctreeNode<T>(data, location);
OctreeNode<T> current = this.root;
RealOctreeNode<T> node = new RealOctreeNode<T>(data, location);
RealOctreeNode<T> current = this.root;
while(current.children.size() == FULL_NODE_SIZE){
if(current.hasChildInQuadrant(location)){
OctreeNode<T> child = current.getChildInQuadrant(location);
RealOctreeNode<T> child = current.getChildInQuadrant(location);
if(child.isLeaf()){
//get parent of the new fork
OctreeNode<T> parent = child.parent;
RealOctreeNode<T> parent = child.parent;
parent.removeChild(child);
//while the newly forked child isn't between the two children, keep forking
OctreeNode<T> nonLeaf = parent.forkQuadrant(child.getLocation());
RealOctreeNode<T> nonLeaf = parent.forkQuadrant(child.getLocation());
while(nonLeaf.getQuadrant(child.getLocation()) == nonLeaf.getQuadrant(node.getLocation())){
nonLeaf = nonLeaf.forkQuadrant(child.getLocation());
}
@ -89,13 +89,15 @@ public class Octree<T> {
return lookupTable.containsKey(this.getLocationKey(location));
}
/**
* Gets the leaf at an exact location
* @param location The location
* @return The leaf
* @throws ArrayIndexOutOfBoundsException Thrown if a location is queries that does not have an associated leaf in the octree
*/
public OctreeNode<T> getLeaf(Vector3d location) throws ArrayIndexOutOfBoundsException {
public RealOctreeNode<T> getLeaf(Vector3d location) throws ArrayIndexOutOfBoundsException {
if(!this.containsLeaf(location)){
throw new ArrayIndexOutOfBoundsException("Tried to get leaf at position that does not contain leaf!");
}
@ -106,8 +108,8 @@ public class Octree<T> {
* Removes a node from the octree
* @param node The node
*/
public void removeNode(OctreeNode<T> node){
OctreeNode<T> parent = node.parent;
public void removeNode(RealOctreeNode<T> node){
RealOctreeNode<T> parent = node.parent;
parent.removeChild(node);
if(node.isLeaf()){
this.lookupTable.remove(this.getLocationKey(node.getLocation()));
@ -118,7 +120,7 @@ public class Octree<T> {
* Gets the root node
* @return The root node
*/
public OctreeNode<T> getRoot(){
public RealOctreeNode<T> getRoot(){
return this.root;
}
@ -143,7 +145,7 @@ public class Octree<T> {
/**
* A single node in the octree
*/
public static class OctreeNode<T> {
public static class RealOctreeNode<T> implements OctreeNode<T> {
/*
6 +----------+ 7
@ -178,10 +180,10 @@ public class Octree<T> {
private boolean isLeaf;
//the parent node
private OctreeNode<T> parent;
private RealOctreeNode<T> parent;
//the children of this node
private List<OctreeNode<T>> children = new LinkedList<OctreeNode<T>>();
private List<RealOctreeNode<T>> children = new LinkedList<RealOctreeNode<T>>();
//The data at the node
private T data;
@ -191,7 +193,7 @@ public class Octree<T> {
* @param data The data at this octree node's position
* @param location The location of the node
*/
private OctreeNode(T data, Vector3d location){
private RealOctreeNode(T data, Vector3d location){
this.data = data;
this.midpoint = location;
this.isLeaf = true;
@ -202,7 +204,7 @@ public class Octree<T> {
* @param lowerBound The lower bound of the node
* @param upperBound The upper bound of the node
*/
private OctreeNode(Vector3d lowerBound, Vector3d upperBound){
private RealOctreeNode(Vector3d lowerBound, Vector3d upperBound){
this.data = null;
this.midpoint = new Vector3d(
((upperBound.x - lowerBound.x) / 2.0) + lowerBound.x,
@ -221,7 +223,7 @@ public class Octree<T> {
* Creates a non-leaf node
* @param midpoint The midpoint of the node
*/
private OctreeNode(Vector3d midpoint){
private RealOctreeNode(Vector3d midpoint){
this.data = null;
this.midpoint = midpoint;
for(int i = 0; i < FULL_NODE_SIZE; i++){
@ -250,7 +252,7 @@ public class Octree<T> {
* Gets the parent node of this node
* @return The parent node
*/
public OctreeNode<T> getParent(){
public RealOctreeNode<T> getParent(){
return parent;
}
@ -269,7 +271,7 @@ public class Octree<T> {
*/
public boolean hasChildInQuadrant(Vector3d positionToCheck){
int positionQuadrant = this.getQuadrant(positionToCheck);
OctreeNode<T> child = this.children.get(positionQuadrant);
RealOctreeNode<T> child = this.children.get(positionQuadrant);
return child != null;
}
@ -278,9 +280,9 @@ public class Octree<T> {
* @param positionToQuery The position of the quadrant
* @return The child if it exists, null otherwise
*/
public OctreeNode<T> getChildInQuadrant(Vector3d positionToQuery){
public RealOctreeNode<T> getChildInQuadrant(Vector3d positionToQuery){
int positionQuadrant = this.getQuadrant(positionToQuery);
OctreeNode<T> child = this.children.get(positionQuadrant);
RealOctreeNode<T> child = this.children.get(positionQuadrant);
return child;
}
@ -298,7 +300,7 @@ public class Octree<T> {
*/
public int getNumChildren(){
int acc = 0;
for(OctreeNode<T> child : children){
for(RealOctreeNode<T> child : children){
if(child != null){
acc++;
}
@ -315,7 +317,7 @@ public class Octree<T> {
if(this.isLeaf()){
return 1;
} else {
for(OctreeNode<T> child : this.children){
for(RealOctreeNode<T> child : this.children){
if(child != null){
if(child.isLeaf()){
acc++;
@ -332,7 +334,7 @@ public class Octree<T> {
* Adds a child to this node
* @param child The child
*/
private void addChild(OctreeNode<T> child){
private void addChild(RealOctreeNode<T> child){
if(hasChildInQuadrant(child.midpoint)){
throw new IllegalArgumentException("Trying to add child in occupied quadrant!");
}
@ -346,7 +348,7 @@ public class Octree<T> {
* Removes a child from this node
* @param child The child
*/
private void removeChild(OctreeNode<T> child){
private void removeChild(RealOctreeNode<T> child){
if(child == null){
throw new IllegalArgumentException("Child cannot be null!");
}
@ -396,7 +398,7 @@ public class Octree<T> {
* Sets the bounds of the child based on the quadrant it falls under
* @param child The child
*/
private void setChildBounds(OctreeNode<T> child){
private void setChildBounds(RealOctreeNode<T> child){
if(child.midpoint.z < this.midpoint.z){
if(child.midpoint.y < this.midpoint.y){
if(child.midpoint.x < this.midpoint.x){
@ -580,11 +582,11 @@ public class Octree<T> {
* @param location The location within the quadrant
* @return The midpoint of the new node
*/
private OctreeNode<T> forkQuadrant(Vector3d location){
private RealOctreeNode<T> forkQuadrant(Vector3d location){
int quadrant = this.getQuadrant(location);
Vector3d midpoint = this.getQuadrantMidpoint(quadrant);
//create and add the non-leaf node
OctreeNode<T> nonLeaf = new OctreeNode<T>(midpoint);
RealOctreeNode<T> nonLeaf = new RealOctreeNode<T>(midpoint);
this.addChild(nonLeaf);
return nonLeaf;
}

View File

@ -1,4 +1,4 @@
package electrosphere.util.ds;
package electrosphere.util.ds.octree;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
@ -13,19 +13,19 @@ import java.util.Random;
import org.joml.Vector3d;
import electrosphere.test.annotations.UnitTest;
import electrosphere.util.ds.Octree.OctreeNode;
import electrosphere.util.ds.octree.RealOctree.RealOctreeNode;
/**
* Unit testing for the octree implementation
*/
public class OctreeTests {
public class RealOctreeTests {
/**
* Creates an octree
*/
@UnitTest
public void testCreateOctree(){
new Octree<Integer>(new Vector3d(0,0,0), new Vector3d(32,32,32));
new RealOctree<Integer>(new Vector3d(0,0,0), new Vector3d(32,32,32));
}
/**
@ -33,15 +33,15 @@ public class OctreeTests {
*/
@UnitTest
public void testAddNode(){
Octree<Integer> octree = new Octree<Integer>(new Vector3d(0,0,0), new Vector3d(32,32,32));
RealOctree<Integer> octree = new RealOctree<Integer>(new Vector3d(0,0,0), new Vector3d(32,32,32));
octree.addLeaf(new Vector3d(1,1,1), 1);
//
//validate the tree
//
List<OctreeNode<Integer>> openSet = new LinkedList<OctreeNode<Integer>>();
List<RealOctreeNode<Integer>> openSet = new LinkedList<RealOctreeNode<Integer>>();
openSet.add(octree.getRoot());
while(openSet.size() > 0){
OctreeNode<Integer> current = openSet.remove(0);
RealOctreeNode<Integer> current = openSet.remove(0);
if(current.isLeaf()){
assertNotNull(current.getData());
} else {
@ -51,7 +51,7 @@ public class OctreeTests {
fail("Child not attached to parent!");
}
if(child != null){
openSet.add(child);
openSet.add((RealOctreeNode<Integer>)child);
}
}
}
@ -63,16 +63,16 @@ public class OctreeTests {
*/
@UnitTest
public void testAddTwo(){
Octree<Integer> octree = new Octree<Integer>(new Vector3d(0,0,0), new Vector3d(32,32,32));
RealOctree<Integer> octree = new RealOctree<Integer>(new Vector3d(0,0,0), new Vector3d(32,32,32));
octree.addLeaf(new Vector3d(1,1,1), 1);
octree.addLeaf(new Vector3d(2,1,1), 2);
//
//validate the tree
//
List<OctreeNode<Integer>> openSet = new LinkedList<OctreeNode<Integer>>();
List<RealOctreeNode<Integer>> openSet = new LinkedList<RealOctreeNode<Integer>>();
openSet.add(octree.getRoot());
while(openSet.size() > 0){
OctreeNode<Integer> current = openSet.remove(0);
RealOctreeNode<Integer> current = openSet.remove(0);
if(current.isLeaf()){
assertNotNull(current.getData());
} else {
@ -82,7 +82,7 @@ public class OctreeTests {
fail("Child not attached to parent!");
}
if(child != null){
openSet.add(child);
openSet.add((RealOctreeNode<Integer>)child);
}
}
}
@ -90,26 +90,26 @@ public class OctreeTests {
//
//Specific, expected values to check. Verifies that the octree construct itself correctly
//
OctreeNode<Integer> current = octree.getRoot().getChildren().get(0);
RealOctreeNode<Integer> current = (RealOctreeNode<Integer>)octree.getRoot().getChildren().get(0);
assertEquals(8, current.getLocation().x);
assertEquals(8, current.getLocation().y);
assertEquals(8, current.getLocation().z);
assertTrue(!current.isLeaf());
assertEquals(1, current.getNumChildren());
assertEquals(1, ((RealOctreeNode<Integer>)current).getNumChildren());
current = current.getChildren().get(0);
current = (RealOctreeNode<Integer>)current.getChildren().get(0);
assertEquals(4, current.getLocation().x);
assertEquals(4, current.getLocation().y);
assertEquals(4, current.getLocation().z);
assertTrue(!current.isLeaf());
assertEquals(1, current.getNumChildren());
assertEquals(1, ((RealOctreeNode<Integer>)current).getNumChildren());
current = current.getChildren().get(0);
current = (RealOctreeNode<Integer>)current.getChildren().get(0);
assertEquals(2, current.getLocation().x);
assertEquals(2, current.getLocation().y);
assertEquals(2, current.getLocation().z);
assertTrue(!current.isLeaf());
assertEquals(2, current.getNumChildren());
assertEquals(2, ((RealOctreeNode<Integer>)current).getNumChildren());
OctreeNode<Integer> leaf1 = current.getChildren().get(0);
OctreeNode<Integer> leaf2 = current.getChildren().get(1);
@ -122,17 +122,17 @@ public class OctreeTests {
*/
@UnitTest
public void testAddLine(){
Octree<Integer> octree = new Octree<Integer>(new Vector3d(0,0,0), new Vector3d(32,32,32));
RealOctree<Integer> octree = new RealOctree<Integer>(new Vector3d(0,0,0), new Vector3d(32,32,32));
for(int i = 0; i < 31; i++){
octree.addLeaf(new Vector3d(i,1,0), i);
}
//
//validate the tree
//
List<OctreeNode<Integer>> openSet = new LinkedList<OctreeNode<Integer>>();
List<RealOctreeNode<Integer>> openSet = new LinkedList<RealOctreeNode<Integer>>();
openSet.add(octree.getRoot());
while(openSet.size() > 0){
OctreeNode<Integer> current = openSet.remove(0);
RealOctreeNode<Integer> current = openSet.remove(0);
if(current.isLeaf()){
assertNotNull(current.getData());
} else {
@ -142,7 +142,7 @@ public class OctreeTests {
fail("Child not attached to parent!");
}
if(child != null){
openSet.add(child);
openSet.add(((RealOctreeNode<Integer>)child));
}
}
}
@ -154,17 +154,17 @@ public class OctreeTests {
*/
@UnitTest
public void testGetLeaf(){
Octree<Integer> octree = new Octree<Integer>(new Vector3d(0,0,0), new Vector3d(32,32,32));
RealOctree<Integer> octree = new RealOctree<Integer>(new Vector3d(0,0,0), new Vector3d(32,32,32));
octree.addLeaf(new Vector3d(1,1,1), 1);
OctreeNode<Integer> leaf = octree.getLeaf(new Vector3d(1,1,1));
RealOctreeNode<Integer> leaf = octree.getLeaf(new Vector3d(1,1,1));
assertNotNull(leaf);
//
//validate the tree
//
List<OctreeNode<Integer>> openSet = new LinkedList<OctreeNode<Integer>>();
List<RealOctreeNode<Integer>> openSet = new LinkedList<RealOctreeNode<Integer>>();
openSet.add(octree.getRoot());
while(openSet.size() > 0){
OctreeNode<Integer> current = openSet.remove(0);
RealOctreeNode<Integer> current = openSet.remove(0);
if(current.isLeaf()){
assertNotNull(current.getData());
} else {
@ -174,7 +174,7 @@ public class OctreeTests {
fail("Child not attached to parent!");
}
if(child != null){
openSet.add(child);
openSet.add((RealOctreeNode<Integer>)child);
}
}
}
@ -186,19 +186,19 @@ public class OctreeTests {
*/
@UnitTest
public void testRemoveLeaf(){
Octree<Integer> octree = new Octree<Integer>(new Vector3d(0,0,0), new Vector3d(32,32,32));
RealOctree<Integer> octree = new RealOctree<Integer>(new Vector3d(0,0,0), new Vector3d(32,32,32));
octree.addLeaf(new Vector3d(1,1,1), 1);
OctreeNode<Integer> leaf = octree.getLeaf(new Vector3d(1,1,1));
RealOctreeNode<Integer> leaf = octree.getLeaf(new Vector3d(1,1,1));
assertNotNull(leaf);
assertNotNull(leaf.getParent());
octree.removeNode(leaf);
//
//validate the tree
//
List<OctreeNode<Integer>> openSet = new LinkedList<OctreeNode<Integer>>();
List<RealOctreeNode<Integer>> openSet = new LinkedList<RealOctreeNode<Integer>>();
openSet.add(octree.getRoot());
while(openSet.size() > 0){
OctreeNode<Integer> current = openSet.remove(0);
RealOctreeNode<Integer> current = openSet.remove(0);
if(current.isLeaf()){
assertNotNull(current.getData());
} else {
@ -208,7 +208,7 @@ public class OctreeTests {
fail("Child not attached to parent!");
}
if(child != null){
openSet.add(child);
openSet.add((RealOctreeNode<Integer>)child);
}
}
}
@ -220,11 +220,11 @@ public class OctreeTests {
*/
@UnitTest
public void testRemoveLeaf2(){
Octree<Integer> octree = new Octree<Integer>(new Vector3d(0,0,0), new Vector3d(32,32,32));
RealOctree<Integer> octree = new RealOctree<Integer>(new Vector3d(0,0,0), new Vector3d(32,32,32));
octree.addLeaf(new Vector3d(1,1,1), 1);
octree.addLeaf(new Vector3d(2,1,1), 2);
octree.addLeaf(new Vector3d(3,1,1), 3);
OctreeNode<Integer> leaf = octree.getLeaf(new Vector3d(2,1,1));
RealOctreeNode<Integer> leaf = octree.getLeaf(new Vector3d(2,1,1));
assertNotNull(leaf);
assertNotNull(leaf.getParent());
@ -242,10 +242,10 @@ public class OctreeTests {
//
//validate the tree
//
List<OctreeNode<Integer>> openSet = new LinkedList<OctreeNode<Integer>>();
List<RealOctreeNode<Integer>> openSet = new LinkedList<RealOctreeNode<Integer>>();
openSet.add(octree.getRoot());
while(openSet.size() > 0){
OctreeNode<Integer> current = openSet.remove(0);
RealOctreeNode<Integer> current = openSet.remove(0);
if(current.isLeaf()){
assertNotNull(current.getData());
} else {
@ -255,7 +255,7 @@ public class OctreeTests {
fail("Child not attached to parent!");
}
if(child != null){
openSet.add(child);
openSet.add((RealOctreeNode<Integer>)child);
}
}
}
@ -267,7 +267,7 @@ public class OctreeTests {
*/
@UnitTest
public void testAddManyPoints(){
Octree<Integer> octree = new Octree<Integer>(new Vector3d(0,0,0), new Vector3d(256,256,256));
RealOctree<Integer> octree = new RealOctree<Integer>(new Vector3d(0,0,0), new Vector3d(256,256,256));
//