org.ams.prettypaint.PrettyPolygonBatch.java Source code

Java tutorial

Introduction

Here is the source code for org.ams.prettypaint.PrettyPolygonBatch.java

Source

/*
 *
 *  The MIT License (MIT)
 *
 *  Copyright (c) <2015> <Andreas Modahl>
 *
 *  Permission is hereby granted, free of charge, to any person obtaining a copy
 *  of this software and associated documentation files (the "Software"), to deal
 *  in the Software without restriction, including without limitation the rights
 *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 *  copies of the Software, and to permit persons to whom the Software is
 *  furnished to do so, subject to the following conditions:
 *
 *  The above copyright notice and this permission notice shall be included in
 *  all copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 *  THE SOFTWARE.
 *
 */

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */
package org.ams.prettypaint;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.*;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.PolygonRegion;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.graphics.glutils.ShaderProgram;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.math.*;
import com.badlogic.gdx.utils.Array;

/**
 * Use together with {@link OutlinePolygon]s and {@link TexturePolygon }}s.
 * The batch accumulates data and flushes it to the gpu in large portions,
 * this is faster than sending smaller portions of data more often.
 */
public class PrettyPolygonBatch {

    /**
     * The frustum is set every time one of the begin methods are called.
     * The Polygons then check if they overlap with this rectangle before drawing.
     */
    public final Rectangle frustum = new Rectangle();

    /**
     * When enough data is accumulated or another texture is given in a draw call
     * all the data is then handed over to this mesh. The mesh then sends the data
     * to the gpu for drawing.
     */
    private final Mesh mesh;

    /**
     * This number is currently very large, i hope to learn some tricks so that
     * i can get by with less data per vertex.
     */
    private final int dataPerVertex = 2 + 1 + 2 + 2 + 2;

    /** Max data before we must flush. */
    private final int maxData = 512 * 64;

    /** Data from the draw calls are accumulated in this array. */
    private final float[] data = new float[maxData];

    private final Vector2 tempVector = new Vector2();

    private final Color tempColor = new Color();

    /** I save the worldview for debugging, */
    protected Matrix4 worldView = new Matrix4();

    private ShaderProgram shaderProgram;
    private boolean drawFrustum = false;

    /** How much data currently stored in {@link #data}. */
    private int dataCount = 0;

    public boolean isStarted = false;
    private Texture lastTexture = null;

    /** Draws lines and shapes used for debugging. */
    private ShapeRenderer shapeRenderer;

    /** Font for debugging. */
    private BitmapFont debugFont;

    /** Draws text used for debugging. */
    private SpriteBatch debugSpriteBatch;

    /** Used to inspect the frustum culling. */
    private float frustumScale = 1f;

    private DebugRenderer debugRenderer;

    private Array<Color> debugColorsTaken = new Array<Color>();

    /**
     * When other classes want to draw something for debugging purposes they can add
     * a {@link DebugRenderer} to this array. These will be drawn when you call {@link #end()}.
     */
    protected Array<DebugRenderer> debugRendererArray = new Array<DebugRenderer>(true, 4, DebugRenderer.class);

    private OrthographicCamera debugCamera;

    /**
     * For every frame: call one of the begin methods, then pass this batch to a {@link PrettyPolygon}s draw method. After you have done
     * that with all your Polygons you call {@link #end()};
     */
    public PrettyPolygonBatch() {

        debugRenderer = new DebugRenderer() {

            @Override
            void draw(ShapeRenderer shapeRenderer) {
                drawFrustum(shapeRenderer, debugColors.first().color);
            }

            @Override
            void update() {
                super.update();
                boolean enabled = drawFrustum;
                boolean change = enabled != this.enabled;
                if (!change)
                    return;
                this.enabled = enabled;

                debugColors.clear();
                if (drawFrustum) {
                    debugColors.add(new DebugColor(Color.CYAN, "Frustum"));
                }

                if (!enabled) {
                    debugRendererArray.removeValue(this, true);
                }
            }
        };

        shaderProgram = new ShaderProgram(Shader.vertexShader, Shader.fragmentShader);

        if (!shaderProgram.isCompiled())
            Gdx.app.log("PrettyPolygonBatch", "PrettyPolygonBatch shader-program not compiled!");

        Mesh.VertexDataType vertexDataType = Mesh.VertexDataType.VertexBufferObject;
        if (Gdx.gl30 != null) {
            vertexDataType = Mesh.VertexDataType.VertexBufferObjectWithVAO;
        }

        mesh = new Mesh(vertexDataType, true, maxData, 0, Shader.Attribute.position.vertexAttribute, // position of vertex
                Shader.Attribute.colorOrJustOpacity.vertexAttribute, // are packed into one float
                Shader.Attribute.positionInRegion.vertexAttribute, // texture translation, alpha values
                Shader.Attribute.regionPositionOrBoldness.vertexAttribute, // bottom left of textureRegionName in texture, alpha values
                Shader.Attribute.regionSizeAndShaderChooser.vertexAttribute // size of textureRegionName(region must be square), alpha value. (<-0.5 when outline)
        );
    }

    /**
     * Dispose the resources associated with this batch. Must be called when the batch is no longer used.
     */
    public void dispose() {
        if (shaderProgram != null)
            shaderProgram.dispose();
        if (mesh != null)
            mesh.dispose();
        if (shapeRenderer != null)
            shapeRenderer.dispose();
        if (debugFont != null)
            debugFont.dispose();
        if (debugSpriteBatch != null)
            debugSpriteBatch.dispose();
    }

    /**
     * You must call begin before you can use this batch to draw stuff.
     *
     * @param camera the camera tells me where to draw things, it also defines
     *               the frustum, everything that is outside the frustum is not drawn.
     */
    public void begin(OrthographicCamera camera) {

        this.worldView.set(camera.combined);

        frustum.x = camera.position.x - camera.zoom * camera.viewportWidth / 2f;
        frustum.y = camera.position.y - camera.zoom * camera.viewportHeight / 2f;
        frustum.width = camera.viewportWidth * camera.zoom;
        frustum.height = camera.viewportHeight * camera.zoom;

        begin();

    }

    /**
     * You must call begin before you can use this batch to draw stuff.
     *
     * @param worldView this matrix tells me where to draw things.
     * @param frustum   everything that is outside the frustum is not drawn.
     */
    public void begin(Matrix4 worldView, Rectangle frustum) {
        this.frustum.set(frustum);
        this.worldView.set(worldView);

        begin();

    }

    private void begin() {
        isStarted = true;

        debugRenderer.queueIfEnabled(this);

        shaderProgram.begin();
        shaderProgram.setUniformMatrix("u_worldView", this.worldView);

        if (frustumScale != 1f) {
            float addX = this.frustum.width * (frustumScale - 1);
            float addY = this.frustum.height * (frustumScale - 1);

            this.frustum.x -= addX * 0.5f;
            this.frustum.y -= addY * 0.5f;
            this.frustum.width += addX;
            this.frustum.height += addY;

        }
    }

    // TODO Comment
    public void end() {
        isStarted = false;
        flush();
        shaderProgram.end();
        doAllDebugDrawing();

    }

    /**
     * For debugging.
     * This can be used to verify that the frustum culling is working.
     *
     * @return the frustum scaling factor.
     */
    public float getFrustumScale() {
        return frustumScale;
    }

    /**
     * For debugging.
     * This can be used to verify that the frustum culling is working.
     *
     * @param frustumScale the new frustum scaling factor.
     */
    public void setFrustumScale(float frustumScale) {
        this.frustumScale = frustumScale;
    }

    /**
     * For debugging.
     * This can be used to verify that the frustum culling is working.
     * See also {@link #setFrustumScale(float)}.
     *
     * @param drawFrustum whether to draw an outline showing the frustum.
     */
    public void setDrawFrustum(boolean drawFrustum) {
        this.drawFrustum = drawFrustum;
        debugRenderer.update();
    }

    /**
     * For debugging.
     * This can be used to verify that the frustum culling is working.
     *
     * @return whether the frustum is drawn.
     */
    public boolean isDrawingFrustum() {
        return drawFrustum;
    }

    // TODO Comment
    protected void drawTexture(PolygonRegion region, float pos_x, float pos_y, float width, float height,
            float scaleX, float scaleY, float rotation, float texture_pos_x, float texture_pos_y, float tex_trans_x,
            float tex_trans_y, float region_width, float region_height, float packedColor) {
        if (!isStarted)
            throw new RuntimeException("You must call begin() before calling this method.");

        final float[] regionVertices = region.getVertices();
        final int regionVerticesLength = regionVertices.length;
        final TextureRegion textureRegion = region.getRegion();

        Texture texture = textureRegion.getTexture();

        float vertexCount = 3f;

        float degenerateVertexCount = 2f;

        float totalData = dataPerVertex * (vertexCount + degenerateVertexCount);

        if (texture != lastTexture) {
            flush();
            lastTexture = texture;
        } else if (dataCount + totalData > maxData) {
            flush();
        }

        final float[] textureCoordinates = region.getTextureCoords();

        final float worldOriginX = pos_x;
        final float worldOriginY = pos_y;
        final float sX = width / textureRegion.getRegionWidth();
        final float sY = height / textureRegion.getRegionHeight();
        final float cos = MathUtils.cos(rotation);
        final float sin = MathUtils.sin(rotation);

        float fx, fy;

        int i = 0;
        {
            fx = (regionVertices[i] * sX) * scaleX;
            fy = (regionVertices[i + 1] * sY) * scaleY;

            data[dataCount++] = cos * fx - sin * fy + worldOriginX;
            data[dataCount++] = sin * fx + cos * fy + worldOriginY;
            data[dataCount++] = packedColor;

            data[dataCount++] = textureCoordinates[i] + tex_trans_x;
            data[dataCount++] = textureCoordinates[i + 1] + tex_trans_y;

            data[dataCount++] = texture_pos_x;
            data[dataCount++] = texture_pos_y;

            data[dataCount++] = region_width;
            data[dataCount++] = region_height;

        }

        for (; i < regionVerticesLength; i += 2) {
            fx = (regionVertices[i] * sX) * scaleX;
            fy = (regionVertices[i + 1] * sY) * scaleY;

            data[dataCount++] = cos * fx - sin * fy + worldOriginX;
            data[dataCount++] = sin * fx + cos * fy + worldOriginY;
            data[dataCount++] = packedColor;

            data[dataCount++] = textureCoordinates[i] + tex_trans_x;
            data[dataCount++] = textureCoordinates[i + 1] + tex_trans_y;

            data[dataCount++] = texture_pos_x;
            data[dataCount++] = texture_pos_y;

            data[dataCount++] = region_width;
            data[dataCount++] = region_height;

        }

        {
            i -= 2;
            fx = (regionVertices[i] * sX) * scaleX;
            fy = (regionVertices[i + 1] * sY) * scaleY;

            data[dataCount++] = cos * fx - sin * fy + worldOriginX;
            data[dataCount++] = sin * fx + cos * fy + worldOriginY;
            data[dataCount++] = packedColor;

            data[dataCount++] = textureCoordinates[i] + tex_trans_x;
            data[dataCount++] = textureCoordinates[i + 1] + tex_trans_y;

            data[dataCount++] = texture_pos_x;
            data[dataCount++] = texture_pos_y;

            data[dataCount++] = region_width;
            data[dataCount++] = region_height;
        }
    }

    /**
     * Used by OutlinePolygon to draw triangle strips. These triangle strips
     * form anti-aliased polygon outlines.
     *
     * @param stripVertices
     * @param inside
     * @param begin         the index in {@code vertexData} to begin at(inclusive)
     * @param end           the index in {@code vertexData} to stop at(not inclusive)
     * @param color         the color of the outline
     * @param scale         how much to scale the outline
     * @param angleRad      how much to rotate before drawing
     * @param translation_x how much to translate the outline horizontally
     * @param translation_y how much to translate the outline vertically
     * @param weight        the weight of the outline(higher gives bolder edges)
     */
    protected void drawOutline(Array<OutlinePolygon.StripVertex> stripVertices, boolean closed, boolean inside,
            int begin, int end, int total, Color color, float scale, float angleRad, float translation_x,
            float translation_y, float weight) {
        if (!isStarted)
            throw new RuntimeException("You must call begin() before calling this method.");

        // this color is used for all the user vertices
        float colorAsFloatBits = color.toFloatBits();

        tempColor.set(color.r, color.g, color.b, 0f);
        // this color is used for all the aux vertices
        float colorInvisibleAsFloatBits = tempColor.toFloatBits();

        Vector2 pos = tempVector;

        // fixing a problem i don't understand:
        if (!closed && end >= stripVertices.size) {
            end = stripVertices.size;
        }

        int amountOfDataThisTime = (2 + total) * dataPerVertex;
        if (dataCount + amountOfDataThisTime > maxData) {
            flush();
        }

        {

            OutlinePolygon.StripVertex stripVertex = stripVertices.items[begin];
            Array<Float> vertexData = inside ? stripVertex.insideVertexData : stripVertex.outsideVertexData;

            pos.x = vertexData.items[0] * scale;
            pos.y = vertexData.items[1] * scale;
            pos.rotateRad(angleRad);
            pos.x += translation_x;
            pos.y += translation_y;

            dataCount = setOutlineVertexData(pos.x, pos.y, colorInvisibleAsFloatBits, dataCount, weight);
        }

        for (int i = begin; i < end; i++) {

            int k = i % stripVertices.size;

            OutlinePolygon.StripVertex stripVertex = stripVertices.items[k];
            Array<Float> vertexData = inside ? stripVertex.insideVertexData : stripVertex.outsideVertexData;

            // fixing a problem i don't understand:
            int n = i >= end - 1 ? 4 : vertexData.size;
            if (!closed && i >= stripVertices.size - 1 && (end - begin) > 1)
                n = vertexData.size;

            for (int j = 0; j < n;) {
                pos.x = vertexData.items[j++] * scale;
                pos.y = vertexData.items[j++] * scale;
                pos.rotateRad(angleRad);
                pos.x += translation_x;
                pos.y += translation_y;

                float _colorAsFloatBits = getColor(vertexData.items[j++], colorAsFloatBits,
                        colorInvisibleAsFloatBits);

                dataCount = setOutlineVertexData(pos.x, pos.y, _colorAsFloatBits, dataCount, weight);
            }
        }

        { // degenerate in order to travel from the previous vertex without drawing anything

            dataCount = setOutlineVertexData(pos.x, pos.y, colorInvisibleAsFloatBits, dataCount, weight);
        }

    }

    /** Append the information about this vertex to the data array. */
    private int setOutlineVertexData(float x, float y, float colorAsFloatBits, int dataCount, float weight) {
        // so much overhead :(
        data[dataCount++] = x;
        data[dataCount++] = y;
        data[dataCount++] = colorAsFloatBits;
        data[dataCount++] = 0f;
        data[dataCount++] = 0f;
        data[dataCount++] = weight;
        data[dataCount++] = 0f;
        data[dataCount++] = -1f;
        data[dataCount++] = 0f;

        return dataCount;
    }

    /**
     * When drawing aux vertices the color should be the same as the color of the user vertex it belongs to, except
     * for the opacity which should be 0.
     */
    private float getColor(float VERTEX_TYPE, float colorAsFloatBits, float colorInvisibleAsFloatBits) {
        if (VERTEX_TYPE == OutlinePolygon.VERTEX_TYPE_USER)
            return colorAsFloatBits;
        return colorInvisibleAsFloatBits;
    }

    /** Send all the accumulated data to the gpu. */
    private void flush() {
        mesh.setVertices(data, 0, dataCount);

        Gdx.gl.glEnable(GL20.GL_BLEND);

        if (lastTexture != null)
            lastTexture.bind();
        mesh.render(shaderProgram, GL20.GL_TRIANGLE_STRIP, 0, dataCount / dataPerVertex);

        dataCount = 0;

    }

    private void doAllDebugDrawing() {
        if (debugRendererArray.size == 0)
            return;

        if (shapeRenderer == null) {
            shapeRenderer = new ShapeRenderer();
            shapeRenderer.setAutoShapeType(true);
        }

        if (debugSpriteBatch == null) {
            debugSpriteBatch = new SpriteBatch();
        }

        if (debugFont == null) {
            debugFont = new BitmapFont();
        }

        if (debugCamera == null) {
            debugCamera = new OrthographicCamera();
        }

        shapeRenderer.begin();

        // draw the debug stuff
        shapeRenderer.setProjectionMatrix(worldView);
        long now = System.currentTimeMillis();
        for (int i = debugRendererArray.size - 1; i >= 0; i--) {
            DebugRenderer debugRenderer = debugRendererArray.items[i];

            debugRenderer.draw(shapeRenderer);

            // if it is more than one second since the last time
            // the owner of this debugRenderer did any normal drawing
            // then we stop the debug rendering of it

            if (debugRenderer.owner != null && debugRenderer.owner.getTimeOfLastDrawCall() + 1000 < now) {
                debugRendererArray.removeIndex(i);
            }
        }

        // draw short colored lines that will be beside some text explaining what the color of the line means
        debugCamera.setToOrtho(false, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
        debugCamera.position.set(Gdx.graphics.getWidth() * 0.5f, Gdx.graphics.getHeight() * -0.5f + 10,
                debugCamera.position.z);

        debugCamera.update();
        shapeRenderer.setProjectionMatrix(debugCamera.combined);

        debugColorsTaken.clear();

        float verticalSpacing = 20;

        int n = 0;
        for (DebugRenderer debugRenderer : debugRendererArray) {
            Array<DebugRenderer.DebugColor> colors = debugRenderer.getDebugColors();

            for (DebugRenderer.DebugColor debugColor : colors) {
                if (debugColorsTaken.contains(debugColor.color, false))
                    continue;
                debugColorsTaken.add(debugColor.color);

                float y = n++ * -verticalSpacing - 5;
                shapeRenderer.setColor(debugColor.color);
                shapeRenderer.line(10, y, 25, y);
            }
        }

        shapeRenderer.end();

        debugSpriteBatch.begin();
        debugSpriteBatch.setProjectionMatrix(debugCamera.combined);

        // draw explanatory text
        n = 0;
        for (DebugRenderer debugRenderer : debugRendererArray) {
            Array<DebugRenderer.DebugColor> colors = debugRenderer.getDebugColors();

            for (DebugRenderer.DebugColor debugColor : colors) {
                if (!debugColorsTaken.contains(debugColor.color, true))
                    continue;

                drawText(debugColor.charSequence, 30, n++ * -verticalSpacing);
            }
        }

        debugSpriteBatch.end();

    }

    private void drawText(CharSequence text, float x, float y) {
        debugFont.draw(debugSpriteBatch, text, x, y);
    }

    private void drawFrustum(ShapeRenderer shapeRenderer, Color color) {
        shapeRenderer.set(ShapeRenderer.ShapeType.Line);
        shapeRenderer.setColor(color);
        shapeRenderer.rect(frustum.x, frustum.y, frustum.width, frustum.height);
    }
}