做材质的时候我经常遇到一类需求:一张贴图要驱动另一张贴图的坐标、缩放或者颜色。每次做到这一步,第一个绕不开的问题就是——我手上这张纹理到底多大?是 512×512 还是 2048×2048?如果靠肉眼去猜,或者干脆写死一个常量,换台设备、换个平台就很容易出问题。
TextureSize 这个节点,就是专门干这件事的。它在主流引擎的材质编辑器里都能找到,作用很简单:把一张纹理的宽、高和像素尺寸信息读取出来,供后续节点做计算。听起来没什么技术含量,但真正用起来之后你会发现,几乎所有涉及“纹理密度”“平铺次数”“屏幕适配”的材质,都离不开它。这篇就聊点实际经验:它底层到底读的是什么,怎么接才不出错,以及哪些应用场景能最大化发挥它的价值。
1. 这个节点到底在做什么:从底层返回值说起
1.1 一条底层语句和它背后的含义
在大部分渲染管线里,纹理尺寸并不是一个需要“请求”才能拿到的数据,而是一个跟随纹理采样器一路传递过来的元信息。以大家最熟悉的着色语言为例,当你声明了一张纹理_MainTex,编译器实际上还会顺便生成一个配套的float4变量,名字大概是_MainTex_TexelSize。这个变量的内容分成四份:
- x 分量:1 / 宽度,也就是单个像素在 UV 空间里占用的横向宽度;
- y 分量:1 / 高度,单个像素在 UV 空间里占用的纵向高度;
- z 分量:纹理的宽度(像素单位);
- w 分量:纹理的高度(像素单位)。
很多人第一次看到_TexelSize这个名字会误以为它只存了“像素大小”,实际上zw两个分量才是我们真正需要的尺寸信息。把它理解为“像素比 + 分辨率”的组合体比较准确。
TextureSize 节点在材质编辑器里做的事,就是把zw两个分量拆出来,转成方便直接使用的输出端口。有些编辑器会额外输出Texel Size(即xy分量),方便做和像素对齐相关的操作。理解了这个底层结构,后面所有应用场景其实都是围绕着“宽度”“高度”“像素比”这三组数字展开的。
1.2 节点封装后的典型输出形态
不同引擎对这个节点的命名和输出端口略有差异,但核心信息是同一套。以我很常用的材质编辑流程为例,拖入 TextureSize 节点后通常会看到这样几路输出:
| 输出项 | 含义 | 典型用途 |
|---|---|---|
| Width | 纹理宽度(像素) | 计算宽高比、平铺次数 |
| Height | 纹理高度(像素) | 计算宽高比、垂直缩放 |
| Size (float2) | 宽高打包后的二维向量 | 直接乘 UV 或做除法 |
| Texel Size | 单个像素对应的 UV 尺寸 | 像素精确对齐、抖动偏移 |
这套输出结构非常直观:宽、高、长度 2 的向量、长度 2 的向量(像素比)。为啥要同时给宽高和打包向量?因为不同的后续运算对数据形态的要求不一样。比如你要算宽高比,直接把 Width 和 Height 接出来做除法就行;但你要给 UV 做整数倍的缩放,把它当成一个 float2 整体去乘,计算效率更高,连线也更干净。
我用过一个很省事的办法:把 Size 输出接给一个 Tiling 参数,然后把它连到纹理采样的 UV 输入端。这样做的好处是,不管这张纹理未来在哪些平台被压缩到什么程度,UV 的平铺密度始终跟随纹理自身的实际尺寸,不会因为发布设置改了而失配。这是 TextureSize 节点最基础也最实用的一个思路。
2. 为什么不能直接写死常量?平台差异、压缩格式与尺寸漂移
2.1 你写死的 4096,在真机上可能根本不是 4096
贴一张 4096×4096 的图到工程里,很多人都默认运行时读到的也是 4096×4096。但实际渲染时纹理尺寸并不总是等于原始文件尺寸,中间隔着一层层设置:导入时最大尺寸限制、压缩格式、生成 Mipmap 与否、平台覆盖配置。这些设置叠加下来,最终在 GPU 上真正驻留的纹理尺寸可能已经变成了 2048、1024 甚至更小。
关键在于,TextureSize 节点返回的是 GPU 实际驻留尺寸,而不是硬盘里的原始文件尺寸。这一点极其重要,也是我周围很多同事真正踩坑的起点。
举个例子:原始美术资源是 4096 高度的环境贴图,导入时为了控制包体,在 Android/iOS 平台把 Max Size 设成了 1024。你在 PC 编辑器里看效果,一切正常,因为 PC 对应的平台设置可能还是 4096;打包到移动端之后,Shader 里所有和纹理尺寸相关的计算结果全部变成了基于 1024 的那套值。如果你在材质里手动把某个平铺参数写死成“匹配 4096”,轻则纹理密度不一致,重则 UV 采样完全乱掉。
2.2 压缩格式如何悄悄改变尺寸
更隐蔽的问题是压缩格式。部分压缩格式(例如移动端常见的某些块状压缩格式)要求纹理尺寸必须满足特定对齐规则。如果你给了一张 1000×1000 的图,GPU 实际分配的尺寸可能是 1024×1024,同时内部用 4×4 的块做压缩存储,边缘会有部分浪费。TextureSize 节点在这种情况下读出的尺寸究竟是 1024 还是 1000?
不同引擎、不同图形 API 的底层实现会有差异,但普遍规律是:读出来的值更倾向于“显存中真实存在的那一份尺寸”,也就是经过格式对齐后的尺寸。这意味着什么?意味着我们不能拿来就用,要做一层防御性处理。最稳妥的做法是先把读到的尺寸做一个合法性判断,排除 0 值和异常比例,然后再参与后续运算。这个习惯我是在一次快速迭代中被迫养成的——当时某个通道的贴图漏配了平台覆盖参数,尺寸在真机上直接变成了 1×1,而 Shader 里还在用这个值做 UV 除法,最后渲染结果就是全屏噪点。
2.3 为什么这个节点是动态适配的关键
正因为尺寸会随平台漂移,材质里凡是和“纹理密度”相关的逻辑,都应该尽量用 TextureSize 实时读取,而不是用一个暴露出去的手动参数。这个理念可以总结成一句话:让纹理自己描述自己。
举个例子,你想做一套景物贴花系统,贴花纹理需要根据受击面的大小自动调整重复次数。如果写死一个Tiling = 4,那这张贴花在 512 纹理的物体和 2048 纹理的物体上表现出的疏密完全不同。而如果用 TextureSize 读到的尺寸除以一个基准值(比如 1024),得到的缩放系数就能让贴花在不同纹理分辨率下保持类似视觉密度。这种做法既省了美术逐个调整的功夫,也避免了“看着调好了,换平台又崩”的尴尬。
3. 实际应用一:动态平铺与纹理密度统一
3.1 需求场景:要让地面的石头纹理密度始终如一
做地形或者大面积墙体材质时,我经常被要求“不管模型大小怎么变,贴图的细节密度要保持一致”。模型大的时候,如果 UV 是平的,同一张贴图会被拉得很大,细节全丢;模型小的时候又显得纹路过密。传统做法是美术手动调整 Tiling,但这只对固定尺寸的模型有效。
有了 TextureSize 节点,思路就变成:以纹理像素为单位做平铺基准。具体来说,我可以先定一个“期望物理密度”,比如“每米 256 像素”。然后用 TextureSize 读到的实际高度,去除以 256,得到这张纹理在 1 米距离里应该重复的次数。把这个值连到采样节点的 Tiling 输入上,模型不管怎么缩放,纹理密度都能贴合这个物理基准。
实际操作里,我一般会额外加一个可调倍率,因为“每米多少像素”取决于视觉风格。倍率参数保留在材质实例上,方便地编临时调整。不要把这个倍率完全写死,也不要完全依赖节点自动算,折中是最好的状态。
3.2 在材质编辑器里怎么接:一份可直接照抄的操作流程
拿最常见的材质编辑器流程来说,操作大致如下:
- 新建基础材质,拖入目标纹理。
- 添加 TextureSize 节点,把目标纹理引用连到它的输入端口。
- 取 Width 或 Height 输出,接一个除法节点,除以你设定的“基准像素密度”常量(例如 256)。
- 把除法结果接到目标纹理采样节点的 Tiling 输入。
- 采样 UV 输出传给后续颜色/法线/粗糙度通道的计算。
这里有几个必须注意的细节:
- 除法节点连的是 Width/Height 这种“绝对像素数”,不是 Texel Size。如果要处理的是非正方形纹理,注意保持比例,不要只用宽度去驱动 Tiling 的 X 和 Y,否则图案会被拉伸。
- 除法计算的基准值建议放在材质参数集合里,不要直接表达成常量节点。原因很简单——不同地图块、不同层级的贴花诉求不一样,做成参数才能在材质实例里快速覆盖。
- 如果你处理的是法线贴图,记得平铺逻辑要和基础颜色贴图保持完全同步,否则出现“颜色和光影细节错位”的观感,排查起来非常麻烦。
3.3 这个方案的后续演进思路
这个思路再往下走一步,可以用来做“多纹理混合的密度匹配”。比如地形材质需要把沙地、草地、岩石三张纹理混合,三张纹理的分辨率往往不统一:草地 2048、沙地 1024、岩石 512。如果直接用同一个 Tiling 去采样,混合边缘会出现明显的疏密跳变。
我习惯的做法是:选一张基准纹理(通常是分辨率最高的草地),用它的 TextureSize 作为参考,然后对另外两张纹理的 UV 做一次坐标换算,缩放系数就是“基准纹理尺寸 / 当前纹理尺寸”。这样三张纹理的实际采样密度就完全统一了,混合边缘也不会因为分辨率的差异出现“放大镜”式的突变。这个思路我没有在其他教程里见过系统讲解,是我在一次植被地形叠层材质迭代中反复调试总结出来的,效果非常稳定。
4. 实际应用二:基于纹理尺寸的屏幕空间适配与程序化图案
4.1 屏幕空间特效里的“像素对齐”需求
另一个我很常用的方向是屏幕空间特效。做溶解、扫描线、边缘发光这类效果时,经常需要让特效密度和纹理的像素密度对齐,或者和屏幕分辨率产生某种关联。
举个例子:做扫描线效果,你希望扫描的间隔在屏幕不同分辨率下看起来是固定不变的。如果直接用一个常量做频率,低分辨率屏幕和高分辨率屏幕上的间隔视觉宽度完全不同。这时候可以引入屏幕纹理尺寸作为基准,但这通常需要专门的屏幕信息节点。更简单的做法是:用场景中某张主要纹理的 TextureSize,间接推算出像素密度,再为程序化图案的频率提供一个参考基准。
在节点连线层面,我会把 TextureSize 读出的 Height 值,做成一个“每像素频率”的系数:Frequency = 目标间隔(像素) / TextureHeight。然后把这个系数乘到 UV 的 V 分量上,再做正弦/条纹运算。这样图案的疏密就和纹理实际尺寸挂钩了。
4.2 程序化纹理坐标生成:规避“变形感”的关键
程序化纹理(网格、斑点、条纹)有个很常见的问题:图案容易在非均匀缩放模型上产生拉伸变形,或者因为贴图本身的宽高比不是 1:1 而变成“椭圆形”。
用 TextureSize 能很好解决这类变形问题。核心思路是:读取纹理宽高比,然后对 UV 的某一轴做反向补偿。
比如说,我想在一套 UV 上画一个完美的圆形斑点图案。如果直接采样的纹理不是正方形,画出来的圆就会变成椭圆。修正方法是:
- 用 TextureSize 读出 Width 和 Height。
- 计算宽高比
Aspect = Width / Height。 - 对 UV 的 U 轴乘以 Aspect(或者对 V 轴除以 Aspect),再传入后续的距离计算节点。
这样斑点在任何宽高比的纹理上仍然是正圆。早年我还没掌握这个方法时,都是靠美术手动把纹理画成正方形来规避问题,既浪费资源又限制创作自由度。现在直接在材质层面解决,美术那边传来任何比例的贴图都能正常显示。
4.3 配合渐变纹理做“尺寸映射”
还有一种偏冷门但很实用的玩法:把 TextureSize 和渐变纹理(Gradient Texture)配合使用,根据纹理的分辨率缩放渐变范围。
比如做一个雪地覆盖效果,希望雪线的高低随着地形纹理分辨率的提升而变化——分辨率越高,过渡越平滑。实践中可以把 Height 值归一化后,映射到渐变采样的控制参数上。这样做的好处是,美术在低分辨率预览时看到的“过渡硬度”和高分辨率最终输出时保持一致,不用来回调参数。
这个做法特别适合地形场景的全景渲染。地形纹理往往是被大幅拆分的,不同 tile 分辨率不完全一致,如果不做这种映射,地形块交接处会出现明显的过渡不一致。用 TextureSize 统一口径之后,所有块的表现就整齐多了。
5. 常见问题与排查技巧实录
5.1 值为 0 或异常小,导致画面变黑/条纹
这是排查频率最高的问题。现象是材质里接了 TextureSize 之后,画面某一通道突然全黑,或者出现大量规则噪点。
排查步骤:
- 第一步,先确认纹理引用是否真的连到了节点输入端口。这种节点经常被选中后误拖动,引用丢失时输出默认是 0。
- 第二步,检查纹理导入设置里的“可读/读写”选项。部分引擎在关闭纹理可读状态下,读取尺寸返回的值可能是占位值。
- 第三步,确认平台覆盖参数。打开平台相关 Override,看 Max Size 是否被压到极小值,比如 1×1。
- 第四步,如果纹理是渲染纹理(RenderTexture)动态生成的,注意它是否已经完成初始化。未初始化的渲染纹理拿到的尺寸可能不是预期值。
5.2 平台间表现不一致,PC 上正常,移动端偏大/偏小
前面讲过的“尺寸漂移”是这里的主要原因。PC 上通常用的是未压缩或高质量压缩,移动端则容易启用低尺寸限制和块状压缩,导致同一张贴图在不同平台上的实际驻留尺寸不同。
最直接的排查方式:在目标平台下跑一段 Debug 输出,把 TextureSize 的值显示到屏幕上,或者打到日志里。对比苹果、安卓、PC 的三组数值,基本立刻能定位差异来源。
定位到问题后,处理逻辑不是去强制让所有平台的纹理尺寸一致(这会严重影响包体和内存),而是让你的材质计算逻辑具备“自适应”能力——凡是和尺寸相关的系数,都用读取值去推演,而不是写死。
5.3 与 Mipmap 相关的“远处闪烁”问题
用了 TextureSize 做平铺或频率计算后,有时会发现远处出现密集的闪烁噪点,或者纹理远景出现摩尔纹。这个问题的根源在于:尺寸关联的平铺可能让 UV 频率超过了纹理的 Mipmap 极限。
解决方案不是关掉 Mipmap,而是:
- 将关联计算后的 UV,接入到采样节点的“采样级别”控制里,让 Mip 选择也跟随密度变化。
- 或者在除法计算后,先对结果做一次
clamp,限制最大/最小平铺次数,避免 UV 频率过高。 - 如果仍然不够,再考虑在材质内部做一层平滑处理(比如对 UV 做低通滤波)。
不过要提醒的是,Mipmap 相关的调试非常依赖视角和距离,建议在场景里放一个固定视角相机,方便反复对照微调。
5.4 边缘接缝与重复采样瑕疵
用尺寸信息做多纹理混合时,有时接缝处会出现一条明显的颜色断层。这通常是两张贴图的 Wrap Mode 不一致导致的:一张是 Repeat,另一张是 Clamp,采样到边缘时表现完全不同。
把两张纹理的 Wrap Mode 统一,或者在 UV 传入采样前做一次fract(取小数部分)处理,可以消除这类接缝。如果接缝还是存在,检查纹理有没有生成 Mipmap——没生成的话,远景接缝处的过滤会非常生硬。
| 现象 | 优先排查方向 | 常见解决办法 |
|---|---|---|
| 输出值为 0 | 纹理引用丢失/TexelSize 未初始化 | 重新连线;检查渲染纹理初始化 |
| 平台表现不一致 | 平台覆盖参数/Max Size/压缩格式 | 统一用节点读取值做计算 |
| 远景闪烁 | Mipmap 选择异常 | clamp 平铺次数;显式采样级别 |
| 接缝断层 | Wrap Mode 不一致 | 统一 Repeat;UV 做 fract |
6. 一点个人经验
最后分享一个我最近常用的“防御性连线”习惯。所有非正方形纹理接入 TextureSize 参与计算前,我会先把宽高比算出来,再对后续所有需要等比缩放的逻辑做一次比例修正。这个习惯帮我省掉了很多次“真机上图案变形”的返工。
另一个实打实的经验:不要在材质图表里堆太多 TextureSize 节点,尤其是同一张贴图重复读尺寸。有些材质我一开始图方便,三四个地方都去读同一张贴图的尺寸,后来发现中间节点一多,排查效率明显下降。现在我的做法是进入材质函数(或子图)里读取一次,把尺寸和宽高比作为输出端口暴露出来,上层图表只引用这个统一出口。维护成本低很多,改动一处就能全部生效。
材质系统的乐趣就在于,很多看似不起眼的小节点,组合起来能解决相当复杂的问题。TextureSize 就是其中典型的“信息型节点”——它不直接产生视觉颜色,却决定了整个材质风格的稳定底线。花点时间把它吃透,后面做跨平台适配、多纹理混合、地形材质的时候,会省下大量的试错时间。