SpriteBatch类详解
在之前所有的例子中,涉及到画图我们都是使用SpriteBatch来处理的,这里把SpriteBatch详细说明一下。
绘图
图片在存储时通常具有一定的格式,我们这里只说png类型,当一个png的文件被读入到GPU(图形处理器)后,我们称其为texture(纹理)。为了在屏幕上画一张图(一个纹理),我们需要设定几何图形来与该图片(纹理)进行对应,比如我们可以设定一个矩形,然后图片的四个角与该矩形的四个角对应。如果这个矩形只占该图片的一部分,我们称其为texture region(纹理区域),这个概念在我们从一个精灵集中摘取一部分图像十分有用。
在实际进行绘制时,首先纹理需要被装载(bind),然后需要告诉OpenGL一个几何形状,该纹理所占的大小和位置需要根据该纹理的几何形状与viewport的设定来处理。许多2D游戏都将viewport设定为与屏幕的像素一致,也就是说,几何图形是以像素为单位的,这样十分容易处理图像在屏幕上的大小和位置。
实际应用中,绘制一个矩形的图像是十分常见的,反复绘制同一个矩形或图像的某一个部分也是很常见的。因此,如果每次需要绘制的时候都向GPU发送矩形信息是没有效率的,相反,我们可以将描述同一个纹理的全部矩形信息整理好,然后一次性发给GPU,这样就可以大大提高GPU的效率。SpriteBatch类就是用来做这个打包的动作。
我们把每个需要绘制的纹理及其坐标发给SpriteBatch类的对象,该对象会收集所有的几何图形信息,如果最新的纹理与上一次的不一样,它会更新为最新的纹理信息,然后统一发送给GPU进行绘制,然后再次收集下一次需要绘制的纹理信息。
每次更改少量的矩形会导致SpriteBatch无法大量的预存更多几何图形,在代码中,频繁的读取图像文件也是不被建议的,通常的做法是,将大量图形素材打包成一个图像文件,然后每次仅绘制其中几个区域,以此来最大的利用SpriteBatch的能力。打包图像请参考TextruePacker。
SpriteBatch
在应用中使用SpriteBatch代码如下
public class Game implements ApplicationListener {
private SpriteBatch batch;
public void create () {
batch = new SpriteBatch();
}
public void render () {
Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT); // This cryptic line clears the screen.
batch.begin();
// Drawing goes here!
batch.end();
}
public void resize (int width, int height) { }
public void pause () { }
public void resume () { }
public void dispose () { }
}
所有SpriteBatch的绘图函数,必须放在begin和end函数之间,在begin和end之间不能放非SpriteBatch的其他绘图方法。
Texture
Texture类用来读取一个图像文件,将其load到GPU的内存中,该图像文件应该放到assets目录下,每个图像的尺寸必须是2的幂,比如16x16, 16x256,最大不建议超过1024x1024
private Texture texture;
...
texture = new Texture(Gdx.files.internal("image.png"));
...
batch.begin();
batch.draw(texture, 10, 10);
batch.end();
以上代码就是读取image.png文件,并将其在(10,10)的位置上绘制出来,其宽、高与图形的实际尺寸相等。SpriteBatch的draw支持以下调用方式绘图
| `draw(Texture texture, float x, float y)` |
最简单的调用方式,按照纹理实际的宽高在(x,y)位置绘图
| `draw(Texture texture, float x, float y, int srcX, int srcY, int srcWidth, int srcHeight)` |
仅绘制一部分
| `draw(Texture texture, float x, float y, float width, float height, int srcX, int srcY, int srcWidth, int srcHeight, boolean flipX, boolean flipY)` |
仅绘制一部分,但是该部分会被缩放到指定大小,并支持翻转
| `draw(Texture texture, float x, float y, float originX, float originY, float width, float height, float scaleX, float scaleY, float rotation,int srcX, int srcY, int srcWidth, int srcHeight,boolean flipX, boolean flipY)` |
仅绘制一部分,并对该部分进行缩放到指定宽,高,可以选则是否翻转,并且进行比例缩放和旋转
| `draw(Texture texture, float x, float y, float width, float height, float u, float v, float u2, float v2)` |
功能是绘制一部分,并缩放到指定的width和height,但是方法使用了纹理坐标(0~1),而不是像素单位
| `draw(Texture texture, float[] spriteVertices, int offset, int length)` |
这里使用了更底层的处理方法,可以用来绘制任意的四边形,不单单是矩形绘制
TextureRegion
TextureRegion类表示了一个图像(纹理)里的一个矩形,通常被用来绘制一个大图形的某一个部分。
private TextureRegion region;
...
texture = new Texture(Gdx.files.internal("image.png"));
region = new TextureRegion(texture, 20, 20, 50, 50);
...
batch.begin();
batch.draw(region, 10, 10);
batch.end();
这里20,20,50,50描述了图像的某一个部分,该部分会被绘制到(10,10)位置。使用Texture类可以实现同样的效果,但是使用TextureRegion显得更加方便和直观。
SpriteBatch对于TextureRegion的绘制同样提供了多种支持
| `draw(TextureRegion region, float x, float y)` |
使用区域的宽、高在(x,y)位置绘制
| `draw(TextureRegion region, float x, float y, float width, float height)` |
绘制该区域,并且拉伸到指定的width和height
| `draw(TextureRegion region, float x, float y, float originX, float originY, float width, float height, float scaleX, float scaleY, float rotation)` |
绘制该区域,并拉伸到指定width和height,同时按照origin坐标进行比例调整和旋转
Sprite
Sprite类在TextureRegion的基础上,又增加了几何形状和颜色属性
private Sprite sprite;
...
texture = new Texture(Gdx.files.internal("image.png"));
sprite = new Sprite(texture, 20, 20, 50, 50);
sprite.setPosition(10, 10);
sprite.setRotation(45);
...
batch.begin();
sprite.draw(batch);
batch.end();
这段代码将image.png读进显存,然后取出其中一块区域(20,20,50,50),然后将其旋转45度,绘制到(10,10)位置,用Texture和TextureRegion都可实现该动作,但是使用Sprite可以更加直观和方便。除此之外,spite还会存储纹理的几何形状,这样仅仅在需要的时候才会重新计算几何形状,这样,如果每一帧中,比例尺,旋转以及其他属性不变化的话,使用sprite会更加高效。
注意的是,使用Sprite会混合model(位置,旋转)和view(纹理本身)的信息,因此如果坚持使用mvc分离的设计模式,那么使用sprite就不太方便了,这时应该使用Texture或TextureRegion。
另一个需要注意的是,Sprite构造函数没有与位置信息相关的参数,在调用Sprite(Texture,int, int,int,int)是不会处理任何位置信息。你必须通过调用setPosition(float,float)来处理位置信息,否则sprite会绘制到(0,0)位置。
Tint(上色)
当绘制一个纹理(texture)时,可以为它上额外的颜色(tinting)
private Texture texture;
private TextureRegion region;
private Sprite sprite;
...
texture = new Texture(Gdx.files.internal("image.png"));
region = new TextureRegion(texture, 20, 20, 50, 50);
sprite = new Sprite(texture, 20, 20, 50, 50);
sprite.setPosition(100, 10);
sprite.setColor(0, 0, 1, 1);
...
batch.begin();
batch.setColor(1, 0, 0, 1);
batch.draw(texture, 10, 10);
batch.setColor(0, 1, 0, 1);
batch.draw(region, 50, 10);
sprite.draw(batch);
batch.end();
这段例子显示了,如何对Texture,TextureRegion和Sprite进行上色,颜色的描述是按照RGBA,四个在0到1之间的数,如果没有使能blending,A值被忽略。
Blending
Blending缺省是使能的,这意为着在绘制一个图像时,该图像中透明的部分会直接与屏幕上已有的纹理重合,显示出屏幕上已有的图像。
如果关闭Blending,那么屏幕上原有的图像会被新的纹理替代,这样在绘制背景时会更有效率,在实际应用中,也要尽量可能关闭blending,除非你确认需要这个效果。
在下面的例子中,在开始绘制时关闭blending会大大提升性能
Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT); // This cryptic line clears the screen.
batch.begin();
batch.disableBlending();
backgroundSprite.draw(batch);
batch.enableBlending();
// Other drawing here.
batch.end();
注意:在绘制开始前,最好做一个清屏Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);,如果不这样做,反复绘制一个具有alpha属性的图像会导致成百个图像的重叠,最后图像越来越不清晰。另外,一些GPU也倾向与在干净的屏幕上绘制,这样会获得更好的性能。
Viewport
SpriteBatch管理着自己的投射和变换矩阵,当创建一个SpriteBatch时,它会使用当前应用/游戏的设定来建立一个orthographic 投射,使用y轴向上的坐标系(原点在左下角),当调用Begin时,SpriteBatch会建立viewport。
性能优化
SpriteBatch提供了一个构造函数,来设定在向GPU发送前缓存的最大sprite数量,这个值如果太低,会导致向GPU额外的调用,如果太高,会浪费显存。
SpriteBatch具有一个公共的int 对象maxSpritesInBatch,该值记录了该SpriteBatch整个生命周期中向GPU发送的最高Sprites数量(峰值),首先设定一个较大的SpriteBatch,然后通过检查该maxSpritesInBatch来进行调整,可以获得更合理的设置。该值可以在任意时候设为0来进行复位。
SprintBatch具有一个公共的int对象renderCalls,在结束end调用后,该值记录了上一个begin到end周期中,向GPU发送了多少次渲染调用。该值仅仅在图像被装载(bind)或者当SpriteBatch没有被充满时存在。如果SpriteBatch是大小合适的,而renderCalls偏大(超过15~20),表示存在了过多的图像装载。
SpriteBatch具有另外一个构造函数,接受大小和缓冲数量为参数,这是一种更接近底层的用法,可以使用VBO而不是使用传统的VA。会保留一个缓冲区列表,每次的render会使用该列表的下一个缓冲区。当maxSpriteInBatch较低,renderClalls很大时,使用这种方法可以极大的改善性能。