diff --git a/.gitignore b/.gitignore index 359475f..475016f 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ chunk_models chunk_models_new chunks_test chunk_models_test -facets \ No newline at end of file +facets +polygons diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Mesh.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Mesh.java index 7c4ff05..596b7bd 100644 --- a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Mesh.java +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/mesh/Mesh.java @@ -16,6 +16,7 @@ import java.util.Queue; import java.util.Set; import java.util.Stack; import java.util.TreeSet; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -92,7 +93,8 @@ public class Mesh< T extends Region > { protected MeshState state = MeshState.TRIANGULATION_READY; - protected Collection< MeshEventHandler > handlers = new LinkedHashSet< MeshEventHandler >(); + protected Collection< MeshChainEventHandler > chainHandlers = new LinkedHashSet< MeshChainEventHandler >(); + protected Collection< MeshPartitionEventHandler > partitionHandlers = new LinkedHashSet< MeshPartitionEventHandler >(); protected boolean mergeChains = true; @@ -113,7 +115,7 @@ public class Mesh< T extends Region > { } HalfEdge edge = null; - for ( Point point : poly.getPoints() ) { + for ( final Point point : poly.getPoints() ) { if ( edge == null ) { edge = new HalfEdge(); HalfEdge.splice( edge, edge.getSym() ); @@ -138,16 +140,28 @@ public class Mesh< T extends Region > { } } - public boolean addHandler( final MeshEventHandler handler ) { - return handlers.add( handler ); + public boolean addPartitionHandler( final MeshPartitionEventHandler handler ) { + return partitionHandlers.add( handler ); } - public boolean removeHandler( final MeshEventHandler handler ) { - return handlers.remove( handler ); + public boolean removePartitionHandler( final MeshPartitionEventHandler handler ) { + return partitionHandlers.remove( handler ); } - public Collection< MeshEventHandler > getHandlers() { - return Collections.unmodifiableCollection( handlers ); + public boolean addChainHandler( final MeshChainEventHandler handler ) { + return chainHandlers.add( handler ); + } + + public boolean removeChainHandler( final MeshChainEventHandler handler ) { + return chainHandlers.remove( handler ); + } + + public Collection< MeshChainEventHandler > getChainHandlers() { + return Collections.unmodifiableCollection( chainHandlers ); + } + + public Collection< MeshPartitionEventHandler > getPartitionHandlers() { + return Collections.unmodifiableCollection( partitionHandlers ); } public Set< Point > getVertices() { @@ -163,12 +177,35 @@ public class Mesh< T extends Region > { return set.parallelStream().map( e -> new Segment( pointMap.get( e.getOrigin() ), pointMap.get( e.getDest() ) ) ).collect( Collectors.toSet() ); } + public Collection< Polygon > getPolygons() { + if ( state != MeshState.TRIANGULATION_READY ) { + throw new IllegalStateException( "Cannot form polygons!" ); + } + + final Collection< Polygon > polygons = new ArrayDeque< Polygon >(); + + final Map< Vertex, Point > pointMap = vertices.parallelStream().collect( Collectors.toMap( Function.identity(), v -> new Point( v.getPosition() ) ) ); + final Collection< HalfEdge > scanned = new HashSet< HalfEdge >(); + + for ( HalfEdge edge : interiorEdges ) { + if ( scanned.add( edge ) ) { + final List< Point > points = new ArrayList< Point >(); + do { + points.add( pointMap.get( edge.getOrigin() ) ); + } while ( scanned.add( edge = edge.getNext() ) ); + polygons.add( new Polygon( points ) ); + } + } + + return polygons; + } + public int getVertexCount() { return vertices.size(); } public int getEdgeCount() { - return rules.size(); + return rules.size() >> 1; } public boolean isMergeChains() { @@ -305,7 +342,8 @@ public class Mesh< T extends Region > { vertexSet.parallelStream().forEach( Vertex::update ); // Do a quick merging of edges so we don't have to deal with them later - mergeOverlappingEdges( vertexSet ); + // Not really necessary, but _may_ offer a speedup. +// mergeOverlappingEdges( vertexSet ); /* * As we iterate through each vertex, there are certain operations @@ -320,6 +358,8 @@ public class Mesh< T extends Region > { // This set contains the non-redundant vertices. final Set< Vertex > scannedVertices = new HashSet< Vertex >(); + // TODO Remove zero edges + while ( !vertexSet.isEmpty() ) { final Vertex vertex = vertexSet.pollFirst(); final Vector2d pos = vertex.getPosition(); @@ -346,11 +386,11 @@ public class Mesh< T extends Region > { it.remove(); } } - + { // Remove any zero-edges HalfEdge validEdge = null; - for ( HalfEdge edge : vertex ) { + for ( final HalfEdge edge : vertex ) { if ( edge.isZero() ) { // At this point in time, if the edge is zero, // then the origin and destination must be the same @@ -537,11 +577,18 @@ public class Mesh< T extends Region > { // This assumes that if a and b are collinear, then they _must_ share // the same origin, since we are traversing through the vertices // starting with the smallest. - for ( final HalfEdge edge : vertex ) { + final Collection< HalfEdge > scanned = new HashSet< HalfEdge >(); + HalfEdge edge = vertex.getEdge(); + while ( scanned.add( edge ) ) { // Only check this edge if it is not already scanned if ( checked.add( edge ) ) { final Vector2d r = edge.toVector2d().normalize(); - for ( final HalfEdge other : vertex ) { + + // Keep track of which edges we have already looped over during this sub-loop + // which we have not checked with the main loop + final Collection< HalfEdge > subScanned = new HashSet< HalfEdge >( scanned ); + HalfEdge other = edge.getPrev(); + while ( subScanned.add( other ) ) { if ( !checked.contains( other ) ) { // Do not split edges that are pretty much equal, since they will get merged later if ( !isSimilar( edge.getDest(), other.getDest() ) ) { @@ -550,7 +597,6 @@ public class Mesh< T extends Region > { // Ignore edges that are not collinear if ( Math.abs( angle ) <= CROSS_TOLERANCE ) { - HalfEdge shorter; HalfEdge longer; @@ -563,24 +609,54 @@ public class Mesh< T extends Region > { longer = edge; } - // Update the winding rule for the split - final HalfEdge split = longer.split(); - RegionRule< T > rule = rules.get( longer ); - rules.put( split, rule ); - rules.put( split.getSym(), rule.inverse() ); - - // Update the vertex for the split - final Vertex vert = shorter.getDest(); - split.setOrigin( vert ); - longer.getSym().setOrigin( vert ); - - // Splice the two together - HalfEdge.splice( shorter.getSym(), split ); + { + // Update the winding rule for the split + final HalfEdge split = longer.split(); + final RegionRule< T > rule = rules.get( longer ); + rules.put( split, rule ); + rules.put( split.getSym(), rule.inverse() ); + + // Update the vertex for the split + final Vertex vert = shorter.getDest(); + split.setOrigin( vert ); + longer.getSym().setOrigin( vert ); + vert.setEdge( split ); + + // Splice the two together + HalfEdge.splice( shorter.getSym(), split ); + } + + /* + * Now that we've successfully split the shorter and longer edges, + * remove the other edge, but keep the current edge that we are + * iterating over... + */ + { + // Get and merge the rule + RegionRule< T > rule = rules.get( edge ); + + rule = rule.apply( rules.remove( other ) ); + rules.remove( other.getSym() ); + + // Now remove the edge + HalfEdge.splice( other, other.getSym().getNext() ); + HalfEdge.splice( other.getSym(), other.getNext() ); + + rules.put( edge, rule ); + rules.put( edge.getSym(), rule.inverse() ); + } + + + other = edge.getPrev(); + } else { + other = other.getPrev(); } } } } } + + edge = edge.getPrev(); } } @@ -1103,7 +1179,7 @@ public class Mesh< T extends Region > { if ( mergeChains ) { final Collection< Link > allChains = findChains( vertices, edges ); final Collection< Link > chains = maximizeChains( allChains ); - if ( !handlers.isEmpty() ) { + if ( !chainHandlers.isEmpty() ) { final Collection< Chain > newChains = new HashSet< Chain >(); final Collection< Link > seen = new HashSet< Link >(); @@ -1127,7 +1203,7 @@ public class Mesh< T extends Region > { } } - handlers.forEach( h -> h.onChainGenerationEvent( newChains ) ); + chainHandlers.forEach( h -> h.onChainGenerationEvent( newChains ) ); final Collection< Link > selectedSeen = new HashSet< Link >(); final Collection< Chain > selectedChains = new HashSet< Chain >(); @@ -1151,7 +1227,7 @@ public class Mesh< T extends Region > { } } - handlers.forEach( h -> h.onPreChainMergeEvent( selectedChains ) ); + chainHandlers.forEach( h -> h.onPreChainMergeEvent( selectedChains ) ); } mergeChains( chains, edges ); @@ -1159,7 +1235,7 @@ public class Mesh< T extends Region > { final Collection< EdgePolygon > polys = partitionMonotone( vertices, edges ); - if ( handlers != null ) { + if ( partitionHandlers != null ) { final Collection< Polygon > polygons = new LinkedHashSet< Polygon >(); for ( EdgePolygon p : polys ) { @@ -1172,7 +1248,7 @@ public class Mesh< T extends Region > { polygons.add( new Polygon( points ) ); } - handlers.forEach( h -> h.onPartitionEvent( polygons ) ); + partitionHandlers.forEach( h -> h.onPartitionEvent( polygons ) ); } if ( mergeChains ) { @@ -2896,7 +2972,7 @@ public class Mesh< T extends Region > { prev = edge; } - + // The last edge must be a negative edge, since // the last vertex must terminate the polygon and // therefore have no right-going edges. @@ -2922,7 +2998,7 @@ public class Mesh< T extends Region > { polygon.addEdge( newEdge ); } - + // Convert each vertex to a point final Map< Vertex, Point > pointMap = polygon.vertices.parallelStream().collect( Collectors.toMap( Function.identity(), v -> new Point( v.getPosition() ) ) ); @@ -3076,10 +3152,13 @@ public class Mesh< T extends Region > { } - public static interface MeshEventHandler { + public static interface MeshPartitionEventHandler { + default void onPartitionEvent( final Collection< Polygon > polygons ) {}; + } + + public static interface MeshChainEventHandler { default void onChainGenerationEvent( final Collection< Chain > chains ) {}; default void onPreChainMergeEvent( final Collection< Chain > chains ) {}; - default void onPartitionEvent( final Collection< Polygon > polygons ) {}; } protected enum MeshState { diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/ChunkMeshCache.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/ChunkMeshCache.java new file mode 100644 index 0000000..41a3848 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/ChunkMeshCache.java @@ -0,0 +1,51 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest; + +import java.io.File; +import java.util.Collection; +import java.util.HashSet; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.locks.ReentrantLock; + +import org.bukkit.Chunk; + +import com.aaaaahhhhhhh.bananapuncher714.minietest.objects.ChunkLocation; +import com.aaaaahhhhhhh.bananapuncher714.minietest.objects.ChunkMesh; + +public class ChunkMeshCache { + final File directory; + Map< ChunkLocation, ChunkMesh > meshes = new TreeMap< ChunkLocation, ChunkMesh >(); + Collection< ChunkLocation > shouldRemove = new HashSet< ChunkLocation >(); + ReentrantLock lock = new ReentrantLock(); + + public ChunkMeshCache( final File directory ) { + this.directory = directory; + } + + public void onChunkLoad( final Chunk chunk ) { + final ChunkLocation location = new ChunkLocation( chunk ); + + // Unqueue this chunk for removal if it's queued + shouldRemove.remove( location ); + + // TODO Add a ticket + chunk.addPluginChunkTicket( null ); + + // Does the mesh map contain the current location? + { + // If not, then load or mesh the chunk + // Emit a chunk mesh complete event + } + + } + + public void saveAndRemove( final ChunkLocation location ) { + lock.lock(); + meshes.remove( location ); + lock.unlock(); + } + + public void saveAndRemoveAll() { + lock.unlock(); + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshingTest2.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshingTest2.java index f372460..91101e9 100644 --- a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshingTest2.java +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshingTest2.java @@ -17,6 +17,7 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -38,7 +39,8 @@ import org.bukkit.util.Vector; import com.aaaaahhhhhhh.bananapuncher714.mesh.Chain; import com.aaaaahhhhhhh.bananapuncher714.mesh.Mesh; -import com.aaaaahhhhhhh.bananapuncher714.mesh.Mesh.MeshEventHandler; +import com.aaaaahhhhhhh.bananapuncher714.mesh.Mesh.MeshChainEventHandler; +import com.aaaaahhhhhhh.bananapuncher714.mesh.Mesh.MeshPartitionEventHandler; import com.aaaaahhhhhhh.bananapuncher714.mesh.Point; import com.aaaaahhhhhhh.bananapuncher714.mesh.Polygon; import com.aaaaahhhhhhh.bananapuncher714.mesh.Segment; @@ -134,7 +136,7 @@ public class MeshingTest2 extends JPanel { final List< Plane > masterPlanes = new ArrayList< Plane >(); System.out.println( "Adding test plane" ); - masterPlanes.add( getTestPlane() ); + masterPlanes.addAll( getTestPlanes() ); System.out.println( "Chunk files: " + CHUNK_DIR.listFiles().length ); int limit = 1; @@ -279,7 +281,7 @@ public class MeshingTest2 extends JPanel { System.out.println( "After generating regions vertex count: " + mesh.getVertices().size() ); System.out.println( "After generating regions edge count: " + ( mesh.getEdgeCount() / 2 ) ); - mesh.addHandler( new MeshEventHandler() { + mesh.addChainHandler( new MeshChainEventHandler() { @Override public void onChainGenerationEvent( Collection< Chain > chains ) { data.chains = chains; @@ -310,7 +312,9 @@ public class MeshingTest2 extends JPanel { // } // } } - + } ); + + mesh.addPartitionHandler( new MeshPartitionEventHandler() { @Override public void onPartitionEvent( Collection< Polygon > polygons ) { data.polygons = polygons; @@ -391,7 +395,7 @@ public class MeshingTest2 extends JPanel { private static PlaneData test() { final PlaneData data = new PlaneData(); final Mesh< RegionSimple > mesh = new Mesh< RegionSimple >( () -> { return new RegionSimple( GluWindingRule.ODD ); } ); - mesh.addHandler( new MeshEventHandler() { + mesh.addChainHandler( new MeshChainEventHandler() { @Override public void onChainGenerationEvent( Collection< Chain > chains ) { data.chains = chains; @@ -552,11 +556,13 @@ public class MeshingTest2 extends JPanel { return data; } - private static Plane getTestPlane() { - final Plane plane = new Plane(); - - plane.normal = new Vector( 0, 0, 1 ); - plane.point = new Vector( 1, 0, 0 ); + private static Collection< Plane > getTestPlanes() { + final Collection< Plane > planes = new ArrayDeque< Plane >(); + { + final Plane plane = new Plane(); + + plane.normal = new Vector( 0, 0, 1 ); + plane.point = new Vector( 1, 0, 0 ); // plane.polygons.add( new Polygon( Arrays.asList( // new Point( 1, 0 ), @@ -586,62 +592,113 @@ public class MeshingTest2 extends JPanel { // new Point( 0, 1 ) // ) ) ); - plane.polygons.add( new Polygon( Arrays.asList( - new Point( -1, -1 ), - new Point( 1, -1 ), - new Point( 1, 1 ), - new Point( -1, 1 ) - ) ) ); - plane.polygons.add( new Polygon( Arrays.asList( - new Point( -1, -1 ), - new Point( 1, -1 ), - new Point( 1, 1 ), - new Point( -1, 1 ) - ) ) ); + plane.polygons.add( new Polygon( Arrays.asList( + new Point( -1, -1 ), + new Point( 1, -1 ), + new Point( 1, 1 ), + new Point( -1, 1 ) + ) ) ); + plane.polygons.add( new Polygon( Arrays.asList( + new Point( -1, -1 ), + new Point( 1, -1 ), + new Point( 1, 1 ), + new Point( -1, 1 ) + ) ) ); + + plane.polygons.add( new Polygon( Arrays.asList( + new Point( -1, -1 ), + new Point( 1, -1 ), + new Point( 1, 1 ), + new Point( -1, 1 ) + ) ) ); + plane.polygons.add( new Polygon( Arrays.asList( + new Point( -1, 1 ), + new Point( 1, 1 ), + new Point( 1, 3 ), + new Point( -1, 3 ) + ) ) ); + plane.polygons.add( new Polygon( Arrays.asList( + new Point( -2, -1 ), + new Point( -1, -1 ), + new Point( -1, 2 ), + new Point( -2, 2 ) + ) ) ); + plane.polygons.add( new Polygon( Arrays.asList( + new Point( -4, -2 ), + new Point( -1, -2 ), + new Point( -1, 1.5 ), + new Point( -4, 1.5 ) + ) ) ); + plane.polygons.add( new Polygon( Arrays.asList( + new Point( -3, 0 ), + new Point( 3, 0 ), + new Point( -1.5, -3 ), + new Point( 0, 1.5 ), + new Point( 1.5, -3 ) + ) ) ); + plane.polygons.add( new Polygon( Arrays.asList( + new Point( -1, 0 ), + new Point( 0, 1.5 ), + new Point( 1, 0 ) + ) ) ); + plane.polygons.add( new Polygon( Arrays.asList( + new Point( 1, 0 ), + new Point( 0, -2 ), + new Point( -1, 0 ) + ) ) ); + + planes.add( plane ); + } - plane.polygons.add( new Polygon( Arrays.asList( - new Point( -1, -1 ), - new Point( 1, -1 ), - new Point( 1, 1 ), - new Point( -1, 1 ) - ) ) ); - plane.polygons.add( new Polygon( Arrays.asList( - new Point( -1, 1 ), - new Point( 1, 1 ), - new Point( 1, 3 ), - new Point( -1, 3 ) - ) ) ); - plane.polygons.add( new Polygon( Arrays.asList( - new Point( -2, -1 ), - new Point( -1, -1 ), - new Point( -1, 2 ), - new Point( -2, 2 ) - ) ) ); - plane.polygons.add( new Polygon( Arrays.asList( - new Point( -4, -2 ), - new Point( -1, -2 ), - new Point( -1, 1.5 ), - new Point( -4, 1.5 ) - ) ) ); - plane.polygons.add( new Polygon( Arrays.asList( - new Point( -3, 0 ), - new Point( 3, 0 ), - new Point( -1.5, -3 ), - new Point( 0, 1.5 ), - new Point( 1.5, -3 ) - ) ) ); - plane.polygons.add( new Polygon( Arrays.asList( - new Point( -1, 0 ), - new Point( 0, 1.5 ), - new Point( 1, 0 ) - ) ) ); - plane.polygons.add( new Polygon( Arrays.asList( - new Point( 1, 0 ), - new Point( 0, -2 ), - new Point( -1, 0 ) - ) ) ); + { + final Plane plane = new Plane(); + + plane.normal = new Vector( 0, 0, 1 ); + plane.point = new Vector( 1, 0, 0 ); + + plane.polygons.add( new Polygon( Arrays.asList( + new Point( 0, 0 ), + new Point( 3, 0 ), + new Point( 3, 2 ), + new Point( 2, 2 ), + new Point( 2, 1 ), + new Point( 1, 1 ), + new Point( 1, 2 ), + new Point( 0, 2 ) + ) ) ); + plane.polygons.add( new Polygon( Arrays.asList( + new Point( 1, 1 ), + new Point( 2, 1 ), + new Point( 2, 2 ), + new Point( 1, 2 ) + ) ) ); + + planes.add( plane ); + } - return plane; + { + final Plane plane = new Plane(); + + plane.normal = new Vector( 0, 0, 1 ); + plane.point = new Vector( 1, 0, 0 ); + + plane.polygons.add( new Polygon( Arrays.asList( + new Point( 0, 0 ), + new Point( 3, 0 ), + new Point( 3, 2 ), + new Point( 0, 2 ) + ) ) ); + plane.polygons.add( new Polygon( Arrays.asList( + new Point( 1, 1 ), + new Point( 2, 1 ), + new Point( 2, 2 ), + new Point( 1, 2 ) + ) ) ); + + planes.add( plane ); + } + + return planes; } private static List< Facet > generateFacetsFor( AABB box ) { diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshingTest5.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshingTest5.java index 90c4194..46701e7 100644 --- a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshingTest5.java +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshingTest5.java @@ -8,7 +8,11 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import org.bukkit.util.Vector; @@ -27,6 +31,8 @@ public class MeshingTest5 { private static final File BASE = new File( System.getProperty( "user.dir" ) ); private static final File FACET_DIR = new File( BASE, "facets" ); + private static Collection< Facet > facets = Collections.synchronizedCollection( new LinkedHashSet< Facet >() ); + public static void main( String[] args ) { if ( FACET_DIR.exists() && FACET_DIR.isDirectory() ) { System.out.println( "Found " + FACET_DIR.list().length + " files" ); @@ -100,7 +106,9 @@ public class MeshingTest5 { mesh.addPolygon( poly, RegionRuleWinding.CLOCKWISE ); } - mesh.meshify(); + synchronized( facets ) { + facets.addAll( plane.convert( mesh.meshify() ) ); + } } else { System.out.println( "No data!" ); } diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshingTest6.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshingTest6.java index 44c6100..efad81e 100644 --- a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshingTest6.java +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshingTest6.java @@ -38,7 +38,8 @@ import org.bukkit.util.Vector; import com.aaaaahhhhhhh.bananapuncher714.mesh.Chain; import com.aaaaahhhhhhh.bananapuncher714.mesh.Mesh; -import com.aaaaahhhhhhh.bananapuncher714.mesh.Mesh.MeshEventHandler; +import com.aaaaahhhhhhh.bananapuncher714.mesh.Mesh.MeshChainEventHandler; +import com.aaaaahhhhhhh.bananapuncher714.mesh.Mesh.MeshPartitionEventHandler; import com.aaaaahhhhhhh.bananapuncher714.mesh.Point; import com.aaaaahhhhhhh.bananapuncher714.mesh.Polygon; import com.aaaaahhhhhhh.bananapuncher714.mesh.Segment; @@ -292,7 +293,7 @@ public class MeshingTest6 extends JPanel { System.out.println( "After generating regions vertex count: " + mesh.getVertices().size() ); System.out.println( "After generating regions edge count: " + ( mesh.getEdgeCount() / 2 ) ); - mesh.addHandler( new MeshEventHandler() { + mesh.addChainHandler( new MeshChainEventHandler() { @Override public void onChainGenerationEvent( Collection< Chain > chains ) { data.chains = chains; @@ -323,13 +324,15 @@ public class MeshingTest6 extends JPanel { } } } - + } ); + + mesh.addPartitionHandler( new MeshPartitionEventHandler() { @Override public void onPartitionEvent( Collection< Polygon > polygons ) { data.polygons = polygons; } } ); - + try { // If the chains are the issue, set this to false and see if it still meshes mesh.setMergeChains( true ); @@ -411,7 +414,7 @@ public class MeshingTest6 extends JPanel { private static PlaneData test() { final PlaneData data = new PlaneData(); final Mesh< RegionSimple > mesh = new Mesh< RegionSimple >( () -> { return new RegionSimple( GluWindingRule.ODD ); } ); - mesh.addHandler( new MeshEventHandler() { + mesh.addChainHandler( new MeshChainEventHandler() { @Override public void onChainGenerationEvent( Collection< Chain > chains ) { data.chains = chains; diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshingTest7.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshingTest7.java new file mode 100644 index 0000000..a749b33 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MeshingTest7.java @@ -0,0 +1,95 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import com.aaaaahhhhhhh.bananapuncher714.mesh.Mesh; +import com.aaaaahhhhhhh.bananapuncher714.mesh.Point; +import com.aaaaahhhhhhh.bananapuncher714.mesh.Polygon; +import com.aaaaahhhhhhh.bananapuncher714.mesh.region.simple.RegionRuleWinding; +import com.aaaaahhhhhhh.bananapuncher714.mesh.region.simple.RegionSimple; +import com.aaaaahhhhhhh.bananapuncher714.mesh.region.simple.RegionSimple.GluWindingRule; + +public class MeshingTest7 { + private static final File BASE = new File( System.getProperty( "user.dir" ) ); + private static final File POLYGON_DIR = new File( BASE, "polygons" ); + + public static void main( String[] args ) { + if ( POLYGON_DIR.exists() && POLYGON_DIR.isDirectory() ) { + System.out.println( "Found " + POLYGON_DIR.list().length + " files" ); + + final long allStart = System.currentTimeMillis(); + final AtomicInteger index = new AtomicInteger( 0 ); + final Collection< File > files = Arrays.asList( POLYGON_DIR.listFiles() ); + files.parallelStream().forEach( f -> { + final StringBuilder builder = new StringBuilder(); + + builder.append( "Meshing file: " + f + "\n" ); + + final long start = System.currentTimeMillis(); + final List< Polygon > polygons = mesh( f, builder ); + + Mesh< RegionSimple > mesh = new Mesh< RegionSimple >( () -> { return new RegionSimple( GluWindingRule.ODD ); } ); + for ( Polygon poly : polygons ) { + mesh.addPolygon( poly, RegionRuleWinding.CLOCKWISE ); + } + + mesh.meshify(); + + long time = System.currentTimeMillis() - start; + + builder.append( "\tTook " + time + "ms\n" ); + builder.append( "\tCompleted " + index.incrementAndGet() + "/" + files.size() ); + + System.out.println( builder.toString() ); + } ); + System.out.println( "Took a total of " + ( System.currentTimeMillis() - allStart ) + "ms" ); + } else { + System.err.println( "No such directory exists: " + POLYGON_DIR ); + } + } + + private static List< Polygon > mesh( File file, final StringBuilder stringBuilder ) { + // First parse the file to get a list of all polygons + final List< Polygon > polys = new ArrayList< Polygon >(); + try ( BufferedReader reader = new BufferedReader( new FileReader( file ) ) ) { + String line; + while ( ( line = reader.readLine() ) != null ) { + if ( !line.isEmpty() ) { + final int pointCount = Integer.parseInt( line ); + + List< Point > points = new ArrayList< Point >( pointCount ); + + int point = 0; + while ( point < pointCount && ( line = reader.readLine() ) != null ) { + if ( !line.isEmpty() ) { + String[] values2 = line.split( " " ); + final double px = Double.parseDouble( values2[ 0 ] ); + final double py = Double.parseDouble( values2[ 1 ] ); + points.add( new Point( px, py ) ); + ++point; + } + } + + polys.add( new Polygon( points ) ) ; + } + } + } catch ( FileNotFoundException e ) { + e.printStackTrace(); + return null; + } catch ( IOException e ) { + e.printStackTrace(); + return null; + } + + return polys; + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MiniePlugin.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MiniePlugin.java index ba95417..a30048c 100644 --- a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MiniePlugin.java +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MiniePlugin.java @@ -5,6 +5,7 @@ import java.io.FileWriter; import java.io.IOException; import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -13,12 +14,18 @@ import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.stream.Collectors; import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; import org.apache.commons.lang.SystemUtils; import org.bukkit.Bukkit; import org.bukkit.Chunk; +import org.bukkit.ChunkSnapshot; import org.bukkit.Location; import org.bukkit.Material; import org.bukkit.World; @@ -32,11 +39,14 @@ import org.bukkit.entity.BlockDisplay; import org.bukkit.entity.Display; import org.bukkit.entity.Player; import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; import org.bukkit.event.Listener; +import org.bukkit.event.block.BlockPhysicsEvent; import org.bukkit.event.world.ChunkLoadEvent; import org.bukkit.event.world.ChunkUnloadEvent; import org.bukkit.inventory.ItemStack; import org.bukkit.plugin.java.JavaPlugin; +import org.bukkit.scheduler.BukkitTask; import org.bukkit.util.BoundingBox; import org.bukkit.util.Transformation; import org.bukkit.util.Vector; @@ -44,18 +54,25 @@ import org.joml.Matrix4f; import org.joml.Quaternionf; import com.aaaaahhhhhhh.bananapuncher714.mesh.Mesh; +import com.aaaaahhhhhhh.bananapuncher714.mesh.Point; import com.aaaaahhhhhhh.bananapuncher714.mesh.Polygon; import com.aaaaahhhhhhh.bananapuncher714.mesh.region.simple.RegionRuleWinding; import com.aaaaahhhhhhh.bananapuncher714.mesh.region.simple.RegionSimple; import com.aaaaahhhhhhh.bananapuncher714.mesh.region.simple.RegionSimple.GluWindingRule; -import com.aaaaahhhhhhh.bananapuncher714.minietest.MeshingTest2.AABB; import com.aaaaahhhhhhh.bananapuncher714.minietest.command.CommandParameters; import com.aaaaahhhhhhh.bananapuncher714.minietest.command.SubCommand; import com.aaaaahhhhhhh.bananapuncher714.minietest.command.executor.CommandExecutableMessage; import com.aaaaahhhhhhh.bananapuncher714.minietest.command.validator.InputValidatorDouble; import com.aaaaahhhhhhh.bananapuncher714.minietest.command.validator.InputValidatorInt; import com.aaaaahhhhhhh.bananapuncher714.minietest.command.validator.sender.SenderValidatorPlayer; +import com.aaaaahhhhhhh.bananapuncher714.minietest.objects.BlockDataType; import com.aaaaahhhhhhh.bananapuncher714.minietest.objects.ChunkLocation; +import com.aaaaahhhhhhh.bananapuncher714.minietest.objects.ChunkMesh; +import com.aaaaahhhhhhh.bananapuncher714.minietest.objects.IndexedPolygon; +import com.aaaaahhhhhhh.bananapuncher714.minietest.objects.MinecraftPlane; +import com.aaaaahhhhhhh.bananapuncher714.minietest.objects.MinecraftPlane.PlaneAxis; +import com.aaaaahhhhhhh.bananapuncher714.minietest.objects.PlaneMesh; +import com.aaaaahhhhhhh.bananapuncher714.minietest.objects.VectorReference; import com.aaaaahhhhhhh.bananapuncher714.minietest.objects.mesh.Facet; import com.aaaaahhhhhhh.bananapuncher714.minietest.objects.mesh.MeshBuilder; import com.aaaaahhhhhhh.bananapuncher714.minietest.util.FileUtil; @@ -85,6 +102,7 @@ import com.jme3.math.Vector3f; import net.minecraft.core.BlockPosition; import net.minecraft.server.level.WorldServer; +import net.minecraft.world.phys.AxisAlignedBB; import net.minecraft.world.phys.shapes.VoxelShape; import net.minecraft.world.phys.shapes.VoxelShapeCollision; @@ -109,6 +127,11 @@ public class MiniePlugin extends JavaPlugin { private Map< Vector3f, CollisionShape > collisionShapes = new HashMap< Vector3f, CollisionShape >(); + private Map< ChunkLocation, ChunkMesh > chunkMeshes = new HashMap< ChunkLocation, ChunkMesh >(); + + private Map< ChunkLocation, BukkitTask > loadMeshTasks = new ConcurrentHashMap< ChunkLocation, BukkitTask >(); + private Map< ChunkLocation, BukkitTask > saveMeshTasks = new ConcurrentHashMap< ChunkLocation, BukkitTask >(); + @Override public void onEnable() { FileUtil.saveToFile( getResource( "native/windows/x86_64/bulletjme.dll" ), new File( getDataFolder() + "/lib", "bulletjme.dll" ), false ); @@ -162,30 +185,71 @@ public class MiniePlugin extends JavaPlugin { @EventHandler private void onChunkLoad( ChunkLoadEvent event ) { if ( event.getWorld().getName().equalsIgnoreCase( "world" ) ) { - getLogger().info( "Loading chunk " + event.getChunk().getX() + ", " + event.getChunk().getZ() ); - scanAndGenerate( event.getChunk() ); - saveFacets( event.getChunk() ); +// getLogger().info( "Loading chunk " + event.getChunk().getX() + ", " + event.getChunk().getZ() ); + generateMesh( event.getChunk() ); +// scanAndGenerate( event.getChunk() ); +// saveFacets( event.getChunk() ); +// loadOrGenerateMesh( event.getChunk() ); } } @EventHandler private void onChunkUnload( ChunkUnloadEvent event ) { if ( event.getWorld().getName().equalsIgnoreCase( "world" ) ) { - ChunkLocation loc = new ChunkLocation( event.getChunk() ); + final ChunkLocation loc = new ChunkLocation( event.getChunk() ); if ( chunks.containsKey( loc ) ) { space.removeCollisionObject( chunks.get( loc ) ); chunks.remove( loc ); + chunkMeshes.remove( loc ); } + +// final ChunkMesh mesh = chunkMeshes.get( loc ); +// if ( mesh != null ) { +// attemptToSaveMesh( loc, mesh ); +// } } } + + final Map< ChunkLocation, Collection< Location > > updateLocations = new TreeMap< ChunkLocation, Collection< Location > >(); + final BukkitTask task = Bukkit.getScheduler().runTaskTimer( MiniePlugin.this, () -> { + 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 ) + private void onBlockUpdateEvent( final BlockPhysicsEvent event ) { + final ChunkLocation chunkLocation = new ChunkLocation( event.getBlock().getChunk() ); + getLocationFor.apply( chunkLocation ).add( event.getBlock().getLocation() ); + } }, this ); getLogger().info( "There are " + Bukkit.getWorld( "world" ).getLoadedChunks().length + " chunks" ); // getLogger().info( "Converted " + saveAllChunks( Bukkit.getWorld( "world" ) ) ); +// loadOrGenerateWorld( Bukkit.getWorld( "world" ) ); scanAndGenerate( Bukkit.getWorld( "world" ) ); - saveAllFacets( Bukkit.getWorld( "world" ) ); +// saveAllFacets( Bukkit.getWorld( "world" ) ); Bukkit.getScheduler().runTaskTimer( this, () -> { if ( !paused ) { @@ -287,6 +351,13 @@ public class MiniePlugin extends JavaPlugin { }, 0, 1 ); } + @Override + public void onDisable() { + for ( final Entry< ChunkLocation, ChunkMesh > entry : chunkMeshes.entrySet() ) { + saveMesh( entry.getKey(), entry.getValue() ); + } + } + private void registerCommands() { new SubCommand( "minie" ) .addSenderValidator( new SenderValidatorPlayer() ) @@ -303,7 +374,7 @@ public class MiniePlugin extends JavaPlugin { BlockData displayData = Material.IRON_TRAPDOOR.createBlockData(); - final Vector scale = new Vector( 5, 2, 5 ); + final Vector scale = new Vector( 10, 3, 10 ); BoundingBox[] boxes = convertFrom( getShape( displayData, loc, BlockShapeType.VISUAL_SHAPE ) ); for ( BoundingBox box : boxes ) { @@ -362,6 +433,7 @@ public class MiniePlugin extends JavaPlugin { // Create the display BlockDisplay display = loc.getWorld().spawn( loc, BlockDisplay.class, d -> { d.setBlock( displayData ); + d.setViewRange( 1000 ); } ); linkedDisplays.put( rigid, new AuxiliaryDisplayStruct( display, blockCenter ).setScale( scale ) ); @@ -377,6 +449,13 @@ public class MiniePlugin extends JavaPlugin { player.sendMessage( "Fixed" ); } ) ) .add( new SubCommand( "chunk" ) + .defaultTo( ( sender, args, params ) -> { + Player player = ( Player ) sender; + + player.sendMessage( "Registering chunk..." ); + generateMesh( player.getLocation().getChunk() ); + } ) ) + .add( new SubCommand( "old_chunk" ) .defaultTo( ( sender, args, params ) -> { Player player = ( Player ) sender; @@ -393,11 +472,12 @@ public class MiniePlugin extends JavaPlugin { final int x = chunk.getX(); final int z = chunk.getZ(); - int radius = 5; + int radius = 3; for ( int i = -radius; i < radius + 1; ++i ) { for ( int j = -radius; j < radius + 1; ++j ) { getLogger().info( "Queueing chunk scan at (" + ( x + i ) + ", " + ( z + j ) + ")" ); - scanAndGenerate( world.getChunkAt( x + i, z + j ) ); +// scanAndGenerate( world.getChunkAt( x + i, z + j ) ); + generateMesh( world.getChunkAt( x + i, z + j ) ); } } } ) ) @@ -620,6 +700,102 @@ public class MiniePlugin extends JavaPlugin { saveFacets( new File( getDataFolder() + "/facets", chunk.getWorld().getName() + "," + chunk.getX() + "," + chunk.getZ() + ".facet" ), facets ); } + private void loadOrGenerateWorld( final World world ) { + for ( final Chunk chunk : world.getLoadedChunks() ) { + loadOrGenerateMesh( chunk ); + } + } + + private void loadOrGenerateMesh( final Chunk chunk ) { +// chunk.addPluginChunkTicket( this ); + final ChunkLocation location = new ChunkLocation( chunk ); + + if ( !chunkMeshes.containsKey( location ) && !loadMeshTasks.containsKey( location ) ) { + loadMeshTasks.put( location, Bukkit.getScheduler().runTaskAsynchronously( this, () -> { + final ChunkMesh mesh = loadMesh( location ); + if ( mesh == null ) { + Bukkit.getScheduler().runTask( this, () -> { + generateMesh( chunk ); + } ); + } else { + // Now, create a physics object from the chunk mesh + final IndexedMesh nativeMesh = mesh.toIndexedMesh(); + // Enable quantized AABB compression... hopefully that doesn't mess with the accuracy or anything... + final MeshCollisionShape meshShape = new MeshCollisionShape( true, nativeMesh ); + + // Create a rigid body + PhysicsRigidBody rigid = new PhysicsRigidBody( meshShape, PhysicsBody.massForStatic ); + rigid.setPhysicsLocation( new Vector3f( chunk.getX() << 4, chunk.getWorld().getMinHeight(), chunk.getZ() << 4 ) ); + rigid.setPhysicsRotation( new Quaternion( 0, 0, 0, 1 ) ); + rigid.setKinematic( false ); + + Bukkit.getScheduler().runTask( this, () -> { + // Save our chunk mesh + chunkMeshes.put( location, mesh ); + + space.addCollisionObject( rigid ); + + PhysicsRigidBody old = chunks.put( new ChunkLocation( chunk ), rigid ); + if ( old != null ) { + getLogger().warning( "Old rigid body found!" ); + space.remove( old ); + } + + chunk.removePluginChunkTicket( this ); + + loadMeshTasks.remove( location ); + } ); + } + } ) ); + } else if ( !saveMeshTasks.containsKey( location ) ) { + chunk.removePluginChunkTicket( this ); + } + } + + private void attemptToSaveMesh( final ChunkLocation location, final ChunkMesh mesh ) { +// getLogger().info( "Attempting to save " + location ); + saveMeshTasks.put( location, Bukkit.getScheduler().runTaskAsynchronously( this, () -> { + saveMesh( location, mesh ); + + Bukkit.getScheduler().runTask( this, () -> { + if ( location.isLoaded() ) { + location.getChunk().removePluginChunkTicket( this ); + } else { + chunkMeshes.remove( location ); + + final PhysicsRigidBody old = chunks.remove( location ); + if ( old != null ) { + space.remove( old ); + } + } + + saveMeshTasks.remove( location ); + } ); + } ) ); + } + + private ChunkMesh loadMesh( final ChunkLocation location ) { +// getLogger().info( "Attempting to load " + location ); + final File file = new File( getDataFolder(), "meshes/" + location.getWorldName() + "/" + location.getX() + "/" + location.getZ() ); + if ( file.exists() ) { +// try { +// final ChunkMesh mesh = FileUtil.readObject( file ); +// +// return mesh; +// } catch ( ClassNotFoundException | IOException e ) { +// e.printStackTrace(); +// } + } + return null; + } + + private void saveMesh( final ChunkLocation location, final ChunkMesh mesh ) { + final File file = new File( getDataFolder(), "meshes/" + location.getWorldName() + "/" + location.getX() + "/" + location.getZ() ); + file.getParentFile().mkdirs(); + +// FileUtil.writeObject( mesh, file ); + } + private Collection< Facet > getFacetsConservative( Chunk chunk ) { final World world = chunk.getWorld(); final int worldMinHeight = world.getMinHeight(); @@ -743,12 +919,747 @@ public class MiniePlugin extends JavaPlugin { return facets; } + private Collection< Facet > getFacetsConservative( final ChunkSnapshot chunk, final int worldMinHeight, final int worldMaxHeight ) { + final int worldHeight = worldMaxHeight - worldMinHeight; + + final Collection< Facet > facets = new HashSet< Facet >(); + + BlockVoxelType[] types = new BlockVoxelType[ worldHeight << 8 ]; + + for ( int y = worldMinHeight; y < worldMaxHeight; ++y ) { + final int yIndex = ( y - worldMinHeight ) << 8; + for ( int z = 0; z < 16; ++z ) { + final int zIndex = yIndex + ( z << 4 ); + for ( int x = 0; x < 16; ++x ) { + final int index = zIndex + x; + final BlockData data= chunk.getBlockData( x, y, z ); + if ( !data.getMaterial().isAir() ) { + // Start the box at 0, 0, 0 + final BoundingBox[] boundingBoxes = convertFrom( getShape( data, null, BlockShapeType.COLLISION_SHAPE ), new Vector( x, y - worldMinHeight, z ) ); + + if ( boundingBoxes.length == 0 ) { + // No bounding box + types[ index ] = BlockVoxelType.NONE; + } else if ( boundingBoxes.length == 1 && boundingBoxes[ 0 ].getVolume() == 1 ) { + // Solid + types[ index ] = BlockVoxelType.SOLID; + } else { + types[ index ] = BlockVoxelType.TRANSPARENT; + // Complex bounding shape, and transparent + + // Convert the bounding box to facets + for ( final BoundingBox box : boundingBoxes ) { + facets.addAll( generateFacetsFor( box ) ); + } + } + } else { + types[ index ] = BlockVoxelType.NONE; + } + } + } + } + + // Now generate facets for each solid block based on if the side they are on is visible + for ( int y = 0; y < worldHeight; ++y ) { + final int yIndex = y << 8; + for ( int z = 0; z < 16; ++z ) { + final int zIndex = z << 4; + for ( int x = 0; x < 16; ++x ) { + final int index = yIndex + zIndex + x; + if ( types[ index ] == BlockVoxelType.SOLID ) { + // Check each face + if ( y == 0 || types[ index - 256 ] != BlockVoxelType.SOLID ) { + Facet facet = new Facet(); + facet.points.add( new Vector( x, y, z ) ); + facet.points.add( new Vector( x, y, z + 1 ) ); + facet.points.add( new Vector( x + 1, y, z + 1 ) ); + facet.points.add( new Vector( x + 1, y, z ) ); + facet.normal = new Vector( 0, -1, 0 ); + facets.add( facet ); + } + + if ( y == worldHeight - 1 || types[ index + 256 ] != BlockVoxelType.SOLID ) { + Facet facet = new Facet(); + facet.points.add( new Vector( x, y + 1, z ) ); + facet.points.add( new Vector( x, y + 1, z + 1 ) ); + facet.points.add( new Vector( x + 1, y + 1, z + 1 ) ); + facet.points.add( new Vector( x + 1, y + 1, z ) ); + facet.normal = new Vector( 0, 1, 0 ); + facets.add( facet ); + } + + if ( z == 0 || types[ index - 16 ] != BlockVoxelType.SOLID ) { + Facet facet = new Facet(); + facet.points.add( new Vector( x, y, z ) ); + facet.points.add( new Vector( x, y + 1, z ) ); + facet.points.add( new Vector( x + 1, y + 1, z ) ); + facet.points.add( new Vector( x + 1, y, z ) ); + facet.normal = new Vector( 0, 0, -1 ); + facets.add( facet ); + } + + if ( z == 15 || types[ index + 16 ] != BlockVoxelType.SOLID ) { + Facet facet = new Facet(); + facet.points.add( new Vector( x, y, z + 1 ) ); + facet.points.add( new Vector( x, y + 1, z + 1 ) ); + facet.points.add( new Vector( x + 1, y + 1, z + 1 ) ); + facet.points.add( new Vector( x + 1, y, z + 1 ) ); + facet.normal = new Vector( 0, 0, 1 ); + facets.add( facet ); + } + + if ( x == 0 || types[ index - 1 ] != BlockVoxelType.SOLID ) { + Facet facet = new Facet(); + facet.points.add( new Vector( x, y, z ) ); + facet.points.add( new Vector( x, y, z + 1 ) ); + facet.points.add( new Vector( x, y + 1, z + 1 ) ); + facet.points.add( new Vector( x, y + 1, z ) ); + facet.normal = new Vector( -1, 0, 0 ); + facets.add( facet ); + } + + if ( x == 15 || types[ index + 1 ] != BlockVoxelType.SOLID ) { + Facet facet = new Facet(); + facet.points.add( new Vector( x + 1, y, z ) ); + facet.points.add( new Vector( x + 1, y, z + 1 ) ); + facet.points.add( new Vector( x + 1, y + 1, z + 1 ) ); + facet.points.add( new Vector( x + 1, y + 1, z ) ); + facet.normal = new Vector( 1, 0, 0 ); + facets.add( facet ); + } + } + } + } + } + + return facets; + } + private void scanAndGenerate( World world ) { for ( Chunk chunk : world.getLoadedChunks() ) { - scanAndGenerate( chunk ); + generateMesh( chunk ); } } + private void scanAndGenerate( final ChunkSnapshot chunk, final int worldMinHeight, final int worldMaxHeight ) { + final long start = System.currentTimeMillis(); + + final Collection< Facet > originalFacets = getFacetsConservative( chunk, worldMinHeight, worldMaxHeight ); + + final MeshBuilder builder = new MeshBuilder(); + for ( final Facet facet : originalFacets ) { + builder.addFacet( facet ); + } + + Collection< Facet > facets = builder.planes.parallelStream().flatMap( p -> process( p ).parallelStream().map( p::convert ) ).collect( Collectors.toSet() ); + + final class VectorRef { + int index; + } + + final class Triangle { + List< VectorRef > refs = new ArrayList< VectorRef >(); + } + + final Map< Vector, VectorRef > vertices = new HashMap< Vector, VectorRef >(); + final List< Triangle > triangles = new ArrayList< Triangle >(); + + // Convert each facet to a triangle + for ( final Facet facet : facets ) { + if ( facet.points.size() != 3 ) { + System.out.println( "Warning: Facet is not a triangle!" ); + } + + final Triangle triangle = new Triangle(); + for ( final Vector vec : facet.points ) { + VectorRef ref = null; + for ( final Entry< Vector, VectorRef > entry : vertices.entrySet() ) { + final Vector existingVec = entry.getKey(); + + if ( existingVec.distanceSquared( vec ) < 1e-8 ) { + ref = entry.getValue(); + break; + } + } + + if ( ref == null ) { + ref = new VectorRef(); + vertices.put( vec, ref ); + } + + triangle.refs.add( ref ); + } + triangles.add( triangle ); + } + + // Sort the vertices + final List< Entry< Vector, VectorRef > > sorted = new ArrayList< Entry< Vector, VectorRef > >( vertices.entrySet() ); + Collections.sort( sorted, ( aEntry, bEntry ) -> { + final Vector a = aEntry.getKey(); + final Vector b = bEntry.getKey(); + + final double xDiff = a.getX() - b.getX(); + if ( xDiff == 0 ) { + final double yDiff = a.getY() - b.getY(); + if ( yDiff == 0 ) { + return Double.compare( a.getZ(), b.getZ() ); + } else { + return Double.compare( yDiff, 0 ); + } + } else { + return Double.compare( xDiff, 0 ); + } + } ); + + final Vector3f[] positionArray = new Vector3f[ sorted.size() ]; + for ( int i = 0; i < sorted.size(); ++i ) { + final Entry< Vector, VectorRef > entry = sorted.get( i ); + entry.getValue().index = i; + + final Vector vector = entry.getKey(); + + positionArray[ i ] = new Vector3f( ( float ) vector.getX(), ( float ) vector.getY(), ( float ) vector.getZ() ); + } + + final int[] indexArray = new int[ triangles.size() * 3 ]; + for ( int i = 0; i < triangles.size(); ++i ) { + final Triangle triangle = triangles.get( i ); + if ( triangle.refs.size() != 3 ) { + throw new IllegalStateException( "Triangle does not have exactly 3 vertices!" ); + } + + final int indexArrayStart = i * 3; + indexArray[ indexArrayStart ] = triangle.refs.get( 0 ).index; + indexArray[ indexArrayStart + 1 ] = triangle.refs.get( 1 ).index; + indexArray[ indexArrayStart + 2 ] = triangle.refs.get( 2 ).index; + } + + final IndexedMesh nativeMesh = new IndexedMesh( positionArray, indexArray ); + // 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, worldMinHeight, chunk.getZ() << 4 ) ); + rigid.setPhysicsRotation( new Quaternion( 0, 0, 0, 1 ) ); + rigid.setKinematic( false ); + + Bukkit.getScheduler().runTask( this, () -> { + 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 from " + facets.size() + " facets" ); + } ); + } + + private void generateMesh( final Chunk chunk ) { + final long start = System.currentTimeMillis(); + final World world = chunk.getWorld(); + + final ChunkLocation chunkLocation = new ChunkLocation( chunk ); + final int worldMinHeight = world.getMinHeight(); + final int worldMaxHeight = world.getMaxHeight(); + final int worldHeight = worldMaxHeight - worldMinHeight; + + final ChunkMesh chunkMesh = new ChunkMesh( new BoundingBox( 0, 0, 0, 16, worldHeight, 16 ) ); + final Map< MinecraftPlane, Collection< Polygon > > polygons = new TreeMap< MinecraftPlane, Collection< Polygon > >(); + + { + final Function< MinecraftPlane, Collection< Polygon > > getPolygons = p -> { + Collection< Polygon > polygonCollection = polygons.get( p ); + if ( polygonCollection == null ) { + polygonCollection = new ArrayDeque< Polygon >(); + polygons.put( p, polygonCollection ); + } + return polygonCollection; + }; + + // When we would have inserted each facet, instead, find or create the + // corresponding plane, and add directly. + // Also, create a chunk mesh and populate the structs + for ( int y = worldMinHeight; y < worldMaxHeight; ++y ) { + final int relY = y - worldMinHeight; + final int yIndex = relY << 8; + for ( int z = 0; z < 16; ++z ) { + final int zIndex = yIndex + ( z << 4 ); + for ( int x = 0; x < 16; ++x ) { + final int index = zIndex + x; + final Block block = chunk.getBlock( x, y, z ); + if ( !( block.isEmpty() && block.isLiquid() ) ) { + final BlockData data = block.getBlockData(); + final Location loc = block.getLocation(); + + // Start the box at 0, 0, 0 + final BoundingBox[] boundingBoxes = getShape( data, loc, BlockShapeType.COLLISION_SHAPE ).e().stream().map( aabb -> new BoundingBox( aabb.a, aabb.b, aabb.c, aabb.d, aabb.e, aabb.f ) ).toArray( BoundingBox[]::new ); + + if ( boundingBoxes.length == 0 ) { + // No bounding box + chunkMesh.setType( index, BlockDataType.NONE ); + } else if ( boundingBoxes.length == 1 && boundingBoxes[ 0 ].getVolume() == 1 ) { + // Solid + chunkMesh.setType( index, BlockDataType.SOLID ); + } else { + chunkMesh.setType( index, BlockDataType.COMPLEX ); + // Complex bounding shape + + // Only store the old block data if we know it is not empty or solid!! + chunkMesh.setData( index, data ); + + // Add the voxel shape to the chunk mesh + for ( final BoundingBox aabb : boundingBoxes ) { + final double minX = aabb.getMinX() + x; + final double minY = aabb.getMinY() + relY; + final double minZ = aabb.getMinZ() + z; + final double maxX = aabb.getMaxX() + x; + final double maxY = aabb.getMaxY() + relY; + final double maxZ = aabb.getMaxZ() + z; + + final Polygon xPoly = new Polygon( Arrays.asList( + new Point( minY, minZ ), + new Point( minY, maxZ ), + new Point( maxY, maxZ ), + new Point( maxY, minZ ) + ) ); + final Polygon yPoly = new Polygon( Arrays.asList( + new Point( minX, minZ ), + new Point( minX, maxZ ), + new Point( maxX, maxZ ), + new Point( maxX, minZ ) + ) ); + final Polygon zPoly = new Polygon( Arrays.asList( + new Point( minX, minY ), + new Point( minX, maxY ), + new Point( maxX, maxY ), + new Point( maxX, minY ) + ) ); + + getPolygons.apply( new MinecraftPlane( PlaneAxis.X, minX ) ).add( xPoly ); + getPolygons.apply( new MinecraftPlane( PlaneAxis.X, maxX ) ).add( xPoly ); + getPolygons.apply( new MinecraftPlane( PlaneAxis.Y, minY ) ).add( yPoly ); + getPolygons.apply( new MinecraftPlane( PlaneAxis.Y, maxY ) ).add( yPoly ); + getPolygons.apply( new MinecraftPlane( PlaneAxis.Z, minZ ) ).add( zPoly ); + getPolygons.apply( new MinecraftPlane( PlaneAxis.Z, maxZ ) ).add( zPoly ); + } + + chunkMesh.setBoxes( index, boundingBoxes ); + } + } else { + chunkMesh.setType( index, BlockDataType.NONE ); + } + } + } + } + + // Now generate facets for each solid block based on if the side they are on is visible + for ( int y = 0; y < worldHeight; ++y ) { + final int yIndex = y << 8; + for ( int z = 0; z < 16; ++z ) { + final int zIndex = z << 4; + for ( int x = 0; x < 16; ++x ) { + final int index = yIndex + zIndex + x; + if ( chunkMesh.getType( index ) == BlockDataType.SOLID ) { + final Polygon xPoly = new Polygon( Arrays.asList( + new Point( y, z ), + new Point( y, z + 1 ), + new Point( y + 1, z + 1 ), + new Point( y + 1, z ) + ) ); + final Polygon yPoly = new Polygon( Arrays.asList( + new Point( x, z ), + new Point( x, z + 1 ), + new Point( x + 1, z + 1 ), + new Point( x + 1, z ) + ) ); + final Polygon zPoly = new Polygon( Arrays.asList( + new Point( x, y ), + new Point( x, y + 1 ), + new Point( x + 1, y + 1 ), + new Point( x + 1, y ) + ) ); + + // Check each face + if ( y == 0 || chunkMesh.getType( index - 256 ) != BlockDataType.SOLID ) { + getPolygons.apply( new MinecraftPlane( PlaneAxis.Y, y ) ).add( yPoly ); + } + + if ( y == worldHeight - 1 || chunkMesh.getType( index + 256 ) != BlockDataType.SOLID ) { + getPolygons.apply( new MinecraftPlane( PlaneAxis.Y, y + 1 ) ).add( yPoly ); + } + + if ( z == 0 || chunkMesh.getType( index - 16 ) != BlockDataType.SOLID ) { + getPolygons.apply( new MinecraftPlane( PlaneAxis.Z, z ) ).add( zPoly ); + } + + if ( z == 15 || chunkMesh.getType( index + 16 ) != BlockDataType.SOLID ) { + getPolygons.apply( new MinecraftPlane( PlaneAxis.Z, z + 1 ) ).add( zPoly ); + } + + if ( x == 0 || chunkMesh.getType( index - 1 ) != BlockDataType.SOLID ) { + getPolygons.apply( new MinecraftPlane( PlaneAxis.X, x ) ).add( xPoly ); + } + + if ( x == 15 || chunkMesh.getType( index + 1 ) != BlockDataType.SOLID ) { + getPolygons.apply( new MinecraftPlane( PlaneAxis.X, x + 1 ) ).add( xPoly ); + } + } + } + } + } + } + + Bukkit.getScheduler().runTaskAsynchronously( this, () -> { + final Map< MinecraftPlane, Mesh< RegionSimple > > meshMap = new HashMap< MinecraftPlane, Mesh< RegionSimple > >(); + for ( final Entry< MinecraftPlane, Collection< Polygon > > entry : polygons.entrySet() ) { + final MinecraftPlane plane = entry.getKey(); + final Mesh< RegionSimple > mesh = new Mesh< RegionSimple >( () -> new RegionSimple( GluWindingRule.ODD ) ); + for ( final Polygon polygon : entry.getValue() ) { + mesh.addPolygon( polygon, RegionRuleWinding.CLOCKWISE ); + } + meshMap.put( plane, mesh ); + } + + // Mesh each plane + final Map< MinecraftPlane, CompleteMesh > triangles = meshMap.entrySet().parallelStream().collect( Collectors.toConcurrentMap( e -> e.getKey(), e -> new CompleteMesh( e.getKey(), e.getValue() ) ) ); + + // Construct a series of indexed polygons + final TreeSet< VectorReference > vertices = new TreeSet< VectorReference >(); + + for ( final Entry< MinecraftPlane, CompleteMesh > entry : triangles.entrySet() ) { + final MinecraftPlane plane = entry.getKey(); + final CompleteMesh completeMesh = entry.getValue(); + final Collection< Polygon > tris = completeMesh.triangles; + final Mesh< RegionSimple > mesh = completeMesh.mesh; + + // Ignore any planes that are empty + if ( !tris.isEmpty() ) { + final PlaneMesh newMesh = new PlaneMesh(); + newMesh.completedPolygons = new ArrayList< IndexedPolygon >(); + for ( final Polygon tri : tris ) { + final IndexedPolygon newPoly = new IndexedPolygon(); + for ( final Point point : tri.getPoints() ) { + final Vector vert = plane.convert( point ); + + final VectorReference tempRef = new VectorReference( vert ); + // Find an existing vertex reference if one exists + VectorReference ref = null; + if ( ref == null ) { + final VectorReference upperRef = vertices.ceiling( tempRef ); + if ( upperRef != null && upperRef.vector.distanceSquared( vert ) < 1e-8 ) { + ref = upperRef; + } else { + final VectorReference lowerRef = vertices.floor( tempRef ); + if ( lowerRef != null && lowerRef.vector.distanceSquared( vert ) < 1e-8 ) { + ref = lowerRef; + } + } + } + + if ( ref == null ) { + ref = tempRef; + ref.referenceCount = 1; + vertices.add( ref ); + } else { + ++ref.referenceCount; + } + + newPoly.points.add( ref ); + } + newMesh.completedPolygons.add( newPoly ); + } + + // Save the generated regions as an intermediary step so we can re-use them later + newMesh.generatedRegions = mesh.getPolygons(); + + chunkMesh.planes.put( plane, newMesh ); + } + } + + // Add all vector references + chunkMesh.pointReferences = vertices; + + // 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( this, () -> { + // 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" ); + + chunk.removePluginChunkTicket( this ); + + loadMeshTasks.remove( chunkLocation ); + } ); + } ); + } + + private Collection< Location > update( final Collection< Location > locations ) { + if ( locations.isEmpty() ) { + return Collections.emptySet(); + } + + final Chunk chunk = locations.iterator().next().getChunk(); + final World world = chunk.getWorld(); + final ChunkLocation chunkLocation = new ChunkLocation( chunk ); + final ChunkMesh chunkMesh = chunkMeshes.get( chunkLocation ); + + final Collection< Location > canRemove = new ArrayDeque< Location >(); + if ( chunkMesh != null ) { + // Check each location + final Map< MinecraftPlane, Collection< Polygon > > polygons = new TreeMap< MinecraftPlane, Collection< Polygon > >(); + final Function< MinecraftPlane, Collection< Polygon > > getPolygons = p -> { + Collection< Polygon > polygonCollection = polygons.get( p ); + if ( polygonCollection == null ) { + polygonCollection = new ArrayDeque< Polygon >(); + polygons.put( p, polygonCollection ); + } + return polygonCollection; + }; + + final BiFunction< List< BoundingBox >, BoundingBox[], Boolean > isEqual = ( a, b ) -> { + if ( a.size() == b.length ) { + for ( int i = 0; i < b.length; ++i ) { + final BoundingBox b1 = a.get( i ); + final BoundingBox b2 = b[ i ]; + + if ( !b1.equals( b2 ) ) { + return false; + } + } + } + + return true; + }; + + for ( final Location location : locations ) { + final BlockData data = location.getBlock().getBlockData(); + + // Defer moving piston updates until another tick when it's done moving + if ( data.getMaterial() == Material.MOVING_PISTON ) { + continue; + } + + final int x = location.getBlockX() - ( chunkLocation.getX() << 4 ); + final int z = location.getBlockZ() - ( chunkLocation.getZ() << 4 ); + final int y = location.getBlockY() - location.getWorld().getMinHeight(); + final int index = x + ( z << 4 ) + ( y << 8 ); + + canRemove.add( location ); + + if ( chunkMesh.isBlockAt( index, data ) ) { + continue; + } else { + final List< BoundingBox > boxes = getShape( data, location, BlockShapeType.COLLISION_SHAPE ).e().stream() + .map( aabb -> new BoundingBox( aabb.a, aabb.b, aabb.c, aabb.d, aabb.e, aabb.f ) ).collect( Collectors.toList() ); + final BlockDataType oldType = chunkMesh.getType( index ); + final BoundingBox[] otherBoxes = chunkMesh.getBoxesAt( index ); + if ( boxes.size() == 0 ) { + if ( oldType == BlockDataType.NONE ) { + continue; + } + + chunkMesh.setType( index, BlockDataType.NONE ); + chunkMesh.setBoxes( index, null ); + } else if ( boxes.size() == 1 && boxes.get( 0 ).getVolume() == 1 ) { + if ( oldType == BlockDataType.SOLID ) { + continue; + } + + chunkMesh.setType( index, BlockDataType.SOLID ); + chunkMesh.setBoxes( index, null ); + } else { + if ( otherBoxes != null && isEqual.apply( boxes, otherBoxes ) ) { + continue; + } + + chunkMesh.setType( index, BlockDataType.COMPLEX ); + chunkMesh.setData( index, data ); + chunkMesh.setBoxes( index, boxes.toArray( BoundingBox[]::new ) ); + } + + if ( oldType == BlockDataType.SOLID ) { + boxes.add( new BoundingBox( 0, 0, 0, 1, 1, 1 ) ); + } else if ( oldType == BlockDataType.COMPLEX ) { + if ( otherBoxes == null || otherBoxes.length == 0 ) { + throw new IllegalStateException( "Complex shape but no bounding boxes!" ); + } + + for ( final BoundingBox box : otherBoxes ) { + boxes.add( box ); + } + } + + // Add the voxel shape to the chunk mesh + for ( final BoundingBox aabb : boxes ) { + final double minX = aabb.getMinX() + x; + final double minY = aabb.getMinY() + y; + final double minZ = aabb.getMinZ() + z; + final double maxX = aabb.getMaxX() + x; + final double maxY = aabb.getMaxY() + y; + final double maxZ = aabb.getMaxZ() + z; + + final Polygon xPoly = new Polygon( Arrays.asList( + new Point( minY, minZ ), + new Point( minY, maxZ ), + new Point( maxY, maxZ ), + new Point( maxY, minZ ) + ) ); + final Polygon yPoly = new Polygon( Arrays.asList( + new Point( minX, minZ ), + new Point( minX, maxZ ), + new Point( maxX, maxZ ), + new Point( maxX, minZ ) + ) ); + final Polygon zPoly = new Polygon( Arrays.asList( + new Point( minX, minY ), + new Point( minX, maxY ), + new Point( maxX, maxY ), + new Point( maxX, minY ) + ) ); + + getPolygons.apply( new MinecraftPlane( PlaneAxis.X, minX ) ).add( xPoly ); + getPolygons.apply( new MinecraftPlane( PlaneAxis.X, maxX ) ).add( xPoly ); + getPolygons.apply( new MinecraftPlane( PlaneAxis.Y, minY ) ).add( yPoly ); + getPolygons.apply( new MinecraftPlane( PlaneAxis.Y, maxY ) ).add( yPoly ); + getPolygons.apply( new MinecraftPlane( PlaneAxis.Z, minZ ) ).add( zPoly ); + getPolygons.apply( new MinecraftPlane( PlaneAxis.Z, maxZ ) ).add( zPoly ); + } + } + } + + // No polygons found... which means nothing changed + if ( polygons.isEmpty() ) { + return canRemove; + } + + final Map< MinecraftPlane, Mesh< RegionSimple > > meshMap = new HashMap< MinecraftPlane, Mesh< RegionSimple > >(); + + // Now mesh the blocks asynchronously and combine, then do the bare minimum to regenerate the chunk mesh + for ( final Entry< MinecraftPlane, Collection< Polygon > > entry : polygons.entrySet() ) { + final MinecraftPlane plane = entry.getKey(); + final PlaneMesh planeMesh = chunkMesh.planes.remove( plane ); + final Mesh< RegionSimple > mesh = new Mesh< RegionSimple >( () -> new RegionSimple( GluWindingRule.ODD ) ); + + if ( planeMesh != null ) { + // Reset all indexed polygons(triangles) for this plane + for ( final IndexedPolygon poly : planeMesh.completedPolygons ) { + poly.points.parallelStream().forEach( p -> --p.referenceCount ); + } + + // Add all polygons that were previously generated + for ( final Polygon polygon : planeMesh.generatedRegions ) { + mesh.addPolygon( polygon, RegionRuleWinding.CLOCKWISE ); + } + } + for ( final Polygon polygon : entry.getValue() ) { + mesh.addPolygon( polygon, RegionRuleWinding.CLOCKWISE ); + } + + meshMap.put( plane, mesh ); + } + + // Mesh each plane + final Map< MinecraftPlane, CompleteMesh > triangles = meshMap.entrySet().parallelStream().collect( Collectors.toConcurrentMap( e -> e.getKey(), e -> new CompleteMesh( e.getKey(), e.getValue() ) ) ); + + for ( final Entry< MinecraftPlane, CompleteMesh > entry : triangles.entrySet() ) { + final MinecraftPlane plane = entry.getKey(); + final CompleteMesh completeMesh = entry.getValue(); + final Collection< Polygon > tris = completeMesh.triangles; + final Mesh< RegionSimple > mesh = completeMesh.mesh; + + // Ignore any planes that are empty + if ( !tris.isEmpty() ) { + final PlaneMesh newMesh = new PlaneMesh(); + newMesh.completedPolygons = new ArrayList< IndexedPolygon >(); + for ( final Polygon tri : tris ) { + final IndexedPolygon newPoly = new IndexedPolygon(); + for ( final Point point : tri.getPoints() ) { + final Vector vert = plane.convert( point ); + + // Find an existing vertex reference if one exists + final VectorReference tempRef = new VectorReference( vert ); + + VectorReference ref = null; + final VectorReference upperRef = chunkMesh.pointReferences.ceiling( tempRef ); + if ( upperRef != null && upperRef.vector.distanceSquared( vert ) < 1e-8 ) { + ref = upperRef; + } else { + final VectorReference lowerRef = chunkMesh.pointReferences.floor( tempRef ); + if ( lowerRef != null && lowerRef.vector.distanceSquared( vert ) < 1e-8 ) { + ref = lowerRef; + } + } + + if ( ref == null ) { + ref = tempRef; + ref.referenceCount = 1; + chunkMesh.pointReferences.add( ref ); + } else { + ++ref.referenceCount; + } + + newPoly.points.add( ref ); + } + newMesh.completedPolygons.add( newPoly ); + } + + // Save the generated regions as an intermediary step so we can re-use them later + newMesh.generatedRegions = mesh.getPolygons(); + + chunkMesh.planes.put( plane, newMesh ); + } + } + + // Remove all vector references that have a reference count of 0 + chunkMesh.pointReferences.removeIf( v -> v.referenceCount <= 0 ); + + // 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 ); + + synchronized ( space ) { + space.addCollisionObject( rigid ); + + final PhysicsRigidBody old = chunks.put( new ChunkLocation( chunk ), rigid ); + if ( old != null ) { + space.remove( old ); + } + } + return canRemove; + } + return canRemove; + } + private void scanAndGenerate( final Chunk chunk ) { final long start = System.currentTimeMillis(); @@ -1104,6 +2015,20 @@ public class MiniePlugin extends JavaPlugin { return facets; } + private static double getVolume( final AxisAlignedBB aabb ) { + return ( aabb.d - aabb.a ) * ( aabb.e - aabb.b ) * ( aabb.f - aabb.c ); + } + + private class CompleteMesh { + Mesh< RegionSimple > mesh; + Collection< Polygon > triangles; + + CompleteMesh( final MinecraftPlane plane, final Mesh< RegionSimple > mesh ) { + this.mesh = mesh; + triangles = mesh.meshify(); + } + } + private class JointStruct { BlockState state; PhysicsRigidBody object; diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/Aabb.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/Aabb.java new file mode 100644 index 0000000..a5d9c2a --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/Aabb.java @@ -0,0 +1,37 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.objects; + +import java.io.Serializable; + +import org.bukkit.util.BoundingBox; + +public class Aabb implements Serializable { + private static final long serialVersionUID = 6518512405568765913L; + + public final int minX; + public final int minY; + public final int minZ; + public final int maxX; + public final int maxY; + public final int maxZ; + + public final int width; + public final int depth; + public final int height; + + public Aabb( final BoundingBox box ) { + this.minX = ( int ) box.getMinX(); + this.maxX = ( int ) box.getMaxX(); + this.minY = ( int ) box.getMinY(); + this.maxY = ( int ) box.getMaxY(); + this.minZ = ( int ) box.getMinZ(); + this.maxZ = ( int ) box.getMaxZ(); + + this.width = maxX - minX; + this.depth = maxZ - minZ; + this.height = maxY - minY; + } + + public int getVolume() { + return width * depth * height; + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/BlockDataType.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/BlockDataType.java new file mode 100644 index 0000000..b641836 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/BlockDataType.java @@ -0,0 +1,5 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.objects; + +public enum BlockDataType { + NONE, COMPLEX, SOLID +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/BlockDataTypeSet.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/BlockDataTypeSet.java new file mode 100644 index 0000000..64a1148 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/BlockDataTypeSet.java @@ -0,0 +1,45 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.objects; + +import java.util.BitSet; + +public class BlockDataTypeSet { + private static final int BLOCK_SIZE; + private static final BitSet[] SETS; + + static { + final int length = BlockDataType.values().length; + BLOCK_SIZE = 32 - Integer.numberOfLeadingZeros( length ); + SETS = new BitSet[ length ]; + + // Perhaps not the fastest, since it sets each bit in N time + for ( int i = 0; i < length; ++i ) { + final BitSet set = new BitSet(); + for ( int j = 0; j < BLOCK_SIZE; ++j ) { + set.set( j, ( ( i >> j ) & 1 ) == 1 ); + } + SETS[ i ] = set; + } + } + + private BitSet buffer; + + public BlockDataTypeSet( final int size ) { + buffer = new BitSet( size * BLOCK_SIZE ); + } + + public void set( final int index, final BlockDataType type ) { + final BitSet set = SETS[ type.ordinal() ]; + for ( int i = 0; i < BLOCK_SIZE; ++i ) { + buffer.set( ( index * BLOCK_SIZE ) + i, set.get( i ) ); + } + } + + public BlockDataType get( final int index ) { + final int bufferIndex = index * BLOCK_SIZE; + int ord = 0; + for ( int i = buffer.nextSetBit( bufferIndex ); i >= bufferIndex && i < bufferIndex + BLOCK_SIZE; i = buffer.nextSetBit( i + 1 ) ) { + ord |= 1 << ( i - bufferIndex ); + } + return BlockDataType.values()[ ord ]; + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/ChunkLocation.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/ChunkLocation.java index fd74c61..ed08df4 100644 --- a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/ChunkLocation.java +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/ChunkLocation.java @@ -12,7 +12,7 @@ import org.bukkit.World; * * @author BananaPuncher714 */ -public class ChunkLocation { +public class ChunkLocation implements Comparable< ChunkLocation > { private int x, z; private String worldName; private World world; @@ -283,4 +283,16 @@ public class ChunkLocation { public String toString() { return "ChunkLocation{x:" + x + ",z:" + z + ",world:" + worldName + "}"; } + + @Override + public int compareTo( ChunkLocation o ) { + final int worldCompare = worldName.compareTo( o.worldName ); + if ( worldCompare == 0 ) { + if ( x == o.x ) { + return Integer.compare( z, o.z ); + } + return Integer.compare( x, o.x ); + } + return worldCompare; + } } diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/ChunkMesh.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/ChunkMesh.java new file mode 100644 index 0000000..f0cc757 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/ChunkMesh.java @@ -0,0 +1,98 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.objects; + +import java.util.Map; +import java.util.TreeMap; +import java.util.TreeSet; + +import org.bukkit.block.data.BlockData; +import org.bukkit.util.BoundingBox; +import org.bukkit.util.Vector; + +import com.jme3.bullet.collision.shapes.infos.IndexedMesh; +import com.jme3.math.Vector3f; + +public class ChunkMesh { + // TODO Public for now while I figure out how to organize the stuff properly + private int[] data; + private BoundingBox[][] boxes; + private BlockDataTypeSet blockTypes; + public TreeSet< VectorReference > pointReferences = new TreeSet< VectorReference >(); + public Map< MinecraftPlane, PlaneMesh > planes = new TreeMap< MinecraftPlane, PlaneMesh >(); + private Aabb box; + + public ChunkMesh( BoundingBox box ) { + this.box = new Aabb( box ); + + final int volume = this.box.getVolume(); + data = new int[ volume ]; + boxes = new BoundingBox[ volume ][]; + blockTypes = new BlockDataTypeSet( volume ); + } + + public void setType( final int index, final BlockDataType type ) { + blockTypes.set( index, type ); + } + + public BlockDataType getType( final int index ) { + return blockTypes.get( index ); + } + + public void setData( final int index, final BlockData data ) { + this.data[ index ] = data.getAsString().hashCode(); + } + + public void setBoxes( final int index, final BoundingBox[] boxes ) { + this.boxes[ index ] = boxes; + } + + public boolean isBlockAt( final int index, final BlockData data ) { + return blockTypes.get( index ) == BlockDataType.COMPLEX && this.data[ index ] == data.getAsString().hashCode(); + } + + public BoundingBox[] getBoxesAt( final int index ) { + return boxes[ index ]; + } + + public PlaneMesh getOrAdd( final MinecraftPlane plane ) { + PlaneMesh results = planes.get( plane ); + if ( results == null ) { + results = new PlaneMesh(); + planes.put( plane, results ); + } + return results; + } + + public IndexedMesh toIndexedMesh() { + final Vector3f[] positionArray = new Vector3f[ pointReferences.size() ]; + int vertexIndex = 0; + for ( final VectorReference ref : pointReferences ) { + final Vector vector = ref.vector; + + positionArray[ vertexIndex ] = new Vector3f( ( float ) vector.getX(), ( float ) vector.getY(), ( float ) vector.getZ() ); + ref.index = vertexIndex; + + ++vertexIndex; + } + + final int totalTriangleCount = planes.values().stream().mapToInt( m -> m.completedPolygons.size() ).sum(); + + int triangleIndex = 0; + final int[] indexArray = new int[ totalTriangleCount * 3 ]; + for ( final PlaneMesh mesh : planes.values() ) { + for ( final IndexedPolygon triangle : mesh.completedPolygons ) { + if ( triangle.points.size() != 3 ) { + throw new IllegalStateException( "Triangle does not have exactly 3 vertices!" ); + } + + final int indexArrayStart = triangleIndex * 3; + indexArray[ indexArrayStart ] = triangle.points.get( 0 ).index; + indexArray[ indexArrayStart + 1 ] = triangle.points.get( 1 ).index; + indexArray[ indexArrayStart + 2 ] = triangle.points.get( 2 ).index; + + ++triangleIndex; + } + } + + return new IndexedMesh( positionArray, indexArray ); + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/Component.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/Component.java deleted file mode 100644 index 2b9d2df..0000000 --- a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/Component.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.aaaaahhhhhhh.bananapuncher714.minietest.objects; - -import java.util.Collection; -import java.util.Collections; - -public class Component { - - - public Collection< Component > getComponents() { - return Collections.emptySet(); - } -} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/IndexedPolygon.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/IndexedPolygon.java new file mode 100644 index 0000000..5c24e97 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/IndexedPolygon.java @@ -0,0 +1,8 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.objects; + +import java.util.ArrayList; +import java.util.List; + +public class IndexedPolygon { + public List< VectorReference > points = new ArrayList< VectorReference >(); +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/MinecraftPlane.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/MinecraftPlane.java new file mode 100644 index 0000000..26191cc --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/MinecraftPlane.java @@ -0,0 +1,74 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.objects; + +import java.io.Serializable; + +import org.bukkit.util.Vector; + +import com.aaaaahhhhhhh.bananapuncher714.mesh.Point; + +public class MinecraftPlane implements Comparable< MinecraftPlane > { + public final PlaneAxis normal; + public final double offset; + + public MinecraftPlane( final PlaneAxis axis, final double offset ) { + this.normal = axis; + this.offset = offset; + } + + public Vector convert( final Point point ) { + switch ( normal ) { + case X: + return new Vector( offset, point.getX(), point.getY() ); + case Y: + return new Vector( point.getX(), offset, point.getY() ); + case Z: + return new Vector( point.getX(), point.getY(), offset ); + } + + throw new IllegalStateException( "No normal set!" ); + } + + @Override + public int compareTo( final MinecraftPlane o ) { + if ( normal == o.normal ) { + return Double.compare( offset, o.offset ); + } + return normal.compareTo( o.normal ); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((normal == null) ? 0 : normal.hashCode()); + long temp; + temp = Double.doubleToLongBits(offset); + result = prime * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + MinecraftPlane other = (MinecraftPlane) obj; + if (normal != other.normal) + return false; + if (Double.doubleToLongBits(offset) != Double.doubleToLongBits(other.offset)) + return false; + return true; + } + + @Override + public String toString() { + return "MinecraftPlane{axis=" + normal + ",offset=" + offset + "}"; + } + + public static enum PlaneAxis { + X, Y, Z + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/PlaneMesh.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/PlaneMesh.java new file mode 100644 index 0000000..fa4631f --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/PlaneMesh.java @@ -0,0 +1,10 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.objects; + +import java.util.Collection; + +import com.aaaaahhhhhhh.bananapuncher714.mesh.Polygon; + +public class PlaneMesh { + public Collection< Polygon > generatedRegions; + public Collection< IndexedPolygon > completedPolygons; +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/VectorReference.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/VectorReference.java new file mode 100644 index 0000000..0655cc6 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/objects/VectorReference.java @@ -0,0 +1,29 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.objects; + +import org.bukkit.util.Vector; + +public class VectorReference implements Comparable< VectorReference > { + public Vector vector; + public int referenceCount = 0; + public int index = 0; + + public VectorReference( final Vector vector ) { + this.vector = vector; + } + + @Override + public int compareTo( final VectorReference o ) { + final Vector oVec = o.vector; + + if ( vector.getX() == oVec.getX() ) { + if ( vector.getY() == oVec.getY() ) { + if ( vector.getZ() == oVec.getZ() ) { + return 0; + } + return Double.compare( vector.getZ(), oVec.getZ() ); + } + return Double.compare( vector.getY(), oVec.getY() ); + } + return Double.compare( vector.getX(), oVec.getX() ); + } +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/util/ChunkMeshUtil.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/util/ChunkMeshUtil.java new file mode 100644 index 0000000..80c2ed6 --- /dev/null +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/util/ChunkMeshUtil.java @@ -0,0 +1,5 @@ +package com.aaaaahhhhhhh.bananapuncher714.minietest.util; + +public class ChunkMeshUtil { + +} diff --git a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/util/FileUtil.java b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/util/FileUtil.java index 9deecbb..e19c5d4 100644 --- a/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/util/FileUtil.java +++ b/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/util/FileUtil.java @@ -105,7 +105,7 @@ public final class FileUtil { file.delete(); } - public static < T extends Serializable > T readObject( Class< T > clazz, File file ) throws IOException, ClassNotFoundException { + public static < T extends Serializable > T readObject( File file ) throws IOException, ClassNotFoundException { if ( !file.exists() ) { return null; }