Finish prototype mesh environment

This commit is contained in:
2026-03-17 23:17:51 -04:00
parent 45c65f4f78
commit 70049922c5
4 changed files with 2354 additions and 2201 deletions

1
.gitignore vendored
View File

@@ -3,3 +3,4 @@
target target
.settings .settings
data data
bin

View File

@@ -1,5 +1,46 @@
package com.aaaaahhhhhhh.bananapuncher714.mesh.environment; package com.aaaaahhhhhhh.bananapuncher714.mesh.environment;
public class ChunkMeshCache { import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.ChunkLocation;
import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.ChunkMesh;
import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.util.ChunkMeshUtil;
public class ChunkMeshCache {
File directory;
public ChunkMeshCache( File directory ) {
this.directory = directory;
}
public ChunkMesh load( ChunkLocation location ) {
final File file = new File( directory, location.getWorldName() + "/" + location.getX() + "/" + location.getZ() + ".cmesh" );
if ( file.exists() ) {
try {
final byte[] data = Files.readAllBytes( file.toPath() );
final ChunkMesh mesh = ChunkMeshUtil.deserialize( data );
return mesh;
} catch ( IOException e ) {
e.printStackTrace();
}
}
return null;
}
public void save( ChunkLocation location, ChunkMesh mesh ) {
final File file = new File( directory, location.getWorldName() + "/" + location.getX() + "/" + location.getZ() + ".cmesh" );
file.getParentFile().mkdirs();
final byte[] data = ChunkMeshUtil.serialize( mesh );
try {
Files.write( file.toPath(), data, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE, StandardOpenOption.WRITE );
} catch ( IOException e ) {
e.printStackTrace();
}
}
} }

View File

@@ -1,9 +1,6 @@
package com.aaaaahhhhhhh.bananapuncher714.mesh.environment; package com.aaaaahhhhhhh.bananapuncher714.mesh.environment;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.util.ArrayDeque; import java.util.ArrayDeque;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@@ -16,7 +13,9 @@ import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.TreeSet; import java.util.TreeSet;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function; import java.util.function.Function;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -50,79 +49,140 @@ import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.BlockDataInfor
import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.BlockDataType; import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.BlockDataType;
import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.ChunkLocation; import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.ChunkLocation;
import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.ChunkMesh; import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.ChunkMesh;
import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.IndexedPolygon;
import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.IndexedTriangle; import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.IndexedTriangle;
import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.MinecraftPlane; import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.MinecraftPlane;
import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.MinecraftPlane.PlaneAxis; import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.MinecraftPlane.PlaneAxis;
import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.PlaneMesh; import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.PlaneMesh;
import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.SparseMesh; import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.SparseMesh;
import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.VectorReference; import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.objects.VectorReference;
import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.util.ChunkMeshUtil;
import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.util.NmsUtil; import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.util.NmsUtil;
import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.util.NmsUtil.BlockShapeType; import com.aaaaahhhhhhh.bananapuncher714.mesh.environment.util.NmsUtil.BlockShapeType;
import com.jme3.bullet.collision.shapes.MeshCollisionShape;
import com.jme3.bullet.collision.shapes.infos.IndexedMesh;
import com.jme3.bullet.objects.PhysicsBody;
import com.jme3.bullet.objects.PhysicsRigidBody;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
public class MeshEnvironment { public class MeshEnvironment {
private JavaPlugin plugin; private JavaPlugin plugin;
private File cacheDirectory; private File cacheDirectory;
private Map< ChunkLocation, ChunkMesh > chunkMeshes; private Map< ChunkLocation, Collection< Location > > updateLocations = new TreeMap< ChunkLocation, Collection< Location > >();
private Map< ChunkLocation, ChunkMesh > chunkMeshes = new HashMap< ChunkLocation, ChunkMesh >();
private Map< ChunkLocation, ChunkTask > tasks = new HashMap< ChunkLocation, ChunkTask >();
private ChunkMeshCache cache;
private ReentrantLock lock = new ReentrantLock();
final BukkitTask task = Bukkit.getScheduler().runTaskTimer( plugin, () -> {
// final Collection< ChunkLocation > locations = new ArrayDeque< ChunkLocation >();
lock.lock();
updateLocations.entrySet().stream().forEach( e -> {
ChunkMesh mesh = chunkMeshes.get( e.getKey() );
if ( mesh != null ) {
final Collection< Location > locs = update( mesh, e.getValue() );
e.getValue().removeAll( locs );
// if ( !locs.isEmpty() ) {
// locations.add( e.getKey() );
// }
}
} );
// if ( !locations.isEmpty() ) {
// space.activateAll( true );
// }
updateLocations.values().removeIf( c -> c.isEmpty() );
lock.unlock();
}, 1, 1 );
final Function< ChunkLocation, Collection< Location > > getLocationFor = c -> {
lock.lock();
Collection< Location > locations = updateLocations.get( c );
if ( locations == null ) {
locations = new HashSet< Location >();
updateLocations.put( c, locations );
}
lock.unlock();
return locations;
};
private Listener eventListener = new Listener() { private Listener eventListener = new Listener() {
@EventHandler @EventHandler
private void onChunkLoad( ChunkLoadEvent event ) { private void onChunkLoad( ChunkLoadEvent event ) {
// TODO Queue chunk load final ChunkLocation location = new ChunkLocation( event.getChunk() );
loadOrGenerateMesh( event.getChunk() );
lock.lock();
try {
if ( chunkMeshes.containsKey( location ) ) {
// Do nothing
} else {
final ChunkTask task = tasks.get( location );
if ( task == null ) {
loadMesh( location );
} else if ( task.type == ChunkTaskType.LOAD ) {
// Do nothing
} else if ( task.type == ChunkTaskType.UNLOAD ) {
// Do nothing
}
}
} finally {
lock.unlock();
}
/*
* Order of operations:
* - Check if:
* - The mesh is loaded
* - Do nothing
* - There is an unload queued
* - Do nothing
* - There is a load queued
* - Do nothing
* - Nothing is queued
* - Queue a load
*/
} }
@EventHandler @EventHandler
private void onChunkUnload( ChunkUnloadEvent event ) { private void onChunkUnload( ChunkUnloadEvent event ) {
// TODO Queue chunk unload final ChunkLocation location = new ChunkLocation( event.getChunk() );
if ( event.getWorld().getName().equalsIgnoreCase( "world" ) ) {
final ChunkLocation loc = new ChunkLocation( event.getChunk() );
if ( chunks.containsKey( loc ) ) {
space.removeCollisionObject( chunks.get( loc ) );
chunks.remove( loc );
}
final ChunkMesh mesh = chunkMeshes.remove( loc ); lock.lock();
if ( mesh != null ) { try {
saveMesh( loc, mesh ); if ( chunkMeshes.containsKey( location ) ) {
if ( tasks.containsKey( location ) ) {
// Wait for the chunk to finish loading
} else {
// Force all updates for this chunk
// Queue an unload
unloadMesh( location );
}
} else {
final ChunkTask task = tasks.get( location );
if ( task == null ) {
// Do nothing
} else if ( task.type == ChunkTaskType.LOAD ) {
// Do nothing
} else if ( task.type == ChunkTaskType.UNLOAD ) {
// Do nothing
}
} }
} finally {
lock.unlock();
} }
/*
* Order of operations:
* - Check if:
* - The mesh is unloaded
* - Do nothing
* - There is an unload queued
* - Do nothing
* - There is a load queued
* - Do nothing
* - Nothing is queued
* - Queue an unload
*/
} }
final Map< ChunkLocation, Collection< Location > > updateLocations = new TreeMap< ChunkLocation, Collection< Location > >();
final BukkitTask task = Bukkit.getScheduler().runTaskTimer( plugin, () -> {
final Collection< ChunkLocation > locations = new ArrayDeque< ChunkLocation >();
updateLocations.entrySet().stream().forEach( e -> {
final Collection< Location > locs = update( e.getValue() );
e.getValue().removeAll( locs );
if ( !locs.isEmpty() ) {
locations.add( e.getKey() );
}
} );
if ( !locations.isEmpty() ) {
space.activateAll( true );
}
updateLocations.values().removeIf( c -> c.isEmpty() );
}, 1, 1 );
final Function< ChunkLocation, Collection< Location > > getLocationFor = c -> {
Collection< Location > locations = updateLocations.get( c );
if ( locations == null ) {
locations = new HashSet< Location >();
updateLocations.put( c, locations );
}
return locations;
};
@EventHandler( priority = EventPriority.MONITOR ) @EventHandler( priority = EventPriority.MONITOR )
private void onBlockUpdateEvent( final BlockPhysicsEvent event ) { private void onBlockUpdateEvent( final BlockPhysicsEvent event ) {
final ChunkLocation chunkLocation = new ChunkLocation( event.getBlock().getChunk() ); final ChunkLocation chunkLocation = new ChunkLocation( event.getBlock().getChunk() );
@@ -133,6 +193,7 @@ public class MeshEnvironment {
public MeshEnvironment( JavaPlugin plugin, File cacheDirectory ) { public MeshEnvironment( JavaPlugin plugin, File cacheDirectory ) {
this.plugin = plugin; this.plugin = plugin;
this.cacheDirectory = cacheDirectory; this.cacheDirectory = cacheDirectory;
cache = new ChunkMeshCache( new File( cacheDirectory, "meshes" ) );
} }
public void activate() { public void activate() {
@@ -143,52 +204,136 @@ public class MeshEnvironment {
HandlerList.unregisterAll( eventListener ); HandlerList.unregisterAll( eventListener );
} }
private void loadOrGenerateMesh( final Chunk chunk ) { private void completeTask( ChunkLocation location ) {
final ChunkLocation location = new ChunkLocation( chunk ); lock.lock();
try {
final ChunkTask task = tasks.remove( location );
if ( task.type == ChunkTaskType.LOAD ) {
if ( !location.isLoaded() ) {
// Update all and unload
unloadMesh( location );
} else {
// Chunk is still loaded, do nothing
}
} else if ( task.type == ChunkTaskType.UNLOAD ) {
if ( !location.isLoaded() ) {
// Check if there are any updates that we missed
// If so, then queue another unload with the newest mesh
// Otherwise, remove the location from our list of locations
if ( updateLocations.containsKey( location ) ) {
unloadMesh( location );
} else {
chunkMeshes.remove( location );
}
} else {
// Chunk is loaded, but we saved. Do nothing
}
}
} finally {
lock.unlock();
}
}
final ChunkMesh mesh = loadMesh( location ); private void loadMesh( ChunkLocation location ) {
if ( mesh == null ) { ChunkTask task = new ChunkTask();
generateMesh( chunk ); lock.lock();
} else { try {
// Save our chunk mesh tasks.put( location, task );
chunkMeshes.put( location, mesh );
}
}
private ChunkMesh loadMesh( final ChunkLocation location ) { task.type = ChunkTaskType.LOAD;
final File file = new File( cacheDirectory, "meshes/" + location.getWorldName() + "/" + location.getX() + "/" + location.getZ() + ".cmesh" ); task.task = Bukkit.getScheduler().runTaskAsynchronously( plugin, () -> {
if ( file.exists() ) { final ChunkMesh mesh = cache.load( location );
try { if ( mesh != null ) {
final byte[] data = Files.readAllBytes( file.toPath() ); lock.lock();
final ChunkMesh mesh = ChunkMeshUtil.deserialize( data ); try {
// Save the mesh
if ( !chunkMeshes.containsKey( location ) ) {
chunkMeshes.put( location, mesh );
}
return mesh; completeTask( location );
} catch ( IOException e ) { } finally {
e.printStackTrace(); lock.unlock();
} }
} } else {
return null; // Process and generate the mesh
} Bukkit.getScheduler().runTask( plugin, () -> {
generateMesh( plugin, location.getWorld(), location.getChunk(), m -> {
lock.lock();
try {
chunkMeshes.put( location, m );
private void saveMesh( final ChunkLocation location, final ChunkMesh mesh ) { completeTask( location );
final File file = new File( cacheDirectory, "meshes/" + location.getWorldName() + "/" + location.getX() + "/" + location.getZ() + ".cmesh" ); } finally {
file.getParentFile().mkdirs(); lock.unlock();
}
final byte[] data = ChunkMeshUtil.serialize( mesh ); } );
} );
}
} );
} finally {
lock.unlock();
}
}
private void unloadMesh( ChunkLocation location ) {
ChunkTask task = new ChunkTask();
lock.lock();
try { try {
Files.write( file.toPath(), data, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE, StandardOpenOption.WRITE ); tasks.put( location, task );
} catch ( IOException e ) {
e.printStackTrace();
}
}
private SparseMesh generateSpareMesh( World world, Collection< Vector > locations ) { if ( !applyUpdatesToMesh( location ) ) {
final long start = System.currentTimeMillis(); // Not good! Piston issue somewhere
}
task.type = ChunkTaskType.UNLOAD;
task.task = Bukkit.getScheduler().runTaskAsynchronously( plugin, () -> {
lock.lock();
try {
ChunkMesh mesh = chunkMeshes.get( location );
if ( mesh != null ) {
// Save the mesh
cache.save( location, mesh );
} else {
// Not really good
}
completeTask( location );
} finally {
lock.unlock();
}
} );
} finally {
lock.unlock();
}
}
private boolean applyUpdatesToMesh( ChunkLocation location ) {
lock.lock();
try {
final Collection< Location > locations = updateLocations.get( location );
final ChunkMesh mesh = chunkMeshes.get( location );
if ( locations != null && !locations.isEmpty() && mesh != null ) {
locations.removeAll( update( mesh, locations ) );
return locations.isEmpty();
}
} finally {
lock.unlock();
}
return true;
}
private static void generateSpareMesh( JavaPlugin plugin, World world, Collection< Vector > locations, Consumer< SparseMesh > callback ) {
if ( locations.isEmpty() ) {
return;
}
boolean set = false; boolean set = false;
Vector min; Vector min = new Vector();
Vector max; Vector max = new Vector();
for ( Vector v : locations ) { for ( Vector v : locations ) {
if ( !set ) { if ( !set ) {
@@ -335,22 +480,19 @@ public class MeshEnvironment {
} }
} }
// Mesh the data and get the output Bukkit.getScheduler().runTaskAsynchronously( plugin, () -> {
final MeshResult meshResult = generateMeshFrom( polygons ); // Mesh the data and get the output
final MeshResult meshResult = generateMeshFrom( polygons );
sparseMesh.planes = meshResult.meshes; sparseMesh.planes = meshResult.meshes;
sparseMesh.pointReferences = meshResult.points; sparseMesh.pointReferences = meshResult.points;
// DONE callback.accept( sparseMesh );
} );
return sparseMesh;
} }
private void generateMesh( final Chunk chunk ) { // TODO Use ChunkSnapshot in the future
final long start = System.currentTimeMillis(); private static void generateMesh( JavaPlugin plugin, World world, final Chunk chunk, Consumer< ChunkMesh > callback ) {
final World world = chunk.getWorld();
final ChunkLocation chunkLocation = new ChunkLocation( chunk );
final int worldMinHeight = world.getMinHeight(); final int worldMinHeight = world.getMinHeight();
final int worldMaxHeight = world.getMaxHeight(); final int worldMaxHeight = world.getMaxHeight();
final int worldHeight = worldMaxHeight - worldMinHeight; final int worldHeight = worldMaxHeight - worldMinHeight;
@@ -508,44 +650,15 @@ public class MeshEnvironment {
chunkMesh.planes = meshResult.meshes; chunkMesh.planes = meshResult.meshes;
chunkMesh.pointReferences = meshResult.points; chunkMesh.pointReferences = meshResult.points;
// DONE callback.accept( chunkMesh );
// Now, create a physics object from the chunk mesh
final IndexedMesh nativeMesh = chunkMesh.toIndexedMesh();
// Enable quantized AABB compression... hopefully that doesn't mess with the accuracy or anything...
final MeshCollisionShape mesh = new MeshCollisionShape( true, nativeMesh );
// Create a rigid body
PhysicsRigidBody rigid = new PhysicsRigidBody( mesh, PhysicsBody.massForStatic );
rigid.setPhysicsLocation( new Vector3f( chunk.getX() << 4, world.getMinHeight(), chunk.getZ() << 4 ) );
rigid.setPhysicsRotation( new Quaternion( 0, 0, 0, 1 ) );
rigid.setKinematic( false );
Bukkit.getScheduler().runTask( plugin, () -> {
// Save our chunk mesh
chunkMeshes.put( chunkLocation, chunkMesh );
space.addCollisionObject( rigid );
PhysicsRigidBody old = chunks.put( new ChunkLocation( chunk ), rigid );
if ( old != null ) {
getLogger().warning( "Old rigid body found!" );
space.remove( old );
}
final long time = System.currentTimeMillis() - start;
// getLogger().info( "Took " + time + "ms to mesh chunk " + chunk.getX() + ", " + chunk.getZ() + " into " + mesh.countMeshVertices() + " vertices and " + mesh.countMeshTriangles() + " triangles" );
} );
} ); } );
} }
private Collection< Location > update( final AbstractMesh mesh, final Collection< Location > locations ) { private static Collection< Location > update( final AbstractMesh mesh, final Collection< Location > locations ) {
if ( locations.isEmpty() ) { if ( locations.isEmpty() ) {
return Collections.emptySet(); return Collections.emptySet();
} }
final World world = locations.iterator().next().getWorld();
final Collection< Location > canRemove = new ArrayDeque< Location >(); final Collection< Location > canRemove = new ArrayDeque< Location >();
// Check each location // Check each location
final Map< MinecraftPlane, Collection< Polygon > > polygons = new TreeMap< MinecraftPlane, Collection< Polygon > >(); final Map< MinecraftPlane, Collection< Polygon > > polygons = new TreeMap< MinecraftPlane, Collection< Polygon > >();
@@ -775,23 +888,10 @@ public class MeshEnvironment {
// Remove all vector references that have a reference count of 0 // Remove all vector references that have a reference count of 0
mesh.pointReferences.removeIf( v -> v.referenceCount <= 0 ); mesh.pointReferences.removeIf( v -> v.referenceCount <= 0 );
// DONE
// Now, create a physics object from the chunk mesh
final IndexedMesh nativeMesh = chunkMesh.toIndexedMesh();
// Enable quantized AABB compression... hopefully that doesn't mess with the accuracy or anything...
final MeshCollisionShape mesh = new MeshCollisionShape( true, nativeMesh );
// Create a rigid body
PhysicsRigidBody rigid = new PhysicsRigidBody( mesh, PhysicsBody.massForStatic );
rigid.setPhysicsLocation( new Vector3f( chunkLocation.getX() << 4, world.getMinHeight(), chunkLocation.getZ() << 4 ) );
rigid.setPhysicsRotation( new Quaternion( 0, 0, 0, 1 ) );
rigid.setKinematic( false );
return canRemove; return canRemove;
} }
private MeshResult generateMeshFrom( final Map< MinecraftPlane, Collection< Polygon > > polygons ) { private static MeshResult generateMeshFrom( final Map< MinecraftPlane, Collection< Polygon > > polygons ) {
final Map< MinecraftPlane, Mesh< RegionSimple > > meshMap = new HashMap< MinecraftPlane, Mesh< RegionSimple > >(); final Map< MinecraftPlane, Mesh< RegionSimple > > meshMap = new HashMap< MinecraftPlane, Mesh< RegionSimple > >();
for ( final Entry< MinecraftPlane, Collection< Polygon > > entry : polygons.entrySet() ) { for ( final Entry< MinecraftPlane, Collection< Polygon > > entry : polygons.entrySet() ) {
final MinecraftPlane plane = entry.getKey(); final MinecraftPlane plane = entry.getKey();
@@ -875,9 +975,11 @@ public class MeshEnvironment {
// Add all vector references // Add all vector references
result.points = vertices; result.points = vertices;
return result;
} }
private class CompleteMesh { private static class CompleteMesh {
Mesh< RegionSimple > mesh; Mesh< RegionSimple > mesh;
Collection< Polygon > triangles; Collection< Polygon > triangles;
@@ -887,8 +989,17 @@ public class MeshEnvironment {
} }
} }
private class MeshResult { private static class MeshResult {
Map< MinecraftPlane, PlaneMesh > meshes; Map< MinecraftPlane, PlaneMesh > meshes;
TreeSet< VectorReference > points; TreeSet< VectorReference > points;
} }
private enum ChunkTaskType {
LOAD, UNLOAD
}
private class ChunkTask {
BukkitTask task;
ChunkTaskType type;
}
} }