From b3ac8a7f728c6a525c87cca979c43e168dae67e9 Mon Sep 17 00:00:00 2001 From: badlogic Date: Mon, 27 Mar 2017 15:20:00 +0200 Subject: [PATCH] [libgdx] Added SutherlandHodgmanClipper and corresponding SoftwareClippingTest --- .../spine/SoftwareClippingTest.java | 159 ++++++++++++++++++ .../spine/utils/SutherlandHodgmanClipper.java | 129 ++++++++++++++ 2 files changed, 288 insertions(+) create mode 100644 spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/SoftwareClippingTest.java create mode 100644 spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/utils/SutherlandHodgmanClipper.java 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 new file mode 100644 index 000000000..f7f1d5cfd --- /dev/null +++ b/spine-libgdx/spine-libgdx-tests/src/com/esotericsoftware/spine/SoftwareClippingTest.java @@ -0,0 +1,159 @@ + +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.glutils.ShapeRenderer; +import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType; +import com.badlogic.gdx.math.Vector3; +import com.badlogic.gdx.utils.FloatArray; +import com.esotericsoftware.spine.utils.SutherlandHodgmanClipper; + +public class SoftwareClippingTest extends ApplicationAdapter { + OrthographicCamera sceneCamera; + ShapeRenderer shapes; + + float[] triangle = {100, 100, 300, 100, 200, 300}; + FloatArray clippingPolygon = new FloatArray(); + FloatArray clippedPolygon = new FloatArray(); + + boolean isCreatingClippingArea = false; + Vector3 tmp = new Vector3(); + SutherlandHodgmanClipper clipper; + + @Override + public void create () { + sceneCamera = new OrthographicCamera(); + shapes = new ShapeRenderer(); + clipper = new SutherlandHodgmanClipper(); + } + + @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 (!isCreatingClippingArea) { + clippingPolygon.clear(); + isCreatingClippingArea = true; + } + + clippingPolygon.add(tmp.x); + clippingPolygon.add(tmp.y); + + if (Gdx.input.isButtonPressed(Buttons.RIGHT)) { + isCreatingClippingArea = false; + clip(); + } + } + } + + private void renderScene () { + sceneCamera.update(); + shapes.setProjectionMatrix(sceneCamera.combined); + shapes.begin(ShapeType.Line); + + // triangle + shapes.setColor(Color.GREEN); + shapes.polygon(triangle); + + // clipped polygons + shapes.setColor(Color.RED); + if (isCreatingClippingArea) { + tmp.set(Gdx.input.getX(), Gdx.input.getY(), 0); + sceneCamera.unproject(tmp); + clippingPolygon.add(tmp.x); + clippingPolygon.add(tmp.y); + } + + switch (clippingPolygon.size) { + case 0: + break; + case 2: + shapes.end(); + shapes.begin(ShapeType.Point); + GL11.glPointSize(4); + shapes.point(clippingPolygon.get(0), clippingPolygon.get(1), 0); + shapes.end(); + shapes.begin(ShapeType.Line); + break; + case 4: + shapes.line(clippingPolygon.get(0), clippingPolygon.get(1), clippingPolygon.get(2), clippingPolygon.get(3)); + break; + default: + shapes.polygon(clippingPolygon.items, 0, clippingPolygon.size); + } + + // edge normals + shapes.setColor(Color.YELLOW); + if (clippingPolygon.size > 2) { + boolean clockwise = SutherlandHodgmanClipper.clockwise(clippingPolygon); + for (int i = 0; i < clippingPolygon.size; i += 2) { + float x = clippingPolygon.get(i); + float y = clippingPolygon.get(i + 1); + float x2 = clippingPolygon.get((i + 2) % clippingPolygon.size); + float y2 = clippingPolygon.get((i + 3) % clippingPolygon.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); + } + } + + if (isCreatingClippingArea) { + clippingPolygon.setSize(clippingPolygon.size - 2); + } + + // clipped polygon + shapes.setColor(Color.PINK); + if (clippedPolygon.size > 0) { + shapes.polygon(clippedPolygon.items, 0, clippedPolygon.size); + } + + shapes.end(); + } + + private void clip () { + FloatArray input = new FloatArray(); + input.addAll(triangle, 0, triangle.length); + clippedPolygon.clear(); + System.out.println("Clipped: " + (clipper.clip(input, clippingPolygon, clippedPolygon) != null)); + } + + public static void main (String[] args) { + LwjglApplicationConfiguration config = new LwjglApplicationConfiguration(); + new LwjglApplication(new SoftwareClippingTest(), config); + } +} 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 new file mode 100644 index 000000000..9d11c5c5a --- /dev/null +++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/utils/SutherlandHodgmanClipper.java @@ -0,0 +1,129 @@ + +package com.esotericsoftware.spine.utils; + +import com.badlogic.gdx.math.Intersector; +import com.badlogic.gdx.math.Vector2; +import com.badlogic.gdx.utils.FloatArray; + +public class SutherlandHodgmanClipper { + Vector2 tmp = new Vector2(); + + public FloatArray clip (FloatArray input, FloatArray clippingArea, FloatArray output) { + boolean isClockwise = clockwise(clippingArea); + + FloatArray originalOutput = output; + boolean clipped = false; + + for (int i = 0; i < clippingArea.size; i += 2) { + float edgeX = clippingArea.items[i]; + float edgeY = clippingArea.items[i + 1]; + float edgeX2 = clippingArea.items[(i + 2) % clippingArea.size]; + float edgeY2 = clippingArea.items[(i + 3) % clippingArea.size]; + + if (!isClockwise) { + float tmp = edgeX; + edgeX = edgeX2; + edgeX2 = tmp; + + tmp = edgeY; + edgeY = edgeY2; + edgeY2 = tmp; + } + + System.out.println("-- Edge " + i / 2 + ": (" + edgeX + ", " + edgeY + ")-(" + edgeX2 + ", " + edgeY2 + ")"); + + for (int j = 0; j < input.size; j += 2) { + float inputX = input.items[j % input.size]; + float inputY = input.items[(j + 1) % input.size]; + float inputX2 = input.items[(j + 2) % input.size]; + float inputY2 = input.items[(j + 3) % input.size]; + + System.out.println("\tinput " + j / 2 + ": (" + inputX + ", " + inputY + ")-(" + inputX2 + ", " + inputY2 + ")"); + + int side = Intersector.pointLineSide(edgeX2, edgeY2, edgeX, edgeY, inputX, inputY); + int side2 = Intersector.pointLineSide(edgeX2, edgeY2, edgeX, edgeY, inputX2, inputY2); + System.out.println("\tv1: " + (side < 0 ? "outside" : "inside" ) + ", v2: " + (side2 < 0 ? "outside" : "inside")); + + // v1 inside, v2 inside + if (side >= 0 && side2 >= 0) { + output.add(inputX2); + output.add(inputY2); + } + // v1 inside, v2 outside + else if (side >= 0 && side2 < 0) { + if (!Intersector.intersectLines(edgeX, edgeY, edgeX2, edgeY2, inputX, inputY, inputX2, inputY2, tmp)) { + throw new RuntimeException("Lines should intersect, but didn't"); + } + output.add(tmp.x); + output.add(tmp.y); + clipped = true; + } + // v1 outside, v2 outside + else if (side < 0 && side2 < 0) { + // no output + clipped = true; + } + // v1 outside, v2 inside + else if (side < 0 && side2 >= 0) { + if (!Intersector.intersectLines(edgeX, edgeY, edgeX2, edgeY2, inputX, inputY, inputX2, inputY2, tmp)) { + throw new RuntimeException("Lines should intersect, but didn't"); + } + output.add(tmp.x); + output.add(tmp.y); + output.add(inputX2); + output.add(inputY2); + clipped = true; + } + } + + if (i < clippingArea.size - 2) { + FloatArray tmp = output; + output = input; + output.clear(); + input = tmp; + } + } + + if (originalOutput != output) { + originalOutput.clear(); + originalOutput.addAll(output.items, 0, output.size); + } + + return clipped ? output : null; + } + + private int pointLineSide(float lineX, float lineY, float lineX2, float lineY2, float pointX, float pointY) { + return (int)Math.signum((lineX2 - lineX) * (pointY - lineY) - (lineY2 - lineY) * (pointX - lineX)); + } + + public static boolean intersectLines (float x1, float y1, float x2, float y2, float x3, float y3, float x4, float y4, + Vector2 intersection) { + float d = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1); + if (d == 0) return false; + + if (intersection != null) { + float ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / d; + intersection.set(x1 + (x2 - x1) * ua, y1 + (y2 - y1) * ua); + } + return true; + } + + public static boolean clockwise (FloatArray poly) { + return area(poly) < 0; + } + + public static float area (FloatArray poly) { + float area = 0; + + for (int i = 0; i < poly.size; i += 2) { + float x = poly.items[i]; + float y = poly.items[i + 1]; + float x2 = poly.items[(i + 2) % poly.size]; + float y2 = poly.items[(i + 3) % poly.size]; + + area += x * y2 - y * x2; + } + + return area; + } +}