~/ ?.log $
返回文章列表
8 min read
更新于 2026年3月6日

我不知道的 Pixi.js(02)— 纹理与 Sprites 的管理机制

很多人以为 Pixi.js 的纹理就是"把图片贴到 Sprite 上",但实际上纹理管理涉及三层抽象:TextureSource(GPU 上的图像数据)、Texture(纹理上的矩形区域)、Sprite(屏幕上的可渲染对象)。搞清楚这三层关系,大部分纹理相关的性能问题和内存泄漏…

很多人以为 Pixi.js 的纹理就是”把图片贴到 Sprite 上”,但实际上纹理管理涉及三层抽象:TextureSource(GPU 上的图像数据)、Texture(纹理上的矩形区域)、Sprite(屏幕上的可渲染对象)。搞清楚这三层关系,大部分纹理相关的性能问题和内存泄漏都能迎刃而解。


一、v8 的资源加载:从 Loader 到 Assets

Pixi.js v7.3 开始废弃 PIXI.Loader,v8 彻底移除。取而代之的是 Assets 系统——一个基于 Promise 的资源管理器。

先看 v7 的旧写法:

// v7 — 回调式加载,已废弃
const loader = PIXI.Loader.shared;
loader.add('hero', 'hero.png');
loader.load((loader, resources) => {
  const sprite = new PIXI.Sprite(resources.hero.texture);
});

再看 v8 的新写法:

// v8 — Promise 式加载
import { Assets, Sprite } from 'pixi.js';

const texture = await Assets.load('hero.png');
const sprite = new Sprite(texture);

换句话说,Assets.load() 把”加载”和”使用”之间的回调地狱消除了。但更重要的变化在底层——Assets 系统内置了缓存、类型推断和解析链:

// 批量加载
await Assets.load(['hero.png', 'enemy.png', 'spritesheet.json']);

// 预加载(后台下载,不阻塞主线程)
Assets.backgroundLoad(['level2/tilemap.json']);

// 卸载释放内存
Assets.unload('hero.png');

这里有一个很多人会忽略的细节——Assets.load() 对同一路径会自动去重。多次调用 Assets.load('hero.png') 不会发起多次网络请求,它会返回同一个 Promise,指向缓存中的同一份纹理数据。


二、三层纹理架构:TextureSource → Texture → Sprite

说白了,Pixi.js 的纹理系统是一个”一对多”的层级结构:

(1)TextureSource(v7 中叫 BaseTexture):代表一块实际上传到 GPU 的图像数据。一张 PNG 文件加载后,在 GPU 上就是一个 TextureSource。它是内存和显存占用的真正来源。

(2)Texture:代表 TextureSource 上的一个矩形区域。一个 TextureSource 可以衍生出多个 Texture,比如 Spritesheet 里的每一帧。Texture 本身不占额外显存,它只是一个”裁切框”。

(3)Sprite:引用一个 Texture,决定在屏幕上的位置、大小、旋转等。多个 Sprite 可以共享同一个 Texture

import { Assets, Sprite, Texture, Rectangle } from 'pixi.js';

const source = await Assets.load('spritesheet.png');
// source 是一个 Texture,其 source 属性指向 TextureSource

// 手动从 source 裁出子区域
const frame1 = new Texture({ source: source.source, frame: new Rectangle(0, 0, 64, 64) });
const frame2 = new Texture({ source: source.source, frame: new Rectangle(64, 0, 64, 64) });

// 两个 Sprite 共享同一个 TextureSource,只有一份 GPU 数据
const spriteA = new Sprite(frame1);
const spriteB = new Sprite(frame2);

这说明了什么?100 个 Sprite 使用同一张 Spritesheet 的不同帧,GPU 上只存了一份图像数据。这就是为什么 Spritesheet 比零散的小图片高效得多——减少了纹理上传次数和显存占用。


三、Spritesheet 的解析过程

实际开发中很少手动裁切 Texture,通常用 JSON 描述文件配合 Spritesheet 图集:

// spritesheet.json 描述了每帧在图集中的位置和尺寸
const sheet = await Assets.load('spritesheet.json');

// Assets 系统自动解析 JSON,生成 textures 映射
const idleTexture = Texture.from('hero-idle-01');
const walkTexture = Texture.from('hero-walk-01');

解析过程是这样的:Assets 加载 JSON 文件 → 读取其中的 meta.image 字段找到对应的图集图片 → 上传图片到 GPU 创建 TextureSource → 按 JSON 中每帧的 frame 坐标创建多个 Texture → 以帧名称为 key 注册到全局缓存。

问题的关键在于——Spritesheet 的帧名称就是全局缓存的 key。如果两个不同的 Spritesheet 有同名帧(比如都叫 frame_01),后加载的会覆盖先加载的,导致显示错误。命名时加前缀是个好习惯。


四、UV 坐标:GPU 怎么知道裁哪块

Textureframe 属性定义了裁切区域,但 GPU 不认像素坐标——它认的是 UV 坐标,范围 0 到 1。

换句话说,U 和 V 分别对应纹理的水平和垂直方向:(0, 0) 是左上角,(1, 1) 是右下角。一个 256x256 的图集中,左上角 64x64 的区域对应的 UV 范围是 (0, 0)(0.25, 0.25)

Pixi.js 在创建 Texture 时自动计算 UV 坐标,开发者通常不需要手动处理。但在自定义 Mesh 或 Shader 时,理解 UV 映射就变得重要了——它决定了纹理的哪块区域被”贴”到几何体的哪个面上。

// Texture 的 uvs 属性存储了标准化后的 UV 坐标
// frame: (64, 0, 64, 64) 在 256x256 图集上
// 对应 UV: (0.25, 0, 0.5, 0.25)

五、纹理生命周期:创建、缓存与销毁

纹理的生命周期管理是内存泄漏的重灾区。Pixi.js v8 引入了自动垃圾回收(GC)机制,但开发者仍需理解其工作原理。

创建阶段Assets.load() 加载资源后,TextureSource 上传到 GPU,Texture 注册到缓存。

使用阶段Sprite 引用 Texture,渲染器每帧读取其 UV 和 TextureSource 进行绘制。v8 的 GC 系统会追踪 TextureSource 的最近使用时间。

销毁阶段:v8 提供了两种方式——

// 方式一:手动卸载(推荐用于场景切换)
await Assets.unload('hero.png');

// 方式二:自动 GC(v8.15+ 统一的 GCSystem)
await app.init({
  gcActive: true,
  gcMaxUnusedTime: 60000, // 60 秒未使用则释放 GPU 数据
  gcFrequency: 30000, // 每 30 秒检查一次
});

这里有一个很多人会忽略的细节——v8.15 之前有 TextureGCSystemRenderableGCSystem 两套 GC,v8.15 统一为 GCSystem。旧的配置参数(textureGCActive 等)虽然还能用但已标记为废弃。

如果你只记住一句话:TextureSource 不销毁,GPU 显存就不会释放。 Sprite 被移除出场景树不会触发纹理释放——纹理是独立于场景树的资源,需要显式卸载。


总结

Pixi.js 的纹理系统不是”加载图片 → 创建 Sprite”这么简单。它是一个三层架构:TextureSource 管 GPU 数据,Texture 管裁切区域,Sprite 管屏幕呈现。理解这个层级关系,就能理解为什么 Spritesheet 比散图高效、为什么纹理切换会打断批处理、为什么移除 Sprite 不等于释放内存。


本系列其他文章:

延伸阅读:

share.ts

// 觉得这篇文章有帮助?

// 欢迎分享给更多人

export const  subscribe  =  "/rss.xml" ;