这个系列教程 是https://github.com/mattdesl/lwjgl-basics/wiki的系列翻译。
精灵批处理(Sprite Batching)
如果我们用上一节讲到的debugTexture
去渲染所有的图形,那么我们马上就会遇到性能问题。这是因为我们每次只把一个精灵发送到GPU,所以我们要在一次draw call中把精灵批量发送到GPU。这样的话我们就要用到SpriteBatch
;
介绍
我们上一节讲到,一个精灵只是确定一个矩形的一些系列顶点。这里的每个定点有包括若干属性:
Position(x, y) - 在屏幕上的位置
TexCoord(s, t) - 纹理的取样坐标
Color(r, g, b, a) - 定点的颜色和透明度
大多数的SpriteBatch的代码都是很易用的。可能在你游戏的代码中是这样的:
//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的:
- batch被强制调用
end
(),或者别的函数刷新batch 比如flush
() - 当用户绘制一个
Sprite
他的Texture
和上一个Texture
不是同一个的时候batch
是被刷新的。 - 存顶点的栈已经没有空间,需要刷新重新开始。
这些是写一个SpriteBatch之前必须要知道的。如上边所说,用多个Texture
会导致多个draw call。这就是为什么我们总是推荐利用Texture atlas
(纹理集),它可以让我们渲染多个sprite
只用用一个draw call。
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++;
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。