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

This commit is contained in:
badlogic 2017-03-29 17:19:53 +02:00
parent dc53f8d293
commit afd2a95594
5 changed files with 457 additions and 12 deletions

View File

@ -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);
}
}

View File

@ -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));

View File

@ -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]);

View File

@ -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);
}
}

View File

@ -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) {