octree foliage work
This commit is contained in:
parent
f234c6423c
commit
ce8261aa13
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
locationEvaluationCooldownMap.put(key, cooldownTime);
|
||||
}
|
||||
}
|
||||
Globals.profiler.endCpuSample();
|
||||
//invalidate foliage cells that have had their voxel changed
|
||||
invalidateModifiedPositions();
|
||||
for(FoliageChunk chunk : this.chunkUpdateCache){
|
||||
chunk.destroy();
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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,41 +194,340 @@ 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){
|
||||
Matrix4d modelMatrix = new Matrix4d();
|
||||
Vector3f cameraCenter = CameraEntityUtils.getCameraCenter(Globals.playerCamera);
|
||||
|
||||
RenderPipelineState renderPipelineState = Globals.renderingEngine.getRenderPipelineState();
|
||||
OpenGLState openGLState = Globals.renderingEngine.getOpenGLState();
|
||||
|
||||
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));
|
||||
if(shouldRender){
|
||||
//disable frustum check and instead perform at cell level
|
||||
boolean currentFrustumCheckState = renderPipelineState.shouldFrustumCheck();
|
||||
renderPipelineState.setFrustumCheck(false);
|
||||
for(Entity entity : containedEntities){
|
||||
Vector3d grassPosition = EntityUtils.getPosition(entity);
|
||||
Quaterniond grassRotation = EntityUtils.getRotation(entity);
|
||||
TextureInstancedActor actor = TextureInstancedActor.getTextureInstancedActor(entity);
|
||||
|
||||
|
||||
modelMatrix = modelMatrix.identity();
|
||||
cameraModifiedPosition = new Vector3f((float)grassPosition.x,(float)grassPosition.y,(float)grassPosition.z).sub(cameraCenter);
|
||||
modelMatrix.translate(cameraModifiedPosition);
|
||||
modelMatrix.rotate(new Quaterniond(grassRotation));
|
||||
modelMatrix.scale(new Vector3d(EntityUtils.getScale(entity)));
|
||||
actor.applySpatialData(modelMatrix,grassPosition);
|
||||
|
||||
|
||||
//draw
|
||||
actor.draw(renderPipelineState, openGLState);
|
||||
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);
|
||||
}
|
||||
renderPipelineState.setFrustumCheck(currentFrustumCheckState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
RenderPipelineState renderPipelineState = Globals.renderingEngine.getRenderPipelineState();
|
||||
OpenGLState openGLState = Globals.renderingEngine.getOpenGLState();
|
||||
|
||||
Vector3f cameraModifiedPosition = new Vector3f((float)realPosition.x,(float)realPosition.y,(float)realPosition.z).sub(CameraEntityUtils.getCameraCenter(Globals.playerCamera));
|
||||
//frustum check entire cell
|
||||
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();
|
||||
renderPipelineState.setFrustumCheck(false);
|
||||
for(Entity entity : containedEntities){
|
||||
Vector3d grassPosition = EntityUtils.getPosition(entity);
|
||||
Quaterniond grassRotation = EntityUtils.getRotation(entity);
|
||||
TextureInstancedActor actor = TextureInstancedActor.getTextureInstancedActor(entity);
|
||||
|
||||
|
||||
modelMatrix = modelMatrix.identity();
|
||||
cameraModifiedPosition = new Vector3f((float)grassPosition.x,(float)grassPosition.y,(float)grassPosition.z).sub(cameraCenter);
|
||||
modelMatrix.translate(cameraModifiedPosition);
|
||||
modelMatrix.rotate(new Quaterniond(grassRotation));
|
||||
modelMatrix.scale(new Vector3d(EntityUtils.getScale(entity)));
|
||||
actor.applySpatialData(modelMatrix,grassPosition);
|
||||
|
||||
|
||||
//draw
|
||||
actor.draw(renderPipelineState, openGLState);
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
284
src/main/java/electrosphere/util/ds/octree/ChunkTree.java
Normal file
284
src/main/java/electrosphere/util/ds/octree/ChunkTree.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
51
src/main/java/electrosphere/util/ds/octree/Octree.java
Normal file
51
src/main/java/electrosphere/util/ds/octree/Octree.java
Normal 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();
|
||||
|
||||
}
|
||||
34
src/main/java/electrosphere/util/ds/octree/OctreeNode.java
Normal file
34
src/main/java/electrosphere/util/ds/octree/OctreeNode.java
Normal 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();
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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));
|
||||
|
||||
|
||||
//
|
||||
Loading…
Reference in New Issue
Block a user