这个系列教程 是https://github.com/mattdesl/lwjgl-basics/wiki的系列翻译。
精灵批处理(Sprite Batching)
若是咱们用上一节讲到的debugTexture
去渲染全部的图形,那么咱们立刻就会遇到性能问题。这是由于咱们每次只把一个精灵发送到GPU,因此咱们要在一次draw call中把精灵批量发送到GPU。这样的话咱们就要用到SpriteBatch
;git
介绍
咱们上一节讲到,一个精灵只是肯定一个矩形的一些系列顶点。这里的每一个定点有包括若干属性:github
Position(x, y) - 在屏幕上的位置 TexCoord(s, t) - 纹理的取样坐标 Color(r, g, b, a) - 定点的颜色和透明度
大多数的SpriteBatch的代码都是很易用的。可能在你游戏的代码中是这样的:segmentfault
//called on game creation public void create() { //create a single batcher we will use throughout our application spriteBatch = new SpriteBatch(); } //called on frame render public void render() { //prepare the batch for rendering spriteBatch.begin(); //draw all of our sprites spriteBatch.draw(mySprite1, x, y); spriteBatch.draw(mySprite2, x, y); ... //end the batch, flushing the data to GPU spriteBatch.end(); } //called when the display is resized public void resize(int width, int height) { //notify the sprite batcher whenever the screen changes spriteBatch.resize(width, height); }
首先咱们调用begin()
,而后调用 spriteBatch.draw(...)
把精灵的顶点信息(坐标,纹理坐标,颜色)放入一个很是大的栈里。
顶点在下列状况发生前是不会被传入GPU的:app
end
(),或者别的函数刷新batch 好比flush
()Sprite
他的Texture
和上一个Texture
不是同一个的时候batch
是被刷新的。这些是写一个SpriteBatch以前必需要知道的。如上边所说,用多个Texture
会致使多个draw call。这就是为何咱们老是推荐利用Texture atlas
(纹理集),它可让咱们渲染多个sprite
只用用一个draw call。ide
TextureRegion
像咱们上面讨论的。在用Texture atlas
的时候咱们能够得到更好的性能。那么当咱们用要画这个纹理的一部分的时候咱们要用到TextureRegion
这个工具类。它容许咱们在像素级别去指定图片中咱们要渲染区域的位置,长和宽。让咱们来举个例子。下图高亮区域就是咱们要渲染的sprite.
咱们能够用下面的代码去获得一个TextureRegion:函数
//specify x, y, width, height of tile region = new TextureRegion(64, 64, 64, 64);
如咱们看到的TextureRegion
使咱们能够得到一个子图片而没必要关心他的纹理坐标。咱们用下面的代码去渲染一个TextureRegion
:工具
... inside SpriteBatch begin / end ... spriteBatch.draw(region, x, y);
顶点颜色(Vertex Color)
咱们经过调整batch的颜色去改变sprites的颜色(顶点颜色)。
咱们用RGBA赋值的方式来描述颜色,那么(1, 1, 1, 1)表明白色,(1, 0, 0, 1)表明红色,A值用来调整不透明度。性能
spriteBatch.begin(); //draw calls will now use 50% opacity spriteBatch.setColor(1f, 1f, 1f, 0.5f); spriteBatch.draw(...); spriteBatch.draw(...); //draw calls will now use 100% opacity (default) spriteBatch.setColor(1f, 1f, 1f, 1f); spriteBatch.draw(...); spriteBatch.end();
是三角型不是四边形(Triangles, not Quads!)
在以前的学习中咱们可能认为纹理是四边形的,其实在真实的环境当中大部分sprite batchers是用两个三角形去表明一个四边形。顶点的存储顺序是依赖具体引擎的,可是具体概念基本是以下图的:
一个单独的sprite包含两个三角形或者说包含6个顶点。每一个顶点又包含8个属性(X, Y, S, T, R, G, B, A),它们分别表明坐标,纹理坐标和颜色。这表示一个sprite就要把48个浮点数放入栈中。一些优化的sprite batching 可能会把48个值合并到一个浮点数当中,或者把表明颜色的四个浮点数合并到一个浮点数中。
Code学习
下面是一个SpriteBatch的实际代码,其中有一些shader的代码咱们之后会讲到:优化
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++; } }