尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Texture与Sprite彻底理清:从底层原理到图集优化实战

Texture与Sprite彻底理清:从底层原理到图集优化实战 做了这么多年的2D游戏和UI开发我发现很多刚接触引擎的朋友甚至一些做了两三年的开发者会把Texture和Sprite混为一谈。大家平时嘴上说的“一张图片”、“一个贴图”、“这个精灵”到了资源管理阶段却经常出乱子UI上明明切好了图运行时却整张显示美术给的图集被打包工具重新拼了一遍结果边缘出现黑线图集碎片一多DrawCall就压不下来。这些问题多半都出在没把Texture和Sprite的关系理清楚。这篇文章我想结合自己用Unity、Sprite Atlas以及第三方打包工具的实际经验从底层概念讲到图集方案再讲到踩坑排查。不管你是刚入门做2D游戏还是在搞UI性能优化这篇文章应该能帮你少走很多弯路。1. 先把概念拆清楚Texture是容器Sprite是用法1.1 一句话概括它们的关系先说最核心的一句话Texture是真正存在于显存里的图像数据Sprite是引擎对你“如何使用这张Texture”的一种描述。拿一个最容易理解的类比来说Texture就像一块布料Sprite是裁缝从布料上裁出来的一片布。你可以只做一条裙子也可以把这块布料裁成领子、袖口、口袋每个裁片就是一个Sprite。引擎在最终渲染的时候并不需要把每块裁片都单独做成一块新布料而是记住“我从哪块布上裁的、大概什么尺寸、怎么拼接”这就大大节省了存储和带宽。所以当你把一个PNG文件拖进Unity、Unreal或者Godot的时候第一步肯定是导入成Texture资源。如果直接把这个Texture拖到场景里大多数引擎会不知道你想干嘛因为“显示一张图片”这件事是需要引擎根据Texture生成对应绘图指令的。而在2D和UI管线里这个“绘图指令”的载体就是Sprite。1.2 为什么不能直接拿Texture上场景有过Unity经验的朋友一定知道把一个Texture拽到场景中Unity默认会帮你创建一个Sprite对象。这个操作看起来是“Texture可以直接显示”但背后其实自动生成了一张Sprite并把Texture引用挂了上去。从引擎的设计角度来看Texture里面可能装着很多信息不一定是一张整图。Texture可以是一张UI图集里面包含几十个按钮、图标。Texture也可以是RenderTexture用作相机渲染结果中转。Texture还可以是带Mipmap链的纹理不同层级的模糊缩小版本。引擎只拿着Texture本身并不知道你要显示其中哪个区域也不知道透明像素要不要参与碰撞检测。Sprite的作用就是在这些原始像素数据和具体的游戏逻辑之间做一个适配层。1.3 像素、纹素和UV这里顺便把几个关键术语捋一遍像素屏幕上的显示单位游戏画面最终由像素构成。纹素纹理图像里的单个颜色单元也就是图片的像素点但为了跟屏幕像素区分纹理领域叫纹素。UV坐标纹理上的归一化坐标范围0到1。Sprite跟UV的关系特别大。你切一个Sprite的时候实际上就是在记录一组UV原本的整张Texture为0到1的完整区间切出来的小图会被记录为比如(0.12, 0.08)到(0.45, 0.72)这样的四边形区域。这样一来GPU绘制时只需要用这组UV到那块布料上去采样对应的颜色就行。这也是为什么一个图集里哪怕放了上百张小图DrawCall依然能保持很低。因为你把所有小图都放在同一张Texture上引擎一次提交绘制就能画完。2. 一张纹理进入项目的完整旅程2.1 资源导入时引擎做了什么以Unity为例当美术给你一张PNG时从硬盘到屏幕大概经历四个阶段导入与反压缩Unity会读取PNG、JPG这类磁盘压缩格式解压成原始像素数据。纹理格式转换Unity会根据你设置的平台压缩格式把原始像素数据重新编码成目标平台能快速采样的格式。比如移动端常见ASTC、ETC2桌面端ASTC或BC7。生成纹理资源上传到GPU显存得到一个Texture资源。Sprite映射如果你是Sprite模式Unity会生成Sprite对象记录它引用这个Texture以及它的包围盒、枢轴、UV范围。这里比较关键的是第2步。如果你把一张美术原图设置为“不压缩”那么它会以RGBA32的形式完整存放在显存里一张2048×2048的图就占2048×2048×4字节大概16MB。这在PC上问题不大但堆到手机上就是灾难。2.2 Sprite模式Single和MultipleUnity的Texture导入面板里有一个Texture Type选项当你选择Sprite (2D and UI)时下面会多出Sprite Mode设置Single整张纹理只作为一个Sprite使用。Multiple一张纹理切成多个Sprite这就是我们说的图集或者序列帧图。很多新手容易犯的错是美术明明给了是一张已经拼好的图集导入时却选了Single然后又往场景里拖了整张图导致UI上把整张图集当背景显示出来了同事看了当场血压升高。正确做法是选了Multiple之后点Sprite Editor。在里面框选每一小格设置好名字和Pivot枢轴点。这个Pivot就是精灵在旋转、缩放时的参考点对动画帧和UI对齐很关键一般居中比较多。2.3 纹理参数如何影响Sprite效果当你把Texture Type设为Sprite后还有几个参数会直接影响最终效果和性能Pixels Per UnitPPU一个Unity单位对应多少像素。默认100意味着1个Unity单位长度内有100个像素。2D游戏里如果想把像素比例调成1比1可以改成你美术资源的分辨率基准比如16×16单位像素就设1632×32就设32。Generate Mipmap生成多级渐远纹理。对UI和2D精灵来说绝大多数时候应该关掉。因为它会让纹理额外占用约33%内存而且如果设置不当会造成近距离时纹理糊或者边缘异常。Mipmap主要给3D模型用的。Wrap Mode当UV采样超出0到1范围时怎么处理。Sprite一般用Clamp如果设成Repeat一旦图集里某个Sprite坐标算错就出现相邻精灵内容重复或者颜色渗透。Filter Mode缩放时用点采样还是双线性插值。像素风游戏通常要设成Point避免边缘模糊普通UI用Bilinear看起来更顺滑。Compression这里一定要根据用途区分。对于UI图标和角色立绘可以用高质量压缩对于手机上的大尺寸背景图如果压缩率过低内存压力会很明显。3. 图集化纹理从“散装”到“整装”的必修课3.1 为什么散图会吃性能很多第一次接触性能优化的人会发现一个问题同样是10张图标如果分成10个Sprite文件去让引擎绘制跟打包成一张图集再绘制性能差距非常大。原因在于DrawCall。CPU每提交一次渲染指令GPU要经历准备状态、加载纹理绑定、绘制顶点、切换着色器等流程。如果每个小的Sprite都用单独的Texture那就意味着场景里有10个纹理需要切换绑定每个纹理都需要一次DrawCall。当界面元素几十上百个时这个开销会非常夸张。把散图打包进同一张Atlas好处是所有小图共享同一个Texture渲染状态切换变少。Sprite图集能被CPU一次性提交GPU批量绘制。纹理上传速度更快并且降低了显存碎片的产生概率。所以我们要做的就是尽可能把同屏、同界面、同动画里的Sprite塞进尽可能少的图集。3.2 Unity Sprite Atlas怎么用Unity从2017.2开始推荐用原生Sprite Atlas系统替代之前的Sprite Packer。用起来分几步在Project窗口右键选择Create - 2D - Sprite Atlas。选中这个Atlas资产在Inspector里把Type设为Master。在Objects for Packing列表里把你需要打包的Sprite资源拖进去或者直接拖文件夹让它收集目录下所有Sprite。这里提个技巧不要直接拖Texture对象一定是拖Shader/图集或者文件夹里的Sprite资源。如果你漏了这一步打包后图集是空的场景上的Sprite不会自动关联到图集运行时依然走独立Sprite的渲染路径。等打包完成你可以在Sprite Atlas资产的预览面板看到所有被包含的小图和最终的图集纹理。如果你在代码里动态加载过这些SpriteUnity会确保运行时优先使用Atlas版本。3.3 第三方工具怎么选虽然Unity原生Sprite Atlas挺好用但很多团队还是喜欢在资源生成阶段就处理好图集。原因有几个美术素材可能是Photoshop里导出的需要做边缘处理和九宫格标注有的引擎比如Cocos或Godot没有原生图集切割器也有团队想在构建前就统一规范所有碎图命名和分布。这几年我陆续用过几类工具经典老牌工具TexturePacker它的界面很直观拖入散图后自动摆盘支持输出Unity、Cocos2d、LibGDX、Godot等多种引擎的预设格式还能自动导出元数据。缺点是普通版有诸多限制完整功能要付费。网页版Texture Packer工具热词里提到的“free texture packer 网页版”其实说的是那种打开浏览器直接拖图、在线拼好导出PNGJSON的工具。这类工具适合团队里临时补资源、快速验证、或者美术不方便装软件的情况。我在项目里用过一些免费网页版它们一般能设定单张最大尺寸、内边距、是否自动去透明边但功能相对简单不能做非常精细的旋转摆盘策略小图多了以后空间利用率一般。Sprite Forge AI这类AI辅助工具这个方向比较现代它不只是做图集打包还集成了AI抠图、自动切图甚至能够根据输入需求把AI生成的素材直接排列成Sprite Sheet。如果你在做原型验证或者需要大量AI生成风格化素材这类工具能省不少时间。不过要注意AI处理过的图有时候会有不自然的边缘半透明或者混色问题接进项目前一定要检查Alpha通道。我的建议是在个人学习或中小项目里优先用引擎自带的Sprite Atlas等团队人数多了、美术资源规范复杂了再引入第三方工具做离线打包。这样做的好处是让所有编辑器内操作保持一致不会出现“美术在TexturePacker里把九宫格设了到Unity里却丢失”的情况。4. 打包参数与坑从工具回到引擎4.1 图集尺寸、间距和出血打包图集看似是把小图随便拼一拼实际操作里有很多维度要权衡。最大尺寸上限每款GPU规格都有限制常见是2048、4096、8192。如果你做的是手机项目最好把图集最大尺寸控制在2048或者4096之间。超过这个上限一些中低端机要么不能加载要么会自动缩到设备支持的最大尺寸反而会压缩你的原图造成画质劣化。所以大型图集规划时要心里有数角色动画帧特别多就按角色拆UI按界面窗口拆世界环境物件按场景拆控制每个图集不超过1024×1024或2048×2048节省切换耗时。Padding、Extrude与出血碎片之间如果没有一定间隙图形采样时很容易因为浮点精度问题与相邻的碎图内容“串色”。一般打包工具会让设置Padding也就是小图之间的间隔像素。建议最少设置2像素。如果还有描边、阴影效果那间隔要更大一些避免收缩/模糊算法采样到隔壁内容。另外很多工具提供Extrude也就是把边缘像素往外多复制几像素。这样即使抗锯齿或双线性插值采样到了边缘采到的也是本图边缘颜色的延续不会看到黑色或透明区域产生的黑线这个处理在2D像素风游戏里尤其重要。4.2 绕不开的填充与九宫格问题九宫格切片Sliced Sprite是UI制作中的一个重点。什么是九宫格就是把一张图切成9块四角保持原始尺寸上下左右四条边拉伸中间内容区拉伸。这解决了按钮在不同屏幕上伸缩时不失真的问题。比如一个圆角矩形的按钮如果不切九宫格直接拉伸全图圆角会变形。当你在做图集打包时如果小图有九宫格需求务必要把九宫格边框信息保留下来。Unity里是通过Sprite Editor的Border标注的。打包进Sprite Atlas后运行时依然可以读取Border。第三方工具导出到Unity时你要确保类型选的是支持九宫格的模式并在引擎里重新确认Border值。这里有个很常见的坑美术在Photoshop里已经把按钮周围留了一圈透明边方便做投影和描边。你把图打包进图集后发现原来的Border值变了因为九宫格的数值会被自动重新计算。一旦资源进了图集引擎的Packing流程会根据碎图的实际起始坐标和裁剪后的尺寸重新计算边界所以此时必须以最终Atlas里的Border为准。4.3 压缩格式与透明通道的配合在移动端项目里图集纹理的压缩格式选择需要格外注意。对于透明图很多老机型并不支持带Alpha的高压缩率纹理格式。在OpenGL ES 2.0时代大家会采用分离Alpha的方案把RGB压缩成ETC1纹理同时把Alpha单独保存成一张灰度图运行时组合。这个方案能压内存但需要Shader配合采样处理不好Alpha边缘会发灰。好在现在主流设备都支持ASTC和ETC2真机测试环境基本可以放心使用ASTC 4×4或者6×6。对于Android项目你可以设ASTC为优先级最高的格式对于iOSASTC也原生支持。如果画面内容不太复杂的话用ASTC 8×8能明显减少内存占用图片质量肉眼看不出大问题。不过有个隐患如果图集内碎图尺寸较小、图案纹理又很锐利比如细文字、极细描边压缩格式会把边缘细节打散出现毛边。这种情况建议把该UI图集压缩等级调低或者干脆设为高精度格式。不要为了一点内存牺牲UI上的文字可读性。5. 实际项目里遇到的几个经典问题和排查记录5.1 UI图片泛白或发灰有段时间我接到反馈说UI上某个按钮颜色不对看起来像蒙了一层白雾透明区域还隐隐发灰。排查方式先看图片原文件是不是好好的。在Unity里选中小图预览看看正常不正常。如果正常再看Atlas里的打包结果点开Atlas预览看那儿是不是也发灰。如果Atlas里就已经发灰那基本可以确定是压缩格式问题。我当时的情况是项目在Android测试机上使用ASTC 6×6很多高饱和渐变的UI边缘出现颜色扩散尤其红色系特别明显就像脏脏的。后来我建议美术把这套UI里的渐变按钮单独提取出来放一张小尺寸专用图集压缩设为RGBA16或高质量ASTC 4×4问题就消失了。5.2 Sprite边缘黑线/白线这是引擎社区中被问过最多的问题之一。表现就是一张带透明背景的图标放到图集里后周围产生一圈细小的黑边或白边。根源是透明像素与相邻碎图颜色发生渗色。解决办法在打包工具中开启Extrude或者Padding把边缘像素往外扩。在引擎导入设置里勾选Alpha Is Transparency让透明区域参与插值时不会把颜色带进去。如果还不行可以把碎图周围真正的透明像素直接裁掉一部分让边缘半透明像素成为最外层。检查是否勾选了Mipmap。Mipmap在层级缩小时会把相邻像素平均化透明区域和边缘颜色会混成灰黑看起来就像描了边。5.3 内存突然暴涨找不到原因有一次项目里一个商店界面加载时内存从300MB直接跳到500MB我用Profiler一看发现很多Texture资源没有进图集而是被动态生成了独立纹理对象。问题出在代码里我们用Resources.Load加载了一个Sprite但它引用的Texture是零散的结果没有经由Atlas系统。原来的建议是加载UI资源时可以走Atlas的API或者使用可寻址资源系统确保散图在打包时就纳入同一图集。实践中我还会在Profiler里查看“Texture总内存”和“未引用Texture”如果发现大量“Texture2D”格式基本就说明有资源绕过图集直接加载了。5.4 常见问题速查现象可能原因检查/解决办法按钮变色发灰压缩格式不适配或图集尺寸过大更换ASTC 4×4 / 分开小图集精灵边缘黑线打包时边缘无出血透明像素混色开启Extrude设Padding 2~4场景里显示整张图集Texture Type没选Multiple在Import Settings正确切割Sprite图集不生效、DrawCall降不下来Sprite没进入Atlas打包列表确认Objects for Packing里有文件夹或资源动态加载的图片没有图集效果绕过了Atlas加载流程改为图集内引用避免Resources.Load整图使用九宫格时图像拉伸异常Border值在图集后变化确认Atlas中的Sprite Border正确6. 从工作流角度理顺Texture、Sprite与图集的关系经过前面这些细碎知识点你可能会发现管理和使用Texture与Sprite并不只是做单个导入设置而是要从整个项目资源和生产流程的高度去规划。我在工作里养成的习惯是在项目正式启动美术前就定一套资源目录和命名规范。比如Assets/Arts/UI/Common/ Assets/Arts/UI/Shop/ Assets/Arts/Characters/Knight/ Assets/Arts/Effects/目录划分以后再对应生成多个Sprite Atlas全局UI图集放公共按钮、图标、弹窗底色一般要稳定尽量不变。商店界面图集只在商店界面加载。战斗特效图集按特效类型创建追求快进快出。角色动画图集按角色拉一个图集或序列帧尽量在场景切换时集中加载。然后配合引擎的导入设置模板。现代引擎基本都支持预设导入规则按目录设定不同参数。比如UI目录默认关Mipmap、开启Alpha Is Transparency、Sprite模式根据深度选择。规范之后美术丢进来的新文件都会自动走到正确的设置上不会等到测试时才反馈“图变白了”“内存炸了”。这样做还有一个好处当Canvas界面较多你要通过Overdraw和DrawCall分析性能时可以快速判断到底是资源设置问题还是代码合批问题。如果图集设计合理但DrawCall还是高那问题多半出在Image和Text材质差异上跟Sprite的关系就不大了。做2D核心是养成一种思维模式你在场景里操作的每个游戏对象渲染的并不是“图片”而是一段带纹理引用的绘制指令。你能控制这个引用的粒度控制共用纹理的规模你的渲染性能就自然变好了。这也解释了为什么同样一张图有人能做高流畅度2D游戏有人却一开大页面就卡成PPT。如果你现在正好在搭项目建议拿一张角色多帧序列或一套UI图标先手动在引擎里设置Sprite再用Sprite Atlas打包把Profiler打开看看DrawCall的变化。这个过程比死记概念有用得多跑一圈下来你对Texture和Sprite的理解会完全不一样。
返回列表