/* * Copyright (c) 2003-onwards Shaven Puppy Ltd * 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 'Shaven Puppy' nor the names of its 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 OWNER 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 com.shavenpuppy.jglib.sprites; import java.nio.ByteBuffer; import java.nio.FloatBuffer; import java.nio.ShortBuffer; import org.lwjgl.opengl.GLContext; import org.lwjgl.util.ReadableColor; import org.lwjgl.util.vector.Vector2f; import com.shavenpuppy.jglib.Resource; import com.shavenpuppy.jglib.algorithms.RadixSort; import com.shavenpuppy.jglib.opengl.GLBaseTexture; import com.shavenpuppy.jglib.opengl.GLVertexBufferObject; import com.shavenpuppy.jglib.util.FPMath; import com.shavenpuppy.jglib.util.FastMath; import com.shavenpuppy.jglib.util.FloatList; import com.shavenpuppy.jglib.util.ShortList; import static org.lwjgl.opengl.ARBBufferObject.*; import static org.lwjgl.opengl.ARBVertexBufferObject.*; import static org.lwjgl.opengl.GL11.*; import static org.lwjgl.opengl.GL12.*; /** * A default sprite renderer. This sorts incoming sprites by layer, Z, then * optionally by Y coordinate, state, then texture. and stashes vertex, texture, * and colour coordinates in a buffer. The sprites are rendered by OpenGL11. */ class DefaultSpriteRenderer extends Resource implements SpriteRenderer { private static final long serialVersionUID = 1L; private static final float PREMULT_ALPHA = 1.0f / 255.0f; /** Scratch vector */ private static final Vector2f offset = new Vector2f(); /** * Ring buffer, used by all the sprite engines. */ private static class RingBuffer { private static final int DEFAULT_STATE_RUNS = 1024; private class StateRun { GLBaseTexture texture0; Style style; int start, length; ShortBuffer indices; int startIndex, endIndex; void render() { if (style != lastRenderedStyle) { if (lastRenderedStyle != null) { lastRenderedStyle.resetState(); } if (style != null) { style.setupState(); } lastRenderedStyle = style; } if (texture0 != lastRenderedTexture0) { if (texture0 != null) { texture0.render(); } lastRenderedTexture0 = texture0; } if (length == 0) { return; } if (style.getRenderSprite()) { glDrawRangeElements(GL_TRIANGLES, start, start + length, endIndex - startIndex, GL_UNSIGNED_SHORT, startIndex * 2); } else { style.render(start, startIndex); } } } int bufferSize, bufferSizeInVertices, indexBufferSize; int numBuffers; GLVertexBufferObject[] vbo, ibo; FloatBuffer[] vertices; ByteBuffer[] verticesBytes; ShortBuffer[] indices; ByteBuffer[] indicesBytes; int sequence = -1, mark = -1; FloatBuffer currentVertices; ShortBuffer currentIndices; StateRun[] stateRun; GLBaseTexture lastRenderedTexture0, currentTexture0; Style lastRenderedStyle, currentStyle; int vertexCursor, indexCursor; int numRuns; StateRun currentRun; RingBuffer() { } void create() { bufferSize = 1024 * 256; bufferSizeInVertices = bufferSize / VERTEX_SIZE; numBuffers = 8; indexBufferSize = bufferSizeInVertices * 3; vertices = new FloatBuffer[numBuffers]; verticesBytes = new ByteBuffer[numBuffers]; indices = new ShortBuffer[numBuffers]; indicesBytes = new ByteBuffer[numBuffers]; vbo = new GLVertexBufferObject[numBuffers]; ibo = new GLVertexBufferObject[numBuffers]; for (int i = 0; i < numBuffers; i ++) { vbo[i] = new GLVertexBufferObject(bufferSize, GL_ARRAY_BUFFER_ARB, GL_STREAM_DRAW_ARB); vbo[i].create(); ibo[i] = new GLVertexBufferObject(indexBufferSize * 2, GL_ELEMENT_ARRAY_BUFFER_ARB, GL_STREAM_DRAW_ARB); ibo[i].create(); } stateRun = new StateRun[DEFAULT_STATE_RUNS]; for (int i = 0; i < stateRun.length; i ++) { stateRun[i] = new StateRun(); } } private void next() { // Select the next buffer in the sequence. sequence ++; if (sequence == numBuffers) { sequence = 0; } if (sequence == mark) { System.out.println("Buffer overrun"); } vbo[sequence].render(); ibo[sequence].render(); ByteBuffer buf = vbo[sequence].map(); if (vertices[sequence] == null || verticesBytes[sequence] != buf) { verticesBytes[sequence] = buf; vertices[sequence] = buf.asFloatBuffer(); } ByteBuffer ibuf = ibo[sequence].map(); if (indices[sequence] == null || indicesBytes[sequence] != ibuf) { indicesBytes[sequence] = ibuf; indices[sequence] = ibuf.asShortBuffer(); } currentVertices = vertices[sequence]; currentVertices.clear(); currentIndices = indices[sequence]; currentIndices.clear(); glVertexPointer(2, GL_FLOAT, VERTEX_SIZE, 0); glTexCoordPointer(2, GL_FLOAT, VERTEX_SIZE, TEXTURE0_COORD_OFFSET); glColorPointer(4, GL_UNSIGNED_BYTE, VERTEX_SIZE, COLOR_OFFSET); vertexCursor = 0; indexCursor = 0; numRuns = 0; currentRun = null; } void begin() { currentStyle = null; currentTexture0 = null; next(); mark = sequence; glEnableClientState(GL_VERTEX_ARRAY); } void finish() { // Render out what's left over render(); glBindBufferARB(GL_ARRAY_BUFFER_ARB, 0); glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, 0); } void growStateRuns() { StateRun[] newStateRun = new StateRun[(int)(stateRun.length * 1.5f)]; System.arraycopy(stateRun, 0, newStateRun, 0, stateRun.length); for (int i = numRuns; i < newStateRun.length; i ++) { newStateRun[i] = new StateRun(); } stateRun = newStateRun; } void add(Sprite s, Style newStyle, float engineAlpha) { if (vertexCursor + 4 > bufferSizeInVertices || indexCursor + 6 > indexBufferSize) { // Sprite won't fit, so flush first render(); next(); } SpriteImage image = s.getImage(); GLBaseTexture newTexture0 = image != null ? image.getTexture() : null; if (currentRun == null || (newStyle != currentStyle && (currentStyle == null || newStyle.getStyleID() != currentStyle.getStyleID())) || newTexture0 != currentTexture0) { // Changed state. Start new state. currentRun = stateRun[numRuns]; currentRun.start = vertexCursor; currentRun.length = 0; currentRun.style = newStyle; currentRun.texture0 = newTexture0; currentRun.indices = currentIndices; currentRun.startIndex = indexCursor; currentRun.endIndex = indexCursor; numRuns ++; if (numRuns == stateRun.length) { // Grow the array growStateRuns(); } currentStyle = newStyle; currentTexture0 = newTexture0; } final float w = image.getWidth(); final float h = image.getHeight(); final float tx0 = image.getTx0(); final float tx1 = image.getTx1(); final float ty0 = image.getTy0(); final float ty1 = image.getTy1(); final int xscale = s.getXScale(); // 16 bits fraction final int yscale = s.getYScale(); // 16 bits fraction s.getOffset(offset); final float x = s.getX() + offset.getX(); final float y = s.getY() + offset.getY(); final int alpha = (int) (engineAlpha * s.getAlpha()); final float angle = FPMath.floatValue(s.getAngle()) * FastMath.TAU; // First scale then rotate coordinates float scaledx0 = -image.getHotspotX(); float scaledy0 = -image.getHotspotY(); float scaledx1 = scaledx0 + w; float scaledy1 = scaledy0 + h; // Scale 'em first if (xscale != NO_SCALE || yscale != NO_SCALE) { float fxScale = FPMath.floatValue(xscale); float fyScale = FPMath.floatValue(yscale); scaledx0 = scaledx0 * fxScale; scaledx1 = scaledx1 * fxScale; scaledy0 = scaledy0 * fyScale; scaledy1 = scaledy1 * fyScale; } float scaledx00, scaledx10, scaledx11, scaledx01, scaledy00, scaledy10, scaledy11, scaledy01; // Then rotate if (angle != 0) { float cos = (float) Math.cos(angle); float sin = (float) Math.sin(angle); scaledx00 = cos * scaledx0 - sin * scaledy0; scaledx10 = cos * scaledx1 - sin * scaledy0; scaledx11 = cos * scaledx1 - sin * scaledy1; scaledx01 = cos * scaledx0 - sin * scaledy1; scaledy00 = sin * scaledx0 + cos * scaledy0; scaledy10 = sin * scaledx1 + cos * scaledy0; scaledy11 = sin * scaledx1 + cos * scaledy1; scaledy01 = sin * scaledx0 + cos * scaledy1; } else { scaledx00 = scaledx0; scaledx10 = scaledx1; scaledx11 = scaledx1; scaledx01 = scaledx0; scaledy00 = scaledy0; scaledy10 = scaledy0; scaledy11 = scaledy1; scaledy01 = scaledy1; } // Then translate them final float x00 = scaledx00 + x; final float x01 = scaledx01 + x; final float x11 = scaledx11 + x; final float x10 = scaledx10 + x; final float y00 = scaledy00 + y; final float y01 = scaledy01 + y; final float y11 = scaledy11 + y; final float y10 = scaledy10 + y; AlphaOp alphaOp = newStyle.getAlphaOp(); FloatBuffer floats = currentVertices; float mtx = s.isMirrored() ? tx1 : tx0; float fty = s.isFlipped() ? ty0 : ty1; float mtx1 = s.isMirrored() ? tx0 : tx1; float mty1 = s.isFlipped() ? ty1 : ty0; floats.put(x00); floats.put(y00); floats.put(mtx); floats.put(fty); ReadableColor color = s.getColor(0); alphaOp.op(color, alpha, floats); floats.put(x10); floats.put(y10); floats.put(mtx1); floats.put(fty); color = s.getColor(1); alphaOp.op(color, alpha, floats); floats.put(x11); floats.put(y11); floats.put(mtx1); floats.put(mty1); color = s.getColor(2); alphaOp.op(color, alpha, floats); floats.put(x01); floats.put(y01); floats.put(mtx); floats.put(mty1); color = s.getColor(3); alphaOp.op(color, alpha, floats); // Write indices: need 6, for two triangles currentIndices.put((short) vertexCursor); currentIndices.put((short) (vertexCursor + 1)); currentIndices.put((short) (vertexCursor + 2)); currentIndices.put((short) vertexCursor); currentIndices.put((short) (vertexCursor + 2)); currentIndices.put((short) (vertexCursor + 3)); indexCursor += 6; vertexCursor += 4; currentRun.endIndex += 6; currentRun.length += 4; } void add(Style s) { // Build the geometry GeometryData data = s.build(); // Does it fit? FloatList vertexData = data.getVertexData(); ShortList indexData = data.getIndexData(); int vertsToWrite = vertexData.size() / 5; if (vertexCursor + vertsToWrite > bufferSizeInVertices) { // No. Flush what we have so far. // System.out.println(" FLUSH 2"); render(); next(); } // Always create a new run currentRun = stateRun[numRuns]; currentRun.start = vertexCursor; currentRun.length = vertsToWrite; currentRun.style = s; currentRun.texture0 = null; currentRun.startIndex = indexCursor; numRuns ++; if (numRuns == stateRun.length) { growStateRuns(); } // Write the data out currentVertices.position(vertexCursor * VERTEX_SIZE >> 2); currentVertices.put(vertexData.array(), 0, vertexData.size()); currentIndices.position(indexCursor); short[] idx = indexData.array(); int indicesToWrite = indexData.size(); // Note that this irritation will go away once glDrawRangeElementsOffset is available generally... for (int i = 0; i < indicesToWrite; i ++) { idx[i] += vertexCursor; } currentIndices.put(idx, 0, indicesToWrite); vertexCursor += vertsToWrite; indexCursor += indicesToWrite; currentStyle = null; currentTexture0 = null; } void render() { //System.out.println(" RENDER "+numRuns+" RUNS"); vbo[sequence].unmap(); ibo[sequence].unmap(); lastRenderedStyle = null; lastRenderedTexture0 = null; for (int i = 0; i < numRuns; i ++) { stateRun[i].render(); } if (lastRenderedStyle != null) { lastRenderedStyle.resetState(); } } } private static RingBuffer ringBuffer; /** A radix sorter for sorting */ private final RadixSort sort = new RadixSort(); /** Handy constant */ private static final int NO_SCALE = FPMath.ONE; /** Debug */ private static final boolean DEBUG = true; /** Alpha */ private float alpha; /** Whether sprites are unique (rendered only once) */ private final boolean uniqueSprites; /* * Arrays for sorting */ private int[] sort_y; private int[] sort_x; private int[] sort_layer; private int[] sort_sublayer; private int[] sort_style; private int[] sort_texture0; /** Whether to sort Y coords */ private final boolean sortY; /** The sprites being rendered */ private Sprite[] sprite; /** Sprite styles */ private Style[] style; /** The number of sprites being rendered */ private int numSprites; /** Sort layer - sprites above this layer won't be Y sorted */ private int sortLayer; /** The size of a single vertex in bytes: x,y,tx,ty,r,g,b,a */ static final int VERTEX_SIZE = 20; static final int TEXTURE0_COORD_OFFSET = 8; static final int COLOR_OFFSET = 16; /** * Constructor for SpriteRenderer. * @param sortY * sort the sprites by Y coordinate * @param sortLayer TODO * @param uniqueSprites * If true, then you may submit a sprite more than once to the * render() function; this comes at some penalty in speed and * memory bandwidth. If false, then each sprite may be rendered * only once. Set to true if you are doing scrolling and need to * render sprites multiple times. */ DefaultSpriteRenderer(boolean sortY, int sortLayer, boolean uniqueSprites) { this.sortY = sortY; this.sortLayer = sortLayer; this.uniqueSprites = uniqueSprites; sprite = new Sprite[1]; if (uniqueSprites) { for (int i = 0; i < sprite.length; i++) { sprite[i] = new Sprite(null); } } style = new Style[1]; // Initialize radix sort sort_layer = new int[1]; sort_sublayer = new int[1]; sort_texture0 = new int[1]; sort_style = new int[1]; if (sortY) { sort_y = new int[1]; sort_x = new int[1]; } else { sort_y = null; sort_x = null; } sort.resetIndices(); } @Override protected void doCreate() { if (ringBuffer == null) { ringBuffer = new RingBuffer(); ringBuffer.create(); } // Ensure the flash style is created too if (FlashStyle.instance == null) { FlashStyle.instance = new FlashStyle("flash.style"); FlashStyle.instance.create(); } } @Override protected void doDestroy() { } @Override public void render(Sprite s) { if (!s.isVisible()) { return; } SpriteImage image = s.getImage(); Style spriteStyle = s.getStyle(); if (image == null && spriteStyle == null) { return; // No point in doing anything with this sprite } // Check for flashing sprites, which overrides their default image rendering style with the FlashingStyle: if (s.isFlashing() && GLContext.getCapabilities().GL_EXT_secondary_color) { // Only need to render the sprite once, which will make it stark // white: addSprite(FlashStyle.instance, s); // if (uniqueSprites) { // Sprite copy = sprite[numSprites++]; // copy.copy(s); // } else { // sprite[numSprites++] = s; // } } else { addSprite(spriteStyle, s); // style[numSprites] = spriteStyle; // if (uniqueSprites) { // sprite[numSprites++].copy(s); // } else { // sprite[numSprites++] = s; // } } } private void addSprite(Style spriteStyle, Sprite s) { if (numSprites == sprite.length) { if (sortY) { int[] old_sort_y = sort_y; sort_y = new int[numSprites * 2]; System.arraycopy(old_sort_y, 0, sort_y, 0, numSprites); old_sort_y = null; int[] old_sort_x = sort_x; sort_x = new int[numSprites * 2]; System.arraycopy(old_sort_x, 0, sort_x, 0, numSprites); old_sort_x = null; } int[] old_sort_layer = sort_layer; sort_layer = new int[numSprites * 2]; System.arraycopy(old_sort_layer, 0, sort_layer, 0, numSprites); old_sort_layer = null; int[] old_sort_sublayer = sort_sublayer; sort_sublayer = new int[numSprites * 2]; System.arraycopy(old_sort_sublayer, 0, sort_sublayer, 0, numSprites); old_sort_sublayer = null; int[] old_sort_style = sort_style; sort_style = new int[numSprites * 2]; System.arraycopy(old_sort_style, 0, sort_style, 0, numSprites); old_sort_style = null; int[] old_sort_texture0 = sort_texture0; sort_texture0 = new int[numSprites * 2]; System.arraycopy(old_sort_texture0, 0, sort_texture0, 0, numSprites); old_sort_texture0 = null; Sprite[] old_sprite = sprite; sprite = new Sprite[numSprites * 2]; System.arraycopy(old_sprite, 0, sprite, 0, numSprites); old_sprite = null; Style[] old_style = style; style = new Style[numSprites * 2]; System.arraycopy(old_style, 0, style, 0, numSprites); old_style = null; } style[numSprites] = spriteStyle; if (uniqueSprites) { Sprite copy = sprite[numSprites]; if (copy == null) { copy = sprite[numSprites] = new Sprite(null); } copy.copy(s); } else { sprite[numSprites] = s; } numSprites ++; } @Override public void postRender() { // If there are no sprites, do nothing if (numSprites == 0) { return; } // Sort sprites first sort(); // Build the texture/style runs up ringBuffer.begin(); build(); ringBuffer.finish(); } /** * Build up a list of texture runs. We scan through the sorted list of * sprites so far and build up runs of sprites which share the same texture. */ private void build() { final int[] index = sort.getIndices(); for (int i = 0; i < numSprites; i++) { int idx = index[i]; Sprite s = sprite[idx]; if (s.isVisible()) { Style newStyle = style[idx]; if (newStyle.getRenderSprite()) { // It's a sprite ringBuffer.add(s, newStyle, alpha); } else { // It's geometry; we build it to find out how many vertices it's got ringBuffer.add(newStyle); } } } } @Override public void preRender() { // Reset everything numSprites = 0; } /** * Sort sprites */ private void sort() { final int n = numSprites; if (sortY) { for (int i = 0; i < n; i++) { final Sprite s = sprite[i]; if (s.isVisible()) { sort_x[i] = (int) s.getX(); sort_layer[i] = s.getLayer(); sort_sublayer[i] = s.getSubLayer(); sort_style[i] = style[i].getStyleID(); if (s.getStyle().getRenderSprite()) { final SpriteImage si = s.getImage(); GLBaseTexture tex = si.getTexture(); sort_texture0[i] = tex == null ? 0 : tex.getID(); if (s.getLayer() >= sortLayer) { sort_y[i] = 0; } else { sort_y[i] = (int) -(s.getY() + s.getYSortOffset()); } } else { sort_texture0[i] = 0; sort_y[i] = 0; } } } sort.resetIndices().sort(sort_style, n).sort(sort_texture0, n).sort(sort_x, n).sort( sort_sublayer, n).sort(sort_y, n).sort(sort_layer, n); } else { for (int i = 0; i < n; i++) { final Sprite s = sprite[i]; if (s.isVisible()) { sort_layer[i] = s.getLayer(); sort_sublayer[i] = s.getSubLayer(); sort_style[i] = style[i].getStyleID(); if (s.getStyle().getRenderSprite()) { final SpriteImage si = s.getImage(); GLBaseTexture tex = si.getTexture(); sort_texture0[i] = tex == null ? 0 : tex.getID(); } else { sort_texture0[i] = 0; } } } sort.resetIndices().sort(sort_style, n).sort(sort_texture0, n).sort(sort_sublayer, n).sort(sort_layer, n); } } /** * @param alpha * The alpha to set. */ @Override public void setAlpha(float alpha) { this.alpha = alpha; } }