Renderer/src/main/java/electrosphere/client/terrain/foliage/FoliageCell.java
austin af1daecdac
All checks were successful
studiorailgun/Renderer/pipeline/head This commit looks good
Start to standardize on doubles for positions
2024-12-03 20:54:21 -05:00

688 lines
27 KiB
Java

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