Browse Source

[libgdx] Added first iteration of convex decomposer. Needs testing.2

badlogic 8 years ago
parent
commit
afd2a95594

+ 184 - 0
spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/ConvexDecomposerTest.java

@@ -0,0 +1,184 @@
+
+package com.esotericsoftware.spine;
+
+import org.lwjgl.opengl.GL11;
+
+import com.badlogic.gdx.ApplicationAdapter;
+import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.Input.Buttons;
+import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
+import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration;
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.graphics.GL20;
+import com.badlogic.gdx.graphics.OrthographicCamera;
+import com.badlogic.gdx.graphics.Texture;
+import com.badlogic.gdx.graphics.g2d.BitmapFont;
+import com.badlogic.gdx.graphics.g2d.PolygonSpriteBatch;
+import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
+import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType;
+import com.badlogic.gdx.math.MathUtils;
+import com.badlogic.gdx.math.Vector3;
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.FloatArray;
+import com.esotericsoftware.spine.utils.ConvexDecomposer;
+import com.esotericsoftware.spine.utils.SutherlandHodgmanClipper;
+
+public class ConvexDecomposerTest extends ApplicationAdapter {
+	OrthographicCamera sceneCamera;
+	ShapeRenderer shapes;
+	PolygonSpriteBatch polyBatcher;
+	Texture image;
+	ConvexDecomposer decomposer = new ConvexDecomposer();
+	FloatArray polygon = new FloatArray();
+	Array<FloatArray> convexPolygons = new Array<FloatArray>();
+	boolean isCreatingPolygon = false;
+	Vector3 tmp = new Vector3();
+	Array<Color> colors = new Array<Color>();
+	BitmapFont font;
+
+	@Override
+	public void create () {
+		sceneCamera = new OrthographicCamera();
+		shapes = new ShapeRenderer();
+		polyBatcher = new PolygonSpriteBatch();
+		image = new Texture("skin/skin.png");
+		font = new BitmapFont();
+	}
+
+	@Override
+	public void resize (int width, int height) {
+		sceneCamera.setToOrtho(false);
+	}
+
+	@Override
+	public void render () {
+		Gdx.gl.glClearColor(0.3f, 0.3f, 0.3f, 1);
+		Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
+
+		processInput();
+		renderScene();
+	}
+
+	private void processInput () {
+		tmp.set(Gdx.input.getX(), Gdx.input.getY(), 0);
+		sceneCamera.unproject(tmp);
+
+		if (Gdx.input.justTouched()) {
+			if (!isCreatingPolygon) {
+				polygon.clear();
+				convexPolygons = null;
+				isCreatingPolygon = true;
+			}
+
+			polygon.add(tmp.x);
+			polygon.add(tmp.y);
+
+			if (Gdx.input.isButtonPressed(Buttons.RIGHT)) {
+				isCreatingPolygon = false;
+				triangulate();
+			}
+		}
+	}
+
+	private void renderScene () {
+		sceneCamera.update();
+		shapes.setProjectionMatrix(sceneCamera.combined);
+		polyBatcher.setProjectionMatrix(sceneCamera.combined);
+
+		polyBatcher.begin();
+		polyBatcher.disableBlending();
+
+		polyBatcher.end();
+		
+		// polygon
+		shapes.setColor(Color.RED);
+		shapes.begin(ShapeType.Line);
+		if (isCreatingPolygon) {
+			tmp.set(Gdx.input.getX(), Gdx.input.getY(), 0);
+			sceneCamera.unproject(tmp);
+			polygon.add(tmp.x);
+			polygon.add(tmp.y);
+		}
+
+		// polygon while drawing
+		switch (polygon.size) {
+		case 0:
+			break;
+		case 2:
+			shapes.end();
+			shapes.begin(ShapeType.Point);
+			GL11.glPointSize(4);
+			shapes.point(polygon.get(0), polygon.get(1), 0);
+			shapes.end();
+			shapes.begin(ShapeType.Line);
+			break;
+		case 4:
+			shapes.line(polygon.get(0), polygon.get(1), polygon.get(2), polygon.get(3));
+			break;
+		default:
+			shapes.polygon(polygon.items, 0, polygon.size);
+		}
+
+		// edge normals
+		shapes.setColor(Color.YELLOW);
+		if (polygon.size > 2) {
+			boolean clockwise = SutherlandHodgmanClipper.isClockwise(polygon);
+			for (int i = 0; i < polygon.size; i += 2) {
+				float x = polygon.get(i);
+				float y = polygon.get(i + 1);
+				float x2 = polygon.get((i + 2) % polygon.size);
+				float y2 = polygon.get((i + 3) % polygon.size);
+
+				float mx = x + (x2 - x) / 2;
+				float my = y + (y2 - y) / 2;
+				float nx = (y2 - y);
+				float ny = -(x2 - x);
+				if (clockwise) {
+					nx = -nx;
+					ny = -ny;
+				}
+				float l = 1 / (float)Math.sqrt(nx * nx + ny * ny);
+				nx *= l * 20;
+				ny *= l * 20;
+
+				shapes.line(mx, my, mx + nx, my + ny);
+			}
+		}
+		
+		// decomposition		
+		if (convexPolygons != null) {
+			for (int i = 0, n = convexPolygons.size; i < n; i++) {
+				if (colors.size <= i) {
+					colors.add(new Color(MathUtils.random(), MathUtils.random(), MathUtils.random(), 1));
+				}
+				shapes.setColor(colors.get(i));
+				shapes.polygon(convexPolygons.get(i).items, 0, convexPolygons.get(i).size);
+//				if (i == 4) break;
+			}
+		}
+
+		if (isCreatingPolygon) {
+			polygon.setSize(polygon.size - 2);
+		}	
+		shapes.end();
+		
+		polyBatcher.begin();
+		polyBatcher.enableBlending();
+		for (int i = 0; i < polygon.size; i+=2) {
+			float x = polygon.get(i);
+			float y = polygon.get(i + 1);
+			font.draw(polyBatcher, "" + (i >> 1), x, y);
+		}
+		polyBatcher.end();
+	}
+
+	private void triangulate () {
+		SutherlandHodgmanClipper.makeClockwise(polygon);
+		convexPolygons = decomposer.decompose(polygon);
+	}
+
+	public static void main (String[] args) {
+		LwjglApplicationConfiguration config = new LwjglApplicationConfiguration();
+		new LwjglApplication(new ConvexDecomposerTest(), config);
+	}
+}

+ 2 - 2
spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/SoftwareClippingTest.java

@@ -137,7 +137,7 @@ public class SoftwareClippingTest extends ApplicationAdapter {
 		// edge normals
 		shapes.setColor(Color.YELLOW);		
 		if (clippingPolygon.size > 2) {
-			boolean clockwise = SutherlandHodgmanClipper.counterClockwise(clippingPolygon);
+			boolean clockwise = SutherlandHodgmanClipper.isClockwise(clippingPolygon);
 			for (int i = 0; i < clippingPolygon.size; i += 2) {
 				float x = clippingPolygon.get(i);
 				float y = clippingPolygon.get(i + 1);
@@ -183,7 +183,7 @@ public class SoftwareClippingTest extends ApplicationAdapter {
 		
 		// must duplicate first vertex at end of polygon
 		// so we can avoid module/branch in clipping code
-		SutherlandHodgmanClipper.makeCounterClockwise(clippingPolygon);
+		SutherlandHodgmanClipper.makeClockwise(clippingPolygon);
 		clippingPolygon.add(clippingPolygon.get(0));
 		clippingPolygon.add(clippingPolygon.get(1));
 		

+ 2 - 2
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonRenderer.java

@@ -406,9 +406,9 @@ public class SkeletonRenderer {
 			int n = clip.getWorldVerticesLength();
 			float[] vertices = this.clippingArea.setSize(n);
 			clip.computeWorldVertices(slot, 0, n, vertices, 0, 2);
-			clippingAreaClockwise = SutherlandHodgmanClipper.counterClockwise(this.clippingArea);
+			clippingAreaClockwise = SutherlandHodgmanClipper.isClockwise(this.clippingArea);
 			if (!clippingAreaClockwise) {
-				SutherlandHodgmanClipper.makeCounterClockwise(clippingArea);
+				SutherlandHodgmanClipper.makeClockwise(clippingArea);
 			}
 			clippingArea.add(clippingArea.items[0]);
 			clippingArea.add(clippingArea.items[1]);

+ 261 - 0
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/utils/ConvexDecomposer.java

@@ -0,0 +1,261 @@
+
+package com.esotericsoftware.spine.utils;
+
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.FloatArray;
+import com.badlogic.gdx.utils.IntArray;
+import com.badlogic.gdx.utils.ShortArray;
+
+public class ConvexDecomposer {
+	static private final int CONCAVE = -1;
+	static private final int TANGENTIAL = 0;
+	static private final int CONVEX = 1;
+
+	private final ShortArray indicesArray = new ShortArray();
+	private short[] indices;
+	private float[] vertices;
+	private int vertexCount;
+	private final IntArray vertexTypes = new IntArray();
+	private final ShortArray triangles = new ShortArray();
+
+	public Array<FloatArray> decompose (FloatArray polygon) {
+		this.vertices = polygon.items;
+		int vertexCount = this.vertexCount = polygon.size / 2;
+
+		ShortArray indicesArray = this.indicesArray;
+		indicesArray.clear();
+		indicesArray.ensureCapacity(vertexCount);
+		indicesArray.size = vertexCount;
+		short[] indices = this.indices = indicesArray.items;
+		for (short i = 0; i < vertexCount; i++)
+			indices[i] = i;
+
+		IntArray vertexTypes = this.vertexTypes;
+		vertexTypes.clear();
+		vertexTypes.ensureCapacity(vertexCount);
+		for (int i = 0, n = vertexCount; i < n; ++i)
+			vertexTypes.add(classifyVertex(i));
+
+		// A polygon with n vertices has a triangulation of n-2 triangles.
+		ShortArray triangles = this.triangles;
+		triangles.clear();
+		triangles.ensureCapacity(Math.max(0, vertexCount - 2) * 4);
+
+		while (this.vertexCount > 3) {
+			int earTipIndex = findEarTip();
+			System.out.println("tip index: " + earTipIndex);
+			cutEarTip(earTipIndex);
+
+			// The type of the two vertices adjacent to the clipped vertex may have changed.
+			int previousIndex = previousIndex(earTipIndex);
+			int nextIndex = earTipIndex == vertexCount ? 0 : earTipIndex;
+			vertexTypes.set(previousIndex, classifyVertex(previousIndex));
+			vertexTypes.set(nextIndex, classifyVertex(nextIndex));
+		}
+
+		if (this.vertexCount == 3) {
+			triangles.add(indicesArray.get(2));
+			triangles.add(indicesArray.get(0));
+			triangles.add(indicesArray.get(1));
+		}
+
+		Array<FloatArray> polyResult = new Array<FloatArray>();
+		Array<ShortArray> polyIndicesResult = new Array<ShortArray>();
+
+		ShortArray polyIndices = new ShortArray();
+		FloatArray poly = new FloatArray();
+		int idx1 = triangles.get(0);
+		polyIndices.add(idx1);
+		idx1 <<= 1;
+		int idx2 = triangles.get(1);
+		polyIndices.add(idx2);
+		idx2 <<= 1;
+		int idx3 = triangles.get(2);
+		polyIndices.add(idx3);
+		idx3 <<= 1;
+		System.out.println("Triangle: " + idx1 / 2 + ", " + idx2 / 2 + ", " + idx3 / 2);
+		poly.add(polygon.get(idx1));
+		poly.add(polygon.get(idx1 + 1));
+		poly.add(polygon.get(idx2));
+		poly.add(polygon.get(idx2 + 1));
+		poly.add(polygon.get(idx3));
+		poly.add(polygon.get(idx3 + 1));
+		int lastWinding = lastWinding(poly);
+		int fanBaseIndex = idx1 >> 1;
+
+		for (int i = 3, n = triangles.size; i < n; i += 3) {
+			idx1 = triangles.get(i);
+			idx2 = triangles.get(i + 1);
+			idx3 = triangles.get(i + 2);
+			System.out.println("Triangle: " + idx1 + ", " + idx2 + ", " + idx3);
+
+			float x1 = polygon.get(idx1 * 2);
+			float y1 = polygon.get(idx1 * 2 + 1);
+			float x2 = polygon.get(idx2 * 2);
+			float y2 = polygon.get(idx2 * 2 + 1);
+			float x3 = polygon.get(idx3 * 2);
+			float y3 = polygon.get(idx3 * 2 + 1);
+
+			// if the base of the last triangle
+			// is the same as this triangle's base
+			// check if they form a convex polygon (triangle fan)
+			boolean merged = false;
+			if (fanBaseIndex == idx1) {
+				poly.add(x3);
+				poly.add(y3);
+				poly.add(poly.get(0));
+				poly.add(poly.get(1));
+				poly.add(poly.get(2));
+				poly.add(poly.get(3));
+				float winding = lastWinding(poly);
+				if (winding == lastWinding) {
+					poly.size -= 4;
+					polyIndices.add(idx3);
+					merged = true;
+				} else {
+					poly.size -= 6;
+				}
+			}
+
+			// otherwise make this triangle
+			// the new base
+			if (!merged) {
+				polyResult.add(poly);
+				polyIndicesResult.add(polyIndices);
+				poly = new FloatArray();
+				poly.add(x1);
+				poly.add(y1);
+				poly.add(x2);
+				poly.add(y2);
+				poly.add(x3);
+				poly.add(y3);
+				polyIndices = new ShortArray();
+				polyIndices.add(idx1);
+				polyIndices.add(idx2);
+				polyIndices.add(idx3);
+				lastWinding = lastWinding(poly);
+				fanBaseIndex = idx1;
+			}
+		}
+
+		if (poly.size > 0) {
+			polyResult.add(poly);
+			polyIndicesResult.add(polyIndices);
+		}
+
+		for (ShortArray pIndices : polyIndicesResult) {
+			System.out.println("Poly: " + pIndices.toString(","));
+		}
+
+		return polyResult;
+	}
+
+	private int lastWinding (FloatArray poly) {
+		float px = poly.get(poly.size - 5);
+		float py = poly.get(poly.size - 6);
+		float tx = poly.get(poly.size - 3);
+		float ty = poly.get(poly.size - 4);
+		float ux = poly.get(poly.size - 1);
+		float uy = poly.get(poly.size - 2);
+		float vx = tx - px;
+		float vy = ty - py;
+		return ux * vy - uy * vx + vx * py - px * vy >= 0 ? 1 : -1;
+	}
+
+	/** @return {@link #CONCAVE}, {@link #TANGENTIAL} or {@link #CONVEX} */
+	private int classifyVertex (int index) {
+		short[] indices = this.indices;
+		int previous = indices[previousIndex(index)] * 2;
+		int current = indices[index] * 2;
+		int next = indices[nextIndex(index)] * 2;
+		float[] vertices = this.vertices;
+		return computeSpannedAreaSign(vertices[previous], vertices[previous + 1], vertices[current], vertices[current + 1],
+			vertices[next], vertices[next + 1]);
+	}
+
+	private int findEarTip () {
+		int vertexCount = this.vertexCount;
+		for (int i = 0; i < vertexCount; i++)
+			if (isEarTip(i)) return i;
+
+		// Desperate mode: if no vertex is an ear tip, we are dealing with a degenerate polygon (e.g. nearly collinear).
+		// Note that the input was not necessarily degenerate, but we could have made it so by clipping some valid ears.
+
+		// Idea taken from Martin Held, "FIST: Fast industrial-strength triangulation of polygons", Algorithmica (1998),
+		// http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.115.291
+
+		// Return a convex or tangential vertex if one exists.
+		int[] vertexTypes = this.vertexTypes.items;
+		for (int i = 0; i < vertexCount; i++)
+			if (vertexTypes[i] != CONCAVE) return i;
+		return 0; // If all vertices are concave, just return the first one.
+	}
+
+	private boolean isEarTip (int earTipIndex) {
+		int[] vertexTypes = this.vertexTypes.items;
+		if (vertexTypes[earTipIndex] == CONCAVE) return false;
+
+		int previousIndex = previousIndex(earTipIndex);
+		int nextIndex = nextIndex(earTipIndex);
+		short[] indices = this.indices;
+		int p1 = indices[previousIndex] * 2;
+		int p2 = indices[earTipIndex] * 2;
+		int p3 = indices[nextIndex] * 2;
+		float[] vertices = this.vertices;
+		float p1x = vertices[p1], p1y = vertices[p1 + 1];
+		float p2x = vertices[p2], p2y = vertices[p2 + 1];
+		float p3x = vertices[p3], p3y = vertices[p3 + 1];
+
+		// Check if any point is inside the triangle formed by previous, current and next vertices.
+		// Only consider vertices that are not part of this triangle, or else we'll always find one inside.
+		for (int i = nextIndex(nextIndex); i != previousIndex; i = nextIndex(i)) {
+			// Concave vertices can obviously be inside the candidate ear, but so can tangential vertices
+			// if they coincide with one of the triangle's vertices.
+			if (vertexTypes[i] != CONVEX) {
+				int v = indices[i] * 2;
+				float vx = vertices[v];
+				float vy = vertices[v + 1];
+				// Because the polygon has clockwise winding order, the area sign will be positive if the point is strictly inside.
+				// It will be 0 on the edge, which we want to include as well.
+				// note: check the edge defined by p1->p3 first since this fails _far_ more then the other 2 checks.
+				if (computeSpannedAreaSign(p3x, p3y, p1x, p1y, vx, vy) >= 0) {
+					if (computeSpannedAreaSign(p1x, p1y, p2x, p2y, vx, vy) >= 0) {
+						if (computeSpannedAreaSign(p2x, p2y, p3x, p3y, vx, vy) >= 0) return false;
+					}
+				}
+			}
+		}
+		return true;
+	}
+
+	private void cutEarTip (int earTipIndex) {
+		short[] indices = this.indices;
+		ShortArray triangles = this.triangles;
+
+		short idx1 = indices[previousIndex(earTipIndex)];
+		short idx2 = indices[earTipIndex];
+		short idx3 = indices[nextIndex(earTipIndex)];
+		triangles.add(idx1);
+		triangles.add(idx2);
+		triangles.add(idx3);
+
+		indicesArray.removeIndex(earTipIndex);
+		vertexTypes.removeIndex(earTipIndex);
+		vertexCount--;
+	}
+
+	private int previousIndex (int index) {
+		return (index == 0 ? vertexCount : index) - 1;
+	}
+
+	private int nextIndex (int index) {
+		return (index + 1) % vertexCount;
+	}
+
+	static private int computeSpannedAreaSign (float p1x, float p1y, float p2x, float p2y, float p3x, float p3y) {
+		float area = p1x * (p3y - p2y);
+		area += p2x * (p1y - p3y);
+		area += p3x * (p2y - p1y);
+		return (int)Math.signum(area);
+	}
+}

+ 8 - 8
spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/utils/SutherlandHodgmanClipper.java

@@ -35,10 +35,10 @@ public class SutherlandHodgmanClipper {
 		final float[] clippingVertices = clippingArea.items;
 		final int clippingVerticesLength = clippingArea.size - 2;
 		for (int i = 0; i < clippingVerticesLength; i += 2) {
-			float edgeX2 = clippingVertices[i];
-			float edgeY2 = clippingVertices[i + 1];
-			float edgeX = clippingVertices[i + 2];
-			float edgeY = clippingVertices[i + 3];
+			float edgeX = clippingVertices[i];
+			float edgeY = clippingVertices[i + 1];
+			float edgeX2 = clippingVertices[i + 2];
+			float edgeY2 = clippingVertices[i + 3];
 
 			final float deltaX = edgeX - edgeX2;
 			final float deltaY = edgeY - edgeY2;
@@ -129,8 +129,8 @@ public class SutherlandHodgmanClipper {
 		return clipped;
 	}
 	
-	public static void makeCounterClockwise (FloatArray poly) {
-		if (counterClockwise(poly)) return;
+	public static void makeClockwise (FloatArray poly) {
+		if (isClockwise(poly)) return;
 		
 		int lastX = poly.size - 2;
 		final float[] polygon = poly.items;
@@ -145,8 +145,8 @@ public class SutherlandHodgmanClipper {
 		}
 	}
 
-	public static boolean counterClockwise (FloatArray poly) {
-		return area(poly) > 0;
+	public static boolean isClockwise (FloatArray poly) {
+		return area(poly) < 0;
 	}
 
 	public static float area (FloatArray poly) {