net.neilcsmith.praxis.video.opengl.internal.GLRenderer.java Source code

Java tutorial

Introduction

Here is the source code for net.neilcsmith.praxis.video.opengl.internal.GLRenderer.java

Source

/**
 * *****************************************************************************
 * Copyright 2013 See AUTHORS file.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 * ****************************************************************************
 */
package net.neilcsmith.praxis.video.opengl.internal;

import java.nio.IntBuffer;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.neilcsmith.praxis.video.opengl.internal.VertexAttributes.Usage;
import net.neilcsmith.praxis.video.render.Surface;
import net.neilcsmith.praxis.video.render.utils.PixelArrayCache;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import org.lwjgl.opengl.GL14;

/**
 * <p>
 *
 * @author mzechner
 */
// derived from SpriteBatch in libgdx
public class GLRenderer implements Disposable {

    private final static Logger LOGGER = Logger.getLogger(GLRenderer.class.getName());
    //    private final static Map<GLSurface, GLRenderer> renderers =
    //            new HashMap<GLSurface, GLRenderer>();
    //    private static GLRenderer active;
    private GLContext context;
    static final int VERTEX_SIZE = 2 + 1 + 2;
    static final int SPRITE_SIZE = 4 * VERTEX_SIZE;
    private Mesh mesh;
    private Mesh[] buffers;
    private Texture tex0 = null;
    private int idx = 0;
    private int currBufferIdx = 0;
    private final float[] vertices;
    private final Matrix4 transformMatrix = new Matrix4();
    private final Matrix4 projectionMatrix = new Matrix4();
    private final Matrix4 combinedMatrix = new Matrix4();
    private boolean blendingDisabled = false;
    private int blendSrcFunc = GL11.GL_ONE;
    private int blendDstFunc = GL11.GL_ZERO;
    private int blendSrcAlphaFunc = GL11.GL_ONE;
    private int blendDstAlphaFunc = GL11.GL_ZERO;
    private ShaderProgram shader;
    private float color = Color.WHITE.toFloatBits();
    private ShaderProgram customShader = null;
    private GLSurface surface;
    private Texture target;
    private boolean active;
    private IntBuffer scratchBuffer;
    private Texture clearTexture;
    private Texture blackTexture;
    //    private Texture whiteTexture;

    GLRenderer(GLContext context) {
        this(context, 1000, 1);
    }

    /**
     * <p> Constructs a new TextureRenderer. Sets the projection matrix to an
     * orthographic projection with y-axis point upwards, x-axis point to the
     * right and the origin being in the bottom left corner of the screen. The
     * projection will be pixel perfect with respect to the screen resolution.
     * </p>
     *
     * <p> The size parameter specifies the maximum size of a single batch in
     * number of sprites </p>
     *
     * @param size the batch size in number of sprites
     * @param buffers the number of buffers to use. only makes sense with VBOs.
     * This is an expert function.
     */
    private GLRenderer(GLContext context, int size, int buffers) {

        this.context = context;

        this.buffers = new Mesh[buffers];

        for (int i = 0; i < buffers; i++) {
            this.buffers[i] = new Mesh(false, size * 4, size * 6,
                    new VertexAttribute(Usage.Position, 2, ShaderProgram.POSITION_ATTRIBUTE),
                    new VertexAttribute(Usage.ColorPacked, 4, ShaderProgram.COLOR_ATTRIBUTE),
                    new VertexAttribute(Usage.TextureCoordinates, 2, ShaderProgram.TEXCOORD_ATTRIBUTE + "0"));
        }

        vertices = new float[size * SPRITE_SIZE];

        int len = size * 6;
        short[] indices = new short[len];
        short j = 0;
        for (int i = 0; i < len; i += 6, j += 4) {
            indices[i + 0] = (short) (j + 0);
            indices[i + 1] = (short) (j + 1);
            indices[i + 2] = (short) (j + 2);
            indices[i + 3] = (short) (j + 2);
            indices[i + 4] = (short) (j + 3);
            indices[i + 5] = (short) (j + 0);
        }
        for (int i = 0; i < buffers; i++) {
            this.buffers[i].setIndices(indices);
        }
        mesh = this.buffers[0];

        createShader();

        createFallbackTextures();

    }

    private void createShader() {
        String vertexShader = "attribute vec4 " + ShaderProgram.POSITION_ATTRIBUTE + ";\n" //
                + "attribute vec4 " + ShaderProgram.COLOR_ATTRIBUTE + ";\n" //
                + "attribute vec2 " + ShaderProgram.TEXCOORD_ATTRIBUTE + "0;\n" //
                + "uniform mat4 u_projectionViewMatrix;\n" //
                + "varying vec4 v_color;\n" //
                + "varying vec2 v_texCoords;\n" //
                + "\n" //
                + "void main()\n" //
                + "{\n" //
                + "   v_color = " + ShaderProgram.COLOR_ATTRIBUTE + ";\n" //
                + "   v_texCoords = " + ShaderProgram.TEXCOORD_ATTRIBUTE + "0;\n" //
                + "   gl_Position =  u_projectionViewMatrix * " + ShaderProgram.POSITION_ATTRIBUTE + ";\n" //
                + "}\n";
        String fragmentShader = "varying vec4 v_color;\n" //
                + "varying vec2 v_texCoords;\n" //
                + "uniform sampler2D u_texture;\n" //
                + "void main()\n"//
                + "{\n" //
                + "  gl_FragColor = v_color * texture2D(u_texture, v_texCoords);\n" //
                + "}";

        shader = new ShaderProgram(vertexShader, fragmentShader);
        shader.begin();
        if (shader.isCompiled() == false) {
            throw new IllegalArgumentException("couldn't compile shader: " + shader.getLog());
        }
    }

    private void createFallbackTextures() {
        GLSurfaceData sd = new GLSurfaceData(32, 32, true);
        sd.pixels = PixelArrayCache.acquire(32 * 32, false);
        sd.texture = new Texture(32, 32);
        int clear = 0;
        Arrays.fill(sd.pixels, clear);
        syncPixelsToTexture(sd);
        PixelArrayCache.release(sd.pixels);
        clearTexture = sd.texture;

        sd = new GLSurfaceData(32, 32, true);
        sd.pixels = PixelArrayCache.acquire(32 * 32, false);
        sd.texture = new Texture(32, 32);
        int black = 0xFF000000;
        Arrays.fill(sd.pixels, black);
        syncPixelsToTexture(sd);
        PixelArrayCache.release(sd.pixels);
        blackTexture = sd.texture;
    }

    public void clear() {
        activate();
        tex0 = null;
        idx = 0;
        if (surface == null) {
            LOGGER.finest("Clearing screen");
            GL11.glClearColor(0, 0, 0, 1);
            GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);
        } else {
            LOGGER.finest("Clearing surface");
            if (surface.hasAlpha()) {
                GL11.glClearColor(0, 0, 0, 0);
            } else {
                GL11.glClearColor(0, 0, 0, 1);
            }
            GL11.glClear(GL11.GL_COLOR_BUFFER_BIT);
        }
    }

    public void target(GLSurface surface) {
        if (surface == this.surface) {
            return;
        }
        flush();
        this.surface = surface;
    }

    void invalidate(GLSurface surface) {
        if (this.surface == surface) {
            target(null);
        }
    }

    private void activate() {
        if (active) {
            return;
        }

        GL11.glDepthMask(false);
        GL11.glEnable(GL11.GL_TEXTURE_2D);

        FrameBuffer fbo = null;
        boolean clear = false;
        target = null;

        if (surface != null) {
            GLSurfaceData data = surface.getData();
            if (data == null) {
                LOGGER.finest("Setting up render to empty surface");
                data = new GLSurfaceData(surface.getWidth(), surface.getHeight(), surface.hasAlpha());
                data.texture = context.getTextureManager().acquire(data.width, data.height);
                surface.setData(data);
                clear = true;
            } else if (data.usage > 1) {
                LOGGER.finest("Setting up render to shared surface");
                data.usage--;
                if (data.texture == null) {
                    data.texture = context.getTextureManager().acquire(data.width, data.height);
                    syncPixelsToTexture(data);
                }
                data.texture.getFrameBuffer().bind();
                data = new GLSurfaceData(surface.getWidth(), surface.getHeight(), surface.hasAlpha());
                data.texture = context.getTextureManager().acquire(data.width, data.height);
                data.texture.bind();
                GL11.glCopyTexSubImage2D(GL11.GL_TEXTURE_2D, 0, 0, 0, 0, 0, data.width, data.height);
                surface.setData(data);
            } else if (data.texture == null) {
                LOGGER.finest("Setting up render to pixel backed surface");
                data.texture = context.getTextureManager().acquire(data.width, data.height);
                syncPixelsToTexture(data);
            }
            data.pixels = null;
            target = data.texture;
            fbo = data.texture.getFrameBuffer();
        }

        GL11.glBindTexture(GL11.GL_TEXTURE_2D, 0);

        if (fbo == null) {
            LOGGER.finest("Setting up render to screen");
            int w = GLContext.getCurrent().getWidth();
            int h = GLContext.getCurrent().getHeight();
            FrameBuffer.unbind();
            GL11.glViewport(0, 0, w, h);
            projectionMatrix.setToOrtho2D(0, 0, w, h);
        } else {
            LOGGER.finest("Setting up render to texture");
            fbo.bind();
            int w = surface.getWidth();
            int h = surface.getHeight();
            GL11.glViewport(0, 0, w, h);
            projectionMatrix.setToOrtho2D(0, h, w, -h);
        }

        if (customShader != null) {
            customShader.begin();
        } else {
            shader.begin();
        }
        setupMatrices();

        active = true;

        if (clear) {
            clear();
        }

    }

    public void flush() {

        renderMesh();

        tex0 = null;

        if (customShader != null) {
            customShader.end();
        } else {
            shader.end();
        }

        active = false;

    }

    /**
     * Sets the color used to tint images when they are added to the
     * TextureRenderer. Default is {@link Color#WHITE}.
     */
    public void setColor(Color tint) {
        color = tint.toFloatBits();
    }

    /**
     * @see #setColor(Color)
     */
    public void setColor(float r, float g, float b, float a) {
        int intBits = (int) (255 * a) << 24 | (int) (255 * b) << 16 | (int) (255 * g) << 8 | (int) (255 * r);
        color = Float.intBitsToFloat(intBits & 0xfeffffff);
    }

    /**
     * @see #setColor(Color)
     * @see Color#toFloatBits()
     */
    public void setColor(float color) {
        this.color = color;
    }

    //    /**
    //     * @return the rendering color of this TextureRenderer. Manipulating the
    //     * returned instance has no effect.
    //     */
    //    public Color getColor() {
    //        int intBits = Float.floatToRawIntBits(color);
    //        Color color = this.tempColor;
    //        color.r = (intBits & 0xff) / 255f;
    //        color.g = ((intBits >>> 8) & 0xff) / 255f;
    //        color.b = ((intBits >>> 16) & 0xff) / 255f;
    //        color.a = ((intBits >>> 24) & 0xff) / 255f;
    //        return color;
    //    }
    private TextureRegion region = new TextureRegion();

    public void draw(Surface src, float x, float y) {
        draw(src, 0, 0, src.getWidth(), src.getHeight(), x, y);
    }

    public void draw(Surface src, float x, float y, float width, float height) {
        draw(src, 0, 0, src.getWidth(), src.getHeight(), x, y, width, height);
    }

    public void draw(Surface src, float srcX, float srcY, float srcWidth, float srcHeight, float x, float y) {
        draw(src, srcX, srcY, srcWidth, srcHeight, x, y, srcWidth, srcHeight);
    }

    public void draw(Surface src, float srcX, float srcY, float srcWidth, float srcHeight, float x, float y,
            float width, float height) {
        activate();
        TextureRegion tr = initRegion(region, src, (int) srcX, (int) srcY, (int) (srcWidth + 0.5f),
                (int) (srcHeight + 0.5f));
        //        GLSurfaceData data = src.data;
        if (tr == null) {
            LOGGER.finest("Drawing empty Surface - using empty texture");
            if (src.hasAlpha()) {
                region.setRegion(clearTexture);
            } else {
                region.setRegion(blackTexture);
            }
            draw(region, x, y, width, height);
        } else {
            //            if (data.texture == null) {
            //                LOGGER.finest("Surface texture is null - uploading pixels");
            //                data.texture = context.getTextureManager().acquire(data.width, data.height);
            //                syncPixelsToTexture(data);
            //            }
            //            region.setTexture(data.texture);
            //            region.setRegion((int) srcX, (int) srcY, (int) srcWidth, (int) srcHeight);
            draw(region, x, y, width, height);
        }
    }

    public void draw(Surface src, float srcX, float srcY, float srcWidth, float srcHeight, float[] vts) {
        activate();
        TextureRegion tr = initRegion(region, src, (int) srcX, (int) srcY, (int) (srcWidth + 0.5f),
                (int) (srcHeight + 0.5f));
        if (tr == null) {
            LOGGER.finest("Drawing empty Surface - using empty texture");
            if (src.hasAlpha()) {
                region.setRegion(clearTexture);
            } else {
                region.setRegion(blackTexture);
            }
            draw(region, vts);
        } else {
            draw(region, vts);
        }

    }

    /**
     * Draws a rectangle with the bottom left corner at x,y and stretching the
     * region to cover the given width and height.
     */
    private void draw(TextureRegion region, float x, float y, float width, float height) {
        //        if (!drawing) {
        //            throw new IllegalStateException("SpriteBatch.begin must be called before draw.");
        //        }
        Texture texture = region.texture;

        if (texture != tex0) {
            renderMesh();
            tex0 = texture;
            //            invTexWidth = 1f / texture.getWidth();
            //            invTexHeight = 1f / texture.getHeight();
        } else if (idx == vertices.length) {
            //            System.out.println("idx == vertices.length");
            renderMesh();
        }

        final float fx2 = x + width;
        final float fy2 = y + height;
        final float u = region.u;
        final float v = region.v2;
        final float u2 = region.u2;
        final float v2 = region.v;

        vertices[idx++] = x;
        vertices[idx++] = y;
        vertices[idx++] = color;
        vertices[idx++] = u;
        vertices[idx++] = v;

        vertices[idx++] = x;
        vertices[idx++] = fy2;
        vertices[idx++] = color;
        vertices[idx++] = u;
        vertices[idx++] = v2;

        vertices[idx++] = fx2;
        vertices[idx++] = fy2;
        vertices[idx++] = color;
        vertices[idx++] = u2;
        vertices[idx++] = v2;

        vertices[idx++] = fx2;
        vertices[idx++] = y;
        vertices[idx++] = color;
        vertices[idx++] = u2;
        vertices[idx++] = v;
    }

    private void draw(TextureRegion region, float[] vts) {
        Texture texture = region.texture;

        if (texture != tex0) {
            renderMesh();
            tex0 = texture;
        } else if (idx == vertices.length) {
            renderMesh();
        }

        //        final float fx2 = x + width;
        //        final float fy2 = y + height;
        final float u = region.u;
        final float v = region.v2;
        final float u2 = region.u2;
        final float v2 = region.v;

        vertices[idx++] = vts[0];
        vertices[idx++] = vts[1];
        vertices[idx++] = color;
        vertices[idx++] = u;
        vertices[idx++] = v;

        vertices[idx++] = vts[2];
        vertices[idx++] = vts[3];
        vertices[idx++] = color;
        vertices[idx++] = u;
        vertices[idx++] = v2;

        vertices[idx++] = vts[4];
        vertices[idx++] = vts[5];
        vertices[idx++] = color;
        vertices[idx++] = u2;
        vertices[idx++] = v2;

        vertices[idx++] = vts[6];
        vertices[idx++] = vts[7];
        vertices[idx++] = color;
        vertices[idx++] = u2;
        vertices[idx++] = v;
    }

    private TextureRegion initRegion(TextureRegion region, Surface src, int x, int y, int width, int height) {
        if (src instanceof GLSurface) {
            GLSurfaceData data = ((GLSurface) src).getData();
            if (data == null) {
                return null;
            }
            if (data.texture == null) {
                LOGGER.finest("Surface texture is null - uploading pixels");
                data.texture = context.getTextureManager().acquire(data.width, data.height);
                syncPixelsToTexture(data);
            }
            region.setTexture(data.texture);
            region.setRegion(x, y, width, height);
            return region;
        } else {
            context.getTextureManager().initTextureRegion(region, src, x, y, width, height);
            return region;
        }
    }

    private void renderMesh() {

        if (idx == 0) {
            return;
        }

        int spritesInBatch = idx / 20;

        if (tex0 == null) {
            LOGGER.log(Level.WARNING, "Texture is null - returning : idx = {0}", idx);
            return;
        }

        tex0.bind();
        mesh.setVertices(vertices, 0, idx);

        if (blendingDisabled) {
            GL11.glDisable(GL11.GL_BLEND);
        } else {
            GL11.glEnable(GL11.GL_BLEND);
            if (blendSrcFunc == blendSrcAlphaFunc && blendDstFunc == blendDstAlphaFunc) {
                GL11.glBlendFunc(blendSrcFunc, blendDstFunc);
            } else {
                GL14.glBlendFuncSeparate(blendSrcFunc, blendDstFunc, blendSrcAlphaFunc, blendDstAlphaFunc);
            }
        }

        if (customShader != null) {
            LOGGER.finest("Rendering with custom shader");
            mesh.render(customShader, GL11.GL_TRIANGLES, 0, spritesInBatch * 6);
        } else {
            LOGGER.finest("Rendering with default shader");
            mesh.render(shader, GL11.GL_TRIANGLES, 0, spritesInBatch * 6);
        }

        idx = 0;
        currBufferIdx++;
        if (currBufferIdx == buffers.length) {
            currBufferIdx = 0;
        }
        mesh = buffers[currBufferIdx];
    }

    /**
     * Sets the blending function to be used when rendering sprites.
     *
     * @param srcFunc the source function, e.g. GL11.GL_SRC_ALPHA
     * @param dstFunc the destination function, e.g. GL11.GL_ONE_MINUS_SRC_ALPHA
     */
    public void setBlendFunction(int srcFunc, int dstFunc) {
        setBlendFunction(srcFunc, dstFunc, srcFunc, dstFunc);
    }

    public void setBlendFunction(int srcRGBFunc, int dstRGBFunc, int srcAlphaFunc, int dstAlphaFunc) {
        if (blendSrcFunc != srcRGBFunc || blendDstFunc != dstRGBFunc || blendSrcAlphaFunc != srcAlphaFunc
                || blendDstAlphaFunc != dstAlphaFunc) {
            renderMesh();
            blendSrcFunc = srcRGBFunc;
            blendDstFunc = dstRGBFunc;
            blendSrcAlphaFunc = srcAlphaFunc;
            blendDstAlphaFunc = dstAlphaFunc;
        }

    }

    /**
     * Disposes all resources associated with this TextureRenderer
     */
    @Override
    public void dispose() {
        for (int i = 0; i < buffers.length; i++) {
            buffers[i].dispose();
        }
        if (shader != null) {
            shader.dispose();
            shader = null;
        }
        if (clearTexture != null) {
            clearTexture.dispose();
            clearTexture = null;
        }
        if (blackTexture != null) {
            blackTexture.dispose();
            blackTexture = null;
        }
    }

    private void setupMatrices() {
        combinedMatrix.set(projectionMatrix).mul(transformMatrix);
        ShaderProgram prog = customShader == null ? shader : customShader;
        if (prog.hasUniform("u_projectionViewMatrix")) {
            prog.setUniformMatrix("u_projectionViewMatrix", combinedMatrix);
        }
        if (prog.hasUniform("u_texture")) {
            prog.setUniformi("u_texture", 0);
        }

    }

    private void setShader(ShaderProgram shader) {
        activate();
        renderMesh();
        if (customShader != null) {
            LOGGER.log(Level.FINE, "Unbinding custom shader");
            customShader.end();
        } else {
            LOGGER.log(Level.FINE, "Unbinding default shader");
            this.shader.end();
        }

        customShader = shader;

        if (customShader != null) {
            customShader.begin();
            LOGGER.log(Level.FINE, "Binding custom shader");
        } else {
            this.shader.begin();
            LOGGER.log(Level.FINE, "Binding default shader");
        }
        setupMatrices();

    }

    public void bind(ShaderProgram shader) {
        setShader(shader);
    }

    public void unbind(ShaderProgram shader) {
        if (customShader == shader) {
            setShader(null);
        }
    }

    void syncTextureToPixels(GLSurfaceData data) {
        LOGGER.fine("Copying texture to pixels");
        if (data.texture == target) {
            LOGGER.fine("Texture is current render target");
            flush();
        }
        int tWidth = data.texture.getWidth();
        int size = tWidth * data.texture.getHeight();
        if (scratchBuffer == null || scratchBuffer.capacity() < size) {
            scratchBuffer = BufferUtils.createIntBuffer(size);
        }
        scratchBuffer.rewind();
        data.texture.bind();
        GL11.glGetTexImage(GL11.GL_TEXTURE_2D, 0, GL12.GL_BGRA, GL12.GL_UNSIGNED_INT_8_8_8_8_REV, scratchBuffer);
        scratchBuffer.rewind();
        int offset = 0;
        for (int y = 0; y < data.height; y++) {
            scratchBuffer.position(offset);
            scratchBuffer.get(data.pixels, y * data.width, data.width);
            offset += tWidth;
        }
    }

    void syncPixelsToTexture(GLSurfaceData data) {
        LOGGER.fine("Copying pixels to texture");
        if (data.texture == target) {
            LOGGER.log(Level.FINE, "Texture is current render target");
            flush();
        }
        int size = data.width * data.height;
        if (scratchBuffer == null || scratchBuffer.capacity() < size) {
            scratchBuffer = BufferUtils.createIntBuffer(size);
        }
        scratchBuffer.rewind();
        scratchBuffer.put(data.pixels, 0, size);
        scratchBuffer.rewind();
        syncPixelBufferToTexture(scratchBuffer, data.texture, data.alpha, 0, 0, data.width, data.height);

    }

    void syncPixelBufferToTexture(IntBuffer buffer, Texture texture, boolean alpha, int x, int y, int width,
            int height) {
        texture.bind();
        if (!alpha) {
            GL11.glPixelTransferf(GL11.GL_ALPHA_BIAS, 1);
        }
        GL11.glTexSubImage2D(GL11.GL_TEXTURE_2D, 0, x, y, width, height, GL12.GL_BGRA,
                GL12.GL_UNSIGNED_INT_8_8_8_8_REV, buffer);
        if (!alpha) {
            GL11.glPixelTransferf(GL11.GL_ALPHA_BIAS, 0);
        }
    }

}