Improve tessellation algorithm

Added new mesh class
Rewrite mesh algorithms to be less error prone
Make code more concise
Tried to add some documentation
Added half edge set
This commit is contained in:
2025-05-03 23:52:54 -04:00
parent a1899a52b7
commit d16041f977
30 changed files with 2923 additions and 227 deletions

View File

@@ -8,7 +8,6 @@ import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.swing.JFrame;
@@ -17,16 +16,14 @@ import javax.swing.SwingUtilities;
import org.bukkit.util.Vector;
import com.aaaaahhhhhhh.bananapuncher714.minietest.MeshTest.AABB;
import com.aaaaahhhhhhh.bananapuncher714.mesh.Point;
import com.aaaaahhhhhhh.bananapuncher714.mesh.Polygon;
import com.aaaaahhhhhhh.bananapuncher714.mesh.Vector2d;
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.objects.mesh.Plane;
import com.aaaaahhhhhhh.bananapuncher714.tess4j.Point;
import com.aaaaahhhhhhh.bananapuncher714.tess4j.Polygon;
import com.aaaaahhhhhhh.bananapuncher714.tess4j.Tess4j;
import com.aaaaahhhhhhh.bananapuncher714.tess4j.Vector2d;
import com.aaaaahhhhhhh.bananapuncher714.tess4j.Vector2dComparator;
import com.aaaaahhhhhhh.bananapuncher714.tess4j.region.RegionSimple;
import com.aaaaahhhhhhh.bananapuncher714.tess4j.region.RegionSimple.GluWindingRule;
import com.aaaaahhhhhhh.bananapuncher714.tess4j.region.WindingRuleSimple;

View File

@@ -0,0 +1,485 @@
package com.aaaaahhhhhhh.bananapuncher714.minietest;
import java.awt.Color;
import java.awt.Graphics;
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.Collections;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import org.bukkit.util.Vector;
import com.aaaaahhhhhhh.bananapuncher714.mesh.EdgeSet;
import com.aaaaahhhhhhh.bananapuncher714.mesh.HalfEdge;
import com.aaaaahhhhhhh.bananapuncher714.mesh.Mesh;
import com.aaaaahhhhhhh.bananapuncher714.mesh.Mesh.EdgePolygon;
import com.aaaaahhhhhhh.bananapuncher714.mesh.Point;
import com.aaaaahhhhhhh.bananapuncher714.mesh.Polygon;
import com.aaaaahhhhhhh.bananapuncher714.mesh.Vector2d;
import com.aaaaahhhhhhh.bananapuncher714.mesh.Vertex;
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.objects.ChunkLocation;
import com.aaaaahhhhhhh.bananapuncher714.minietest.objects.mesh.Facet;
import com.aaaaahhhhhhh.bananapuncher714.minietest.objects.mesh.MeshBuilder;
import com.aaaaahhhhhhh.bananapuncher714.minietest.objects.mesh.Plane;
public class MeshingTest2 extends JPanel {
private static final File BASE = new File( System.getProperty( "user.dir" ) );
private static final File CHUNK_DIR = new File( BASE, "chunks" );
JFrame f;
private int windowWidth = 1200;
private int windowHeight = 1000;
// private int centerX = 600;
// private int centerY = 400;
private int centerX = 400;
private int centerY = 1200;
private Graphics g;
private int scale = 8;
private Collection< Vertex > data;
private Collection< EdgePolygon > polygons;
public static void main( String[] args ) {
if ( CHUNK_DIR.exists() && CHUNK_DIR.isDirectory() ) {
System.out.println( "Found " + CHUNK_DIR.list().length + " files" );
for ( File file : CHUNK_DIR.listFiles() ) {
Collection< Plane > planes = mesh( file );
Plane draw = null;
System.out.println( "Planes: " + planes.size() );
for ( Plane plane : planes ) {
if ( plane.polygons.size() > 100 ) {
draw = plane;
break;
}
}
// draw = planes.get( new Random().nextInt( planes.size() ) );
// test( planes );
final Collection< Vertex > data = process( draw );
final Collection< EdgePolygon > polys = triangulate( draw );
// final Collection< EdgePolygon > polys = test();
SwingUtilities.invokeLater( new Runnable() {
@Override
public void run() {
new MeshingTest2( data, polys );
}
} );
break;
}
} else {
System.err.println( "No such directory exists: " + CHUNK_DIR );
}
}
private static Collection< Plane > mesh( File file ) {
String name = file.getName();
String[] split = name.split( "," );
ChunkLocation location = new ChunkLocation( split[ 0 ], Integer.parseInt( split[ 1 ] ), Integer.parseInt( split[ 2 ] ) );
System.out.println( "Parsing chunk " + location );
// First parse the file to get a list of all bounding boxes that we can use
List< AABB > boxes = new ArrayList< AABB >();
try ( BufferedReader reader = new BufferedReader( new FileReader( file ) ) ) {
String line;
while ( ( line = reader.readLine() ) != null ) {
if ( !line.isEmpty() ) {
String[] values = line.split( "," );
double minX = Double.parseDouble( values[ 0 ] );
double minY = Double.parseDouble( values[ 1 ] );
double minZ = Double.parseDouble( values[ 2 ] );
double maxX = Double.parseDouble( values[ 3 ] );
double maxY = Double.parseDouble( values[ 4 ] );
double maxZ = Double.parseDouble( values[ 5 ] );
boxes.add( new AABB( minX, minY, minZ, maxX, maxY, maxZ ) );
}
}
} catch ( FileNotFoundException e ) {
e.printStackTrace();
return null;
} catch ( IOException e ) {
e.printStackTrace();
return null;
}
System.out.println( "Found " + boxes.size() + " boxes" );
// Now that we have a bunch of bounding boxes, do whatever
MeshBuilder builder = new MeshBuilder();
Plane draw = null;
for ( AABB box : boxes ) {
for ( Facet facet : generateFacetsFor( box ) ) {
builder.addFacet( facet );
}
}
return builder.planes;
}
private static void test( final Collection< Plane > planes ) {
final long processAllStart = System.currentTimeMillis();
planes.parallelStream().forEach( plane -> {
try {
Mesh< RegionSimple > mesh = new Mesh< RegionSimple >( () -> { return new RegionSimple( GluWindingRule.ODD ); } );
for ( Polygon poly : plane.polygons ) {
mesh.addPolygon( poly, RegionRuleWinding.CLOCKWISE );
}
long start = System.currentTimeMillis();
mesh.simplify();
mesh.generateRegions();
long end = System.currentTimeMillis();
System.out.println( plane.polygons.size() + ":\t " + ( end - start ) + "ms" );
} catch ( IllegalStateException e ) {
e.printStackTrace();
}
} );
final long processAllEnd = System.currentTimeMillis();
System.out.println( "Took " + ( processAllEnd - processAllStart ) + "ms to process " + planes.size() + " planes" );
}
private static Collection< Vertex > process( final Plane plane ) {
if ( plane != null ) {
System.out.println( "Norm:\t" + plane.normal );
System.out.println( "Ref:\t" + plane.point );
System.out.println( "Size:\t" + plane.polygons.size() );
Mesh< RegionSimple > mesh = new Mesh< RegionSimple >( () -> { return new RegionSimple( GluWindingRule.ODD ); } );
for ( Polygon poly : plane.polygons ) {
mesh.addPolygon( poly, RegionRuleWinding.CLOCKWISE );
}
System.out.println( "Initial vertex count: " + mesh.getVertices().size() );
System.out.println( "Initial edge count: " + ( mesh.getRuleSize() / 2 ) );
long start = System.currentTimeMillis();
mesh.simplify();
System.out.println( "After simplify vertex count: " + mesh.getVertices().size() );
System.out.println( "After simplify edge count: " + ( mesh.getRuleSize() / 2 ) );
mesh.generateRegions();
long end = System.currentTimeMillis();
System.out.println( "After generating regions vertex count: " + mesh.getVertices().size() );
System.out.println( "After generating regions edge count: " + ( mesh.getRuleSize() / 2 ) );
System.out.println( "Took " + ( end - start ) + "ms" );
return mesh.getVertices();
} else {
System.out.println( "No data!" );
return Collections.emptySet();
}
}
private static Collection< EdgePolygon > triangulate( final Plane plane ) {
if ( plane != null ) {
System.out.println( "Norm:\t" + plane.normal );
System.out.println( "Ref:\t" + plane.point );
System.out.println( "Size:\t" + plane.polygons.size() );
Mesh< RegionSimple > mesh = new Mesh< RegionSimple >( () -> { return new RegionSimple( GluWindingRule.ODD ); } );
for ( Polygon poly : plane.polygons ) {
mesh.addPolygon( poly, RegionRuleWinding.CLOCKWISE );
}
System.out.println( "Initial vertex count: " + mesh.getVertices().size() );
System.out.println( "Initial edge count: " + ( mesh.getRuleSize() / 2 ) );
long start = System.currentTimeMillis();
mesh.simplify();
System.out.println( "After simplify vertex count: " + mesh.getVertices().size() );
System.out.println( "After simplify edge count: " + ( mesh.getRuleSize() / 2 ) );
mesh.generateRegions();
System.out.println( "After generating regions vertex count: " + mesh.getVertices().size() );
System.out.println( "After generating regions edge count: " + ( mesh.getRuleSize() / 2 ) );
final Collection< EdgePolygon > polys = mesh.mesh();
long end = System.currentTimeMillis();
System.out.println( "Took " + ( end - start ) + "ms" );
return polys;
} else {
System.out.println( "No data!" );
return Collections.emptySet();
}
}
private static Collection< EdgePolygon > test() {
Mesh< RegionSimple > mesh = new Mesh< RegionSimple >( () -> { return new RegionSimple( GluWindingRule.ODD ); } );
mesh.addPolygon( new Polygon( Arrays.asList(
new Point( -1, -1 ),
new Point( 1, -1 ),
new Point( 1, 1 ),
new Point( -1, 1 )
) ), RegionRuleWinding.CLOCKWISE );
mesh.simplify();
mesh.generateRegions();
return mesh.mesh();
}
private static List< Facet > generateFacetsFor( AABB box ) {
List< Facet > facets = new ArrayList< Facet >();
Vector p1 = new Vector( box.xmin, box.ymin, box.zmin );
Vector p2 = new Vector( box.xmin, box.ymin, box.zmax );
Vector p3 = new Vector( box.xmin, box.ymax, box.zmin );
Vector p4 = new Vector( box.xmin, box.ymax, box.zmax );
Vector p5 = new Vector( box.xmax, box.ymin, box.zmin );
Vector p6 = new Vector( box.xmax, box.ymin, box.zmax );
Vector p7 = new Vector( box.xmax, box.ymax, box.zmin );
Vector p8 = new Vector( box.xmax, box.ymax, box.zmax );
{
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;
}
static class AABB {
double xmin, ymin, zmin;
double xmax, ymax, zmax;
AABB( double xmin, double ymin, double zmin, double xmax, double ymax, double zmax ) {
this.xmin = xmin;
this.ymin = ymin;
this.zmin = zmin;
this.xmax = xmax;
this.ymax = ymax;
this.zmax = zmax;
}
double getVolume() {
return ( xmax - xmin ) * ( ymax - ymin ) * ( zmax - zmin );
}
@Override
public String toString() {
return String.format( "(%f, %f, %f) -> (%f, %f, %f)", xmin, ymin, zmin, xmax, ymax, zmax );
}
}
public MeshingTest2( Collection< Vertex > polys, Collection< EdgePolygon > polygons ) {
data = polys;
this.polygons = polygons;
f = new JFrame( "Drawing Board" );
f.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
f.add( this );
f.setSize( windowWidth, windowHeight );
f.setVisible( true );
f.setResizable( true );
f.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
}
public void drawPoint( double x, double y ) {
g.fillRect( ( int ) x * scale, ( int ) y * scale, scale, scale );
}
private static Vector2d rotate( Vector2d a, double angle ) {
double cos = Math.cos( angle );
double sin = Math.sin( angle );
double ax = a.getX() * cos - a.getY() * sin;
double ay = a.getX() * sin + a.getY() * cos;
return new Vector2d( ax, ay );
}
@Override
public void paintComponent( Graphics g ) {
super.paintComponent( g );
this.g = g;
g.setColor( new Color( 200, 200, 200 ) );
g.drawLine( centerX, 0, centerX, 2000 );
g.drawLine( 0, centerY, 2000, centerY );
g.setColor( Color.BLACK );
if ( data != null ) {
Collection< Vertex > polygons = data;
EdgeSet edges = new EdgeSet();
for ( Vertex vert : polygons ) {
for ( HalfEdge edge : vert ) {
if ( edges.add( edge ) ) {
Point p1 = edge.getOrigin().getPosition();
Point p2 = edge.getDest().getPosition();
g.drawLine(
( int ) ( p1.getX() * scale ) + centerX,
( int ) ( p1.getY() * scale ) + centerY,
( int ) ( p2.getX() * scale ) + centerX,
( int ) ( p2.getY() * scale ) + centerY
);
}
}
final double diff = scale * 0.3;
final Point point = vert.getPosition();
g.drawRect( ( int ) ( point.getX() * scale ) + centerX - ( int ) diff, ( int ) ( point.getY() * scale ) + centerY - ( int ) diff, ( int ) ( diff * 2 ), ( int ) ( diff * 2 ) );
}
}
if ( polygons != null ) {
System.out.println( "Polygon count: " + polygons.size() );
g.setColor( Color.BLACK );
for ( final EdgePolygon p : polygons ) {
final Collection< HalfEdge > edges = p.getEdges();
for ( HalfEdge edge : edges ) {
Point p1 = edge.getOrigin().getPosition();
Point p2 = edge.getDest().getPosition();
g.drawLine(
( int ) ( p1.getX() * scale ) + centerX + 400,
( int ) ( p1.getY() * scale ) + centerY,
( int ) ( p2.getX() * scale ) + centerX + 400,
( int ) ( p2.getY() * scale ) + centerY
);
}
// int size = edges.size();
// int[] xPoints = new int[ size ];
// int[] yPoints = new int[ size ];
//
// HalfEdge edge = edges.iterator().next();
// for ( int i = 0; i < edges.size(); ++i ) {
// final Point point = edge.getOrigin().getPosition();
// xPoints[ i ] = ( int ) ( point.getX() * scale ) + centerX;
// yPoints[ i ] = ( int ) ( point.getY() * scale ) + centerY;
// }
//
// g.setColor( new Color( ( int ) ( Math.random() * 0xFFFFFF ) ) );
// g.fillPolygon( xPoints, yPoints, size );
}
g.setColor( Color.BLACK );
for ( final EdgePolygon p : polygons ) {
for ( final Vertex v : p.getVertices() ) {
final Point point = v.getPosition();
double diff = scale * .3;
g.drawRect( 400 + ( int ) ( point.getX() * scale ) + centerX - ( int ) diff, ( int ) ( point.getY() * scale ) + centerY - ( int ) diff, ( int ) ( diff * 2 ), ( int ) ( diff * 2 ) );
}
}
}
// Point[] points = {
// new Point( 0, -2 ),
// new Point( 1.5, -3.0 ),
// new Point( 0.0, -2.0 ),
// new Point( 0.9545454545454546, -1.3636363636363638 ),
// new Point( -0.8333333333333334, -1.0 ),
// new Point( 0.8333333333333334, -1.0 ),
// new Point( -0.5, 0.0 ),
// new Point( 0.5, 0.0 ),
// new Point( -0.16666666666666669, 1.0 ),
// new Point( 0.16666666666666669, 1.0 ),
// new Point( -0.16666666666666669, 1.0 ),
// new Point( 1, 1 ),
// new Point( -0.16666666666666669, 1.0 ),
// new Point( 0, 1.5 ),
// new Point( 0, 1.5 ),
// new Point( 0.16666666666666669, 1.0 ),
// new Point( -1, 2 ),
// new Point( 1, 2 ),
// };
//
// int size = points.length >> 1;
// for ( int i = 0; i < size;i++ ) {
// Point a = points[ i << 1 ];
// Point b = points[ ( i << 1 ) + 1 ];
//
// g.drawLine( ( int ) ( a.x * scale ) + centerX, ( int ) ( a.y * scale ) + centerY, ( int ) ( b.x * scale ) + centerX, ( int ) ( b.y * scale ) + centerY );
// }
}
}

View File

@@ -22,12 +22,6 @@ public class MeshBuilder {
Optional< Vector > ref = facet.points.stream().filter( v -> v.lengthSquared() > 0.001 && Math.abs( v.clone().normalize().dot( facet.normal ) ) < .999999 ).findAny();
if ( ref.isPresent() ) {
plane.point = ref.get();
} else {
System.out.println( "Normal " + facet.normal );
for ( Vector point : facet.points ) {
System.out.println( point );
}
System.exit( 1 );
}
plane.addShape( facet );
planes.add( plane );

View File

@@ -7,8 +7,8 @@ import org.bukkit.util.Vector;
import org.joml.Matrix3d;
import org.joml.Vector3d;
import com.aaaaahhhhhhh.bananapuncher714.tess4j.Point;
import com.aaaaahhhhhhh.bananapuncher714.tess4j.Polygon;
import com.aaaaahhhhhhh.bananapuncher714.mesh.Point;
import com.aaaaahhhhhhh.bananapuncher714.mesh.Polygon;
public class Plane {
public Vector normal;
@@ -17,30 +17,35 @@ public class Plane {
public List< Polygon > polygons = new ArrayList< Polygon >();
public void addShape( Facet facet ) {
// TODO Flatten the polygons into 2d points
// Get the basis of this plane
Vector basis1 = normal.clone().multiply( point.clone().multiply( -1 ).dot( normal ) ).add( point ).normalize();
Vector3d b1 = new Vector3d( basis1.getX(), basis1.getY(), basis1.getZ() );
Vector3d b3 = new Vector3d( normal.getX(), normal.getY(), normal.getZ() );
Vector3d b2 = new Vector3d( b1 ).cross( b3 ).normalize();
Matrix3d mat = new Matrix3d( b1, b2, b3 ).transpose();
Matrix3d mat = getProjectionMatrix();
List< Point > polygon = new ArrayList< Point >();
for ( Vector p : facet.points ) {
double t = point.clone().subtract( p ).dot( normal );
// TODO Subtract point or to not subtract point?!?
Vector i = normal.clone().multiply( t ).add( p ).subtract( point );
Vector3d v = new Vector3d( i.getX(), i.getY(), i.getZ() );
Vector3d r = v.mul( mat );
polygon.add( new Point( r.x(), r.y() ) );
}
polygons.add( new Polygon( polygon ) );
}
public boolean matches( Vector normal, Vector point ) {
return Math.abs( this.normal.dot( normal ) ) > 0.99 && Math.abs( this.point.clone().subtract( point ).dot( normal ) ) < 0.01;
}
private Matrix3d getProjectionMatrix() {
final Vector basis1 = normal.clone().multiply( point.clone().multiply( -1 ).dot( normal ) ).add( point ).normalize();
final Vector3d b1 = new Vector3d( basis1.getX(), basis1.getY(), basis1.getZ() );
final Vector3d b3 = new Vector3d( normal.getX(), normal.getY(), normal.getZ() );
final Vector3d b2 = new Vector3d( b1 ).cross( b3 ).normalize();
final Matrix3d mat = new Matrix3d( b1, b2, b3 ).transpose();
return mat;
}
}