LibGDX Cut texture with an angle

Viewed 84

Suppose I have a Texture and I want to split it in half on a line rotated with a given angle.

enter image description here

In result I expect to get two Texture objects with the corresponding halfs of the original texture. Would be very nice if it could be done with some kind of transformation/masking so that I wouldn't have to store copies of two new textures at runtime.

Is this even possible?

I tried to work around Gdx.gl.glSsisors() but couldn't make it work as it requires screen coords to be passed as parameters. Also I tried to go with Pixmap but couldn't find anything that would indicate it's even possible.

1 Answers

This can be achieved by manually calculating two skewed quads and render those using a SpriteBatch.

Cutting a sprite along angle angle we get a directional vector representing the cut by

public void cut(float angle) {
    Vector2 d = (new Vector2(1.0f, 0.0f)).rotate(angle);

if we also define the four corners for the UV Mapping of the Texture, along with the center and one vector la going d away from the center and lb going -d

Vector2 c = new Vector2(0.5f, 0.5f);
Vector2 la = (new Vector2(d)).scl( 1.0f).add(c);
Vector2 lb = (new Vector2(d)).scl(-1.0f).add(c);

Vector2 tl = new Vector2(0, 1);
Vector2 tr = new Vector2(1, 1);
Vector2 bl = new Vector2(0, 0);
Vector2 br = new Vector2(1, 0);

we can then calculate the intersection of the cut

Vector2 i1 = new Vector2();
Vector2 i2 = new Vector2();

if (Intersector.intersectSegments(c, la, tl, tr, i1) || Intersector.intersectSegments(c, lb, tl, tr, i1))
    i2.set(1.0f - i1.x, 1.0f - i1.y);
else {
    if (Intersector.intersectSegments(c, la, tl, bl, i1) || Intersector.intersectSegments(c, lb, tl, bl, i1))
        i2.set(1.0f - i1.x, 1.0f - i1.y);
}

At this point we know that one half of the cut will consist of vertices i1, i2 and two of tl, tr, bl and br, so if we sort them by angle away from the cut and then take the first 4 from i1 we get the vertices required to build the skewed quad:

Vector2[] vertexList = new Vector2[] {
        tl, tr, bl, br, i1, i2
};

Array<VertexAngle> vas = new Array<>();
for (Vector2 v : vertexList) {
    Vector2 vd = (new Vector2(v)).sub(c);
    float a = d.angle(vd);
    VertexAngle va = new VertexAngle();
    va.v = v;
    va.a = a;
    vas.add(va);
}

vas.sort(new Comparator<VertexAngle>() {
    @Override
    public int compare(VertexAngle a, VertexAngle b) {
        return Float.compare(a.a, b.a);
    }
});

Array<Vector2> nv = new Array<>();
for (VertexAngle va : vas)
    nv.add(va.v);

int index = nv.indexOf(i1, true);

The a float array containing the vertex data for the draw call can be constructed and rendered with a call to draw on SpriteBatch.

For example:

enter image description here

Full source code for the above example is:

package com.bornander.sandbox;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
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.SpriteBatch;
import com.badlogic.gdx.math.Intersector;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.Array;

import java.util.Comparator;

public class SandboxGame extends ApplicationAdapter {
    OrthographicCamera camera;
    SpriteBatch batch;
    Texture texture;
    CutTexture cutTexture;

    public static class CutTexture
    {
        public static class VertexAngle {
            public Vector2 v;
            public float a;
        }

        public static class CutHalf {
            public float[] vertices = new float[4 * 5];

            public void translate(float x, float y) {
                for(int i = 0; i < vertices.length; i += 5) {
                    vertices[i + 0] += x;
                    vertices[i + 1] += y;
                }
            }
        }

        public Vector2 position = new Vector2();
        public Vector2 driftDirection = new Vector2();
        public float drift = 0.0f;
        public Texture source;
        public CutHalf halfA = new CutHalf();
        public CutHalf halfB = new CutHalf();

        public void cut(float angle) {
            Vector2 d = (new Vector2(1.0f, 0.0f)).rotate(angle);
            Vector2 c = new Vector2(0.5f, 0.5f);
            driftDirection.set(d).rotate(90.0f);
            Vector2 la = (new Vector2(d)).scl( 1.0f).add(c);
            Vector2 lb = (new Vector2(d)).scl(-1.0f).add(c);

            Vector2 tl = new Vector2(0, 1);
            Vector2 tr = new Vector2(1, 1);
            Vector2 bl = new Vector2(0, 0);
            Vector2 br = new Vector2(1, 0);

            Vector2 i1 = new Vector2();
            Vector2 i2 = new Vector2();

            if (Intersector.intersectSegments(c, la, tl, tr, i1) || Intersector.intersectSegments(c, lb, tl, tr, i1))
                i2.set(1.0f - i1.x, 1.0f - i1.y);
            else {
                if (Intersector.intersectSegments(c, la, tl, bl, i1) || Intersector.intersectSegments(c, lb, tl, bl, i1))
                    i2.set(1.0f - i1.x, 1.0f - i1.y);
            }

            Vector2[] vertexList = new Vector2[] {
                    tl, tr, bl, br, i1, i2
            };

            Array<VertexAngle> vas = new Array<>();
            for (Vector2 v : vertexList) {
                Vector2 vd = (new Vector2(v)).sub(c);
                float a = d.angle(vd);
                VertexAngle va = new VertexAngle();
                va.v = v;
                va.a = a;
                vas.add(va);
            }

            vas.sort(new Comparator<VertexAngle>() {
                @Override
                public int compare(VertexAngle a, VertexAngle b) {
                    return Float.compare(a.a, b.a);
                }
            });

            Array<Vector2> nv = new Array<>();
            for (VertexAngle va : vas)
                nv.add(va.v);

            int index = nv.indexOf(i1, true);
            int idx = 0;
            int lastIndex = 0;
            for(int j = 0; j < 4; ++j) {
                lastIndex = (index + j) % nv.size;
                Vector2 vertex = nv.get(lastIndex);
                float width = source.getWidth();
                float height = source.getWidth();
                float fx2 = position.x + width * vertex.x - width / 2.0f;
                float fy2 = position.y + height * vertex.y - height / 2.0f;

                halfA.vertices[idx++] = fx2;
                halfA.vertices[idx++] = fy2;
                halfA.vertices[idx++] = Color.WHITE_FLOAT_BITS;
                halfA.vertices[idx++] = vertex.x;
                halfA.vertices[idx++] = 1.0f - vertex.y;
            }

            idx = 0;
            for(int j = 0; j < 4; ++j) {
                Vector2 vertex = nv.get((lastIndex + j) % nv.size);
                float width = source.getWidth();
                float height = source.getWidth();
                float fx2 = position.x + width * vertex.x - width / 2.0f;
                float fy2 = position.y + height * vertex.y - height / 2.0f;

                halfB.vertices[idx++] = fx2;
                halfB.vertices[idx++] = fy2;
                halfB.vertices[idx++] = Color.WHITE_FLOAT_BITS;
                halfB.vertices[idx++] = vertex.x;
                halfB.vertices[idx++] = 1.0f - vertex.y;
            }
        }

        public void render(SpriteBatch batch) {

            float dx = driftDirection.x * drift;
            float dy = driftDirection.y * drift;

            halfA.translate(dx, dy);
            halfB.translate(-dx, -dy);

            batch.draw(source, halfA.vertices, 0, 20);
            batch.draw(source, halfB.vertices, 0, 20);

            halfA.translate(-dx, -dy);
            halfB.translate(dx, dy);
        }
    }

    @Override
    public void create () {
        float aspectRatio = (float)Gdx.graphics.getHeight()/(float)Gdx.graphics.getWidth();
        camera = new OrthographicCamera(800, 800 * aspectRatio);
        camera.position.set(camera.viewportWidth / 2.0f, camera.viewportHeight / 2.0f, 0.0f);
        batch = new SpriteBatch();
        texture = new Texture(Gdx.files.internal("badlogic.jpg"));

        Gdx.gl.glCullFace(0);
        cutTexture = new CutTexture();
        cutTexture.position.set(camera.viewportWidth / 2.0f, camera.viewportHeight / 2.0f);
        cutTexture.source = texture;
        cutTexture.cut(0);
    }

    float[] cutAngles = new float[] { 0.0f, -22.5f, -45.0f, -12.0f, -75.0f, -90.0f};
    int ai = 0;

    @Override
    public void render () {
        if (Gdx.input.isKeyJustPressed(Input.Keys.SPACE))
        {
            cutTexture.drift = 0.0f;
            cutTexture.cut(cutAngles[(ai++) % cutAngles.length]);
        }
        cutTexture.drift -= 64.0f * Gdx.graphics.getDeltaTime();
        Gdx.gl.glClearColor(0.6f, 0.6f, 1.0f, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
        Gdx.gl.glCullFace(GL20.GL_NONE);
        camera.update();
        batch.setProjectionMatrix(camera.combined);
        batch.begin();
        cutTexture.render(batch);
        batch.end();
    }
}
Related