透明合成
全库目录221
本文目录17
两个半透明面交叉时,简单排序很难给所有像素同一个前后顺序。透明颜色需要保留多层贡献,深度写入和混合决定哪些层参与。排序、深度剥离、链表与加权方案保存的信息不同,我们按交叉面和覆盖层数比较结果,再测内存与采样成本。
Alpha Test
Alpha Test/Clip 根据阈值保留或丢弃片元。保留下来的部分仍可以写深度,因此适合树叶、栅栏和硬边镂空。
主要问题:
discard/clip可能限制 Early-Z;- 边缘会产生采样锯齿;
- Mipmap 会平均 Alpha,远处覆盖率可能变化;
- 双面叶片容易增加 Overdraw。
Alpha-to-Coverage 可以把 Alpha 转换成 MSAA Coverage,改善边缘,但依赖 MSAA 样本数和平台实现。
Alpha Blend
普通非预乘 Alpha 的常见公式:
是源颜色, 是目标颜色。
预乘 Alpha 的源 RGB 已经乘过 Alpha:
预乘表示在纹理过滤、边缘颜色和多次合成时通常更稳定,但资源制作、Shader 输出和 Blend State 必须使用同一约定。
透明排序
标准 Alpha Blend 不满足交换律。先画 A 再画 B,和先画 B 再画 A 的结果不同。因此透明物体通常从远到近绘制,并开启深度测试、关闭深度写入。
对象级排序不能解决:
- 两个透明网格相交;
- 一个网格内部三角形顺序错误;
- 粒子互相穿插;
- 玻璃需要折射后方画面。
两个事实叠加造成了这个长期存在的问题。其一,SrcAlpha, OneMinusSrcAlpha 这一混合公式对前后两项不对称,结果依赖绘制顺序;相比之下 One, One 加法混合前后对称,所以发光特效通常不受排序困扰。其二,引擎的透明排序粒度是逐对象的,而离线渲染器按逐像素求解可见性,这解释了为什么同一份资产在 DCC 里正常、进引擎却出现穿插错误。
单个网格内部的三角形绘制顺序由索引缓冲的排列决定。仅做对象排序的路径保留该顺序,所以网格内部的自遮挡错误无法靠对象排序修复。
常见透明方案
Sorted Transparency
最常见。成本可控,但排序不可能覆盖所有相交情况。
Additive
加法混合接近可交换,适合火花和发光粒子,但无法表达正常遮挡和吸收。
Weighted Blended OIT
Weighted Blended OIT 用两个 Render Target 累积透明颜色和剩余透射率。对每个透明片元,可使用:
是随深度和 Alpha 变化的权重。累积目标使用加法混合,Revealage 目标使用乘法混合。合成时:
累积颜色目标清为0,Revealage清为1。C表示线性、未预乘的表面颜色,着色器实际向累积目标输出 float4(C * alpha, alpha) * w,再用源/目标因子One/One相加;不能直接把C输出到该目标。Revealage输出Alpha到红通道,用Zero/OneMinusSrcColor相乘。最终颜色为 ,空像素R=1时保持背景。权重应为有限正数,结合目标格式限制范围,检查低Alpha精度和累积溢出。这种方法的资源和绘制次数稳定,适合粒子、烟雾和大量近似透明层。
例如红、蓝两层的Alpha均为0.5、权重均为1、背景为黑,累积值为(0.5,0,0.5,1),R为0.25,合成得到(0.375,0,0.375)。交换提交顺序不改变该结果;若按红在蓝前精确合成,则得到(0.5,0,0.25)。这个差值直接展示了加权方法丢失的层次信息。
它没有保存精确层顺序,因此不同深度的强颜色、接近不透明的表面和玻璃吸收会有偏差。权重函数只能调整误差分布,不能恢复被丢掉的排序信息。
Per-pixel Linked List
片元阶段用原子计数器在全局 Node Buffer 分配节点,并通过 Head Pointer Texture 建立每像素链表。Resolve Pass 遍历链表、按深度排序,再按顺序混合。
在单采样、节点容量足够且层排序规则明确时,它能保留有序合成所需的片元属性。相同深度的层需要稳定身份作为次排序键;MSAA还要逐样本建链,或保存覆盖掩码并逐样本解析,单个像素一条无覆盖信息的链不能保证样本级等价。必须设置最大节点容量。大量 Overdraw 会耗尽 Buffer,溢出策略需要明确。链表写入的随机访存、原子竞争、排序和清空 Head Pointer 都有成本。
链表节点至少保存颜色、Alpha、深度和next索引,每像素head清为无效索引,全局分配计数清零。片元先原子分配唯一nodeIndex,容量有效时用原子交换把该像素head改为nodeIndex,交换返回旧head,随后写node.next=oldHead及其他属性。例如同一像素原head为无效,A获得节点5并接到无效,B获得节点8并接到5,Resolve从8遍历到5。
原子交换只保证head更新不丢失,不能保证另一个并发消费者已经看见节点全部字段。本方案把Resolve放在单独通道,收集完成后通过API资源依赖发布全部节点,再遍历排序。收集通道不遍历未完成的链。计数超容量时应先拒绝该节点并记录溢出,不能把无效节点发布到head;Resolve还需有最大遍历数和明确的溢出回退。按深度排序之后才恢复Alpha层顺序,链的插入顺序本身只是执行先后。
Depth Peeling
Depth Peeling 用多轮重绘逐层“剥离”深度。每一轮只保留当前最靠近相机的那一层,把它的深度记下来;下一轮凡是深度不比记录值更远的片元一律丢弃,于是暴露出第二层,如此递推。
单轮的判据很简单:采样上一轮的深度图,当前片元深度不大于它就 discard。深度图是屏幕空间资源,采样时必须用屏幕坐标而非模型 UV:
float depth = input.deviceDepth;
float opaqueDepth = OpaqueDepth.SampleLevel(PointClamp, input.screenUV, 0).r;
if (depth >= opaqueDepth) discard;
float prevDepth = PreviousDepth.SampleLevel(PointClamp, input.screenUV, 0).r;
if (peelIndex > 0 && depth <= prevDepth + depthTolerance)
discard;
output.depthTarget = depth;
output.colorTarget = shadedColor;片段采用单采样与普通Z,较小值更近;OpaqueDepth保存不透明阶段完成后的只读深度,透明片元先通过这份遮挡约束,同深度按本例策略拒绝。每轮将独立的剥离深度附件清成1、开启Less测试和深度写入,选择剩余层中最近样本。depthTarget是保存获胜样本深度的颜色输出,不能代替实际参与深度测试的附件;颜色和该深度副本应同时随获胜样本更新。反向Z要同步反转比较和清除值。输入是设备深度,不能把片元SV_Position再做任意w除法。第0轮不做剥离判断,直接得到最近层。depthTolerance与设备深度同单位,应按格式和场景范围选择;容差过小可能让同一层反复出现,过大会跳过薄层。前后轮采用两张深度纹理交替读写,避免同一子资源同时作为采样输入和写入目标。
层数用尽后从最远层向前合成,每层用标准公式叠加:
剥离 层就要把透明几何绘制 遍。各层深度可区分、剥离轮数覆盖全部片元时,它能恢复逐像素的有序Alpha合成。相同深度或落在同一容差区间的多个透明片元可能只保留一个,需要明确处理并列层;有限轮数还会丢弃后方贡献。大量低Alpha层的剩余透射率可能很高,截断预算按内容验证。
朴素实现需要为每层保存一张颜色纹理,显存开销可观。两种收敛方式:从后往前剥离并即时混合,只需一张累积纹理;或保持从前往后剥离,用前向累积公式就地合成:
Cacc初始化为0,Tacc初始化为1,层按前到后顺序累积。最后加Tacc乘背景颜色。对相同层和线性Alpha方程,这个前向累积与后到前合成等价,可省去保存全部层颜色。
深度剥离按层重绘,内存上限容易控制,但绘制次数随层数增长。逐像素链表一次收集片元,再排序合成,成本受节点容量、原子竞争与层数影响。两者的速度和截断误差需在相同场景比较;不能从遍历次数直接推定链表更快。
针对性的轻量方案
内容形态固定时,可以用资产约束减少通用OIT需要保存的信息。例如头发中心可近似不透明时,先建立核心深度;只允许有限视角的固定网格,则可离线保存若干索引顺序。节省成本来自接受了这些约束,超出条件的部分仍有排序误差。
固定深度法
适用于头发这类“中心大面积接近不透明、仅边缘半透明”的资产。先用一个 Pass 只把不透明区域的深度写入,不输出颜色;后续半透明绘制受这层深度约束,被遮挡的部分被直接剔除,不可能再画到前面去。
这层深度固定了被定义为不透明的核心区域;半透明边缘之间仍需排序。将接近不透明的部分强制视作不透明也会改变透射量,这项近似是否可接受取决于资产和背景。
它的价值在于同时保住了两端:纯 Alpha Test 边缘生硬,纯 Alpha Blend 边缘柔和但排序错误,这个方案得到柔和边缘加上基本正确的深度关系。
关键在于把“哪些像素算作不透明”与 Alpha Clip 阈值对齐:
// 0 < _Cutoff <= 1,与深度Pass的Alpha Clipping阈值一致
output.a = saturate(mainTex.a / _Cutoff);
clip(mainTex.a - 1e-4);预计算顺序法
既然网格内部顺序由索引缓冲决定,就可以离线为若干观察方向各排好一套索引,运行时按当前视角切换。
做法是把三角形按其顶点在某个方向轴上的坐标和排序,生成正序与反序两套索引数组;运行时用相机到物体中心的方向与物体各轴做点积,挑选最接近的一套写回网格索引。
方向采样密度取决于内容与允许误差。四个水平方向或22个球面方向都只能作为候选配置,不能保证正确:同一方向区间内的三角形顺序仍可能交换,三角形相交时甚至没有统一的前后顺序。应在相邻方向切换处和极端视角检查跳变。
代价是每个方向一套索引数组的内存,以及切换时回写索引缓冲的开销。它只解决网格内部的三角形顺序,不处理不同对象之间的相交,且对顶点数大的网格排序预计算耗时明显。
方案选择
| 方案 | 精度 | 主要成本 | 适用 |
|---|---|---|---|
| 对象排序 | 对象级 | 排序 CPU 开销 | 分离良好的透明体 |
| 固定深度法 | 核心强制不透明的近似 | 一个额外深度 Pass | 头发等中心不透明资产 |
| 预计算顺序 | 三角形级 | 索引内存与回写 | 旋转自由度受限的固定资产 |
| Weighted Blended | 近似 | 两个 RT,一趟绘制 | 粒子、烟雾等低 Alpha 层 |
| Depth Peeling | 层可区分且未截断时恢复有序合成 | 绘制 n 遍 | 层数有限、要求精确 |
| Per-pixel Linked List | 逐像素精确 | 原子操作与 Buffer 容量 | 桌面端、Overdraw 可预估 |
透明性能
透明通常不能利用普通不透明物体的完整深度拒绝。同一区域覆盖很多层时,Pixel Shader 和 Blend 都会重复执行。
优化方向:
- 控制屏幕覆盖面积,而不只是粒子数量;
- 使用紧贴内容的粒子网格;
- 降低透明 Shader 和纹理采样成本;
- 近远景使用不同粒子复杂度;
- 在移动端注意 Blend、带宽和 Tile Memory;
- 用 Overdraw 视图和 GPU Counter 验证。
验证方法
- 用互相穿插的透明网格检查对象排序与逐像素排序差异。
- 固定背景对比 Alpha Blend、加权 OIT 和深度剥离参考图。
- 逐步增加透明层数,记录节点溢出、带宽和图像退化。
本机CPU验证:前后向Alpha合成等价由 completion_reference.py 检查,运行 python 知识库/examples/mechanisms/completion_reference.py。这些检查验证线性Alpha合成模型,未包含GPU执行。
加权OIT的空背景、单层与红蓝双层数值由 raster_models.py 和 test_raster.py 检查,运行 python -B 知识库/examples/revision/test_raster.py。模型使用CPU双精度,不模拟浮点附件溢出与GPU混合精度。
相关主题
实现状态:Alpha前后向合成模型已由链接中的CPU实验检查。剥离HLSL是依赖深度目标与混合状态的片段,正文抽取入口经DXC离线编译;OIT资源、溢出和真实画面尚未在GPU运行。
参考资料
- Morgan McGuire,Implementing Weighted, Blended OIT:2026-10-22核读Algorithm Overview与3D Transparency Pass,支持双目标清除值、累积混合和Revealage配置;本文未复现来源画面。
- Alpha表示维护线性Alpha合成与预乘表示的基础来源。
- 深度剥离前后向等价由本库completion_reference.py验证,具体GPU资源与性能属于待执行项。