跳转到内容

纹理

纹理就是显存里的一张图。你用项目路径引用它,加载成一个句柄(handle),再把句柄放到组件上 ——Sprite.texture、材质采样器、位图字体页。本页讲图片本身:它可以是什么、你能在它上面设什么、 以及怎么知道它有多大。

画纹理的那个组件见精灵;资源系统整体(引用、分组、清单)见 资源

扩展名 说明
.png 带透明通道的美术资源首选。
.jpg / .jpeg 没有 alpha 通道;照片类美术体积更小。
.webp 带 alpha 且压缩率好,由浏览器/运行时解码。
.gif 只取第一帧——这是一张静态图,不是动画。
.bmp 未压缩;能用,但基本不该发出去。
.ktx2 Basis Universal,已经是 GPU 压缩格式。这是构建的 cook 步骤产出的东西——你也可以直接提供。

日常创作用 .png.jpg.webp。开了压缩时,构建的 cook 步骤会把它们转成 .ktx2 (见导入设置)。

assets.loadTexture(ref) 解析引用、上传图片,并连同图片的像素尺寸一起返回句柄—— 「怎么取宽高」的答案就在这里:

import { defineSystem, Res, Assets } from 'esengine';
const measure = defineSystem([Res(Assets)], async (assets) => {
const tex = await assets.loadTexture('assets/textures/player.png');
tex.handle; // TextureHandle —— 组件字段要的就是它
tex.width; // 图片的像素宽
tex.height; // 图片的像素高
});

宽高是源图片的像素数,不是任何东西的绘制尺寸。画这张纹理的 Sprite 有它自己的世界单位 size,是另一个可以随便设的数(初始会跟图片对齐——见精灵按纹理定尺寸)。

按图片自身像素尺寸放到精灵上:

import { defineSystem, Query, Mut, Res, Sprite, Assets } from 'esengine';
const showArt = defineSystem([Query(Mut(Sprite)), Res(Assets)], async (q, assets) => {
const tex = await assets.loadTexture('assets/textures/player.png');
for (const [entity, sprite] of q) {
sprite.texture = tex.handle;
sprite.size = { x: tex.width, y: tex.height }; // 1 图片像素 = 1 世界单位
}
});

从一个不是你加载的句柄取尺寸

Section titled “从一个不是你加载的句柄取尺寸”

编辑器里摆好的精灵早就有纹理了:场景文件写了它,加载器解析了它,你的代码从来没见过 TextureResult。要量这种纹理,直接按句柄问资源管理器:

import { defineSystem, Query, Sprite, getTextureDimensions } from 'esengine';
const measurePlaced = defineSystem([Query(Sprite)], (q) => {
for (const [entity, sprite] of q) {
const dims = getTextureDimensions(sprite.texture); // { width, height } | null
if (dims) { /* … */ }
}
});

纹理真正上传之前它返回 null —— 场景的纹理是异步加载的,所以第一帧就跑的系统看不到东西 是完全正常的。

loadTextureRaw(ref) 上传图片时不做 loadTexture 那次垂直翻转。翻转与不翻转是真正不同的 GPU 对象,所以分开缓存。除非你在喂一条本来就要求自下而上行序的管线,否则用 loadTexture

导入设置属于图片,不属于任何用它的实体。它们存在图片旁边的 .meta 文件里,在内容浏览器里 选中图片即可编辑——用这张纹理的每个精灵都继承这套设置。

设置 默认 作用
Max Size 2048 cook 时的降采样上限(2 的幂)。最长边超过它的源图会被 box 滤波缩下来;更小的源图原样保留。
Compress true cook 时压成 KTX2(Basis Universal)。它在显存里保持压缩、按设备转码。想要锐利 UI 和平滑渐变就关掉
Compress Format uastc uastc —— 质量高、体积大。etc1s —— 小得多、质量低(适合照片类/低频美术)。仅在 Compress 开启时使用。
Filter linear 采样过滤。nearest 保持像素锐利;linear 平滑。
Wrap repeat [0,1] 之外的 UV 如何寻址:repeatclampmirror
Premultiply Alpha false 导入时把 RGB 乘上 alpha。
sRGB Color true 图片存的是 sRGB 编码的颜色(albedo、UI)。法线图、遮罩这类本来就是线性的数据要关掉。仅在项目按线性色渲染时有意义。
9-Slice Border 0,0,0,0 左/右/上/下边框,单位是纹理像素:图片的角在哪里结束。在这里授权一次,每个设成 Sliced 的 UIVisual 都继承。

Max Size、Compress、Compress Format 可以按平台覆盖——就是导入设置里 Default 旁边那排页签。 这就是「编码一次、按 GPU 转码」的管线:同一个源文件,桌面端发 2048 的 UASTC,微信发 1024 的 ETC1S。见烘焙与压缩

Sprite.size 的单位是世界单位,并且刻意与图片无关——正因如此你才能不改文件就缩放美术。 但没人愿意手敲图片尺寸,所以编辑器替你填:

  • 从内容浏览器把图拖进视口,生成的精灵已经按图片像素定好尺寸。
  • 给已有精灵指定纹理同样会按那张纹理定尺寸,之后换图也会跟着走。

一旦这个数是你自己定的,它就停手:你手动设过 size 之后,换纹理不会再动它。 (撤销历史里这次自动匹配是独立的一步 Fit Sprite To Texture,所以你可以保留尺寸、只撤掉匹配。)

代码里没有这个便利:只给 Sprite 一个纹理,size 保持默认值,所以要像上面那样自己传 tex.width / tex.height

精灵可以用 uvOffset / uvScale 只画纹理的一部分,两者都是相对整张图的归一化比例:

// 256×256 图集里第 2 列第 1 行的 32×32 帧。
sprite.uvOffset = { x: 64 / 256, y: 32 / 256 };
sprite.uvScale = { x: 32 / 256, y: 32 / 256 };

做序列帧动画不要手算这些,去授权一个 flipbook——见动画。 瓦片美术见瓦片地图

纹理通常是 2D 游戏里最大的一块,它们是引用计数 + 可驱逐的,不会永远常驻。预算、LRU、以及怎么读 真实占用,见纹理内存与预算

一句话版本:没人引用的纹理会在内存压力下被驱逐,下次按同一路径加载时透明地复活。

  • 精灵 —— 画纹理的那个组件。
  • 资源 —— 引用、分组、清单、生命周期。
  • 材质 —— 把纹理绑到自己的 shader 上。
  • UI —— 九宫格图片与 UI 精灵。