Renderer/src/main/java/electrosphere/client/foliagemanager/ClientFoliageManager.java
austin 97edfacd41
All checks were successful
studiorailgun/Renderer/pipeline/head This commit looks good
fluids are rendering albeit poorly
2024-03-17 18:44:39 -04:00

522 lines
26 KiB
Java

package electrosphere.client.foliagemanager;
import java.nio.ByteBuffer;
import java.nio.DoubleBuffer;
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 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 org.lwjgl.system.MemoryUtil;
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;
/**
* 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 = 5f;
//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?
*/
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;
//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/foliage/foliage.vs";
static final String fragmentPath = "Shaders/foliage/foliage.fs";
/**
* 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);
}
}
ready = true;
}
/**
* Updates all grass entities
*/
public void update(){
if(ready){
//for each invalid cell, see if can be revalidated
for(String key : locationEvaluationCooldownMap.keySet()){
int cooldownTime = locationEvaluationCooldownMap.get(key);
cooldownTime--;
if(cooldownTime <= 0){
String split[] = key.split("_");
Vector3i 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]));
ChunkData data = Globals.clientTerrainManager.getChunkDataAtWorldPoint(worldPos);
//evaluate
if(
data.getWeight(voxelPos) > 0 &&
data.getWeight(new Vector3i(voxelPos.x,voxelPos.y + 1,voxelPos.z)) < 0 &&
typeSupportsFoliage(data.getType(voxelPos))
){
//create foliage cell
createFoliageCell(worldPos,voxelPos,0);
}
locationEvaluationCooldownMap.remove(key);
} else {
locationEvaluationCooldownMap.put(key, cooldownTime);
}
}
//invalidate foliage cells that have had their voxel changed
invalidateModifiedPositions();
}
}
/**
* 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();
}
/**
* Gets a key for a foliage cell in the localCellMap
* @param worldPosition The world position of the cell
* @param voxelPosition The voxel position of the cell
* @return The key for the cell
*/
private 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);
}
}
}
}
}
//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);
}
}
}
}
}
}
//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){
//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);
ByteBuffer buffer = BufferUtils.createByteBuffer(TARGET_FOLIAGE_SPACING * TARGET_FOLIAGE_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 < TARGET_FOLIAGE_SPACING; x++){
for(int z = 0; z < TARGET_FOLIAGE_SPACING; z++){
//get position to place
double rand1 = placementRandomizer.nextDouble();
double rand2 = placementRandomizer.nextDouble();
double relativePositionOnGridX = x / (1.0 * TARGET_FOLIAGE_SPACING) - 0.5 + rand1 / TARGET_FOLIAGE_SPACING;
double relativePositionOnGridZ = z / (1.0 * TARGET_FOLIAGE_SPACING) - 0.5 + rand2 / TARGET_FOLIAGE_SPACING;
double offsetX = relativePositionOnGridX;
double offsetZ = relativePositionOnGridZ;
//determine quadrant we're placing in
double offsetY = 0;
boolean addBlade = false;
if(relativePositionOnGridX >=0){
if(relativePositionOnGridZ >= 0){
relativePositionOnGridX += 0.5;
relativePositionOnGridZ += 0.5;
//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 += 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){
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(TARGET_FOLIAGE_SPACING * TARGET_FOLIAGE_SPACING * SINGLE_FOLIAGE_DATA_SIZE_BYTES);
//construct data texture
Texture dataTexture = new Texture(buffer,SINGLE_FOLIAGE_DATA_SIZE_BYTES / 4,TARGET_FOLIAGE_SPACING * TARGET_FOLIAGE_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(new Vector3d(4.0, 4.0, 4.0));
//add ambient foliage behavior tree
AmbientFoliage.attachAmbientFoliageTree(grassEntity, initialGrowthLevel, foliageType.getGrowthModel().getGrowthRate());
cell.addEntity(grassEntity);
activeCells.add(cell);
locationCellMap.put(getFoliageCellKey(worldPos, voxelPos),cell);
}
}
}
/**
* 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;
}
/**
* 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 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();
}
}
}
/**
* Draws all foliage in the foliage manager
*/
public void draw(){
for(FoliageCell cell : activeCells){
cell.draw(modelMatrixAttribute);
}
}
}