03 透明纹理、黑边与颜色空间
前置知识:第 01 章的普通 Alpha 混合,以及第 02 章的源/目标因子。
本章示例:透明纹理四格对照 · 完整源码。
预期画面:浅背景下,右上角软圆出现明显暗边;左上与左下相近,右下为硬边裁切。
学完本章能做什么
你将能分辨贴图 RGB 与 Alpha 各管什么,解释直通 Alpha 与预乘 Alpha 约定不匹配造成的黑边,并根据软烟雾或硬轮廓选择普通混合、alphaTest、alphaHash。你还会知道纹理 colorSpace 改变颜色解释方式,不能拿它当所有黑边的修复开关。
先看现象
打开示例,把背景改为“浅色”,观察四格:
| 位置 | 数据与材质 | 应看到的结果 |
|---|---|---|
| 左上 | RGB 保持橙色,Alpha 从中心向外下降;普通混合 | 柔和的橙色边缘 |
| 右上 | RGB 已随 Alpha 被压暗,却仍按普通混合处理 | 边缘出现灰黑晕圈 |
| 左下 | 使用左上的直通数据,材质在输出阶段预乘 Alpha,并配套改变混合因子 | 与左上相近的软边 |
| 右下 | 低于阈值的片元直接丢弃 | 轮廓较硬,没有半透明过渡 |
切换棋盘格能看清透明区域是否真的露出背景;切换深色背景则能说明黑边在不同目标颜色上有不同显著程度。
1. RGBA 是四个数,不是一种“透明颜色”
贴图每个采样位置可提供红、绿、蓝和 Alpha。RGB 描述片元颜色,Alpha 描述其参与混合的权重。对普通混合,材质整体透明度又会影响片元 Alpha。简化地看:
片元 Alpha ≈ 纹理 Alpha × 材质 opacity
若纹理里完全透明的像素写成 (0,0,0,0),在不经过纹理过滤的理想单点采样里它不会对普通混合贡献颜色。但贴图放大、缩小、做 mipmap 或从图集边界取样时,GPU 会在相邻像素之间插值,透明黑像素的 RGB 会混进边缘采样。于是采样结果可能是“低 Alpha 的暗 RGB”,再参加混合,形成暗边。
因此,带软边的颜色贴图,透明区域的 RGB 也应经过设计。常见做法是把图形边缘的颜色延伸到透明区域,让过滤后的低 Alpha 采样仍保持合理色相;纹理图集还需留出边缘填充,避免采到邻格。
2. 直通与预乘 Alpha 的配套公式
**直通 Alpha(straight alpha)**存储原始 RGB C 和 Alpha a。普通混合在输出时使用:
C结果 = aC + (1−a)D
**预乘 Alpha(premultiplied alpha)**先准备 P=aC,对应的混合应使用:
C结果 = P + (1−a)D
两种约定配套使用,才会得到相近的结果。若 RGB 已经乘过一次 Alpha,却又按直通方式使用源 Alpha 因子,就近似得到 a²C + (1−a)D。在 0<a<1 的边缘,源颜色被压暗两次,黑边就出现了。示例右上的贴图特意做出这种数据与混合约定不匹配:它不是说“预乘 Alpha 必然产生黑边”。
Three.js r174 的 Material.premultipliedAlpha = true 会在材质输出阶段预乘 RGB,并为内置混合选用配套因子。示例左下使用的仍是未预乘的贴图 RGB,所以它与左上相近。另一个设置 Texture.premultiplyAlpha 控制纹理上传时是否预乘;它和材质设置发生在不同阶段,不能不加分析地同时打开,否则可能再次预乘。外部图片是否已经预乘,也要先确认。
3. 软边、硬边和随机裁切
若效果需要连续透明度,例如烟、雾、柔和光晕,可用 transparent: true 和普通或加法混合。这些效果容易遇到透明排序和过度绘制问题,后续章节会处理。
若图案只有“留或丢”的轮廓,例如树叶、纸片、某些像素风特效,可用 alphaTest:
const cutoutMaterial = new THREE.MeshBasicMaterial( {
map: texture,
alphaTest: 0.4
} );
Alpha 低于阈值的片元被丢弃,其余片元按材质颜色显示。示例右下保持 transparent: false,所以它不是“被裁过的半透明烟”:保留下来的部分是不透明的。阈值越高,轮廓越小、越硬。裁切避免普通透明混合的一些排序问题,但会有锯齿和细节缺失。
alphaHash 用位置相关的随机阈值决定是否保留片元,能把连续透明度近似为颗粒覆盖。示例右下的下拉框可切换到它;没有时间抗锯齿时,你会直接看见噪点。alphaToCoverage 则借助 MSAA 的样本覆盖平滑裁切边缘,需要相应抗锯齿条件,不能和普通透明混合视为同一种技术。
4. 颜色空间不等于 Alpha 约定
颜色贴图通常需要标注其 RGB 的编码方式。例如图片或本示例的颜色数据按 sRGB 编码时:
texture.colorSpace = THREE.SRGBColorSpace;
Three.js 会按该标注解释颜色数据,并在显示前转换到渲染器的输出色彩空间。数据纹理、深度纹理等并非颜色图片,不应盲目设置成 sRGB。Alpha 是覆盖/透明度信息,不应像 RGB 一样按 sRGB 曲线解码。
试着勾选示例的“故意标成 NoColorSpace”。橙色的亮度和色调会变化;右上角由RGB 与混合约定不匹配产生的暗边并不会因此得到通用修复。需要分别诊断:是 RGB 的色彩空间错了,还是透明边缘数据/混合方式错了。
5. 从贴图到材质的排查顺序
- 看源贴图的 Alpha 是否真有连续软边,还是已经被阈值裁掉。
- 检查低 Alpha 区域的 RGB:透明像素是否被黑色填充?图集是否缺边缘扩展?
- 确认 RGB 是直通还是预乘,以及材质混合因子是否与之配套。
- 检查颜色贴图的
colorSpace,不要用错误色彩空间去掩盖黑边。 - 在亮、暗和棋盘背景上分别观察,再检查缩放和 mipmap 后的效果。
常见问题
把 alphaTest 调高能修好烟雾黑边吗? 它可以直接丢掉问题最明显的低 Alpha 像素,但也会抹去烟雾所需的软边。应先修正贴图 RGB 与混合约定。
开启 premultipliedAlpha 就一定正确吗? 取决于输入数据和渲染路径。示例左下输入的是直通 RGB,再由材质预乘;若贴图早已预乘,重复处理可能再次压暗边缘。
alphaHash 的颗粒是坏了吗? 不是。它用随机保留/丢弃来近似透明度;没有合适的时间抗锯齿时,颗粒会明显可见。
完成第 03 章练习后,下一阶段将进入 depthTest、depthWrite、colorWrite 与 renderOrder,解释透明效果的遮挡问题。
参考:Texture.colorSpace 与 premultiplyAlpha、Material.alphaTest、alphaHash 与 premultipliedAlpha。