在Pixi.js项目里处理大面积背景或循环动画时,如果单纯用多个Sprite去拼贴同一张小图,往往会遇到渲染批次过多、画面衔接处有细缝、显存被重复占用等情况。TilingSprite就是引擎专门用来应对这种纹理重复显示需求的对象,它在底层只提交一个四边形,借助纹理坐标偏移让GPU不断采样同一图像。

一、TilingSprite的基本原理
TilingSprite本质上是一个特殊化的网格显示对象,它并不像普通Sprite那样只显示一次纹理,而是通过修改内部的贴图坐标(UV)范围,使一张纹理在指定的宽度和高度内不断平铺。Pixi.js在渲染阶段会将该对象的尺寸、tilePosition以及tileScale等信息传给着色器,由GPU完成重复采样,因此无论显示区域多大,都只产生一次Draw Call。
这种机制带来的直接好处是内存友好。假设我们有一张256×256的砖墙纹理,想铺满1920×1080的屏幕,用Sprite平铺大约需要三十多个实例,而TilingSprite只需一个实例,底层WebGL纹理也只上传一次。同时因为拼接逻辑在着色器里用数学计算完成,不会出现Sprite之间因亚像素 rounding 产生的白边问题。
1.1 与普通Sprite的渲染差异
普通Sprite在批量渲染时,虽然Pixi.js的SpriteBatch能合并同纹理对象,但每个Sprite仍要单独计算变换矩阵并写入顶点缓冲。当数量上涨到几百个,CPU端开销变得明显。TilingSprite则把所有平铺工作交给顶点着色器,通过tilePosition.x和tilePosition.y控制偏移,CPU每帧只需更新几个数值。
下面的表格列出了两种方案在渲染一张重复草地纹理时的典型表现:
| 方案 | 显示对象数 | Draw Call | 显存占用 | 60帧下CPU耗时 |
|---|---|---|---|---|
| 多个Sprite平铺 | 48 | 1(同纹理批处理) | 256KB纹理×1 | 约1.8ms |
| 单个TilingSprite | 1 | 1 | 256KB纹理×1 | 约0.2ms |
二、快速上手TilingSprite
创建一个TilingSprite非常简单,只需要提供纹理以及想要覆盖的宽高。Pixi.js从v5开始使用异步加载资源,我们得到Texture对象后就能直接实例化。下面是一段最基础的代码示例,演示如何把一张砖块图铺满整个画布。
// 假设已经通过 PIXI.Loader 加载了名为 'brick' 的纹理
const app = new PIXI.Application({
width: 800,
height: 600,
backgroundColor: 0x1099bb
});
document.body.appendChild(app.view);
const texture = PIXI.Texture.from('brick.png');
// 创建平铺精灵,宽高设为画布大小
const tiling = new PIXI.TilingSprite(
texture,
app.screen.width,
app.screen.height
);
app.stage.addChild(tiling);
// 在动画循环中移动平铺位置,形成滚动效果
app.ticker.add(() => {
tiling.tilePosition.x += 1;
tiling.tilePosition.y += 0.5;
});
上面代码中,tilePosition是TilingSprite特有的属性,它等价于告诉GPU“从纹理的哪个偏移量开始取像素”。每帧让x增加,视觉上就是背景向左滚动。注意这里的纹理坐标单位是像素,不是归一化值,因此和纹理原始尺寸直接相关。
如果我们想让纹理在平铺时被放大或缩小,可以使用tileScale属性,它也是一个Point对象。比如设置tiling.tileScale.set(2)会让每张小图看起来放大两倍,但显示区域不变,于是屏幕上出现的重复单元更少、单个更大。这个特性在制作远景云雾时特别有用。
2.1 动态修改尺寸
有些场景下画布大小会随窗口变化,TilingSprite也支持运行时调整。直接修改width和height即可,不需要重建对象。引擎会在下次渲染时更新内部顶点数据,开销极小。
window.addEventListener('resize', () => {
app.renderer.resize(window.innerWidth, window.innerHeight);
tiling.width = window.innerWidth;
tiling.height = window.innerHeight;
});
这种写法在响应式网页游戏里很常见。由于TilingSprite内部只缓存了一份几何缓冲,宽高变化不会触发纹理重新上传,所以即便频繁resize也不会造成卡顿。相比之下,若用几十个Sprite去手动重新布局,就要遍历子节点并设position,代码更啰嗦且容易出错。
三、常见误区与避坑
虽然TilingSprite用起来直观,但新手常会踩到几个坑。第一个是纹理尺寸问题:在WebGL1环境下,若纹理不是二次幂尺寸且未设置重复采样模式,某些旧设备会出现边缘拉伸。Pixi.js默认会帮我们开启WRAP_MODES.REPEAT,但如果你手动替换了baseTexture的wrap模式,就要确保尺寸合规或显式设置。
const base = texture.baseTexture; // 明确指定重复模式,避免部分移动端黑边 base.wrapMode = PIXI.WRAP_MODES.REPEAT;
第二个误区是锚点(anchor)的理解。TilingSprite同样有anchor属性,但它影响的是整个平铺区域的定位,而不是单张小图的起始点。如果把anchor设为0.5再配合tilePosition,可能让初学者以为纹理“居中了”,实际上只是显示框中心对齐舞台,纹理依旧从偏移处连续平铺。调试时建议先把anchor保持为0,观察滚动是否符合预期。
3.1 性能剖析与优化思路
当TilingSprite面积特别大,比如做无限横向卷轴且宽度设为十万像素,一些低端GPU仍可能因顶点跨度大产生精度问题。此时更稳妥的做法是固定TilingSprite大小为屏幕尺寸,通过不断累加tilePosition来模拟无限移动,而不是把width设得离谱。这样顶点数据始终在小范围内,浮点精度更有保障。
另外,若场景中同时存在多个TilingSprite,尽量让它们共享同一份Texture,这样Pixi.js的批处理系统可以把它们合并到同一个Draw Call里。可以用PIXI.Texture.from的缓存机制,避免重复decode图片。下面示范两个背景层共用一张星空的写法:
const starTex = PIXI.Texture.from('star.png');
const farLayer = new PIXI.TilingSprite(starTex, 800, 600);
const nearLayer = new PIXI.TilingSprite(starTex, 800, 600);
nearLayer.alpha = 0.6;
nearLayer.tilePosition.x = 50;
app.stage.addChild(farLayer, nearLayer);
这种视差滚动结构在不少2D游戏里是标配。因为共享纹理,两个TilingSprite在渲染时只切换少量uniform,性能几乎等同于单层。若各自加载不同图片,则至少增加一次纹理绑定,在弱网或老显卡上能感到微小掉帧。
四、总结与实践建议
回到最初的问题:纹理重复显示到底该选谁?如果重复数量少、还需要对每个单元做独立交互(比如可点击的格子),那普通Sprite更合适;但若只是背景、地面、天空这类纯视觉平铺,TilingSprite几乎总是更优解。它用极低的CPU开销和简洁的API,把拼接、滚动、缩放等需求浓缩成几个属性。
建议在项目初期就规划好哪些层用TilingSprite,并把相关纹理集中放在同一图集或至少同尺寸规范下,减少wrap模式兼容风险。配合tilePosition与tileScale,你能用几百行代码实现过去需要复杂拼贴逻辑才能搞定的无限世界。
Pixi.jsTilingSprite纹理重复修改时间:2026-08-09 14:24:43