/**
* Copyright (c) 2012, Matt DesLauriers All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* * Redistributions in binary
* form must reproduce the above copyright notice, this list of conditions and
* the following disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* * Neither the name of the Matt DesLauriers nor the names
* of his contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package mdesl.graphics;
import static org.lwjgl.opengl.GL11.GL_TRIANGLES;
import java.nio.FloatBuffer;
import java.util.Arrays;
import java.util.List;
import mdesl.graphics.glutils.ShaderProgram;
import mdesl.graphics.glutils.VertexArray;
import mdesl.graphics.glutils.VertexAttrib;
import mdesl.graphics.glutils.VertexData;
import mdesl.util.MathUtil;
import org.lwjgl.LWJGLException;
import org.lwjgl.opengl.Display;
import org.lwjgl.util.vector.Matrix4f;
/** @author Matt (mdesl) DesLauriers
* @author matheusdev */
public class SpriteBatch {
public static final String U_TEXTURE = "u_texture";
public static final String U_PROJ_VIEW = "u_projView";
public static final String ATTR_COLOR = "Color";
public static final String ATTR_POSITION = "Position";
public static final String ATTR_TEXCOORD = "TexCoord";
public static final String DEFAULT_VERT_SHADER = "uniform mat4 " + U_PROJ_VIEW + ";\n"
+ "attribute vec4 " + ATTR_COLOR + ";\n" + "attribute vec2 " + ATTR_TEXCOORD + ";\n"
+ "attribute vec2 " + ATTR_POSITION + ";\n" + "varying vec4 vColor;\n"
+ "varying vec2 vTexCoord; \n" + "void main() {\n" + " vColor = " + ATTR_COLOR + ";\n"
+ " vTexCoord = " + ATTR_TEXCOORD + ";\n" + " gl_Position = " + U_PROJ_VIEW
+ " * vec4(" + ATTR_POSITION + ".xy, 0.0, 1.0);\n" + "}";
public static final String DEFAULT_FRAG_SHADER = "uniform sampler2D " + U_TEXTURE + ";\n"
+ "varying vec4 vColor;\n" + "varying vec2 vTexCoord;\n" + "void main() {\n"
+ " vec4 texColor = texture2D(" + U_TEXTURE + ", vTexCoord);\n"
+ " gl_FragColor = vColor * texColor;\n" + "}";
public static final List<VertexAttrib> ATTRIBUTES = Arrays.asList(new VertexAttrib(0,
ATTR_POSITION, 2), new VertexAttrib(1, ATTR_COLOR, 4), new VertexAttrib(2,
ATTR_TEXCOORD, 2));
static ShaderProgram defaultShader;
public static int renderCalls = 0;
protected FloatBuffer buf16;
protected Matrix4f projMatrix = new Matrix4f();
protected Matrix4f viewMatrix = new Matrix4f();
protected Matrix4f transpositionPool = new Matrix4f();
private Matrix4f projViewMatrix = new Matrix4f(); //only for re-using Matrix4f objects
protected Texture texture;
protected ShaderProgram program;
protected VertexData data;
private int idx;
private int maxIndex;
private Color color = new Color();
private boolean drawing = false;
public static ShaderProgram getDefaultShader() throws LWJGLException {
return defaultShader == null ? (defaultShader = new ShaderProgram(DEFAULT_VERT_SHADER, DEFAULT_FRAG_SHADER,
ATTRIBUTES)) : defaultShader;
}
public SpriteBatch(ShaderProgram program) {
this(program, 1000);
}
public SpriteBatch(ShaderProgram program, int size) {
this(program, 1000, true);
}
public SpriteBatch(ShaderProgram program, int size, boolean updateUniforms) {
this.program = program;
// later we can do some abstraction to replace this with VBOs...
this.data = new VertexArray(size * 6, ATTRIBUTES);
// max indices before we need to flush the renderer
maxIndex = size * 6;
// default size
resize(Display.getWidth(), Display.getHeight());
}
/**
* Creates a sprite batch with a default shader, shared across all sprite batches.
* @param size
* @throws LWJGLException
*/
public SpriteBatch(int size) throws LWJGLException {
this(getDefaultShader(), size);
}
public SpriteBatch() throws LWJGLException {
this(1000);
}
public Matrix4f getViewMatrix() {
return viewMatrix;
}
public Matrix4f getProjectionMatrix() {
return projMatrix;
}
public Matrix4f getCombinedMatrix() {
Matrix4f.mul(Matrix4f.transpose(projMatrix, transpositionPool),
viewMatrix, projViewMatrix);
return projViewMatrix;
}
/** A convenience method to resize the projection matrix to the given
* dimensions, using y-down ortho 2D. This will invoke a call to
* updateMatrices.
*
* @param width
* @param height */
public void resize(int width, int height) {
projMatrix = MathUtil.toOrtho2D(projMatrix, 0, 0, width, height);
updateUniforms();
}
/** Sets this SpriteBatch's color to the RGBA values of the given color
* object.
*
* @param color the RGBA values to use */
public void setColor(Color color) {
setColor(color.r, color.g, color.b, color.a);
}
/** Sets this SpriteBatch's color to the given RGBA values.
*
* @param r the red value
* @param g the green value
* @param b the blue value
* @param a the alpha value */
public void setColor(float r, float g, float b, float a) {
color.set(r, g, b, a);
}
/** Call to multiply the the projection with the view matrix and save the
* result in the uniform mat4 {@value #U_PROJ_VIEW}, as well as update the
* {@value #U_TEXTURE} uniform. */
public void updateUniforms() {
updateUniforms(program);
}
/** Call to multiply the the projection with the view matrix and save the
* result in the uniform mat4 {@value #U_PROJ_VIEW}, as well as update the
* {@value #U_TEXTURE} uniform. */
public void updateUniforms(ShaderProgram program) {
projViewMatrix = getCombinedMatrix();
// bind the program before sending uniforms
program.use();
boolean oldStrict = ShaderProgram.isStrictMode();
//disable strict mode so we don't run into any problems
ShaderProgram.setStrictMode(false);
// we can now utilize ShaderProgram's hash map which may be better than
// glGetUniformLocation
// Store the the multiplied matrix in the "projViewMatrix"-uniform:
program.setUniformMatrix(U_PROJ_VIEW, false, projViewMatrix);
// upload texcoord 0
program.setUniformi(U_TEXTURE, 0);
//reset strict mode
ShaderProgram.setStrictMode(oldStrict);
}
/** An advanced call that allows you to change the shader without uploading
* shader uniforms. This will flush the batch if we are within begin().
*
* @param program
* @param updateUniforms whether to call updateUniforms after changing the
* programs */
public void setShader(ShaderProgram program, boolean updateUniforms) {
if (program==null)
throw new NullPointerException("shader cannot be null; use getDefaultShader instead");
if (drawing) //if we are already drawing, flush the batch before switching shaders
flush();
this.program = program; //now switch the shader
if (updateUniforms) //send uniform data to shader
updateUniforms();
else if (drawing) //if we don't want to update, then just start the program if we are drawing
program.use();
}
/** Changes the shader and updates it with the current texture and projView
* uniforms. This will flush the batch if we are within begin().
*
* @param program the new program to use */
public void setShader(ShaderProgram program) {
setShader(program, true);
}
public ShaderProgram getShader() {
return program;
}
public void begin() {
if (drawing)
throw new IllegalStateException("must not be drawing before calling begin()");
drawing = true;
program.use();
idx = 0;
renderCalls = 0;
texture = null;
}
public void end() {
if (!drawing)
throw new IllegalStateException("must be drawing before calling end()");
drawing = false;
flush();
}
public void flush() {
if (idx > 0) {
data.flip();
render();
idx = 0;
data.clear();
}
}
public void drawRegion(Texture tex, float srcX, float srcY, float srcWidth, float srcHeight,
float dstX, float dstY) {
drawRegion(tex, srcX, srcY, srcWidth, srcHeight, dstX, dstY, srcWidth, srcHeight);
}
public void drawRegion(Texture tex, float srcX, float srcY, float srcWidth, float srcHeight,
float dstX, float dstY, float dstWidth, float dstHeight) {
float u = srcX / tex.getWidth();
float v = srcY / tex.getHeight();
float u2 = (srcX + srcWidth) / tex.getWidth();
float v2 = (srcY + srcHeight) / tex.getHeight();
draw(tex, dstX, dstY, dstWidth, dstHeight, u, v, u2, v2);
}
public void drawRegion(TextureRegion region, float srcX, float srcY, float srcWidth, float srcHeight, float dstX, float dstY) {
drawRegion(region, srcX, srcY, srcWidth, srcHeight, dstX, dstY, srcWidth, srcHeight);
}
public void drawRegion(TextureRegion region, float srcX, float srcY, float srcWidth, float srcHeight,
float dstX, float dstY, float dstWidth, float dstHeight) {
drawRegion(region.getTexture(), region.getRegionX() + srcX, region.getRegionY() + srcY,
srcWidth, srcHeight, dstX, dstY, dstWidth, dstHeight);
}
public void draw(ITexture tex, float x, float y) {
draw(tex, x, y, tex.getWidth(), tex.getHeight());
}
public void draw(ITexture tex, float x, float y, float width, float height) {
draw(tex, x, y, width, height, tex.getU(), tex.getV(), tex.getU2(), tex.getV2());
}
public void draw(ITexture tex, float x, float y, float originX, float originY, float rotationRadians) {
draw(tex, x, y, tex.getWidth(), tex.getHeight(), originX, originY, rotationRadians);
}
public void draw(ITexture tex, float x, float y, float width, float height,
float originX, float originY, float rotationRadians) {
draw(tex, x, y, width, height, originX, originY, rotationRadians, tex.getU(), tex.getV(), tex.getU2(), tex.getV2());
}
public void draw(ITexture tex, float x, float y, float width, float height,
float originX, float originY, float rotationRadians,
float u, float v,
float u2, float v2) {
checkFlush(tex);
final float r = color.r;
final float g = color.g;
final float b = color.b;
final float a = color.a;
float x1,y1, x2,y2, x3,y3, x4,y4;
if (rotationRadians != 0) {
float scaleX = 1f;//width/tex.getWidth();
float scaleY = 1f;//height/tex.getHeight();
float cx = originX*scaleX;
float cy = originY*scaleY;
float p1x = -cx;
float p1y = -cy;
float p2x = width - cx;
float p2y = -cy;
float p3x = width - cx;
float p3y = height - cy;
float p4x = -cx;
float p4y = height - cy;
final float cos = (float) Math.cos(rotationRadians);
final float sin = (float) Math.sin(rotationRadians);
x1 = x + (cos * p1x - sin * p1y) + cx; // TOP LEFT
y1 = y + (sin * p1x + cos * p1y) + cy;
x2 = x + (cos * p2x - sin * p2y) + cx; // TOP RIGHT
y2 = y + (sin * p2x + cos * p2y) + cy;
x3 = x + (cos * p3x - sin * p3y) + cx; // BOTTOM RIGHT
y3 = y + (sin * p3x + cos * p3y) + cy;
x4 = x + (cos * p4x - sin * p4y) + cx; // BOTTOM LEFT
y4 = y + (sin * p4x + cos * p4y) + cy;
} else {
x1 = x;
y1 = y;
x2 = x+width;
y2 = y;
x3 = x+width;
y3 = y+height;
x4 = x;
y4 = y+height;
}
// top left, top right, bottom left
vertex(x1, y1, r, g, b, a, u, v);
vertex(x2, y2, r, g, b, a, u2, v);
vertex(x4, y4, r, g, b, a, u, v2);
// top right, bottom right, bottom left
vertex(x2, y2, r, g, b, a, u2, v);
vertex(x3, y3, r, g, b, a, u2, v2);
vertex(x4, y4, r, g, b, a, u, v2);
}
public void draw(ITexture tex, float x, float y, float width, float height, float u, float v,
float u2, float v2) {
draw(tex, x, y, width, height, x, y, 0f, u, v, u2, v2);
}
/** Renders a texture using custom vertex attributes; e.g. for different
* vertex colours. This will ignore the current batch color and "x/y translation",
* as well as the U/V coordinates of the given ITexture.
*
* @param tex the texture to use
* @param vertices an array of 6 vertices, each holding 8 attributes (total
* = 48 elements)
* @param offset the offset from the vertices array to start from */
public void draw(ITexture tex, float[] vertices, int offset) {
checkFlush(tex);
data.put(vertices, offset, data.getTotalNumComponents() * 6);
idx += 6;
}
VertexData vertex(float x, float y, float r, float g, float b, float a, float u, float v) {
data.put(x).put(y).put(r).put(g).put(b).put(a).put(u).put(v);
idx++;
return data;
}
protected void checkFlush(ITexture sprite) {
if (sprite == null || sprite.getTexture()==null)
throw new NullPointerException("null texture");
// we need to bind a different texture/type. this is
// for convenience; ideally the user should order
// their rendering wisely to minimize texture binds
if (sprite.getTexture() != this.texture || idx >= maxIndex) {
// apply the last texture
flush();
this.texture = sprite.getTexture();
}
}
private void render() {
if (texture != null)
texture.bind();
data.bind();
data.draw(GL_TRIANGLES, 0, idx);
data.unbind();
renderCalls++;
}
}