From dc53f8d29314757946adfabad9b95e9ceb728c4e Mon Sep 17 00:00:00 2001 From: badlogic Date: Wed, 29 Mar 2017 14:30:11 +0200 Subject: [PATCH 1/4] [libgdx] Clipper assumes counter clockwise order, easier to adapt decomposition algorithm that way --- .../spine/SoftwareClippingTest.java | 4 ++-- .../esotericsoftware/spine/SkeletonRenderer.java | 4 ++-- .../spine/utils/SutherlandHodgmanClipper.java | 16 ++++++++-------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/SoftwareClippingTest.java b/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/SoftwareClippingTest.java index 9c16e3668..db723e0d1 100644 --- a/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/SoftwareClippingTest.java +++ b/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.clockwise(clippingPolygon); + boolean clockwise = SutherlandHodgmanClipper.counterClockwise(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.makeClockwise(clippingPolygon); + SutherlandHodgmanClipper.makeCounterClockwise(clippingPolygon); clippingPolygon.add(clippingPolygon.get(0)); clippingPolygon.add(clippingPolygon.get(1)); diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonRenderer.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonRenderer.java index fc85551fe..0cf6a9761 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonRenderer.java +++ b/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.clockwise(this.clippingArea); + clippingAreaClockwise = SutherlandHodgmanClipper.counterClockwise(this.clippingArea); if (!clippingAreaClockwise) { - SutherlandHodgmanClipper.makeClockwise(clippingArea); + SutherlandHodgmanClipper.makeCounterClockwise(clippingArea); } clippingArea.add(clippingArea.items[0]); clippingArea.add(clippingArea.items[1]); diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/utils/SutherlandHodgmanClipper.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/utils/SutherlandHodgmanClipper.java index b4ec53751..8e55f92cc 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/utils/SutherlandHodgmanClipper.java +++ b/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 edgeX = clippingVertices[i]; - float edgeY = clippingVertices[i + 1]; - float edgeX2 = clippingVertices[i + 2]; - float edgeY2 = clippingVertices[i + 3]; + float edgeX2 = clippingVertices[i]; + float edgeY2 = clippingVertices[i + 1]; + float edgeX = clippingVertices[i + 2]; + float edgeY = 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 makeClockwise (FloatArray poly) { - if (clockwise(poly)) return; + public static void makeCounterClockwise (FloatArray poly) { + if (counterClockwise(poly)) return; int lastX = poly.size - 2; final float[] polygon = poly.items; @@ -145,8 +145,8 @@ public class SutherlandHodgmanClipper { } } - public static boolean clockwise (FloatArray poly) { - return area(poly) < 0; + public static boolean counterClockwise (FloatArray poly) { + return area(poly) > 0; } public static float area (FloatArray poly) { From afd2a9559405933cc603660660945fd8c1776989 Mon Sep 17 00:00:00 2001 From: badlogic Date: Wed, 29 Mar 2017 17:19:53 +0200 Subject: [PATCH 2/4] [libgdx] Added first iteration of convex decomposer. Needs testing.2 --- .../spine/ConvexDecomposerTest.java | 184 ++++++++++++ .../spine/SoftwareClippingTest.java | 4 +- .../spine/SkeletonRenderer.java | 4 +- .../spine/utils/ConvexDecomposer.java | 261 ++++++++++++++++++ .../spine/utils/SutherlandHodgmanClipper.java | 16 +- 5 files changed, 457 insertions(+), 12 deletions(-) create mode 100644 spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/ConvexDecomposerTest.java create mode 100644 spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/utils/ConvexDecomposer.java diff --git a/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/ConvexDecomposerTest.java b/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/ConvexDecomposerTest.java new file mode 100644 index 000000000..74f547ba5 --- /dev/null +++ b/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 convexPolygons = new Array(); + boolean isCreatingPolygon = false; + Vector3 tmp = new Vector3(); + Array colors = new Array(); + 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); + } +} diff --git a/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/SoftwareClippingTest.java b/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/SoftwareClippingTest.java index db723e0d1..f9073ff8b 100644 --- a/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/SoftwareClippingTest.java +++ b/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)); diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonRenderer.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonRenderer.java index 0cf6a9761..7cf693873 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonRenderer.java +++ b/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]); diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/utils/ConvexDecomposer.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/utils/ConvexDecomposer.java new file mode 100644 index 000000000..a4f050615 --- /dev/null +++ b/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 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 polyResult = new Array(); + Array polyIndicesResult = new Array(); + + 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); + } +} diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/utils/SutherlandHodgmanClipper.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/utils/SutherlandHodgmanClipper.java index 8e55f92cc..892751ba3 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/utils/SutherlandHodgmanClipper.java +++ b/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) { From 61d5a3de5ad41d5b24e90019668235ba0a2c4854 Mon Sep 17 00:00:00 2001 From: badlogic Date: Thu, 30 Mar 2017 10:55:11 +0200 Subject: [PATCH 3/4] [libgdx] Working version of convex decomposer, can improve result by merging left over triangles at end of decomposition --- .../spine/ConvexDecomposerTest.java | 20 ++++-- .../spine/utils/ConvexDecomposer.java | 71 +++++++++---------- 2 files changed, 50 insertions(+), 41 deletions(-) diff --git a/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/ConvexDecomposerTest.java b/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/ConvexDecomposerTest.java index 74f547ba5..040c121ec 100644 --- a/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/ConvexDecomposerTest.java +++ b/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/ConvexDecomposerTest.java @@ -43,6 +43,11 @@ public class ConvexDecomposerTest extends ApplicationAdapter { polyBatcher = new PolygonSpriteBatch(); image = new Texture("skin/skin.png"); font = new BitmapFont(); + + // float[] v = { 87, 288, 217, 371, 456, 361, 539, 175, 304, 194, 392, 290, 193, 214, 123, 15, 14, 137 }; + float[] v = { 336, 153, 207, 184, 364, 333, 529, 326, 584, 130, 438, 224 }; + polygon.addAll(v); + triangulate(); } @Override @@ -70,11 +75,17 @@ public class ConvexDecomposerTest extends ApplicationAdapter { isCreatingPolygon = true; } - polygon.add(tmp.x); - polygon.add(tmp.y); + polygon.add((int)tmp.x); + polygon.add((int)tmp.y); if (Gdx.input.isButtonPressed(Buttons.RIGHT)) { isCreatingPolygon = false; + System.out.print("float[] v = { "); + for (int i = 0; i < polygon.size; i++) { + System.out.print(polygon.get(i)); + if (i != polygon.size - 1) System.out.print(", "); + } + System.out.println("};"); triangulate(); } } @@ -133,7 +144,7 @@ public class ConvexDecomposerTest extends ApplicationAdapter { float my = y + (y2 - y) / 2; float nx = (y2 - y); float ny = -(x2 - x); - if (clockwise) { + if (!clockwise) { nx = -nx; ny = -ny; } @@ -167,8 +178,9 @@ public class ConvexDecomposerTest extends ApplicationAdapter { 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); + font.draw(polyBatcher, "" + (i >> 1), x, y); // + ", " + x + ", " + y, x, y); } + font.draw(polyBatcher, Gdx.input.getX() + ", " + (Gdx.graphics.getHeight() - Gdx.input.getY()), 0, 20); polyBatcher.end(); } diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/utils/ConvexDecomposer.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/utils/ConvexDecomposer.java index a4f050615..7fd21b916 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/utils/ConvexDecomposer.java +++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/utils/ConvexDecomposer.java @@ -74,14 +74,22 @@ public class ConvexDecomposer { 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; + + float x1 = polygon.get(idx1); + float y1 = polygon.get(idx1 + 1); + float x2 = polygon.get(idx2); + float y2 = polygon.get(idx2 + 1); + float x3 = polygon.get(idx3); + float y3 = polygon.get(idx3 + 1); + + poly.add(x1); + poly.add(y1); + poly.add(x2); + poly.add(y2); + poly.add(x3); + poly.add(y3); + int lastWinding = winding(x1, y1, x2, y2, x3, y3); + int fanBaseIndex = idx1 >> 1; for (int i = 3, n = triangles.size; i < n; i += 3) { idx1 = triangles.get(i); @@ -89,31 +97,26 @@ public class ConvexDecomposer { 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); + x1 = polygon.get(idx1 * 2); + y1 = polygon.get(idx1 * 2 + 1); + x2 = polygon.get(idx2 * 2); + y2 = polygon.get(idx2 * 2 + 1); + x3 = polygon.get(idx3 * 2); + 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; + if (fanBaseIndex == idx1) { + int o = poly.size - 4; + int winding1 = winding(poly.get(o), poly.get(o + 1), poly.get(o + 2), poly.get(o + 3), x3, y3); + int winding2 = winding(x3, y3, poly.get(0), poly.get(1), poly.get(2), poly.get(3)); + if (winding1 == lastWinding && winding2 == lastWinding) { + poly.add(x3); + poly.add(y3); polyIndices.add(idx3); merged = true; - } else { - poly.size -= 6; } } @@ -133,7 +136,7 @@ public class ConvexDecomposer { polyIndices.add(idx1); polyIndices.add(idx2); polyIndices.add(idx3); - lastWinding = lastWinding(poly); + lastWinding = winding(x1, y1, x2, y2, x3, y3); fanBaseIndex = idx1; } } @@ -150,16 +153,10 @@ public class ConvexDecomposer { 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; + public static int winding (float v1x, float v1y, float v2x, float v2y, float v3x, float v3y) { + float vx = v2x - v1x; + float vy = v2y - v1y; + return v3x * vy - v3y * vx + vx * v1y - v1x * vy >= 0 ? 1 : -1; } /** @return {@link #CONCAVE}, {@link #TANGENTIAL} or {@link #CONVEX} */ From 1617eae1312aca9a4b8e639dfa7da421fdaaebeb Mon Sep 17 00:00:00 2001 From: badlogic Date: Thu, 30 Mar 2017 11:40:19 +0200 Subject: [PATCH 4/4] [libgdx] Generating random concave polygons is hard. --- .../spine/ConvexDecomposerTest.java | 57 ++++++++++++++++++- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/ConvexDecomposerTest.java b/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/ConvexDecomposerTest.java index 040c121ec..1deff9086 100644 --- a/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/ConvexDecomposerTest.java +++ b/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/ConvexDecomposerTest.java @@ -6,6 +6,7 @@ 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.Input.Keys; import com.badlogic.gdx.backends.lwjgl.LwjglApplication; import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration; import com.badlogic.gdx.graphics.Color; @@ -16,7 +17,9 @@ 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.Intersector; import com.badlogic.gdx.math.MathUtils; +import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.math.Vector3; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.FloatArray; @@ -89,6 +92,57 @@ public class ConvexDecomposerTest extends ApplicationAdapter { triangulate(); } } + + if (Gdx.input.isKeyJustPressed(Keys.R)) { + long start = System.nanoTime(); + generateRandomPolygon(); + System.out.println("Took: " + (System.nanoTime() - start) / 1000000000.0f + " secs"); + System.out.print("float[] v = { "); + for (int i = 0; i < polygon.size; i++) { + System.out.print(polygon.get(i)); + if (i != polygon.size - 1) System.out.print(", "); + } + System.out.println("};"); +// triangulate(); + } + } + + private void generateRandomPolygon () { + polygon.clear(); + + int numVertices = 10; // MathUtils.random(3, 3); + for (int i = 0; i < numVertices; i++) { + float x = (float)(50 + Math.random() * (Gdx.graphics.getWidth() - 50)); + float y = (float)(50 + Math.random() * (Gdx.graphics.getHeight() - 50)); + + polygon.add(x); + polygon.add(y); + + if (selfIntersects(polygon)) { + polygon.size -= 2; + i--; + } + } + } + + private boolean selfIntersects(FloatArray polygon) { + Vector2 tmp = new Vector2(); + for(int i = 0, n = polygon.size; i <= n; i+=4) { + float x1 = polygon.get(i % n); + float y1 = polygon.get((i + 1) % n); + float x2 = polygon.get((i + 2) % n); + float y2 = polygon.get((i + 3) % n); + + for (int j = 0; j <= n; j+=4) { + if (j == i || j == i + 1) continue; + float x3 = polygon.get(j % n); + float y3 = polygon.get((j + 1) % n); + float x4 = polygon.get((j + 2) % n); + float y4 = polygon.get((j + 3) % n); + if (Intersector.intersectSegments(x1, y1, x2, y2, x3, y3, x4, y4, tmp)) return true; + } + } + return false; } private void renderScene () { @@ -152,7 +206,7 @@ public class ConvexDecomposerTest extends ApplicationAdapter { nx *= l * 20; ny *= l * 20; - shapes.line(mx, my, mx + nx, my + ny); +// shapes.line(mx, my, mx + nx, my + ny); } } @@ -164,7 +218,6 @@ public class ConvexDecomposerTest extends ApplicationAdapter { } shapes.setColor(colors.get(i)); shapes.polygon(convexPolygons.get(i).items, 0, convexPolygons.get(i).size); -// if (i == 4) break; } }