Files
mc-mesh/src/main/java/com/aaaaahhhhhhh/bananapuncher714/minietest/MiniePlugin.java
BananaPuncher714 7c49e004b7 Speedup the mesh simplification
Change EdgeSet to use LinkedHashSet for better iteration, potentially.
Map polygon points to existing vertices to reduce unnecessary vertex duplication
Attempt to merge overlapping edges initially before doing anything else when simplifying
Use tree sets instead of priority queues for constant time poll
Split intersection calculation method into two separate methods
Rework which edges are actively within the scan zone to reduce the amount of edges to check when looking for intersections. This should help make the entire algorithm closer to O(nlogn)
Removed intersection classes
Added method to resolve only collinear edges around a vertex
Added method to resolve edge intersections directly from the edge, rather than the positive side
Added simple Y check to see if two edges can even intersect
Added method to calculate absolute vector
Added test plane by default to MeshingTest2(the plane viewer)
2025-05-25 21:34:35 -04:00

966 lines
46 KiB
Java

package com.aaaaahhhhhhh.bananapuncher714.minietest;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
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 org.apache.commons.lang.SystemUtils;
import org.bukkit.Bukkit;
import org.bukkit.Chunk;
import org.bukkit.Location;
import org.bukkit.Material;
import org.bukkit.World;
import org.bukkit.block.Block;
import org.bukkit.block.BlockState;
import org.bukkit.block.data.BlockData;
import org.bukkit.craftbukkit.v1_21_R4.CraftWorld;
import org.bukkit.craftbukkit.v1_21_R4.block.data.CraftBlockData;
import org.bukkit.entity.BlockDisplay;
import org.bukkit.entity.Display;
import org.bukkit.entity.Player;
import org.bukkit.event.EventHandler;
import org.bukkit.event.Listener;
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.util.BoundingBox;
import org.bukkit.util.Transformation;
import org.bukkit.util.Vector;
import org.joml.Matrix4f;
import org.joml.Quaternionf;
import com.aaaaahhhhhhh.bananapuncher714.mesh.Mesh;
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.command.SubCommand;
import com.aaaaahhhhhhh.bananapuncher714.minietest.command.executor.CommandExecutableMessage;
import com.aaaaahhhhhhh.bananapuncher714.minietest.command.validator.InputValidatorInt;
import com.aaaaahhhhhhh.bananapuncher714.minietest.command.validator.sender.SenderValidatorPlayer;
import com.aaaaahhhhhhh.bananapuncher714.minietest.objects.ChunkLocation;
import com.aaaaahhhhhhh.bananapuncher714.minietest.objects.mesh.Facet;
import com.aaaaahhhhhhh.bananapuncher714.minietest.objects.mesh.MeshBuilder;
import com.aaaaahhhhhhh.bananapuncher714.minietest.util.FileUtil;
import com.jme3.bullet.PhysicsSpace;
import com.jme3.bullet.PhysicsSpace.BroadphaseType;
import com.jme3.bullet.RotationOrder;
import com.jme3.bullet.collision.ContactListener;
import com.jme3.bullet.collision.PhysicsCollisionEvent;
import com.jme3.bullet.collision.PhysicsCollisionListener;
import com.jme3.bullet.collision.PhysicsCollisionObject;
import com.jme3.bullet.collision.shapes.BoxCollisionShape;
import com.jme3.bullet.collision.shapes.CollisionShape;
import com.jme3.bullet.collision.shapes.CompoundCollisionShape;
import com.jme3.bullet.collision.shapes.MeshCollisionShape;
import com.jme3.bullet.collision.shapes.PlaneCollisionShape;
import com.jme3.bullet.collision.shapes.SphereCollisionShape;
import com.jme3.bullet.collision.shapes.infos.IndexedMesh;
import com.jme3.bullet.joints.New6Dof;
import com.jme3.bullet.joints.PhysicsJoint;
import com.jme3.bullet.joints.motors.MotorParam;
import com.jme3.bullet.objects.PhysicsBody;
import com.jme3.bullet.objects.PhysicsRigidBody;
import com.jme3.math.Matrix3f;
import com.jme3.math.Plane;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import net.minecraft.core.BlockPosition;
import net.minecraft.server.level.WorldServer;
import net.minecraft.world.phys.shapes.VoxelShape;
import net.minecraft.world.phys.shapes.VoxelShapeCollision;
public class MiniePlugin extends JavaPlugin {
private static final int TIME_STEPS = 5;
private static final float SIMULATION_SPEED = 1f;
private PhysicsSpace space;
private boolean paused = false;
private boolean teleportOnDeactivate = false;
private float tick = 0;
private Map< PhysicsCollisionObject, AuxiliaryDisplayStruct > linkedDisplays = new HashMap< PhysicsCollisionObject, AuxiliaryDisplayStruct >();
private PhysicsRigidBody planeBody;
private Set< PhysicsCollisionObject > active = new HashSet< PhysicsCollisionObject >();
private Set< PhysicsCollisionObject > isTnt = new HashSet< PhysicsCollisionObject >();
private Map< PhysicsJoint, JointStruct > joints = new HashMap< PhysicsJoint, JointStruct >();
private Map< ChunkLocation, PhysicsRigidBody > chunks = new HashMap< ChunkLocation, PhysicsRigidBody >();
private Map< Vector3f, CollisionShape > collisionShapes = new HashMap< Vector3f, CollisionShape >();
@Override
public void onEnable() {
FileUtil.saveToFile( getResource( "native/windows/x86_64/bulletjme.dll" ), new File( getDataFolder() + "/lib", "bulletjme.dll" ), false );
FileUtil.saveToFile( getResource( "native/osx/x86_64/libbulletjme.dylib" ), new File( getDataFolder() + "/lib", "libbulletjme.dylib" ), false );
FileUtil.saveToFile( getResource( "native/linux/x86_64/libbulletjme.so" ), new File( getDataFolder() + "/lib", "libbulletjme.so" ), false );
if ( !loadNativeLibraries() ) {
getLogger().severe( "Unable to load Bullet JME native libraries! Disabling plugin" );
Bukkit.getPluginManager().disablePlugin( this );
return;
}
registerCommands();
space = new PhysicsSpace( BroadphaseType.DBVT );
PlaneCollisionShape plane = new PlaneCollisionShape( new Plane( new Vector3f( 0, 1, 0 ), 0 ) );
planeBody = new PhysicsRigidBody( plane, PhysicsBody.massForStatic );
planeBody.setPhysicsLocation( new Vector3f( 0, 128, 0 ) );
// space.addCollisionObject( planeBody );
space.setAccuracy( 1f / ( TIME_STEPS * 20f ) );
space.addContactListener( new ContactListener() {
@Override
public void onContactEnded( long manifoldId ) {
}
@Override
public void onContactProcessed( PhysicsCollisionObject objA, PhysicsCollisionObject objB, long manifoldPointId ) {
if ( isTnt.remove( objA ) ) {
impulse( objA.getPhysicsLocation(), 30f );
if ( linkedDisplays.containsKey( objA ) ) {
linkedDisplays.get( objA ).display.remove();
}
}
if ( isTnt.remove( objB ) ) {
impulse( objB.getPhysicsLocation(), 30f );
if ( linkedDisplays.containsKey( objB ) ) {
linkedDisplays.get( objB ).display.remove();
}
}
}
@Override
public void onContactStarted( long manifoldId ) {
}
} );
Bukkit.getPluginManager().registerEvents( new Listener() {
@EventHandler
private void onChunkLoad( ChunkLoadEvent event ) {
if ( event.getWorld().getName().equalsIgnoreCase( "world" ) ) {
// getLogger().info( "Loading chunk..." );
// scanAndGenerate( event.getChunk() );
// saveChunk( event.getChunk() );
}
}
@EventHandler
private void onChunkUnload( ChunkUnloadEvent event ) {
if ( event.getWorld().getName().equalsIgnoreCase( "world" ) ) {
ChunkLocation loc = new ChunkLocation( event.getChunk() );
if ( chunks.containsKey( loc ) ) {
space.removeCollisionObject( chunks.get( loc ) );
chunks.remove( loc );
}
}
}
}, this );
// getLogger().info( "There are " + Bukkit.getWorld( "world" ).getLoadedChunks().length + " chunks" );
// getLogger().info( "Converted " + saveAllChunks( Bukkit.getWorld( "world" ) ) );
Bukkit.getScheduler().runTaskTimer( this, () -> {
if ( !paused ) {
// Update the real world with the physics space objects
space.distributeEvents();
space.update( tick, TIME_STEPS );
for ( Iterator< Entry< PhysicsJoint, JointStruct > > it = joints.entrySet().iterator(); it.hasNext(); ) {
Entry< PhysicsJoint, JointStruct > entry = it.next();
PhysicsJoint joint = entry.getKey();
JointStruct struct = entry.getValue();
BlockState state = struct.state;
if ( state.getBlock().getType() != state.getType() || !state.getChunk().isLoaded() ) {
space.removeJoint( joint );
space.remove( struct.object );
it.remove();
} else if ( !joint.isEnabled() ) {
Location loc = state.getLocation();
BlockData displayData = state.getBlockData();
// Create the display
BlockDisplay display = loc.getWorld().spawn( loc, BlockDisplay.class, d -> {
d.setBlock( displayData );
} );
linkedDisplays.put( struct.object, new AuxiliaryDisplayStruct( display, struct.offset ) );
state.getBlock().setType( Material.AIR );
struct.object.setProtectGravity( false );
Vector3f grav = new Vector3f();
space.setGravity( grav );
struct.object.setGravity( grav );
space.removeJoint( joint );
it.remove();
}
}
for ( Iterator< Entry< PhysicsCollisionObject, AuxiliaryDisplayStruct > > it = linkedDisplays.entrySet().iterator(); it.hasNext(); ) {
Entry< PhysicsCollisionObject, AuxiliaryDisplayStruct > entry = it.next();
PhysicsCollisionObject obj = entry.getKey();
AuxiliaryDisplayStruct disp = entry.getValue();
if ( !disp.display.isValid() ) {
it.remove();
space.removeCollisionObject( obj );
active.remove( obj );
isTnt.remove( obj );
continue;
}
Location displayLocation = disp.display.getLocation();
// Do not calculate for this object if it is inactive
if ( !obj.isActive() ) {
if ( teleportOnDeactivate && active.remove( obj ) ) {
// Newly deactivated object, teleport the display to the actual position to prevent the translation from getting too large
Transformation currentTransformation = disp.display.getTransformation();
org.joml.Vector3f trans = currentTransformation.getTranslation();
displayLocation.add( trans.x(), trans.y(), trans.z() );
disp.display.teleport( displayLocation );
Transformation newTrans = new Transformation( new org.joml.Vector3f( 0, 0, 0 ), currentTransformation.getLeftRotation(), currentTransformation.getScale(), currentTransformation.getRightRotation() );
disp.display.setInterpolationDelay( 0 );
disp.display.setInterpolationDuration( 0 );
disp.display.setTransformation( newTrans );
}
continue;
} else {
active.add( obj );
}
Vector3f location = obj.getPhysicsLocation();
Quaternion rotation = new Quaternion();
obj.getPhysicsRotation( rotation );
Vector offsetVec = disp.offset;
Vector scale = disp.scale;
Matrix4f transformRot = new Matrix4f();
transformRot.set( new Quaternionf( rotation.getX(), rotation.getY(), rotation.getZ(), rotation.getW() ) );
transformRot.mul( 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, - ( float ) offsetVec.getX(), - ( float ) offsetVec.getY(), - ( float ) offsetVec.getZ(), 1 );
transformRot.mul( ( float ) scale.getX(), 0, 0, 0, 0, ( float ) scale.getY(), 0, 0, 0, 0, ( float ) scale.getZ(), 0, 0, 0, 0, 1 );
Matrix4f transMat = new Matrix4f( 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, ( float ) ( location.getX() - displayLocation.getX() ), ( float ) ( location.getY() - displayLocation.getY() ), ( float ) ( location.getZ() - displayLocation.getZ() ), 1 );
disp.display.setInterpolationDelay( 0 );
disp.display.setInterpolationDuration( 2 );
disp.display.setTransformationMatrix( transMat.mul( transformRot ) );
}
tick += SIMULATION_SPEED / 20f;
}
}, 0, 1 );
}
private void registerCommands() {
new SubCommand( "minie" )
.addSenderValidator( new SenderValidatorPlayer() )
.add( new SubCommand( "spawn" )
.defaultTo( ( sender, args, params ) -> {
Player player = ( Player ) sender;
Location loc = player.getLocation();
loc.setPitch( 0 );
loc.setDirection( new Vector( 0, 0, 0 ) );
BlockData displayData;
ItemStack item = player.getInventory().getItemInMainHand();
if ( item != null && item.getType() != Material.AIR && item.getType().isBlock() ) {
displayData = item.getType().createBlockData();
} else {
displayData = Material.TNT.createBlockData();
}
BoundingBox[] boxes = convertFrom( getShape( displayData, null, BlockShapeType.SHAPE ) );
Vector blockCenter = calculateCenter( boxes );
CollisionShape box;
if ( boxes.length == 0 ) {
player.sendMessage( "No bounding box detected! Wrong method?" );
return;
}
if ( boxes.length == 1 ) {
Vector min = boxes[ 0 ].getMin();
Vector center = boxes[ 0 ].getCenter().subtract( min );
box = new BoxCollisionShape( ( float ) center.getX(), ( float ) center.getY(), ( float ) center.getZ() );
} else {
CompoundCollisionShape compound = new CompoundCollisionShape();
for ( BoundingBox aabb : boxes ) {
Vector min = aabb.getMin();
Vector center = aabb.getCenter();
Vector half = center.clone().subtract( min );
center.subtract( blockCenter );
CollisionShape subShape = new BoxCollisionShape( ( float ) half.getX(), ( float ) half.getY(), ( float ) half.getZ() );
compound.addChildShape( subShape, ( float ) center.getX(), ( float ) center.getY(), ( float ) center.getZ() );
}
box = compound;
}
PhysicsRigidBody rigid = new PhysicsRigidBody( box, 1f );
// Since the center of the rigid body and the box shape are different, we need to offset the location
rigid.setPhysicsLocation( new Vector3f( ( float ) ( loc.getX() + blockCenter.getX() ), ( float ) ( loc.getY() + blockCenter.getY() ), ( float ) ( loc.getZ() + blockCenter.getZ() ) ) );
// Convert the display direction vector to a quaternion
rigid.setPhysicsRotation( new Quaternion( 0, 0, 0, 1 ) );
space.addCollisionObject( rigid );
// Create the display
BlockDisplay display = loc.getWorld().spawn( loc, BlockDisplay.class, d -> {
d.setBlock( displayData );
} );
linkedDisplays.put( rigid, new AuxiliaryDisplayStruct( display, blockCenter ) );
if ( displayData.getMaterial() == Material.TNT ) {
isTnt.add( rigid );
}
player.sendMessage( "Created display" );
} ) )
.add( new SubCommand( "constraint" )
.defaultTo( ( sender, args, params ) -> {
Player player = ( Player ) sender;
Location loc = player.getLocation();
loc.setPitch( 0 );
loc.setDirection( new Vector( 0, 0, 0 ) );
BlockData displayData = Material.IRON_TRAPDOOR.createBlockData();
final Vector scale = new Vector( 5, 2, 5 );
BoundingBox[] boxes = convertFrom( getShape( displayData, loc, BlockShapeType.VISUAL_SHAPE ) );
for ( BoundingBox box : boxes ) {
Vector min = box.getMin().multiply( scale );
Vector max = box.getMax().multiply( scale );
box.resize( min.getX(), min.getY(), min.getZ(), max.getX(), max.getY(), max.getZ() );
}
Vector blockCenter = calculateCenter( boxes );
CollisionShape box;
if ( boxes.length == 0 ) {
player.sendMessage( "No bounding box detected! Wrong method?" );
return;
}
if ( boxes.length == 1 ) {
Vector min = boxes[ 0 ].getMin();
Vector center = boxes[ 0 ].getCenter().subtract( min );
box = new BoxCollisionShape( ( float ) center.getX(), ( float ) center.getY(), ( float ) center.getZ() );
} else {
CompoundCollisionShape compound = new CompoundCollisionShape();
for ( BoundingBox aabb : boxes ) {
Vector min = aabb.getMin();
Vector center = aabb.getCenter();
Vector half = center.clone().subtract( min );
center.subtract( blockCenter );
CollisionShape subShape = new BoxCollisionShape( ( float ) half.getX(), ( float ) half.getY(), ( float ) half.getZ() );
compound.addChildShape( subShape, ( float ) center.getX(), ( float ) center.getY(), ( float ) center.getZ() );
}
box = compound;
}
PhysicsRigidBody rigid = new PhysicsRigidBody( box, 5f );
// Since the center of the rigid body and the box shape are different, we need to offset the location
rigid.setPhysicsLocation( new Vector3f( ( float ) ( loc.getX() + blockCenter.getX() ), ( float ) ( loc.getY() + blockCenter.getY() ), ( float ) ( loc.getZ() + blockCenter.getZ() ) ) );
// Convert the display direction vector to a quaternion
rigid.setPhysicsRotation( new Quaternion( 0, 0, 0, 1 ) );
space.addCollisionObject( rigid );
New6Dof constraint = new New6Dof( rigid, new Vector3f( 0, 0, 0 ), rigid.getPhysicsLocation(), Matrix3f.IDENTITY, Matrix3f.IDENTITY, RotationOrder.XYZ );
constraint.set( MotorParam.UpperLimit, 3, ( float ) Math.PI / 6 );
constraint.set( MotorParam.LowerLimit, 3, ( float ) - Math.PI / 6 );
constraint.set( MotorParam.UpperLimit, 4, 0 );
constraint.set( MotorParam.LowerLimit, 4, 0 );
constraint.set( MotorParam.UpperLimit, 5, 0 );
constraint.set( MotorParam.LowerLimit, 5, 0 );
space.addJoint( constraint );
player.sendMessage( "Location: " + rigid.getPhysicsLocation() );
// Create the display
BlockDisplay display = loc.getWorld().spawn( loc, BlockDisplay.class, d -> {
d.setBlock( displayData );
} );
linkedDisplays.put( rigid, new AuxiliaryDisplayStruct( display, blockCenter ).setScale( scale ) );
player.sendMessage( "Created constraint" );
}) )
.add( new SubCommand( "fixed" )
.defaultTo( ( sender, args, params ) -> {
Player player = ( Player ) sender;
Block block = player.getTargetBlockExact( 20 );
convert( block );
player.sendMessage( "Fixed" );
} ) )
.add( new SubCommand( "chunk" )
.defaultTo( ( sender, args, params ) -> {
Player player = ( Player ) sender;
player.sendMessage( "Registering chunk..." );
scanAndGenerate( player.getLocation().getChunk() );
} ) )
.add( new SubCommand( "plane" )
.defaultTo( ( sender, args, params ) -> {
Player player = ( Player ) sender;
if ( planeBody != null ) {
if ( planeBody.getCollisionGroup() == PhysicsCollisionObject.COLLISION_GROUP_01 ) {
player.sendMessage( "Disabling plane" );
planeBody.setCollisionGroup( PhysicsCollisionObject.COLLISION_GROUP_02 );
} else {
player.sendMessage( "Enabling plane" );
planeBody.setCollisionGroup( PhysicsCollisionObject.COLLISION_GROUP_01 );
}
} else {
player.sendMessage( "Plane does not exist!" );
}
} ) )
.add( new SubCommand( "save" )
.defaultTo( ( sender, args, params ) -> {
Player player = ( Player ) sender;
player.sendMessage( "Saving chunk..." );
player.sendMessage( "Saved " + saveChunk( player.getLocation().getChunk() ) + " boxes" );
} ) )
.add( new SubCommand( "toggle" )
.defaultTo( ( sender, args, params ) -> {
paused = !paused;
} ) )
.add( new SubCommand( "teleport" )
.defaultTo( ( sender, args, params ) -> {
teleportOnDeactivate = !teleportOnDeactivate;
} ) )
.add( new SubCommand( "impulse" )
.add( new SubCommand( new InputValidatorInt() )
.defaultTo( ( sender, args, params ) -> {
Player player = ( Player ) sender;
Location loc = player.getLocation();
impulse( new Vector3f( ( float ) loc.getX(), ( float ) loc.getY(), ( float ) loc.getZ() ), params.getLast( int.class ) );
} ) )
.defaultTo( ( sender, args, params ) -> {
Player player = ( Player ) sender;
Location loc = player.getLocation();
impulse( new Vector3f( ( float ) loc.getX(), ( float ) loc.getY(), ( float ) loc.getZ() ), 15f );
} ) )
.defaultTo( new CommandExecutableMessage( "An argument must be provided" ) )
.whenUnknown( new CommandExecutableMessage( "Unknown argument" ) )
.applyTo( getCommand( "minie" ) );
}
private void impulse( Vector3f location, float power ) {
PhysicsRigidBody rigid = new PhysicsRigidBody( new SphereCollisionShape( 20 ), 1f );
rigid.setPhysicsLocation( location );
space.contactTest( rigid, new PhysicsCollisionListener() {
@Override
public void collision( PhysicsCollisionEvent event ) {
if ( event.getObjectB() instanceof PhysicsRigidBody ) {
PhysicsRigidBody objB = ( PhysicsRigidBody ) event.getObjectB();
Vector3f rLoc = objB.getPhysicsLocation();
Vector3f to = rLoc.subtract( ( float ) location.getX(), ( float ) location.getY(), ( float ) location.getZ() );
float distSq = to.lengthSquared();
if ( distSq != 0 ) {
if ( power / distSq > 1 ) {
objB.applyCentralImpulse( to.normalize().mult( power / distSq ) );
}
}
}
}
} );
}
private int saveAllChunks( World world ) {
int total = 0;
for ( Chunk chunk : world.getLoadedChunks() ) {
total += saveChunk( chunk );
}
return total;
}
private int saveChunk( Chunk chunk ) {
File saveFile = new File( getDataFolder() + "/chunks", chunk.getWorld().getName() + "," + chunk.getX() + "," + chunk.getZ() );
saveFile.getParentFile().mkdirs();
saveFile.delete();
int count = 0;
try ( FileWriter writer = new FileWriter( saveFile ) ) {
World world = chunk.getWorld();
for ( int y = world.getMinHeight(); y < world.getMaxHeight(); ++y ) {
for ( int z = 0; z < 16; ++z ) {
for ( int x = 0; x < 16; ++x ) {
Block block = chunk.getBlock( x, y, z );
if ( !( block.isEmpty() && block.isLiquid() ) ) {
BlockState state = block.getState();
BlockData data = state.getBlockData();
Location loc = block.getLocation();
BoundingBox[] boxes = convertFrom( getShape( data, loc, BlockShapeType.VISUAL_SHAPE ) );
if ( boxes.length > 0 ) {
// Save this block
for ( BoundingBox box : boxes ) {
writer.write( ( box.getMinX() + x ) + "," );
writer.write( ( box.getMinY() + y ) + "," );
writer.write( ( box.getMinZ() + z ) + "," );
writer.write( ( box.getMaxX() + x ) + "," );
writer.write( ( box.getMaxY() + y ) + "," );
writer.write( ( box.getMaxZ() + z ) + "\n" );
count++;
}
}
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
return count;
}
private void scanAndGenerate( World world ) {
for ( Chunk chunk : world.getLoadedChunks() ) {
scanAndGenerate( chunk );
}
}
private void scanAndGenerate( final Chunk chunk ) {
final long start = System.currentTimeMillis();
final World world = chunk.getWorld();
final int worldMinHeight = world.getMinHeight();
final int worldMaxHeight = world.getMaxHeight();
final Collection< BoundingBox > boxes = new ArrayDeque< BoundingBox >();
getLogger().info( "Gathering bounding boxes..." );
for ( int y = worldMinHeight; y < worldMaxHeight; ++y ) {
for ( int z = 0; z < 16; ++z ) {
for ( int x = 0; x < 16; ++x ) {
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 = convertFrom( getShape( data, loc, BlockShapeType.COLLISION_SHAPE ), new Vector( x, y - worldMinHeight, z ) );
for ( BoundingBox box : boundingBoxes ) {
boxes.add( box );
}
}
}
}
}
Bukkit.getScheduler().runTaskAsynchronously( this, () -> {
getLogger().info( "Building planes..." );
final MeshBuilder builder = new MeshBuilder();
for ( final BoundingBox box : boxes ) {
for ( final Facet facet : generateFacetsFor( box ) ) {
builder.addFacet( facet );
}
}
getLogger().info( "Meshing planes..." );
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 >();
getLogger().info( "Generating triangles..." );
// 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 );
}
} );
getLogger().info( "Creating buffers..." );
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;
}
getLogger().info( "Adding mesh collision shape..." );
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 );
getLogger().info( "Mesh has " + mesh.countMeshVertices() + " vertices and " + mesh.countMeshTriangles() + " triangles" );
// 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, () -> {
space.addCollisionObject( rigid );
PhysicsRigidBody old = chunks.put( new ChunkLocation( chunk ), rigid );
if ( old != null ) {
getLogger().warning( "Old rigid body found!" );
space.remove( old );
}
getLogger().info( "Done!" );
final long time = System.currentTimeMillis() - start;
getLogger().info( "Took " + time + "ms" );
} );
} );
}
private boolean convert( Block block ) {
BlockState state = block.getState();
BlockData data = state.getBlockData();
Location loc = block.getLocation();
BoundingBox[] boxes = convertFrom( getShape( data, loc, BlockShapeType.VISUAL_SHAPE ) );
Vector blockCenter = calculateCenter( boxes );
CollisionShape box;
if ( boxes.length == 0 ) {
return false;
}
if ( boxes.length == 1 ) {
Vector min = boxes[ 0 ].getMin();
Vector center = boxes[ 0 ].getCenter().subtract( min );
box = new BoxCollisionShape( ( float ) center.getX(), ( float ) center.getY(), ( float ) center.getZ() );
} else {
CompoundCollisionShape compound = new CompoundCollisionShape();
for ( BoundingBox aabb : boxes ) {
Vector min = aabb.getMin();
Vector center = aabb.getCenter();
Vector half = center.clone().subtract( min );
center.subtract( blockCenter );
CollisionShape subShape = new BoxCollisionShape( ( float ) half.getX(), ( float ) half.getY(), ( float ) half.getZ() );
compound.addChildShape( subShape, ( float ) center.getX(), ( float ) center.getY(), ( float ) center.getZ() );
}
box = compound;
}
PhysicsRigidBody rigid = new PhysicsRigidBody( box, 1f );
rigid.setProtectGravity( true );
rigid.setGravity( new Vector3f( 0, 0, 0 ) );
// Since the center of the rigid body and the box shape are different, we need to offset the location
rigid.setPhysicsLocation( new Vector3f( ( float ) ( loc.getX() + blockCenter.getX() ), ( float ) ( loc.getY() + blockCenter.getY() ), ( float ) ( loc.getZ() + blockCenter.getZ() ) ) );
// Convert the display direction vector to a quaternion
rigid.setPhysicsRotation( new Quaternion( 0, 0, 0, 1 ) );
space.addCollisionObject( rigid );
New6Dof constraint = new New6Dof( rigid, new Vector3f( 0, 0, 0 ), rigid.getPhysicsLocation(), Matrix3f.IDENTITY, Matrix3f.IDENTITY, RotationOrder.XYZ );
constraint.setBreakingImpulseThreshold( 5 );
constraint.set( MotorParam.UpperLimit, 3, 0 );
constraint.set( MotorParam.LowerLimit, 3, 0 );
constraint.set( MotorParam.UpperLimit, 4, 0 );
constraint.set( MotorParam.LowerLimit, 4, 0 );
constraint.set( MotorParam.UpperLimit, 5, 0 );
constraint.set( MotorParam.LowerLimit, 5, 0 );
space.addJoint( constraint );
joints.put( constraint, new JointStruct( state, rigid, blockCenter ) );
return true;
}
private final boolean loadNativeLibraries() {
if ( SystemUtils.IS_OS_MAC ) {
System.load( new File( getDataFolder() + "/lib", "libbulletjme.dylib" ).getAbsolutePath() );
} else if ( SystemUtils.IS_OS_LINUX ) {
System.load( new File( getDataFolder() + "/lib", "libbulletjme.so" ).getAbsolutePath() );
} else if ( SystemUtils.IS_OS_WINDOWS ) {
System.load( new File( getDataFolder() + "/lib", "bulletjme.dll" ).getAbsolutePath() );
} else {
return false;
}
return true;
}
private static Collection< Polygon > process( final com.aaaaahhhhhhh.bananapuncher714.minietest.objects.mesh.Plane plane ) {
final Mesh< RegionSimple > mesh = new Mesh< RegionSimple >( () -> { return new RegionSimple( GluWindingRule.ODD ); } );
for ( Polygon poly : plane.polygons ) {
mesh.addPolygon( poly, RegionRuleWinding.CLOCKWISE );
}
return mesh.meshify();
}
private static Vector calculateCenter( BoundingBox[] boxes ) {
Vector center = new Vector( 0, 0, 0 );
// TODO Throw error, probably
if ( boxes.length == 0 ) {
return center;
}
double totalVolume = 0;
for ( BoundingBox box : boxes ) {
Vector mid = box.getCenter();
center.add( mid.multiply( box.getVolume() ) );
totalVolume += box.getVolume();
}
return center.multiply( 1 / totalVolume );
}
// 1.21.5
private static VoxelShape getShape( BlockData blockData, Location location, BlockShapeType type ) {
final CraftBlockData data = ( CraftBlockData ) blockData;
WorldServer server = null;
BlockPosition pos = null;
if ( location != null ) {
server = ( ( CraftWorld ) location.getWorld() ).getHandle();
pos = new BlockPosition( location.getBlockX(), location.getBlockY(), location.getBlockZ() );
}
switch ( type ) {
default:
case SHAPE:
return data.getState().f( server, pos );
case COLLISION_SHAPE:
return data.getState().g( server, pos );
case VISUAL_SHAPE:
return data.getState().c( server, pos, VoxelShapeCollision.a() );
case INTERACTION_SHAPE:
return data.getState().i( server, pos );
case BLOCK_SUPPORT_SHAPE:
return data.getState().h( server, pos );
}
}
private static BoundingBox[] convertFrom( final VoxelShape shape ) {
return convertFrom( shape, new Vector() );
}
private static BoundingBox[] convertFrom( final VoxelShape shape, final Vector offset ) {
return shape.e().stream()
.map( aabb -> { return new BoundingBox( aabb.a, aabb.b, aabb.c, aabb.d, aabb.e, aabb.f ).shift( offset ); } )
.toArray( BoundingBox[]::new );
}
private static List< Facet > generateFacetsFor( BoundingBox box ) {
List< Facet > facets = new ArrayList< Facet >();
Vector p1 = new Vector( box.getMinX(), box.getMinY(), box.getMinZ() );
Vector p2 = new Vector( box.getMinX(), box.getMinY(), box.getMaxZ() );
Vector p3 = new Vector( box.getMinX(), box.getMaxY(), box.getMinZ() );
Vector p4 = new Vector( box.getMinX(), box.getMaxY(), box.getMaxZ() );
Vector p5 = new Vector( box.getMaxX(), box.getMinY(), box.getMinZ() );
Vector p6 = new Vector( box.getMaxX(), box.getMinY(), box.getMaxZ() );
Vector p7 = new Vector( box.getMaxX(), box.getMaxY(), box.getMinZ() );
Vector p8 = new Vector( box.getMaxX(), box.getMaxY(), box.getMaxZ() );
{
Facet facet = new Facet();
facet.points.add( p1 );
facet.points.add( p2 );
facet.points.add( p4 );
facet.points.add( p3 );
facet.normal = new Vector( -1, 0, 0 );
facets.add( facet );
}
{
Facet facet = new Facet();
facet.points.add( p5 );
facet.points.add( p6 );
facet.points.add( p8 );
facet.points.add( p7 );
facet.normal = new Vector( 1, 0, 0 );
facets.add( facet );
}
{
Facet facet = new Facet();
facet.points.add( p1 );
facet.points.add( p2 );
facet.points.add( p6 );
facet.points.add( p5 );
facet.normal = new Vector( 0, -1, 0 );
facets.add( facet );
}
{
Facet facet = new Facet();
facet.points.add( p3 );
facet.points.add( p4 );
facet.points.add( p8 );
facet.points.add( p7 );
facet.normal = new Vector( 0, 1, 0 );
facets.add( facet );
}
{
Facet facet = new Facet();
facet.points.add( p1 );
facet.points.add( p3 );
facet.points.add( p7 );
facet.points.add( p5 );
facet.normal = new Vector( 0, 0, -1 );
facets.add( facet );
}
{
Facet facet = new Facet();
facet.points.add( p2 );
facet.points.add( p4 );
facet.points.add( p8 );
facet.points.add( p6 );
facet.normal = new Vector( 0, 0, 1 );
facets.add( facet );
}
return facets;
}
private class JointStruct {
BlockState state;
PhysicsRigidBody object;
Vector offset;
JointStruct( BlockState state, PhysicsRigidBody body, Vector offset ) {
this.state = state;
this.object = body;
this.offset = offset.clone();
}
}
private class AuxiliaryDisplayStruct {
AuxiliaryDisplayStruct( Display display, Vector offset ) {
this.display = display;
this.offset = offset.clone();
this.scale = new Vector( 1, 1, 1 );
}
AuxiliaryDisplayStruct setScale( Vector scale ) {
this.scale = scale.clone();
return this;
}
Display display;
Vector offset;
Vector scale;
}
private enum BlockShapeType {
SHAPE,
COLLISION_SHAPE,
VISUAL_SHAPE,
INTERACTION_SHAPE,
BLOCK_SUPPORT_SHAPE
}
}