major pathfinding work
All checks were successful
studiorailgun/Renderer/pipeline/head This commit looks good

This commit is contained in:
austin 2025-05-02 16:29:18 -04:00
parent 4637770997
commit 18023872b0
11 changed files with 674 additions and 49 deletions

View File

@ -1648,6 +1648,7 @@ Fix bug where sync messages eternally bounce if the entity was already deleted
Fix blocks not saving to disk when being ejected from cache Fix blocks not saving to disk when being ejected from cache
Block chunk memory pooling Block chunk memory pooling
Rename MoveToTree Rename MoveToTree
Major pathfinding work -- breaking MoteToTree

View File

@ -84,6 +84,7 @@ import electrosphere.server.datacell.EntityDataCellMapper;
import electrosphere.server.datacell.RealmManager; import electrosphere.server.datacell.RealmManager;
import electrosphere.server.db.DatabaseController; import electrosphere.server.db.DatabaseController;
import electrosphere.server.entity.poseactor.PoseModel; import electrosphere.server.entity.poseactor.PoseModel;
import electrosphere.server.pathfinding.Pathfinder;
import electrosphere.server.saves.Save; import electrosphere.server.saves.Save;
import electrosphere.server.simulation.MacroSimulation; import electrosphere.server.simulation.MacroSimulation;
import electrosphere.server.simulation.MicroSimulation; import electrosphere.server.simulation.MicroSimulation;
@ -430,6 +431,9 @@ public class Globals {
public static Entity draggedItem = null; public static Entity draggedItem = null;
public static Object dragSourceInventory = null; public static Object dragSourceInventory = null;
//pathfinder
public static Pathfinder pathfinder;
@ -518,6 +522,9 @@ public class Globals {
gameConfigCurrent = gameConfigDefault; gameConfigCurrent = gameConfigDefault;
NetConfig.readNetConfig(); NetConfig.readNetConfig();
//pathfinder
Globals.pathfinder = new Pathfinder();
// //
//Values that depend on the loaded config //Values that depend on the loaded config
Globals.clientSelectedVoxelType = (VoxelType)gameConfigCurrent.getVoxelData().getTypes().toArray()[1]; Globals.clientSelectedVoxelType = (VoxelType)gameConfigCurrent.getVoxelData().getTypes().toArray()[1];

View File

@ -70,4 +70,9 @@ public class BlackboardKeys {
*/ */
public static final String HARVEST_TARGET_TYPE = "harvestTargetType"; public static final String HARVEST_TARGET_TYPE = "harvestTargetType";
/**
* The pathfinding data
*/
public static final String PATHFINDING_DATA = "pathfindingData";
} }

View File

@ -0,0 +1,98 @@
package electrosphere.server.ai.nodes.plan;
import java.util.List;
import org.joml.Vector3d;
import electrosphere.engine.Globals;
import electrosphere.entity.Entity;
import electrosphere.entity.EntityUtils;
import electrosphere.server.ai.blackboard.Blackboard;
import electrosphere.server.ai.blackboard.BlackboardKeys;
import electrosphere.server.ai.nodes.AITreeNode;
import electrosphere.server.datacell.Realm;
import electrosphere.server.datacell.interfaces.PathfindingManager;
import electrosphere.server.pathfinding.PathingProgressiveData;
/**
* A node that performs pathfinding
*/
public class PathfindingNode implements AITreeNode {
/**
* The blackboard key to lookup the target entity under
*/
String targetEntityKey;
/**
*
* @param targetEntityKey
*/
public static PathfindingNode createPathEntity(String targetEntityKey){
PathfindingNode rVal = new PathfindingNode();
rVal.targetEntityKey = targetEntityKey;
return rVal;
}
@Override
public AITreeNodeResult evaluate(Entity entity, Blackboard blackboard) {
if(!PathfindingNode.hasPathfindingData(blackboard)){
Vector3d targetPos = null;
if(this.targetEntityKey != null){
Entity targetEnt = (Entity)blackboard.get(targetEntityKey);
targetPos = EntityUtils.getPosition(targetEnt);
} else {
throw new Error("Target position is null!");
}
Realm realm = Globals.realmManager.getEntityRealm(entity);
PathfindingManager pathfindingManager = realm.getPathfindingManager();
Vector3d entityPos = EntityUtils.getPosition(entity);
List<Vector3d> path = pathfindingManager.findPath(entityPos, targetPos);
PathingProgressiveData pathingProgressiveData = new PathingProgressiveData(path);
PathfindingNode.setPathfindingData(blackboard, pathingProgressiveData);
}
return AITreeNodeResult.SUCCESS;
}
/**
* Sets the pathfinding data in the blackboard
* @param blackboard The blackboard
* @param pathfindingData The pathfinding data
*/
public static void setPathfindingData(Blackboard blackboard, PathingProgressiveData pathfindingData){
blackboard.put(BlackboardKeys.PATHFINDING_DATA, pathfindingData);
}
/**
* Gets the current pathfinding data
* @param blackboard The blackboard
* @return The pathfinding data if it exists, null otherwise
*/
public static PathingProgressiveData getPathfindingData(Blackboard blackboard){
return (PathingProgressiveData)blackboard.get(BlackboardKeys.PATHFINDING_DATA);
}
/**
* Checks if the blackboard has pathfinding data
* @param blackboard The blackboard
* @return true if it has pathfinding data, false otherwise
*/
public static boolean hasPathfindingData(Blackboard blackboard){
return blackboard.has(BlackboardKeys.PATHFINDING_DATA);
}
/**
* Clears the pathfinding data
* @param blackboard The pathfinding data
*/
public static void clearPathfindingData(Blackboard blackboard){
blackboard.delete(BlackboardKeys.PATHFINDING_DATA);
}
}

View File

@ -10,6 +10,7 @@ import electrosphere.server.ai.nodes.meta.collections.SelectorNode;
import electrosphere.server.ai.nodes.meta.collections.SequenceNode; import electrosphere.server.ai.nodes.meta.collections.SequenceNode;
import electrosphere.server.ai.nodes.meta.decorators.RunnerNode; import electrosphere.server.ai.nodes.meta.decorators.RunnerNode;
import electrosphere.server.ai.nodes.meta.decorators.SucceederNode; import electrosphere.server.ai.nodes.meta.decorators.SucceederNode;
import electrosphere.server.ai.nodes.plan.PathfindingNode;
/** /**
* Moves to a target * Moves to a target
@ -38,7 +39,7 @@ public class MoveToTree {
//not in range of target, keep moving towards it //not in range of target, keep moving towards it
new SequenceNode( new SequenceNode(
//check that dependencies exist PathfindingNode.createPathEntity(targetKey),
new FaceTargetNode(targetKey), new FaceTargetNode(targetKey),
new RunnerNode(new MoveStartNode(MovementRelativeFacing.FORWARD)) new RunnerNode(new MoveStartNode(MovementRelativeFacing.FORWARD))
) )

View File

@ -15,6 +15,7 @@ import java.util.concurrent.locks.ReentrantLock;
import org.joml.Vector3d; import org.joml.Vector3d;
import org.joml.Vector3i; import org.joml.Vector3i;
import org.recast4j.detour.MeshData;
import electrosphere.client.block.BlockChunkData; import electrosphere.client.block.BlockChunkData;
import electrosphere.client.terrain.data.TerrainChunkData; import electrosphere.client.terrain.data.TerrainChunkData;
@ -364,7 +365,11 @@ public class GriddedDataCellManager implements DataCellManager, VoxelCellManager
ServerDataCell serverDataCell = this.groundDataCells.get(key); ServerDataCell serverDataCell = this.groundDataCells.get(key);
GriddedDataCellTrackingData trackingData = this.cellTrackingMap.get(serverDataCell); GriddedDataCellTrackingData trackingData = this.cellTrackingMap.get(serverDataCell);
if(terrainMeshData.getVertices().length > 0){ if(terrainMeshData.getVertices().length > 0){
trackingData.setNavMeshData(NavMeshConstructor.constructNavmesh(terrainMeshData)); MeshData pathingMeshData = NavMeshConstructor.constructNavmesh(terrainMeshData);
if(pathingMeshData == null){
throw new Error("Failed to build pathing data from existing vertices!");
}
trackingData.setNavMeshData(pathingMeshData);
} }
loadedCellsLock.lock(); loadedCellsLock.lock();
@ -802,7 +807,11 @@ public class GriddedDataCellManager implements DataCellManager, VoxelCellManager
//create pathfinding mesh //create pathfinding mesh
if(terrainMeshData.getVertices().length > 0){ if(terrainMeshData.getVertices().length > 0){
trackingData.setNavMeshData(NavMeshConstructor.constructNavmesh(terrainMeshData)); MeshData pathingMeshData = NavMeshConstructor.constructNavmesh(terrainMeshData);
if(pathingMeshData == null){
throw new Error("Failed to build pathing data from existing vertices!");
}
trackingData.setNavMeshData(pathingMeshData);
} }
//set ready //set ready
@ -1104,7 +1113,18 @@ public class GriddedDataCellManager implements DataCellManager, VoxelCellManager
@Override @Override
public List<Vector3d> findPath(Vector3d start, Vector3d end) { public List<Vector3d> findPath(Vector3d start, Vector3d end) {
throw new UnsupportedOperationException("Unimplemented method 'findPath'"); Vector3i startChunkPos = ServerWorldData.convertRealToChunkSpace(start);
ServerDataCell cell = this.getCellAtWorldPosition(startChunkPos);
GriddedDataCellTrackingData trackingData = this.cellTrackingMap.get(cell);
if(trackingData == null){
throw new Error("Failed to find tracking data for " + start);
}
MeshData trackingMeshData = trackingData.getNavMeshData();
if(trackingMeshData == null){
throw new Error("Tracking mesh data is null!");
}
List<Vector3d> points = Globals.pathfinder.solve(trackingMeshData, start, end);
return points;
} }
/** /**

View File

@ -1,111 +1,131 @@
package electrosphere.server.pathfinding; package electrosphere.server.pathfinding;
import org.joml.Vector3d;
import org.recast4j.detour.MeshData; import org.recast4j.detour.MeshData;
import org.recast4j.detour.NavMeshBuilder; import org.recast4j.detour.NavMeshBuilder;
import org.recast4j.detour.NavMeshDataCreateParams; import org.recast4j.detour.NavMeshDataCreateParams;
import org.recast4j.recast.AreaModification; import org.recast4j.recast.AreaModification;
import org.recast4j.recast.CompactHeightfield;
import org.recast4j.recast.Heightfield;
import org.recast4j.recast.PolyMesh; import org.recast4j.recast.PolyMesh;
import org.recast4j.recast.PolyMeshDetail; import org.recast4j.recast.PolyMeshDetail;
import org.recast4j.recast.RecastBuilder; import org.recast4j.recast.RecastBuilder;
import org.recast4j.recast.RecastBuilder.RecastBuilderResult; import org.recast4j.recast.RecastBuilder.RecastBuilderResult;
import org.recast4j.recast.RecastConstants.PartitionType;
import org.recast4j.recast.RecastBuilderConfig; import org.recast4j.recast.RecastBuilderConfig;
import org.recast4j.recast.RecastConfig; import org.recast4j.recast.RecastConfig;
import org.recast4j.recast.RecastConstants;
import org.recast4j.recast.RecastConstants.PartitionType;
import org.recast4j.recast.Span;
import org.recast4j.recast.geom.SingleTrimeshInputGeomProvider; import org.recast4j.recast.geom.SingleTrimeshInputGeomProvider;
import electrosphere.client.terrain.data.TerrainChunkData; import electrosphere.entity.state.collidable.TriGeomData;
import electrosphere.util.math.GeomUtils;
/** /**
* Constructor methods for nav meshes * Constructor methods for nav meshes
*/ */
public class NavMeshConstructor { public class NavMeshConstructor {
/**
* Minimum size of geometry aabb
*/
public static final float AABB_MIN_SIZE = 0.01f;
/** /**
* Size of a recast cell * Size of a recast cell
*/ */
static final float RECAST_CELL_SIZE = 1.0f; static final float RECAST_CELL_SIZE = 0.3f;
/** /**
* Height of a recast cell * Height of a recast cell
*/ */
static final float RECAST_CELL_HEIGHT = 1.0f; static final float RECAST_CELL_HEIGHT = 0.2f;
/**
* Size of a recast agent
*/
static final float RECAST_AGENT_SIZE = 1.0f;
/** /**
* Height of a recast agent * Height of a recast agent
*/ */
static final float RECAST_AGENT_HEIGHT = 1.0f; static final float RECAST_AGENT_HEIGHT = 1.0f;
/**
* Size of a recast agent
*/
static final float RECAST_AGENT_SIZE = 0.5f;
/** /**
* Maximum height a recast agent can climb * Maximum height a recast agent can climb
*/ */
static final float RECAST_AGENT_MAX_CLIMB = 0.1f; static final float RECAST_AGENT_MAX_CLIMB = 0.9f;
/** /**
* Maximum slope a recast agent can handle * Maximum slope a recast agent can handle
*/ */
static final float RECAST_AGENT_MAX_SLOPE = 0.4f; static final float RECAST_AGENT_MAX_SLOPE = 60.0f;
/** /**
* Minimum size of a recast region * Minimum size of a recast region
*/ */
static final int RECAST_MIN_REGION_SIZE = 1; static final int RECAST_MIN_REGION_SIZE = 0;
/** /**
* Merge size of a recast region * Merge size of a recast region
*/ */
static final int RECAST_REGION_MERGE_SIZE = 1; static final int RECAST_REGION_MERGE_SIZE = 0;
static final float RECAST_REGION_EDGE_MAX_LEN = 1.0f; static final float RECAST_REGION_EDGE_MAX_LEN = 20.0f;
static final float RECAST_REGION_EDGE_MAX_ERROR = 1.0f; static final float RECAST_REGION_EDGE_MAX_ERROR = 3.3f;
static final int RECAST_VERTS_PER_POLY = 3; static final int RECAST_VERTS_PER_POLY = 6;
static final float RECAST_DETAIL_SAMPLE_DIST = 0.1f; static final float RECAST_DETAIL_SAMPLE_DIST = 1.0f;
static final float RECAST_DETAIL_SAMPLE_MAX_ERROR = 0.1f; static final float RECAST_DETAIL_SAMPLE_MAX_ERROR = 1.0f;
/** /**
* Constructs a navmesh * Constructs a navmesh
* @param terrainChunk The terrain chunk * @param terrainChunk The terrain chunk
* @return the MeshData * @return the MeshData
*/ */
public static MeshData constructNavmesh(TerrainChunkData terrainChunkData){ public static MeshData constructNavmesh(TriGeomData geomData){
MeshData rVal = null; MeshData rVal = null;
try { try {
RecastConfig recastConfig = new RecastConfig(
PartitionType.WATERSHED, //build polymesh
RECAST_CELL_SIZE, RecastBuilderResult recastBuilderResult = NavMeshConstructor.buildPolymesh(geomData);
RECAST_CELL_HEIGHT,
RECAST_AGENT_HEIGHT,
RECAST_AGENT_SIZE,
RECAST_AGENT_MAX_CLIMB,
RECAST_AGENT_MAX_SLOPE,
RECAST_MIN_REGION_SIZE,
RECAST_REGION_MERGE_SIZE,
RECAST_REGION_EDGE_MAX_LEN,
RECAST_REGION_EDGE_MAX_ERROR,
RECAST_VERTS_PER_POLY,
RECAST_DETAIL_SAMPLE_DIST,
RECAST_DETAIL_SAMPLE_MAX_ERROR,
new AreaModification(0)
);
SingleTrimeshInputGeomProvider geomProvider = new SingleTrimeshInputGeomProvider(terrainChunkData.getVertices(), terrainChunkData.getFaceElements());
RecastBuilderConfig recastBuilderConfig = new RecastBuilderConfig(recastConfig, geomProvider.getMeshBoundsMin(), geomProvider.getMeshBoundsMax());
RecastBuilder recastBuilder = new RecastBuilder();
RecastBuilderResult recastBuilderResult = recastBuilder.build(geomProvider, recastBuilderConfig);
PolyMesh polyMesh = recastBuilderResult.getMesh(); PolyMesh polyMesh = recastBuilderResult.getMesh();
Vector3d polyMin = new Vector3d(polyMesh.bmin[0],polyMesh.bmin[1],polyMesh.bmin[2]);
Vector3d polyMax = new Vector3d(polyMesh.bmax[0],polyMesh.bmax[1],polyMesh.bmax[2]);
if(polyMin.x < 0 || polyMin.y < 0 || polyMin.z < 0){
String message = "Min bound is less than 0\n" +
NavMeshConstructor.polyMeshToString(polyMesh);
throw new Error(message);
}
if(polyMin.distance(polyMax) < AABB_MIN_SIZE){
String message = "Bounding box is too small for polymesh\n" +
NavMeshConstructor.polyMeshToString(polyMesh);
throw new Error(message);
}
//error check the built result
if(polyMesh.nverts < 1){
String message = "Failed to generate verts in poly mesh\n" +
NavMeshConstructor.polyMeshToString(polyMesh);
throw new Error(message);
}
if(polyMesh.npolys < 1){
String message = "Failed to generate polys in poly mesh\n" +
NavMeshConstructor.polyMeshToString(polyMesh);
throw new Error(message);
}
//set flags
for(int i = 0; i < polyMesh.npolys; i++){ for(int i = 0; i < polyMesh.npolys; i++){
polyMesh.flags[i] = 1; polyMesh.flags[i] = 1;
} }
//set params //set params
NavMeshDataCreateParams params = new NavMeshDataCreateParams(); NavMeshDataCreateParams params = new NavMeshDataCreateParams();
params.verts = polyMesh.verts; params.verts = polyMesh.verts;
@ -163,4 +183,120 @@ public class NavMeshConstructor {
return rVal; return rVal;
} }
/**
* Builds a builder result from a geom data
* @param geomData The geom data
* @return The builder result
*/
protected static RecastBuilderResult buildPolymesh(TriGeomData geomData){
//create the geometry provider and error check
SingleTrimeshInputGeomProvider geomProvider = NavMeshConstructor.getSingleTrimeshInputGeomProvider(geomData);
//build configs
RecastConfig recastConfig = new RecastConfig(
PartitionType.WATERSHED,
RECAST_CELL_SIZE,
RECAST_CELL_HEIGHT,
RECAST_AGENT_HEIGHT,
RECAST_AGENT_SIZE,
RECAST_AGENT_MAX_CLIMB,
RECAST_AGENT_MAX_SLOPE,
RECAST_MIN_REGION_SIZE,
RECAST_REGION_MERGE_SIZE,
RECAST_REGION_EDGE_MAX_LEN,
RECAST_REGION_EDGE_MAX_ERROR,
RECAST_VERTS_PER_POLY,
RECAST_DETAIL_SAMPLE_DIST,
RECAST_DETAIL_SAMPLE_MAX_ERROR,
new AreaModification(1)
);
float[] boundMax = new float[]{
geomProvider.getMeshBoundsMax()[0],
geomProvider.getMeshBoundsMax()[1] + RECAST_AGENT_HEIGHT,
geomProvider.getMeshBoundsMax()[2]
};
RecastBuilderConfig recastBuilderConfig = new RecastBuilderConfig(recastConfig, geomProvider.getMeshBoundsMin(), boundMax);
RecastBuilder recastBuilder = new RecastBuilder();
//actually build polymesh
RecastBuilderResult recastBuilderResult = recastBuilder.build(geomProvider, recastBuilderConfig);
return recastBuilderResult;
}
/**
* Creates the geom provider from a given trimesh
* @return The geom provider
*/
protected static SingleTrimeshInputGeomProvider getSingleTrimeshInputGeomProvider(TriGeomData geomData){
//error check input
if(!GeomUtils.isWindingClockwise(geomData.getVertices(), geomData.getFaceElements())){
throw new Error("Geometry is not wound clockwise!");
}
//create the geometry provider and error check
SingleTrimeshInputGeomProvider geomProvider = new SingleTrimeshInputGeomProvider(geomData.getVertices(), geomData.getFaceElements());
//check the bounding box
Vector3d aabbStart = new Vector3d(geomProvider.getMeshBoundsMin()[0],geomProvider.getMeshBoundsMin()[1],geomProvider.getMeshBoundsMin()[2]);
Vector3d aabbEnd = new Vector3d(geomProvider.getMeshBoundsMax()[0],geomProvider.getMeshBoundsMax()[1],geomProvider.getMeshBoundsMax()[2]);
if(aabbStart.distance(aabbEnd) < AABB_MIN_SIZE){
throw new Error("Geometry provider's AABB is too small " + aabbStart.distance(aabbEnd));
}
return geomProvider;
}
/**
* Counts the spans of a building result
* @param recastBuilderResult The result
* @return The number of spans
*/
protected static int countSpans(RecastBuilderResult recastBuilderResult){
Heightfield heightfield = recastBuilderResult.getSolidHeightfield();
int count = 0;
int w = heightfield.width;
int h = heightfield.height;
for(int y = 0; y < h; ++y) {
for(int x = 0; x < w; ++x) {
for(Span s = heightfield.spans[x + y * w]; s != null; s = s.next) {
if(s.area != RecastConstants.RC_NULL_AREA){
count++;
}
}
}
}
return count;
}
/**
* Counts the walkable spans in the compact heightfield
* @param recastBuilderResult The build result
* @return The number of walkable spans
*/
protected static int countWalkableSpans(RecastBuilderResult recastBuilderResult){
CompactHeightfield chf = recastBuilderResult.getCompactHeightfield();
int count = 0;
for (int i = 0; i < chf.spanCount; i++){
if(chf.spans[i] != null){
count++;
}
}
return count;
}
/**
* Converts a polymesh to a string
* @param polyMesh The polymesh
* @return The string
*/
protected static String polyMeshToString(PolyMesh polyMesh){
return "" +
"nverts: " + polyMesh.nverts + "\n" +
"verts.length: " + polyMesh.verts.length + "\n" +
"npolys: " + polyMesh.npolys + "\n" +
"polys.length: " + polyMesh.polys.length + "\n" +
"bmin: " + polyMesh.bmin[0] + "," + polyMesh.bmin[1] + "," + polyMesh.bmin[2] + "\n" +
"bmin: " + polyMesh.bmax[0] + "," + polyMesh.bmax[1] + "," + polyMesh.bmax[2] + "\n" +
"";
}
} }

View File

@ -2,8 +2,10 @@ package electrosphere.server.pathfinding;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Objects;
import org.joml.Vector3d; import org.joml.Vector3d;
import org.joml.Vector3f;
import org.recast4j.detour.DefaultQueryFilter; import org.recast4j.detour.DefaultQueryFilter;
import org.recast4j.detour.FindNearestPolyResult; import org.recast4j.detour.FindNearestPolyResult;
import org.recast4j.detour.MeshData; import org.recast4j.detour.MeshData;
@ -11,8 +13,11 @@ import org.recast4j.detour.NavMesh;
import org.recast4j.detour.NavMeshQuery; import org.recast4j.detour.NavMeshQuery;
import org.recast4j.detour.QueryFilter; import org.recast4j.detour.QueryFilter;
import org.recast4j.detour.Result; import org.recast4j.detour.Result;
import org.recast4j.detour.Status;
import org.recast4j.detour.StraightPathItem; import org.recast4j.detour.StraightPathItem;
import electrosphere.server.physics.terrain.manager.ServerTerrainChunk;
/** /**
* Performs pathfinding * Performs pathfinding
*/ */
@ -33,6 +38,10 @@ public class Pathfinder {
public List<Vector3d> solve(MeshData mesh, Vector3d startPos, Vector3d endPos){ public List<Vector3d> solve(MeshData mesh, Vector3d startPos, Vector3d endPos){
List<Vector3d> rVal = new LinkedList<Vector3d>(); List<Vector3d> rVal = new LinkedList<Vector3d>();
if(mesh == null){
throw new Error("Mesh data is null!");
}
//construct objects //construct objects
NavMesh navMesh = new NavMesh(mesh,6,0); NavMesh navMesh = new NavMesh(mesh,6,0);
NavMeshQuery query = new NavMeshQuery(navMesh); NavMeshQuery query = new NavMeshQuery(navMesh);
@ -41,11 +50,11 @@ public class Pathfinder {
//convert points to correct datatypes //convert points to correct datatypes
float[] startArr = new float[]{(float)startPos.x, (float)startPos.y, (float)startPos.z}; float[] startArr = new float[]{(float)startPos.x, (float)startPos.y, (float)startPos.z};
float[] endArr = new float[]{(float)endPos.x, (float)endPos.y, (float)endPos.z}; float[] endArr = new float[]{(float)endPos.x, (float)endPos.y, (float)endPos.z};
float[] polySearchBounds = new float[]{10,10,10}; float[] polySearchBounds = new float[]{ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET,ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET,ServerTerrainChunk.CHUNK_PLACEMENT_OFFSET};
//find start poly //find start poly
Result<FindNearestPolyResult> startPolyResult = query.findNearestPoly(startArr, polySearchBounds, filter); Result<FindNearestPolyResult> startPolyResult = query.findNearestPoly(startArr, polySearchBounds, filter);
if(startPolyResult.failed()){ if(!startPolyResult.succeeded()){
String message = "Failed to solve for start polygon!\n" + String message = "Failed to solve for start polygon!\n" +
startPolyResult.message startPolyResult.message
; ;
@ -55,7 +64,7 @@ public class Pathfinder {
//find end poly //find end poly
Result<FindNearestPolyResult> endPolyResult = query.findNearestPoly(endArr, polySearchBounds, filter); Result<FindNearestPolyResult> endPolyResult = query.findNearestPoly(endArr, polySearchBounds, filter);
if(endPolyResult.failed()){ if(!endPolyResult.succeeded()){
String message = "Failed to solve for end polygon!\n" + String message = "Failed to solve for end polygon!\n" +
endPolyResult.message endPolyResult.message
; ;
@ -63,12 +72,29 @@ public class Pathfinder {
} }
long endRef = endPolyResult.result.getNearestRef(); long endRef = endPolyResult.result.getNearestRef();
if(startRef == 0){
throw new Error("Start ref is 0!");
}
if(endRef == 0){
throw new Error("End ref is 0!");
}
//solve path //solve path
Result<List<Long>> pathResult = query.findPath(startRef, endRef, startArr, endArr, filter); Result<List<Long>> pathResult = query.findPath(startRef, endRef, startArr, endArr, filter);
if(pathResult.failed()){ if(pathResult.failed()){
String message = "Failed to solve for path!\n" + String message = "Failed to solve for path!\n" +
pathResult.message pathResult.message + "\n" +
pathResult.status + "\n" +
""
; ;
if(pathResult.status == Status.FAILURE_INVALID_PARAM){
message = "Failed to solve for path -- invalid param!\n" +
"Message: " + pathResult.message + "\n" +
"Status: " + pathResult.status + "\n" +
Pathfinder.checkInvalidParam(navMesh,startRef,endRef,startArr,endArr) + "\n" +
""
;
}
throw new Error(message); throw new Error(message);
} }
@ -89,4 +115,28 @@ public class Pathfinder {
return rVal; return rVal;
} }
/**
* Checks params to a path query
* @param mesh The mesh
* @param startRef The start ref
* @param endRef The end ref
* @param startPos The start pos
* @param endPos THe end pos
* @return The string containing the data
*/
private static String checkInvalidParam(NavMesh mesh, long startRef, long endRef, float[] startPos, float[] endPos){
//none of these should be true
return "" +
"startRef: " + startRef + "\n" +
"endRef: " + endRef + "\n" +
"StartRef poly area succeeded: " + mesh.getPolyArea(startRef).succeeded() + "\n" +
"EndRef poly area succeeded: " + mesh.getPolyArea(endRef).succeeded() + "\n" +
"StartPos is null: " + Objects.isNull(startPos) + "\n" +
"StartPos is finite: " + !new Vector3f(startPos[0],startPos[1],startPos[2]).isFinite() + "\n" +
"EndPos is null: " + Objects.isNull(endPos) + "\n" +
"EndPos is finite: " + !new Vector3f(endPos[0],endPos[1],endPos[2]).isFinite() + "\n" +
""
;
}
} }

View File

@ -0,0 +1,65 @@
package electrosphere.server.pathfinding;
import java.util.List;
import org.joml.Vector3d;
/**
* Data tracking moving along a solved path
*/
public class PathingProgressiveData {
/**
* The list of points that represent the path
*/
List<Vector3d> points;
/**
* The current point to move towards (ie all previous points have already been pathed to)
*/
int currentPoint;
/**
* Constructor
* @param points The points for the path
*/
public PathingProgressiveData(List<Vector3d> points){
this.points = points;
this.currentPoint = 0;
}
/**
* Gets the points that define the path
* @return The points
*/
public List<Vector3d> getPoints() {
return points;
}
/**
* Sets the points that define the path
* @param points The points
*/
public void setPoints(List<Vector3d> points) {
this.points = points;
}
/**
* Gets the current point to move towards
* @return The current point's index
*/
public int getCurrentPoint() {
return currentPoint;
}
/**
* Sets the current point to move towards
* @param currentPoint The current point's index
*/
public void setCurrentPoint(int currentPoint) {
this.currentPoint = currentPoint;
}
}

View File

@ -511,4 +511,64 @@ public class GeomUtils {
} }
} }
/**
* Checks the winding of a given set of geometry
* @param verts The vertices
* @param indices The indices
*/
public static void checkWinding(float[] verts, int[] indices) {
for (int i = 0; i < indices.length; i += 3) {
int ia = indices[i] * 3;
int ib = indices[i + 1] * 3;
int ic = indices[i + 2] * 3;
float ax = verts[ia];
float az = verts[ia + 2];
float bx = verts[ib];
float bz = verts[ib + 2];
float cx = verts[ic];
float cz = verts[ic + 2];
// Compute signed area of the triangle in XZ plane
float signedArea = (bx - ax) * (cz - az) - (cx - ax) * (bz - az);
if (signedArea < 0) {
System.out.println("Triangle " + (i / 3) + " is wound CLOCKWISE (probably incorrect)");
} else if (signedArea > 0) {
System.out.println("Triangle " + (i / 3) + " is wound COUNTER-CLOCKWISE (likely correct)");
} else {
System.out.println("Triangle " + (i / 3) + " is degenerate (zero area)");
}
}
}
/**
* Checks that the winding of the geometry it clockwise
* @param verts The verts
* @param indices The indices
* @return true if all triangles are wound clockwise, false otherwise
*/
public static boolean isWindingClockwise(float[] verts, int[] indices){
for (int i = 0; i < indices.length; i += 3) {
int ia = indices[i] * 3;
int ib = indices[i + 1] * 3;
int ic = indices[i + 2] * 3;
float ax = verts[ia];
float az = verts[ia + 2];
float bx = verts[ib];
float bz = verts[ib + 2];
float cx = verts[ic];
float cz = verts[ic + 2];
// Compute signed area of the triangle in XZ plane
float signedArea = (bx - ax) * (cz - az) - (cx - ax) * (bz - az);
if (signedArea > 0) {
return false;
}
}
return true;
}
} }

View File

@ -0,0 +1,182 @@
package electrosphere.server.pathfinding;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import org.joml.Vector3d;
import org.recast4j.detour.MeshData;
import org.recast4j.recast.ContourSet;
import org.recast4j.recast.RecastBuilder.RecastBuilderResult;
import org.recast4j.recast.geom.SingleTrimeshInputGeomProvider;
import electrosphere.entity.state.collidable.TriGeomData;
import electrosphere.test.annotations.UnitTest;
import electrosphere.util.math.GeomUtils;
/**
* Tests for navmesh constructor
*/
public class NavMeshConstructorTests {
/**
* Expected size of the geom 1 aabb
*/
static final int GEOM_1_EXPECTED_AABB = 1;
/**
* Number of spans expected for geom 1
*/
static final int GEOM_1_SPAN_COUNT = 496;
/**
* Test constructing a simple navmesh
*/
@UnitTest
public void test_constructNavmesh_geom1(){
TriGeomData geom = new TriGeomData() {
@Override
public float[] getVertices() {
return new float[]{
0f, 0f, 0f,
10f, 0f, 0f,
0f, 0f, 10f
};
}
@Override
public int[] getFaceElements() {
return new int[]{
2, 1, 0
};
}
};
MeshData meshData = NavMeshConstructor.constructNavmesh(geom);
assertNotNull(meshData);
}
/**
* Test constructing a simple navmesh
*/
@UnitTest
public void test_countSpans_geom1(){
TriGeomData geom = new TriGeomData() {
@Override
public float[] getVertices() {
return new float[]{
0f, 0f, 0f,
10f, 0f, 0f,
0f, 0f, 10f
};
}
@Override
public int[] getFaceElements() {
return new int[]{
2, 1, 0
};
}
};
//actually build polymesh
RecastBuilderResult recastBuilderResult = NavMeshConstructor.buildPolymesh(geom);
int spanCount = NavMeshConstructor.countSpans(recastBuilderResult);
assertEquals(GEOM_1_SPAN_COUNT, spanCount);
}
/**
* Test constructing a simple navmesh
*/
@UnitTest
public void test_countWalkableSpans_geom1(){
TriGeomData geom = new TriGeomData() {
@Override
public float[] getVertices() {
return new float[]{
0f, 0f, 0f,
10f, 0f, 0f,
0f, 0f, 10f
};
}
@Override
public int[] getFaceElements() {
return new int[]{
2, 1, 0
};
}
};
//actually build polymesh
RecastBuilderResult recastBuilderResult = NavMeshConstructor.buildPolymesh(geom);
int spanCount = NavMeshConstructor.countWalkableSpans(recastBuilderResult);
assertEquals(GEOM_1_SPAN_COUNT, spanCount);
}
/**
* Test constructing a simple navmesh
*/
@UnitTest
public void test_ContourCount_geom1(){
TriGeomData geom = new TriGeomData() {
@Override
public float[] getVertices() {
return new float[]{
0f, 0f, 0f,
10f, 0f, 0f,
0f, 0f, 10f
};
}
@Override
public int[] getFaceElements() {
return new int[]{
2, 1, 0
};
}
};
//actually build polymesh
RecastBuilderResult recastBuilderResult = NavMeshConstructor.buildPolymesh(geom);
GeomUtils.checkWinding(geom.getVertices(), geom.getFaceElements());
ContourSet contourSet = recastBuilderResult.getContourSet();
assertNotEquals(0,contourSet.width);
assertNotEquals(0,contourSet.height);
assertNotEquals(0,contourSet.conts.size());
}
/**
* Test constructing a simple navmesh
*/
@UnitTest
public void test_getSingleTrimeshInputGeomProvider_geom1(){
TriGeomData geom = new TriGeomData() {
@Override
public float[] getVertices() {
return new float[]{
0f, 0f, 0f,
10f, 0f, 0f,
0f, 0f, 10f
};
}
@Override
public int[] getFaceElements() {
return new int[]{
2, 1, 0
};
}
};
SingleTrimeshInputGeomProvider geomProvider = NavMeshConstructor.getSingleTrimeshInputGeomProvider(geom);
assertNotNull(geomProvider);
Vector3d aabbStart = new Vector3d(geomProvider.getMeshBoundsMin()[0],geomProvider.getMeshBoundsMin()[1],geomProvider.getMeshBoundsMin()[2]);
Vector3d aabbEnd = new Vector3d(geomProvider.getMeshBoundsMax()[0],geomProvider.getMeshBoundsMax()[1],geomProvider.getMeshBoundsMax()[2]);
boolean greaterThan = aabbStart.distance(aabbEnd) > GEOM_1_EXPECTED_AABB;
assertEquals(true, greaterThan);
}
}