Spritebatch

mattdesl edited this page Dec 16, 2012 · 5 revisions

Below is the full source of our simple sprite batcher. See the repo for a more complete implementation, including better documentation.

This page is a work in progress.

/**
 * 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 static org.lwjgl.opengl.GL20.glUniform1i;

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.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, 1);\n" +
			"}";

	public static final String DEFAULT_FRAG_SHADER =
			"uniform sampler2D "+U_TEXTURE+";\n" +
			"varying vec4 vColor;\n" +
			"varying vec2 vTexCoord;\n" +
			"void main(void) {\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;
	protected Matrix4f viewMatrix;
	protected Matrix4f projViewMatrix;
	protected Matrix4f transpositionPool;

	protected Texture texture;
	protected ShaderProgram program;

	protected VertexData data;

	private int idx;
	private int maxIndex;

	private float r=1f, g=1f, b=1f, a=1f;
	private boolean drawing = false;

	static ShaderProgram getDefaultShader() {
		return defaultShader==null
				? new ShaderProgram(DEFAULT_VERT_SHADER, DEFAULT_FRAG_SHADER, ATTRIBUTES)
				: defaultShader;
	}

	public SpriteBatch(ShaderProgram program, int size) {
		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;

		updateMatrices();
	}

	public SpriteBatch(int size) {
		this(getDefaultShader(), size);
	}

	public SpriteBatch() {
		this(1000);
	}

	public Matrix4f getViewMatrix() {
		return viewMatrix;
	}

	public void setColor(float r, float g, float b, float a) {
		this.r = r;
		this.g = g;
		this.b = b;
		this.a = a;
	}

	/**
	 * Call to multiply the the projection with the view matrix and save
	 * the result in the uniform mat4 {@value #U_PROJ_VIEW}.
	 */
	public void updateMatrices() {
		// Create projection matrix:
		projMatrix = MathUtil.toOrtho2D(projMatrix, 0, 0, Display.getWidth(), Display.getHeight());
		// Create view Matrix, if not present:
		if (viewMatrix == null) {
			viewMatrix = new Matrix4f();
		}
		// Multiply the transposed projection matrix with the view matrix:
		projViewMatrix = Matrix4f.mul(
				Matrix4f.transpose(projMatrix, transpositionPool),
				viewMatrix,
				projViewMatrix);

		program.use();

		// Store the the multiplied matrix in the "projViewMatrix"-uniform:
		program.storeUniformMat4(U_PROJ_VIEW, projViewMatrix, false);

		//upload texcoord 0
		int tex0 = program.getUniformLocation(U_TEXTURE);
		if (tex0!=-1)
			glUniform1i(tex0, 0);
	}

	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.width;
		float v = srcY / tex.height;
		float u2 = (srcX+srcWidth) / tex.width;
		float v2 = (srcY+srcHeight) / tex.height;
		draw(tex, dstX, dstY, dstWidth, dstHeight, u, v, u2, v2);
	}

	public void draw(Texture tex, float x, float y) {
		draw(tex, x, y, tex.width, tex.height);
	}

	public void draw(Texture tex, float x, float y, float width, float height) {
		draw(tex, x, y, width, height, 0, 0, 1, 1);
	}

	public void draw(Texture tex, float x, float y, float width, float height,
			float u, float v, float u2, float v2) {
		checkFlush(tex);

		//top left, top right, bottom left
		vertex(x, y, r, g, b, a, u, v);
		vertex(x+width, y, r, g, b, a, u2, v);
		vertex(x, y+height, r, g, b, a, u, v2);

		//top right, bottom right, bottom left
		vertex(x+width, y, r, g, b, a, u2, v);
		vertex(x+width, y+height, r, g, b, a, u2, v2);
		vertex(x, y+height, r, g, b, a, u, v2);
	}


	/**
	 * Renders a texture using custom vertex attributes; e.g. for different vertex colours.
	 * This will ignore the current batch color.
	 *
	 * @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(Texture 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(Texture texture) {
		if (texture==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 (texture!=this.texture || idx >= maxIndex) {
			//apply the last texture
			flush();
			this.texture = texture;
		}
	}

	private void render() {
		if (texture!=null)
			texture.bind();
		data.bind();
		data.draw(GL_TRIANGLES, 0, idx);
		data.unbind();
		renderCalls++;
	}


}