
























详细说明顺序无关半透明(OIT)
下面我把 OIT(Order‑Independent Transparency,顺序无关半透明) 讲得非常细:它解决什么问题、原理、主流算法、优缺点、和普通 Alpha Blend 的区别,以及你最关心的:为什么它可以同时开启 Alpha Blend + 开启深度写入。
标准半透明渲染做法:
先画所有不透明物体(深度写入 ON)
把所有半透明物体按 距离相机从远到近排序
关闭深度写入(DepthWrite=OFF),开启 Alpha Blend,按顺序绘制
必须 CPU 排序,性能开销大
每帧排序所有透明物体 / 三角形
场景复杂时(粒子、玻璃、植被)成本很高
物体交叉 / 互相穿透时,根本排不出正确顺序
两个互相交叉的半透明面片:不存在全局正确的绘制顺序
结果必然有部分区域渲染错乱(颜色叠反、边缘闪烁)

半透明物体不能写入深度,导致 EarlyZ 对它们完全无效
一句话:传统 Alpha Blend 是 “物体级排序”,只能处理简单、分离的透明物体;一旦复杂就崩。
OIT 的本质:
不要求提前对物体排序,而是让 GPU 在每个像素位置,收集所有覆盖该像素的透明片元(fragment),在屏幕空间统一按深度排序,最后正确混合。
关键点:
排序从 CPU → 物体级 → 每帧一次
变成 GPU → 像素级 → 每个像素独立排序
渲染时不需要管绘制顺序,随便画

代表算法:
每像素链表 PPL(Per‑Pixel Linked Lists)
深度剥离 Depth Peeling(多遍)
A‑Buffer / S‑Buffer(片元缓存)
共同特点:
存储每个像素的所有片元(颜色 + 深度 + Alpha)
最后 按深度从小到大排序,再做标准的 over 混合
结果和 “完美排序的传统 Alpha Blend” 一模一样,无误差
初始化两个全屏缓冲区
StartOffsetBuffer:每个像素的链表头指针
FragmentBuffer:全局片元池(存颜色 + 深度)
渲染所有半透明物体(不排序!)
开启 Alpha Blend + 开启深度写入(关键!)
片元着色器:
计算颜色、深度、Alpha
向 FragmentBuffer 追加一条记录
更新当前像素链表指针
因为开启深度写入,透明物体之间可以正常遮挡,EarlyZ 也能工作!
后处理 Pass:逐像素排序 + 混合
遍历每个像素的链表
按深度从远到近排序所有片元
执行标准 Alpha 混合:dst = src·α + dst·(1−α)
优点:结果完美、支持复杂交叉透明、透明物体可写深度、EarlyZ 有效缺点:显存开销大(需存所有片元)、GPU 占用高
代表算法:
加权混合 OIT(Weighted Blended OIT)
自适应透明(Adaptive Transparency)
深度剥离(少量层数)
核心思路:
不存储所有片元,而是用数学公式直接累积颜色与透明度
不需要排序,单遍即可完成
牺牲一点精度,换取极高性能
以 Weighted Blended OIT(最经典近似 OIT) 为例:公式(简化):
accumColor += color * alpha * weight
accumAlpha += alpha * weight
最后归一化:
finalColor = accumColor / accumAlpha
weight 通常用 深度函数(远的片元权重高),近似模拟 “远→近” 混合
完全不需要排序、不需要链表、不需要多遍
可以开启深度写入,透明物体之间正常遮挡
优点:单遍、极低开销、易实现、支持 EarlyZ缺点:结果是近似的,厚 / 高透明度物体可能有轻微色差或过亮
回到你之前的疑问:
“Alpha 混合” 与 “禁用深度写入” 不是绑定的吗?
答案:只有传统排序 Alpha Blend 才必须禁用深度写入;OIT 完全不需要!
依赖从远到近绘制顺序
如果开启深度写入:先画的近物会挡住后画的远物,顺序直接报废
所以只能:关深度写入 → 不遮挡 → 靠顺序叠
OIT 不依赖绘制顺序,所有片元都会被收集并在最后排序
开启深度写入带来两大好处:
透明物体之间可以正常遮挡(符合物理直觉)
EarlyZ 对透明物体生效:被不透明物体挡住的透明像素会被提前剔除,大幅节省 PS 开销
一句话总结:
传统 Alpha Blend:关深度写入 → 靠顺序 → 不能遮挡 → EarlyZ 无效
OIT:开深度写入 → 不靠顺序 → 能遮挡 → EarlyZ 有效
复杂玻璃 / 建筑透明(多层、交叉)
粒子系统(烟雾、火焰、魔法特效,大量重叠)
植被渲染(树叶半透明、互相交错)
透明 UI / 体积光 / 镜头光晕
需要透明物体互相遮挡的场景
表格
OIT 是把半透明渲染从 “靠 CPU 排好顺序再画”,变成 “随便画、GPU 每个像素自己排好再混合” 的一类技术;它允许透明物体开启深度写入、互相遮挡并享受 EarlyZ,彻底解决了传统 Alpha Blend 的排序与交叉错乱问题。
结构:RWByteAddressBuffer 或 RWStructuredBuffer<uint>每个像素存一个链表头指针,指向 FragmentBuffer 中的节点索引。初始值:0xFFFFFFFF(-1,表示空)
屏幕像素 (0,0) → head = 2
屏幕像素 (0,1) → head = 5
屏幕像素 (0,2) → head = 0xFFFFFFFF(空)
...
屏幕像素 (x,y) → head = index
每个节点结构:
struct Fragment{float4 color; // 源颜色 + alphafloat depth; // 深度uint next; // 下一个节点索引};
结构像这样:
FragmentBuffer[ 0 ] → { color, depth, next=3 }
FragmentBuffer[ 1 ] → { color, depth, next=... }
FragmentBuffer[ 2 ] → { color, depth, next=1 } → 这是像素(0,0)的链表头
FragmentBuffer[ 3 ] → { color, depth, next=0xFFFFFFFF }
像素(0,0)
StartOffset = 2
↓
Fragment[2] → next=1
↓
Fragment[1] → next=3
↓
Fragment[3] → next=0xFFFFFFFF(结束)
StartOffsetBuffer:每个像素一条链表的头
FragmentBuffer:全局大池子,存所有透明片元
原子计数器:分配下一个可用节点(IncrementUint)
Accum:R16G16B16A16_FLOAT存:accum += rgba * alpha * weight
Reveal:R16_FLOAT存:reveal *= (1 - alpha)
// 输出RT_OUTPUT(float4, 0) accum;RT_OUTPUT(float, 1) reveal;void PS(...){float4 col = ...; // 纹理采样 + 光照float alpha = col.a;// 权重:远处更重要(典型公式)float weight = 1.0 / (0.01 + pixelDepth * pixelDepth);// 累加
accum = float4(col.rgb * alpha * weight, alpha * weight);
reveal = (1.0 - alpha);}
float4 PS(...){float4 accum = AccumTex.Sample(sam, uv);float reveal = RevealTex.Sample(sam, uv).r;float3 rgb = accum.rgb / accum.a;float alpha = 1.0 - reveal;return float4(rgb, alpha);}
优点:单遍、快、显存低、全平台支持。现在游戏里最常用的 OIT。
RWStructuredBuffer<uint> StartOffset;RWStructuredBuffer<Fragment> FragmentBuffer;RWByteAddressBuffer Counter;void PS(...){float4 col = ...;float depth = ...;// 原子分配节点uint newIdx = 0;
Counter.InterlockedAdd(0, 1, newIdx);// 拿到当前像素链表头uint2 pix = uint2(floor(pin.pos.xy));uint head = 0;
StartOffset.InterlockedExchange(pix.x + pix.y * width, newIdx, head);// 写入节点
Fragment f;
f.color = col;
f.depth = depth;
f.next = head;
FragmentBuffer[newIdx] = f;}
void PS(...){// 1. 遍历链表,把所有fragment读到数组
Fragment fragments[8];int count = 0;uint idx = StartOffset[pix];while (idx != 0xFFFFFFFF && count < 8){
fragments[count++] = FragmentBuffer[idx];
idx = FragmentBuffer[idx].next;}// 2. 按深度从远到近排序SortFragmentsByDepth(fragments, count);// 3. 混合float3 color = 0.0;float alpha = 0.0;for (int i=0; i<count; i++){
color = color * (1 - fragments[i].color.a) + fragments[i].color.rgb * fragments[i].color.a;
alpha = alpha * (1 - fragments[i].color.a) + fragments[i].color.a;}return float4(color, alpha);}
优点:完美正确。缺点:显存不可控、性能高、移动端不友好。
使用:UAV + StructuredBuffer
原子操作:InterlockedExchange、InterlockedAdd
绑定:SetGraphicsRootDescriptorTable
使用:StorageImage / StorageBuffer
原子操作:atomicUintAdd、atomicExchange
绑定:DescriptorSet + binding
链表头缓冲
节点缓冲
原子计数器
收集 → 排序 → 合成
OIT 算法本身不区分 API,区别仅在于如何创建 UAV/StorageBuffer。
单遍、快、显存低、不用排序、不用复杂结构
支持所有平台(PC / 主机 / 移动端)
几乎所有粒子、烟雾、火焰、体积云、头发都用它
Unity、Unreal、CE5、自研引擎全都内置
这是工业界唯一 “普及” 的 OIT。
太老、太慢、多遍渲染
几乎已淘汰
结果完美
但显存不可控、性能高、移动端不行
只用于影视、高精度渲染、极少游戏
改进 WBOIT,质量更高
开始在 3A 中出现
光追直接按顺序混合
下一代透明方案,但目前仍小众
工业界最主流、唯一广泛使用的 OIT:Weighted Blended
精确 OIT(PPL/DepthPeeling)只用于研究 / 高精度,不普及
OIT 可以开启深度写入 + 深度测试,Early Z 有效
传统 Alpha Blend 必须关深度写入、必须排序;OIT 不需要
如果你愿意...
给你画一张 PPL OIT 的内存布局图(StartOffset + FragmentBuffer)
或写一份 HLSL 伪代码(PPL 或 Weighted OIT)
或对比 Direct3D 12 / Vulkan 下的实际实现要点
主流的顺序无关半透明算法有哪些,在现在得到广泛使用了吗
已完成思考
先给核心结论:OIT 算法分为精确 OIT(无视觉误差,结果与完美排序的 Alpha Blend 完全一致) 和近似 OIT(性能优先,误差可控) 两大类;其中近似 OIT(尤其是加权混合 WBOIT)已经在游戏、工业渲染领域得到极广泛的应用,精确 OIT 也在高精度渲染场景中普遍使用,早已不是实验室技术。
所有 OIT 算法的核心目标,都是摆脱传统 Alpha Blend 对「从后往前绘制顺序」的强依赖,解决交叉透明、多层透明的渲染错误问题。下面按「精确 / 近似」分类,讲解当前行业主流的算法,淘汰的老旧方案仅做补充说明。
这类算法会完整收集每个像素的所有透明片元,按深度严格排序后执行标准 Alpha 混合,渲染结果 100% 正确,无任何视觉误差,代价是更高的显存与 GPU 计算开销。
当前精确 OIT 的绝对主流方案,也是你之前要求画图的算法,D3D11 引入 UAV(无序访问视图)后开始普及,D3D12/Vulkan 对其支持已非常完善。
在 GPU 显存中构建两套核心缓冲区,为每个像素维护一个单向链表,存储所有覆盖该像素的透明片元数据:
StartOffsetBuffer(起始偏移缓冲区):全屏 2D 缓冲区,每个像素存储一个 32 位无符号整数,代表该像素的链表头在全局片元池中的偏移地址,初始值为0xFFFFFFFF(空链表)。
FragmentBuffer(片元缓冲区):一维结构化缓冲区,作为全局片元内存池,每个节点存储:片元颜色 RGBA、深度值、下一个节点的偏移地址。
预分配缓冲区:根据场景最大透明片元数量,预分配 FragmentBuffer 的大小(通常按全屏像素数的 4~8 倍分配);初始化 StartOffsetBuffer 为全0xFFFFFFFF。
透明物体渲染 Pass:
开启深度测试,可开启深度写入(不透明物体会提前遮挡透明片元,Early Z 生效);
像素着色器中,通过InterlockedAdd原子操作,从 FragmentBuffer 中申请一个空闲节点;
将当前片元的颜色、深度、当前像素的链表头偏移写入节点;
原子更新 StartOffsetBuffer 中当前像素的链表头,指向新申请的节点。
后处理混合 Pass:
遍历每个像素的链表,收集所有片元;
按深度值从远到近排序;
执行标准 Alpha 混合公式C = a_s*C_s + (1-a_s)*C_d,得到最终像素颜色。
✅ 优点:结果完全正确,支持任意复杂的交叉、多层透明,透明物体可开启深度写入,Early Z 优化生效;
❌ 缺点:显存开销高(需预分配片元池)、GPU 原子操作有开销,极端场景下片元数量会超出预分配上限。
建筑可视化、CAD 工业软件、载具座舱渲染、复杂玻璃幕墙等对透明精度要求极高的场景。
PPL 的前身与优化变体,核心思路是为每个像素预分配固定大小的数组,而非动态链表,存储该像素的所有透明片元。
核心差异:无需原子操作构建链表,内存布局更规整,GPU 访问效率更高;
缺点:固定数组大小会浪费显存,或无法容纳极端情况的大量片元;
适用场景:片元数量可控的场景,如粒子系统、UI 透明渲染。
最早的精确 OIT 算法,通过多 Pass 渲染实现,无需 UAV / 原子操作,兼容性极强。
通过 N 次渲染 Pass,逐层剥离透明片元:
第 1Pass:渲染所有物体,记录最近的深度值(最前面的一层);
第 2Pass:仅渲染深度大于上一层的片元,得到第二层透明片元;
重复 N 次,直到剥离完所有透明层;
最后将所有层从后往前混合,得到最终结果。
基础版深度剥离已经很少使用,原因是 Pass 数量与透明层数成正比,性能开销极大;仅双深度剥离(Dual Depth Peeling) 等优化变体,还会在兼容性要求高的老旧平台使用。
A-Buffer 的轻量化变体,仅为每个像素存储前 K 个最近的透明片元,超出 K 层的片元会被合并丢弃。
优点:显存开销可控,性能比完整 PPL 高很多;
缺点:K 值过大会增加开销,K 值过小会丢失远处的透明层,出现误差;
适用场景:移动端、半透明层数可控的游戏场景。
这类算法不存储、不排序所有片元,而是通过数学公式直接累积片元的颜色与透明度信息,单 Pass / 双 Pass 即可完成渲染,性能极高,误差可控,是当前行业应用最广泛的 OIT 方案。
行业绝对主流的 OIT 算法,2013 年由 Morgan McGuire 提出,是目前 90% 以上游戏 / 引擎内置 OIT 的基础方案。
彻底放弃排序,通过深度权重函数为每个透明片元分配权重,直接累积颜色与透明度,最终通过归一化得到近似正确的混合结果。核心公式如下:
累积 Pass(双渲染目标):
plaintext
// RT0:加权颜色累积
accumColor += color.rgb * color.a * weight
// RT1:加权透明度累积
accumAlpha += color.a * weight
合成 Pass:
plaintext
// 归一化得到最终颜色,背景色为不透明场景的渲染结果
finalColor = accumColor / max(accumAlpha, 1e-6) + backgroundColor * (1 - saturate(accumAlpha))
权重函数(核心):权重与片元深度负相关,越远的片元权重越高,近似模拟「从远到近」的混合,最常用的权重公式:
plaintext
weight = max(1e-2, min(1e3, 10000.0 / (1e-5 + pow(depth, 4.0) + 0.01)))
单 Pass 累积,双 Pass 完成,无需 UAV、原子操作、排序,性能开销极低,仅比传统 Alpha Blend 高一点;
显存开销极小,仅需 2 个全屏渲染目标;
完全不依赖绘制顺序,交叉透明、多层透明的效果远超传统排序 Alpha Blend;
完美兼容所有 GPU,包括移动端,支持 Early Z 优化。
✅ 优点:性能极高、实现极简、兼容性极强、无排序开销、显存占用低;
❌ 缺点:结果是近似的,高透明度、厚层透明的场景会出现轻微色差、过亮或过暗的问题。
几乎所有实时渲染场景:游戏中的粒子、烟雾、玻璃、植被、UI,移动端游戏,数字孪生,工业可视化等,是当前使用最广的 OIT 算法。
WBOIT 的进阶方案,通过统计动量来近似片元的深度分布,无需排序即可得到接近精确 OIT 的结果,误差远小于 WBOIT。
核心原理:为每个像素存储多个动量值(如 4 个),通过动量函数重建片元的深度分布,计算出正确的混合权重;
优缺点:精度远高于 WBOIT,性能比 PPL 高很多,显存开销仅需 4 个渲染目标;动量数量越多,精度越高,开销越大;
适用场景:主机 / PC 端 3A 游戏,需要更高精度透明效果、但不想用 PPL 的场景。
WBOIT 的改进版,将透明片元按深度分为 2~4 层,每层单独做加权累积,最后分层混合。
核心优势:分层后大幅降低了 WBOIT 的色差问题,精度接近精确 OIT,性能仅比 WBOIT 略高;
适用场景:PC / 主机游戏,多层透明、高透明度的场景,如水下渲染、多层玻璃。
介于精确 OIT 与近似 OIT 之间的方案,先通过一个 Pass 统计每个像素的透明度分布,生成自适应的权重函数,第二个 Pass 做加权混合。
优缺点:误差比 WBOIT 小很多,性能比精确 OIT 高,需要两个 Pass;
适用场景:影视级实时渲染、数字人渲染等对透明精度要求较高的场景。
结论:OIT 早已不是小众技术,近似 OIT 已经得到全行业的广泛应用,精确 OIT 也在高精度场景中普遍使用,具体情况如下:
所有主流商业 / 开源引擎都已内置 OIT 方案,作为传统 Alpha Blend 的替代选项:
表格
移动端游戏:WBOIT 是绝对主流,几乎所有中重度手游的粒子、烟雾、半透明特效都在使用,解决了传统排序 Alpha Blend 的交叉透明错误问题;
PC / 主机 3A 游戏:WBOIT 是标配,用于粒子、植被、玻璃;部分高精度场景(如座舱、建筑玻璃)会使用 PPL/MBOIT,解决多层交叉透明的渲染错误;
独立游戏 / 二次元游戏:WBOIT 广泛用于角色半透明服饰、特效、场景透明物体,实现成本极低,效果提升显著。
建筑可视化、CAD/CAM 软件:精确 OIT(PPL)是标配,需要精准渲染多层玻璃、钢结构、管线的交叉透明效果,不能有视觉误差;
数字孪生、智慧城市:WBOIT 广泛用于建筑、管网、水体的半透明渲染,平衡性能与效果;
医疗可视化:精确 OIT 用于人体组织、血管的多层透明渲染,需要严格的深度顺序正确。
不是 OIT 不好用,而是传统 Alpha Blend 有不可替代的优势:
实现最简单,无需额外的缓冲区、Pass、复杂代码;
性能开销最低,在半透明物体少、无交叉的场景(如单个 UI 面板、简单玻璃),完全够用;
美术可控性更高,传统 Alpha Blend 的效果完全由绘制顺序决定,美术可以精准控制层级。
OIT 是传统 Alpha Blend 的补充与升级,而非完全替代,二者在当前的渲染管线中是共存的。
两者的核心渲染能力完全一致,都能实现所有主流 OIT 算法,仅在工程实现、内存管理、同步机制上有差异:
表格
完全贴合你 Chapter10 的代码风格,可直接集成到你的渲染管线中:
// 根签名中绑定:2个UAV渲染目标(累积用),1个SRV(不透明场景的渲染结果)RWTexture2D<float4> gAccumColor : register(u0);RWTexture2D<float> gAccumAlpha : register(u1);Texture2D<float4> gSceneColor : register(t0);SamplerState gsamPointClamp : register(s0);cbuffer cbPass : register(b1){float4x4 gViewProj;float3 gEyePosW;float gNearZ;float gFarZ;}
struct VertexOut{float4 PosH : SV_POSITION;float3 PosW : POSITION;float3 NormalW : NORMAL;float2 TexC : TEXCOORD;float Depth : DEPTH;};float4 PS(VertexOut pin) : SV_Target
{// 1. 基础纹理采样与光照计算(和你Chapter10的代码完全一致)float4 diffuseAlbedo = gDiffuseMap.Sample(gsamAnisotropicWrap, pin.TexC) * gDiffuseAlbedo;#ifdef ALPHA_TESTclip(diffuseAlbedo.a - 0.1f);#endif// ... 省略光照计算,得到最终的litColorfloat4 litColor = ComputeLighting(...);
litColor.a = diffuseAlbedo.a;// 2. 线性化深度,计算权重float linearDepth = (pin.Depth - gNearZ) / (gFarZ - gNearZ);// 标准权重函数:深度越大,权重越低,模拟从远到近的混合float weight = max(1e-2, min(1e3, 10000.0 / (1e-5 + pow(linearDepth, 4.0) + 0.01)));// 3. 原子累积到UAV(D3D12支持像素着色器的UAV原子操作)int2 screenPos = int2(pin.PosH.xy);float4 premulColor = float4(litColor.rgb * litColor.a, litColor.a);InterlockedAdd(gAccumColor[screenPos].r, premulColor.r * weight);InterlockedAdd(gAccumColor[screenPos].g, premulColor.g * weight);InterlockedAdd(gAccumColor[screenPos].b, premulColor.b * weight);InterlockedAdd(gAccumAlpha[screenPos], premulColor.a * weight);return litColor;}
float4 PS(float4 posH : SV_POSITION, float2 uv : TEXCOORD) : SV_Target
{int2 screenPos = int2(posH.xy);// 读取累积值float3 accumColor = gAccumColor[screenPos].rgb;float accumAlpha = gAccumAlpha[screenPos];// 读取不透明场景的背景色float3 backgroundColor = gSceneColor.Sample(gsamPointClamp, uv).rgb;// 归一化合成float3 finalColor;if(accumAlpha < 1e-6){
finalColor = backgroundColor;}else{float3 transColor = accumColor / accumAlpha;float transmittance = 1.0 - saturate(accumAlpha);
finalColor = transColor + backgroundColor * transmittance;}return float4(finalColor, 1.0);}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。