Texture Atlas纹理图集

Number of views 157

计算机图形学中,TextureAtlas纹理图集(也称为精灵表或图像精灵),它包含了多个较小贴图的图像,通常它们打包在一起以减少整体尺寸。TextureAtlas可以由大小一致的贴图或不同尺寸的图像组成。它通过使用自定义纹理坐标绘制子纹理以便将它们从TextureAtlas中选取出来。

使用TextureAtlas是游戏中优化的一种手段,它能够有效减少GPU纹理内存的开销,并在某些方面提供了更多的灵活性,比如下图用红线圈起来的立方体,它的前后左右四个面使用了相同纹理,但是立方体的顶面与底面的纹理是不同的,如果不使用TextureAtlas时,那么它需要同时传递3张纹理图作为立方体的贴图,如果有更多立方体需要更多的不同纹理,那么它的开销无疑是巨大的, 而把这些纹理打包进同一个TextureAltas后,那么它们只需要在同一个着色器中开辟一个纹理存储单元即可。

要使用TextureAtlas,必须要有创建TextureAtlas的工具,比较常用的工具就是TexturePacker了,当然,一些引擎也有适用于自己工具链的图集打包工具,比如Unity的Sprite Packer。

以TexturePacker举例,打包出来的TextureAltas除了有图集图片文件外,还会打包额外的图集描述文件,这个描述文件是很重要的,它记录了TextureAltas图集内部每个小图片的位置与大小等信息。当然,TexturePacker的图集描述文件可以打包出不同的文件格式,比如JSON,JSON Hash,XML等。这里以Cocos-2DX的XML文件为例:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>frames</key>
        <dict>
            <key>Calendar.png</key>
            <dict>
                <key>aliases</key>
                <array/>
                <key>spriteOffset</key>
                <string>{0,0}</string>
                <key>spriteSize</key>
                <string>{46,46}</string>
                <key>spriteSourceSize</key>
                <string>{64,64}</string>
                <key>textureRect</key>
                <string>{{73,37},{46,46}}</string>
                <key>textureRotated</key>
                <false/>
            </dict>
            <key>Editor_Arrow_Double_Left.png</key>
            <dict>
                <key>aliases</key>
                <array/>
                <key>spriteOffset</key>
                <string>{-1,0}</string>
                <key>spriteSize</key>
                <string>{54,34}</string>
                <key>spriteSourceSize</key>
                <string>{64,64}</string>
                <key>textureRect</key>
                <string>{{1,1},{54,34}}</string>
                <key>textureRotated</key>
                <false/>
            </dict>
            <key>Editor_Arrow_Double_Right.png</key>
            <dict>
                <key>aliases</key>
                <array/>
                <key>spriteOffset</key>
                <string>{0,0}</string>
                <key>spriteSize</key>
                <string>{54,34}</string>
                <key>spriteSourceSize</key>
                <string>{64,64}</string>
                <key>textureRect</key>
                <string>{{57,1},{54,34}}</string>
                <key>textureRotated</key>
                <false/>
            </dict>
            <key>Editor_Arrow_Left.png</key>
            <dict>
                <key>aliases</key>
                <array/>
                <key>spriteOffset</key>
                <string>{0,0}</string>
                <key>spriteSize</key>
                <string>{34,34}</string>
                <key>spriteSourceSize</key>
                <string>{64,64}</string>
                <key>textureRect</key>
                <string>{{1,37},{34,34}}</string>
                <key>textureRotated</key>
                <false/>
            </dict>
            <key>Editor_Arrow_Right.png</key>
            <dict>
                <key>aliases</key>
                <array/>
                <key>spriteOffset</key>
                <string>{-1,0}</string>
                <key>spriteSize</key>
                <string>{34,34}</string>
                <key>spriteSourceSize</key>
                <string>{64,64}</string>
                <key>textureRect</key>
                <string>{{37,37},{34,34}}</string>
                <key>textureRotated</key>
                <false/>
            </dict>
        </dict>
        <key>metadata</key>
        <dict>
            <key>format</key>
            <integer>3</integer>
            <key>pixelFormat</key>
            <string>RGBA8888</string>
            <key>premultiplyAlpha</key>
            <false/>
            <key>realTextureFileName</key>
            <string>atlas.png</string>
            <key>size</key>
            <string>{120,84}</string>
            <key>smartupdate</key>
            <string>$TexturePacker:SmartUpdate:c4052c07db9ddedfdfa5d1240cd9c0bf:338a048d3bcb79093a0e9e401bbdb15b:65e8580fbff24d925eb9683d4a596721$</string>
            <key>textureFileName</key>
            <string>atlas.png</string>
        </dict>
    </dict>
</plist>

以frames字段组织数据,说明了TextureAtlas也常用于帧动画,这里最常用到的是textureRect字段,它是在Shader中提取具体图片的关键,它指定了对应图集中每张贴图的位置与宽高。其余字段的详细说明可查阅

为了便于说明我们使用一张16x16的TextureAtlas,图集中的每张贴图的大小都是一致的:

并自定义简单的图集描述数据:

var texMap = [
				[3,0, 3,0, 3,0, 2,0, 3,0, 2,9],			//GrassDirt
				[4,1, 4,1, 4,1, 5,1, 4,1, 5,1],			//Log
				[11,1, 10,1, 10,1, 9,1, 10,1, 9,1],		//Chest
				[7,7, 6,7, 6,7, 6,7, 6,7, 6,6],			//Pumpkin
				[8,8, 8,8, 8,8, 9,8, 8,8, 9,8],			//WaterMelon
				[8,0, 8,0, 8,0, 10,0, 8,0, 9,0]			//TNT
			];

texMap对象中的每个元素对应了六个不同立方体六个面所对应的图片贴图的位置,由于我们所用的图集的每张小贴图的尺寸是一致的,所以这里只需要定义每张小贴图的起始位置即可,以[3,0, 3,0, 3,0, 2,0, 3,0, 2,9]进行位置数据分析,(3, 0)位置是图片左上角开始,左数第三个坐标点,以此类推:

也就是位置(3, 0)对应了文章开头的那个立方体前后左右四个面,位置(2, 0)对应着立方体的底面,位置(2, 9)对应着立方体的顶面。

现在可以把所有数据传递到渲染管线中(注意:这里的立方体不是使用共用顶点的方式创建, 每个面都是独立的)。Shader中顶点着色器的代码如下:

in vec4 a_position;
in vec3 a_norm;
in vec2 a_uv;

uniform mat4 uPMatrix;
uniform mat4 uMVMatrix;
uniform mat4 uCameraMatrix;
// uFaces对应着这类数据 [3,0, 3,0, 3,0, 2,0, 3,0, 2,9]
uniform vec2[6] uFaces;
out highp vec2 vUV;
// 由于我们的图集是16x16,所以每个小贴图的大小比例都是1/16
const float size = 1.0/16.0;

void main(void){
        // w设定为立方体面对应的索引(代表哪个面)
	int f = int(a_position.w);
	// a_uv是传递进来的uv数据,范围是[0, 0] ~ [1, 1]
        // 以贴图位置(3, 0)说明:
        // 当a_uv是(0,0)时,求取的实际uv就是(3/16, 0)
        // 当a_uv是 (1, 1) 时,求取的实际uv就是(4/16, 1/16)
        // 所以在片元着色器uv采样过程中就会完整取到实际贴图
        float u = uFaces[f].x * size + a_uv.x * size;
	float v = uFaces[f].y * size + a_uv.y * size;
        // 重新生成uv进行实际采样
	vUV = vec2(u,v);

	gl_Position = uPMatrix * uCameraMatrix * uMVMatrix * vec4(a_position.xyz, 1.0); 
}

因为a_uv的范围是0 ~ 1,所以也可以通过mix函数来取uv坐标:

vec2 uv = mix(uFaces[f]*vec2(size), vec2(uFaces[f]*vec2(size)+vec2(size)), a_uv)

取贴图的过程,代码中已经有详细的解释,如果每个小贴图是不同尺寸,只需要分别求取当前小贴图的宽高与整张TextureAtlas宽高的比值,替换size即可。

0 Answers