/* Copyright 2011 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 com.esotericsoftware.spine; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.GL10; import com.badlogic.gdx.graphics.GL11; import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.graphics.GLCommon; import com.badlogic.gdx.graphics.Mesh; import com.badlogic.gdx.graphics.Mesh.VertexDataType; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.VertexAttribute; import com.badlogic.gdx.graphics.VertexAttributes.Usage; import com.badlogic.gdx.graphics.glutils.ShaderProgram; import com.badlogic.gdx.math.Matrix4; import com.badlogic.gdx.utils.Disposable; /**

* A SpineSpriteBatch is used to draw 2D rectangles that reference a texture (region). The class will batch the drawing commands and * optimize them for processing by the GPU. *

* *

* To draw something with a SpineSpriteBatch one has to first call the {@link SpineSpriteBatch#begin()} method which will setup appropriate * render states. When you are done with drawing you have to call {@link SpineSpriteBatch#end()} which will actually draw the things * you specified. *

* *

* All drawing commands of the SpineSpriteBatch operate in screen coordinates. The screen coordinate system has an x-axis pointing to * the right, an y-axis pointing upwards and the origin is in the lower left corner of the screen. You can also provide your own * transformation and projection matrices if you so wish. *

* *

* A SpineSpriteBatch is managed. In case the OpenGL context is lost all OpenGL resources a SpineSpriteBatch uses internally get * invalidated. A context is lost when a user switches to another application or receives an incoming call on Android. A * SpineSpriteBatch will be automatically reloaded after the OpenGL context is restored. *

* *

* A SpineSpriteBatch is a pretty heavy object so you should only ever have one in your program. *

* *

* A SpineSpriteBatch works with OpenGL ES 1.x and 2.0. In the case of a 2.0 context it will use its own custom shader to draw all * provided sprites. You can set your own custom shader via {@link #setShader(ShaderProgram)}. *

* *

* A SpineSpriteBatch has to be disposed if it is no longer used. *

* * @author mzechner */ public class SpineSpriteBatch implements Disposable { private Mesh mesh; private Mesh[] buffers; private Texture lastTexture = 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 drawing = false; private boolean blendingDisabled = false; private int blendSrcFunc = GL11.GL_SRC_ALPHA; private int blendDstFunc = GL11.GL_ONE_MINUS_SRC_ALPHA; private final ShaderProgram shader; private boolean ownsShader; float color = Color.WHITE.toFloatBits(); /** number of render calls since last {@link #begin()} **/ public int renderCalls = 0; /** number of rendering calls ever, will not be reset, unless it's done manually **/ public int totalRenderCalls = 0; /** the maximum number of sprites rendered in one batch so far **/ public int maxSpritesInBatch = 0; private ShaderProgram customShader = null; /** Constructs a new SpineSpriteBatch. 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. */ public SpineSpriteBatch () { this(1000); } /** Constructs a SpineSpineSpriteBatch with the specified size and (if GL2) the default shader. See * {@link #SpineSpineSpriteBatch(int, ShaderProgram)}. */ public SpineSpriteBatch (int size) { this(size, null); } /**

* Constructs a new SpineSpineSpriteBatch. 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. *

* *

* The size parameter specifies the maximum size of a single batch in number of sprites *

* *

* The defaultShader specifies the shader to use. Note that the names for uniforms for this default shader are different than * the ones expect for shaders set with {@link #setShader(ShaderProgram)}. See the {@link #createDefaultShader()} method. *

* * @param size the batch size in number of sprites * @param defaultShader the default shader to use. This is not owned by the SpineSpineSpriteBatch and must be disposed separately. */ public SpineSpriteBatch (int size, ShaderProgram defaultShader) { this(size, 1, defaultShader); } /** Constructs a SpineSpineSpriteBatch with the specified size and number of buffers and (if GL2) the default shader. See * {@link #SpineSpineSpriteBatch(int, int, ShaderProgram)}. */ public SpineSpriteBatch (int size, int buffers) { this(size, buffers, null); } /**

* Constructs a new SpineSpriteBatch. 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. *

* *

* The size parameter specifies the maximum size of a single batch in number of sprites *

* *

* The defaultShader specifies the shader to use. Note that the names for uniforms for this default shader are different than * the ones expect for shaders set with {@link #setShader(ShaderProgram)}. See the {@link #createDefaultShader()} method. *

* * @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. * @param defaultShader the default shader to use. This is not owned by the SpineSpriteBatch and must be disposed separately. */ public SpineSpriteBatch (int size, int buffers, ShaderProgram defaultShader) { this.buffers = new Mesh[buffers]; for (int i = 0; i < buffers; i++) { this.buffers[i] = new Mesh(VertexDataType.VertexArray, 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"), new VertexAttribute(Usage.Generic, 1, "a_rot")); } projectionMatrix.setToOrtho2D(0, 0, Gdx.graphics.getWidth(), Gdx.graphics.getHeight()); vertices = new float[size * 24]; 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]; if (Gdx.graphics.isGL20Available() && defaultShader == null) { shader = createDefaultShader(); ownsShader = true; } else shader = defaultShader; } /** Returns a new instance of the default shader used by SpineSpriteBatch for GL2 when no shader is specified. */ static public ShaderProgram createDefaultShader () { String vertexShader = "attribute vec4 " + ShaderProgram.POSITION_ATTRIBUTE + ";\n" // + "attribute vec4 " + ShaderProgram.COLOR_ATTRIBUTE + ";\n" // + "attribute vec2 " + ShaderProgram.TEXCOORD_ATTRIBUTE + "0;\n" // + "attribute float a_rot;\n" // + "uniform mat4 u_projTrans;\n" // + "varying vec4 v_color;\n" // + "varying vec2 v_texCoords;\n" // + "varying vec3 v_lightDir;\n" // + "const vec3 lightDir = normalize(vec3(-0.0, -1.0, 0.2));\n" // + "\n" // + "void main()\n" // + "{\n" // + " v_color = " + ShaderProgram.COLOR_ATTRIBUTE + ";\n" // + " vec2 rad = vec2(-sin(a_rot), cos(a_rot));\n" // + " v_lightDir = vec3(mat2(rad.y, -rad.x, rad.x, rad.y) * lightDir.xy,lightDir.z);\n" // + " v_texCoords = " + ShaderProgram.TEXCOORD_ATTRIBUTE + "0;\n" // + " gl_Position = u_projTrans * " + ShaderProgram.POSITION_ATTRIBUTE + ";\n" // + "}\n"; String fragmentShader = "#ifdef GL_ES\n" // + "#define LOWP lowp\n" // + "precision mediump float;\n" // + "#else\n" // + "#define LOWP \n" // + "#endif\n" // + "varying LOWP vec4 v_color;\n" // + "varying vec2 v_texCoords;\n" // + "varying vec3 v_lightDir;\n" // + "uniform sampler2D u_texture;\n" // + "uniform sampler2D u_texture2;\n" // + "void main()\n"// + "{\n" // + " vec4 albedo = texture2D(u_texture, v_texCoords);\n" // + " vec3 normal = texture2D(u_texture2, v_texCoords).xyz * 2.0 - 1.0;\n" // + " float light = clamp(dot(normalize(v_lightDir), normal), 0.0, 1.0) ;\n" // + " gl_FragColor = v_color * vec4((light*light * albedo.rgb), albedo.a);\n" // + "}"; ShaderProgram shader = new ShaderProgram(vertexShader, fragmentShader); if (shader.isCompiled() == false) throw new IllegalArgumentException("couldn't compile shader: " + shader.getLog()); return shader; } /** Sets up the SpineSpriteBatch for drawing. This will disable depth buffer writting. It enables blending and texturing. If you have * more texture units enabled than the first one you have to disable them before calling this. Uses a screen coordinate system * by default where everything is given in pixels. You can specify your own projection and modelview matrices via * {@link #setProjectionMatrix(Matrix4)} and {@link #setTransformMatrix(Matrix4)}. */ public void begin () { if (drawing) throw new IllegalStateException("you have to call SpineSpriteBatch.end() first"); renderCalls = 0; Gdx.gl.glDepthMask(false); if (Gdx.graphics.isGL20Available()) { if (customShader != null) customShader.begin(); else shader.begin(); } else { Gdx.gl.glEnable(GL10.GL_TEXTURE_2D); } setupMatrices(); idx = 0; lastTexture = null; drawing = true; } /** Finishes off rendering. Enables depth writes, disables blending and texturing. Must always be called after a call to * {@link #begin()} */ public void end () { if (!drawing) throw new IllegalStateException("SpineSpriteBatch.begin must be called before end."); if (idx > 0) renderMesh(); lastTexture = null; idx = 0; drawing = false; GLCommon gl = Gdx.gl; gl.glDepthMask(true); if (isBlendingEnabled()) gl.glDisable(GL10.GL_BLEND); if (Gdx.graphics.isGL20Available()) { if (customShader != null) customShader.end(); else shader.end(); } else { gl.glDisable(GL10.GL_TEXTURE_2D); } } /** Sets the color used to tint images when they are added to the SpineSpriteBatch. Default is {@link Color#WHITE}. */ public void setColor (Color tint) { color = tint.toFloatBits(); } /** Draws a rectangle using the given vertices. There must be 4 vertices, each made up of 5 elements in this order: x, y, color, * u, v. The {@link #getColor()} from the SpineSpriteBatch is not applied. */ public void draw (Texture texture, float[] spriteVertices, int offset, int length) { if (!drawing) throw new IllegalStateException("SpineSpriteBatch.begin must be called before draw."); if (texture != lastTexture) { switchTexture(texture); } int remainingVertices = vertices.length - idx; if (remainingVertices == 0) { renderMesh(); remainingVertices = vertices.length; } int vertexCount = Math.min(remainingVertices, length - offset); System.arraycopy(spriteVertices, offset, vertices, idx, vertexCount); offset += vertexCount; idx += vertexCount; while (offset < length) { renderMesh(); vertexCount = Math.min(vertices.length, length - offset); System.arraycopy(spriteVertices, offset, vertices, 0, vertexCount); offset += vertexCount; idx += vertexCount; } } /** Causes any pending sprites to be rendered, without ending the SpineSpriteBatch. */ public void flush () { renderMesh(); } private void renderMesh () { if (idx == 0) return; renderCalls++; totalRenderCalls++; int spritesInBatch = idx / 20; if (spritesInBatch > maxSpritesInBatch) maxSpritesInBatch = spritesInBatch; lastTexture.bind(); mesh.setVertices(vertices, 0, idx); mesh.getIndicesBuffer().position(0); mesh.getIndicesBuffer().limit(spritesInBatch * 6); if (blendingDisabled) { Gdx.gl.glDisable(GL20.GL_BLEND); } else { Gdx.gl.glEnable(GL20.GL_BLEND); if (blendSrcFunc != -1) Gdx.gl.glBlendFunc(blendSrcFunc, blendDstFunc); } if (Gdx.graphics.isGL20Available()) { if (customShader != null) mesh.render(customShader, GL10.GL_TRIANGLES, 0, spritesInBatch * 6); else mesh.render(shader, GL10.GL_TRIANGLES, 0, spritesInBatch * 6); } else { mesh.render(GL10.GL_TRIANGLES, 0, spritesInBatch * 6); } idx = 0; currBufferIdx++; if (currBufferIdx == buffers.length) currBufferIdx = 0; mesh = buffers[currBufferIdx]; } /** Disables blending for drawing sprites. */ public void disableBlending () { if (blendingDisabled) return; renderMesh(); blendingDisabled = true; } /** Enables blending for sprites */ public void enableBlending () { if (!blendingDisabled) return; renderMesh(); blendingDisabled = false; } /** Sets the blending function to be used when rendering sprites. * * @param srcFunc the source function, e.g. GL11.GL_SRC_ALPHA. If set to -1, SpineSpriteBatch won't change the blending function. * @param dstFunc the destination function, e.g. GL11.GL_ONE_MINUS_SRC_ALPHA */ public void setBlendFunction (int srcFunc, int dstFunc) { renderMesh(); blendSrcFunc = srcFunc; blendDstFunc = dstFunc; } /** Disposes all resources associated with this SpineSpriteBatch */ public void dispose () { for (int i = 0; i < buffers.length; i++) buffers[i].dispose(); if (ownsShader && shader != null) shader.dispose(); } /** Returns the current projection matrix. Changing this will result in undefined behaviour. * * @return the currently set projection matrix */ public Matrix4 getProjectionMatrix () { return projectionMatrix; } /** Returns the current transform matrix. Changing this will result in undefined behaviour. * * @return the currently set transform matrix */ public Matrix4 getTransformMatrix () { return transformMatrix; } /** Sets the projection matrix to be used by this SpineSpriteBatch. If this is called inside a {@link #begin()}/{@link #end()} block. * the current batch is flushed to the gpu. * * @param projection the projection matrix */ public void setProjectionMatrix (Matrix4 projection) { if (drawing) flush(); projectionMatrix.set(projection); if (drawing) setupMatrices(); } /** Sets the transform matrix to be used by this SpineSpriteBatch. If this is called inside a {@link #begin()}/{@link #end()} block. * the current batch is flushed to the gpu. * * @param transform the transform matrix */ public void setTransformMatrix (Matrix4 transform) { if (drawing) flush(); transformMatrix.set(transform); if (drawing) setupMatrices(); } private void setupMatrices () { if (!Gdx.graphics.isGL20Available()) { GL10 gl = Gdx.gl10; gl.glMatrixMode(GL10.GL_PROJECTION); gl.glLoadMatrixf(projectionMatrix.val, 0); gl.glMatrixMode(GL10.GL_MODELVIEW); gl.glLoadMatrixf(transformMatrix.val, 0); } else { combinedMatrix.set(projectionMatrix).mul(transformMatrix); if (customShader != null) { customShader.setUniformMatrix("u_projTrans", combinedMatrix); customShader.setUniformi("u_texture", 0); customShader.setUniformi("u_texture2", 1); } else { shader.setUniformMatrix("u_projTrans", combinedMatrix); shader.setUniformi("u_texture", 0); shader.setUniformi("u_texture2", 1); } } } private void switchTexture (Texture texture) { renderMesh(); lastTexture = texture; } /** Sets the shader to be used in a GLES 2.0 environment. Vertex position attribute is called "a_position", the texture * coordinates attribute is called called "a_texCoord0", the color attribute is called "a_color". See * {@link ShaderProgram#POSITION_ATTRIBUTE}, {@link ShaderProgram#COLOR_ATTRIBUTE} and {@link ShaderProgram#TEXCOORD_ATTRIBUTE} * which gets "0" appened to indicate the use of the first texture unit. The combined transform and projection matrx is is * uploaded via a mat4 uniform called "u_projTrans". The texture sampler is passed via a uniform called "u_texture".

* * Call this method with a null argument to use the default shader.

* * This method will flush the batch before setting the new shader, you can call it in between {@link #begin()} and * {@link #end()}. * * @param shader the {@link ShaderProgram} or null to use the default shader. */ public void setShader (ShaderProgram shader) { if (drawing) { flush(); if (customShader != null) customShader.end(); else this.shader.end(); } customShader = shader; if (drawing) { if (customShader != null) customShader.begin(); else this.shader.begin(); setupMatrices(); } } /** @return whether blending for sprites is enabled */ public boolean isBlendingEnabled () { return !blendingDisabled; } static public final int X1 = 0; static public final int Y1 = 1; static public final int C1 = 2; static public final int U1 = 3; static public final int V1 = 4; static public final int R1 = 5; static public final int X2 = 6; static public final int Y2 = 7; static public final int C2 = 8; static public final int U2 = 9; static public final int V2 = 10; static public final int R2 = 11; static public final int X3 = 12; static public final int Y3 = 13; static public final int C3 = 14; static public final int U3 = 15; static public final int V3 = 16; static public final int R3 = 17; static public final int X4 = 18; static public final int Y4 = 19; static public final int C4 = 20; static public final int U4 = 21; static public final int V4 = 22; static public final int R4 = 23; }