From afd2a9559405933cc603660660945fd8c1776989 Mon Sep 17 00:00:00 2001 From: badlogic Date: Wed, 29 Mar 2017 17:19:53 +0200 Subject: [PATCH] [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) {