[OpenGL]基于链表的顺序独立透明渲染(OIT)实现_渲染 顺序无关透明
一、简介
本文介绍了并使用OpenGL实现了基于链表(Linked list)的顺序独立透明渲染(Order-independent transparency, OIT)。包括对透明渲染、顺序独立透明渲染、使用OpenGL在GPU上构建链表的介绍。在文章最后给出了实现的所有代码和模型文件。
按照本文代码运行后可以得到以下渲染结果:
二、透明渲染
1. OVER运算
实现对透明、半透明物体的实时渲染对于提高渲染结果的真实性具有重要意义。
将片段按照从后向前(back to front)的顺序使用传统的α混合方程,也称为OVER运算符,是常用的一种渲染透明物体的方法。OVER运算的计算公式如下:
C o u t= α s r c∗ C s r c+ ( 1 − α s r c) ∗ C d e s α o u t= α s r c+ ( 1 − α s r c) ∗ α d e s C_{out} = α_{src} * C_{src} + (1-α_{src}) * C_{des} \\\\ α_{out} = α_{src} + (1-α_{src}) * α_{des} Cout=αsrc∗Csrc+(1−αsrc)∗Cdesαout=αsrc+(1−αsrc)∗αdes
其中 C d e s C_{des} Cdes和 α d e s α_{des} αdes是后面物体(destination)的颜色和透明度, C s r c C_{src} Csrc和 α s r c α_{src} αsrc是前面物体(source)的颜色和透明度。
想象一下,假设有两个平面A和B,A是一个透明的平面,在B的前面。我们先在画板上绘制平面B,然后再将A覆盖在已经画好B结果的画板上。在这个过程中B就是destination,A就是source。
如下图所示,图中p1处的颜色和透明度分别为:
C p 1= α g r e e n∗ C g r e e n+ ( 1 − α g r e e n) ∗ C r e d α p 1= α g r e e n+ ( 1 − α g r e e n) ∗ α r e d C_{p1} = α_{green} * C_{green} + (1-α_{green}) * C_{red} \\\\ α_{p1} = α_{green} + (1-α_{green}) * α_{red} Cp1=αgreen∗Cgreen+(1−αgreen)∗Credαp1=αgreen+(1−αgreen)∗αred
2. 透明渲染的难点
透明渲染的一个难点是,根据前面的OVER运算符的规则,对透明物体的渲染是依赖于它们的位置顺序的,我们需要在渲染前提前对模型进行排序。
一方面,在渲染时对模型进行排序是一个比较耗时的问题,排序算法的复杂度为O(n*lg(n))。
另一方面,模型的位置关系可能比较复杂。例如可能存在两个物体A和B,物体A的中心在B的前面,但是A的一部分区域却被B的一部分遮挡,简单的根据A、B的包围盒位置进行排序无法得到正确的渲染结果。
三、顺序独立的透明渲染OIT
OIT(Order-independent transparency)是一类顺序无关的透明渲染技术,它不需要对几何体进行排序以实现α混合。
需要注明的是,只要满足不对几何体进行排序(但是可以在shader中对片段进行排序)就可以被称作OIT。顺序独立并不意味着你不能进行排序操作,而是要你需要独立地为自己的程序做出指示,并独立为其负责,你只需要清楚地告诉自己,这种算法是我认可的,我去实现,就够了。
本文介绍的基于链表(linked list)的OIT就是一种不需要在程序种对几何体进行排序,但是需要在fragment shader中对片段进行排序的算法。
0. 基于linked list的OIT
基于链表的OIT的主要思路在针对每个像素,在GPU中构建一个链表,链表中的节点存储了当前像素内所有片段的颜色、透明度和深度信息。然后根据片段的深度信息对链表进行排序,最后使用OVER运算进行混合,得到每个像素的最终颜色。
接下来对算法的实现进行详细介绍。理论上在链表中的节点可以同时记录透明片段和不透明片段,这样只需要两趟pass,第一趟pass构建链表,第二趟pass对链表进行排序然后混合即可。
但是本文实现时将不透明物体和透明物体进行分开渲染,最后再混合两者。即先渲染场景中的不透明物体,然后在使用OIT渲染场景中的透明物体,最后再将两者进行混合。这个流程中使用了三趟pass。虽然过程更为复杂,但是可以减小链表的长度,降低排序链表的运算耗时。
接下来详细介绍基于链表的OIT实现流程。
1. Pass 1,渲染不透明物体
该趟pass仅仅渲染场景中的不透明物体,获取不透明物体的渲染结果(opaqueTexture)和深度信息(opaqueDepthTexture)。
渲染结果存储到opaqueTexture中,用于pass 3中作为背景与根据透明物体构建的链表进行混合。
深度信息存储到opaqueDepthTexture中,用于pass 2中去剔除被不透明物体遮挡的透明物体片段。
之所以需要不透明物体的深度(opaqueDepthTexture)是因为,假如场景中存在一面不透明的墙体,在墙后面有大量透明的物体,由于墙体的阻挡我们看不那些墙后的透明物体,那么这些墙后面的透明物体实际上就不需要进行渲染。
因此可以根据不透明物体(墙)的深度信息,在pass 2中剔除掉深度大于opaqueDepthTesture中对应像素处深度的透明物体片段。
在代码实现时,先生成一个帧缓存opaqueFBO。并且将纹理opaqueTexture和opaqueDepthTexture中分别绑定到opaqueFBO的GL_COLOR_ATTACHMENT0和GL_DEPTH_ATTACHMENT附件上。
这样,当使用opaqueFBO作为渲染的输出目标时,片段着色器的输出FragColor就会保存在opaqueTexture,深度缓存会保存在opaqueDepthTexture中。
用于配置opaqueFBO的代码如下:
// opaqueFBOunsigned int opaqueFBO;glGenFramebuffers(1, &opaqueFBO);unsigned int opaqueTexture;glGenTextures(1, &opaqueTexture); // 生成 texture (opaqueTexture)glBindTexture(GL_TEXTURE_2D, opaqueTexture); // 绑定 texture (opaqueTexture 绑定到 CL_TEXTURE_2D 上)glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGBA, GL_HALF_FLOAT, NULL); // 设置 opaqueTexture 属性glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);glBindTexture(GL_TEXTURE_2D, 0); // 解绑 opaqueTextureunsigned int opaqueDepthTexture; // 生成 depth texture (opaqueDepthTexture)glGenTextures(1, &opaqueDepthTexture); // 绑定 texture (opaqueDepthTexture)glBindTexture(GL_TEXTURE_2D, opaqueDepthTexture); // 设置 opaqueDepthTexture 属性glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32F, SCR_WIDTH, SCR_HEIGHT, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);// texture 必须设置 以下参数,不然无法在 shader 中使用 texture(texture, coord)glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);glBindTexture(GL_TEXTURE_2D, 0);// 将 opaqueTexture, opaqueDepthTexture 绑定到 opaqueGBO 上// opaqueTexture 用于接收 GL_COLOR_ATTACHMENT0// opaqueDepthTexture 用于接收 GL_DEPTH_ATTACHMENTglBindFramebuffer(GL_FRAMEBUFFER, opaqueFBO);glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, opaqueTexture, 0);glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, opaqueDepthTexture, 0);GLenum drawBuffers[] = {GL_COLOR_ATTACHMENT0, GL_DEPTH_ATTACHMENT};glDrawBuffers(2, drawBuffers);if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) std::cout << \"ERROR::FRAMEBUFFER:: Opaque framebuffer is not complete!\" << std::endl;glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);glBindFramebuffer(GL_FRAMEBUFFER, 0); // 解绑 opaqueFBO
渲染不透明物体的vertex shader和fragment shader可以使用任意的光照模型,例如phong、blinnphong或者pbr模型,这部分不是本文的重点。
下面给出一个本文中使用的blinnphong模型的vertext shader和fragemnt shader代码:
// vertex shader#version 430 corelayout(location = 0) in vec3 aPos;layout(location = 1) in vec3 aNor;layout(location = 2) in vec2 aTexCoord;uniform mat4 model;uniform mat4 view;uniform mat4 projection;out vec3 vertexPos;out vec3 vertexNor;out vec2 textureCoord;void main() { textureCoord = aTexCoord; // 裁剪空间坐标系 (clip space) 中 点的位置 gl_Position = projection * view * model * vec4(aPos, 1.0f); // 世界坐标系 (world space) 中 点的位置 vertexPos = (model * vec4(aPos, 1.0f)).xyz; // 世界坐标系 (world space) 中 点的法向 vertexNor = mat3(transpose(inverse(model))) * aNor;}
// fragment shader#version 430 corelayout (location = 0) out vec4 FragColor;in vec3 vertexPos;in vec3 vertexNor;in vec2 textureCoord;uniform vec3 cameraPos;uniform vec3 lightPos;uniform vec3 k;uniform sampler2D texture_diffuse;void main() { vec3 lightColor = vec3(1.0f, 1.0f, 1.0f); // Ambient // Ia = ka * La float ambientStrenth = k[0]; vec3 ambient = ambientStrenth * lightColor; // Diffuse // Id = kd * max(0, normal dot light) * Ld float diffuseStrenth = k[1]; vec3 normalDir = normalize(vertexNor); vec3 lightDir = normalize(lightPos - vertexPos); vec3 diffuse = diffuseStrenth * max(dot(normalDir, lightDir), 0.0) * lightColor; // Specular (Blinn-Phong) // Is = ks * (normal dot halfway)^s Ls float specularStrenth = k[2]; vec3 viewDir = normalize(cameraPos - vertexPos); vec3 halfwayDir = normalize(lightDir + viewDir); vec3 specular = specularStrenth * pow(max(dot(normalDir, halfwayDir), 0.0f), 2) * lightColor; // Obejct color vec3 objectColor = vec3(0.8, 0.8, 0.8); if (textureCoord.x >= 0 && textureCoord.y >= 0) { objectColor = texture(texture_diffuse, textureCoord).xyz; } FragColor = vec4((ambient + diffuse + specular) * objectColor, 1.0f);}
经过pass 1,我们得到两个texture:opaqueTexture中保存了不透明物体的渲染结果,opaqueDepthTexture中保存了不透明物体的深度信息,如下图所示:
2. Pass 2,渲染透明物体(构建链表)
在pass 2中我们将使用《Real-Time Concurrent Linked List Construction on the GPU》中提出的链表构建算法,在GPU上构建描述透明物体片段的链表。
每一个像素都会构建一个链表,链表中的每个节点都对应的一个片段。我们之所以需要针对每个像素都构建一个链表,是为了收集每个像素中存在的所有片段,构建完成后将这些片段根据深度值进行排序,最后在每个链表上从远到近使用OVER运算混合所有的片段,那么就能得到像素的最终着色值。同时,在构建链表时,需要利用pass 1得到的opaqueDepthTexture丢弃被不透明物体遮挡的片段。
在GPU上构建链表并不如在CPU上构建那么容易。一个主要的问题是,我们无法像在CPU上构建链表那样随时使用new运算符申请新的内存空间(在GPU上对应的就是显存空间)。另外,GPU上的shader程序都是并行运行的,在同一个时刻可能存在很多个shader程序并行运行,需要安排好shader对显存的读写操作。
《Real-Time Concurrent Linked List Construction on the GPU》中提出的在GPU上构建链表的算法思路如下:
• 算法需要申请一个原子变量atomic_buffer,用来记录以及处理完的片段个数。链表中的每个节点,都保存一个片段的颜色、透明度和深度信息。所有节点都存储在一个预先申请好的固定大小的buffer,linked_list_buffer,中。变量atomic_buffer,即表示已经处理完的片段个数,也是下一个片段将要存储到linked_list_buffer中的下标位置。linked_list_buffer可以视作一个在GPU上的巨大的数组。数组的元素为链表的节点类型node {color, alpha, depth}。当fragment shader运行一次(处理一个片段时)假如该片段不会被不同透明物体遮挡,atomic_buffer自动+1,将该片段的color,alpha和depth存储到linked_list_buffer[atommic_buffer]处。
• 在渲染时,存在很多个fragment shader并行运行,每个fragment shader处理一个片段。fragment shader运行一次,atomic_buffer就自动+1。
• 还需要申请一个大小等于像素个数的head_ptr_buffer。head_ptr_buffer中存储每个像素对应的链表的head node在linked_list_buffer中的下标。
下面给出了一个利用atomic_buffer, link_list_buffer和head_ptr_buffer构建链表的例子:
- 初始时,atomic_buffer=0,head_ptr_buffer中所有元素初始化为-1,表示每个像素对应的链表的head node是空, linked_list_buffer中为空。
-
当fragment shader处理像素(1,4)处的片段时(左上角的橙色三角形在的像素(1,4)内的片段):
• 将该片段的信息存储到linked_list_buffer[atomic_buffer]内• 同时令linked_list_buffer[atomic_buffer].next = head_ptr_buffer[1][4]
• 令像素(1,4)的链表的头节点等于atomic_buffer,即head_ptr_buffer[1][4] = atomic_buffer
以上操作相当于将新节点插入到像素(1,4)对应的链表的头部。
最后,令atomic_huffer自增1,此后atomic_buffer=1。处理完成该片段后各buffer内的结果如下图所示:
-
当fragment shader处理像素(3,1)处的片段时(右下角的绿色三角形在的像素(3,1)内的片段):
• 将该片段的信息存储到linked_list_buffer[atomic_buffer]内• 然后,令linked_list_buffer[atomic_buffer].next = head_ptr_buffer[3][1]
• 令像素(3,1)的链表的头节点等于atomic_buffer,即head_ptr_buffer[3][1]=atomic_buffer
以上操作相当于将该片段插入到像素(3,1)对应的链表内。最后,令atomic_huffer自增1,此后atomic_buffer=2。处理完成该片段后各buffer内的结果如下图所示:
-
当fragment shader处理像素(4,1)处的片段时(右下角的绿色三角形在的像素(4,1)内的片段):
• 将该片段的信息存储到linked_list_buffer[atomic_buffer]内• 然后,令linked_list_buffer[atomic_buffer].next = head_ptr_buffer[4][1]
• 令像素(4,1)的链表的头节点等于atomic_buffer,即head_ptr_buffer[4][1]=atomic_buffer
以上操作相当于将该片段插入到像素(4,1)对应的链表头部。最后,令atomic_huffer自增1,此后atomic_buffer=3。处理完成该片段后各buffer内的结果如下图所示:
- 前面介绍的都是不同像素内的片段,此时,假如处理像素(1,4)内的下一个新片段时(下图左上角的黄色三角形在的像素(1,4)内的片段),我们需要将新的片段插入到像素(1,4)对应的链表内。
处理流程如下:
• 将该片段的信息存储到linked_list_buffer[atomic_buffer]内
• 然后,令linked_list_buffer[atomic_buffer].next = head_ptr_buffer[1][4]
• 令像素(1,4)的链表的头节点等于atomic_buffer,即head_ptr_buffer[1][4]=atomic_buffer
以上操作相当于将该片段插入到像素(1,4)对应的链表头部,如下图所示:
最后,令Atomic_huffer自增1,此后atomic_buffer=4。
当fragment shader处理完成所有片段后,每个像素都会得到一个链表,链表内的节点存储了该像素内的所有透明片段信息。需要注意的是,尽管此处我们描述时fragment shader串行的处理每个片段,这是因为我们使用了atomic类型的变量,可以保证对atomic_buffer的读写是串行的,但是在整体来看,存在很多和并行运行的fragment shader,因此我们不能保证链表内的节点是有序的。
在Pass 3中将对每个链表根据节点的depth进行排序,同时与Pass 1中得到的opaqueTexture进行混合,得到最终的渲染结果。
在代码实现时,我们使用GL_ATOMIC_COUNTER_BUFFER类型的buffer的变量实现atomic_buffer,使用GL_TEXTURE_2D实现head_ptr_buffer,使用GL_SHADER_STORAGE_BUFFER实现linked_list_buffer。
另外在渲染每帧时,需要重新初始化atomic_buffer和head_ptr_buffer。linked_list_buffer不用每帧初始化,因为我们在fragment shader中会重新对linked_list_buffer进行赋值,每次使用linked_list_buffer都覆盖了linked_list_buffer中的旧数据。
下面给出了Pass 2中构建链表的vertex shader和fragment shader。
// vertex shader#version 430 corelayout(location = 0) in vec3 aPos;layout(location = 1) in vec3 aNor;layout(location = 2) in vec2 aTexCoord;uniform mat4 model;uniform mat4 view;uniform mat4 projection;out vec3 vertexPos;out vec3 vertexNor;out vec2 textureCoord;void main() { textureCoord = aTexCoord; // 裁剪空间坐标系 (clip space) 中 点的位置 gl_Position = projection * view * model * vec4(aPos, 1.0f); // 世界坐标系 (world space) 中 点的位置 vertexPos = (model * vec4(aPos, 1.0f)).xyz; // 世界坐标系 (world space) 中 点的法向 vertexNor = mat3(transpose(inverse(model))) * aNor;}
// fragment shader#version 430 core#define MAX_FRAGMENTS 75layout (location = 0) out vec4 FragColor;in vec3 vertexPos;in vec3 vertexNor;in vec2 textureCoord;uniform vec3 cameraPos;uniform vec3 lightPos;uniform vec3 k;uniform uint MaxNodes;uniform sampler2D texture_diffuse;uniform sampler2D texture_depth;struct NodeType{ vec4 color; // float * 4 float depth; // float * 1 uint next; // uint * 1};layout(binding = 0, r32ui) uniform uimage2D headPointers; // head_pre_bufferlayout(binding = 0, offset = 0) uniform atomic_uint nextNodeCounter; // atomic_bufferlayout( binding = 0, std430 ) buffer linkedLists { NodeType nodes[];};// linked_list_buffervoid main() { // 归一化当前 fragment 的屏幕坐标 (gl_FragCoord.xy) vec2 uv = gl_FragCoord.xy / vec2(800, 600); // 记得传入屏幕宽高! // 从 opaqueDepthTexture 中采样对应位置的深度 float depth = texture(texture_depth, uv).r; // 比较 fragment 当前深度 (gl_FragCoord.z) 和 opaqueDepthTexture 中的深度 if (gl_FragCoord.z > depth + 0.0001) { // 加一个小偏移避免浮点误差 discard; // 丢弃当前 fragment } // 计数器 id (当前可以写入的 id) uint atomic_buffer = atomicCounterIncrement(nextNodeCounter); // 计数器 id 合法 if(atomic_buffer < MaxNodes){ // 将 headPointers[x][y] 处的原始值赋给 preHead, 并将 atomic_buffer 写入 headPointers[x][y] uint preHead = imageAtomicExchange(headPointers, ivec2(gl_FragCoord.xy), atomic_buffer); // 假如一个新的 node vec4 color = texture(texture_diffuse, textureCoord); nodes[atomic_buffer].color = color; nodes[atomic_buffer].depth = gl_FragCoord.z; nodes[atomic_buffer].next = preHead; } FragColor = texture(texture_diffuse, textureCoord);}
3. Pass 3,混合
经过Pass 1我们得到了不透明物体的渲染结果opaqueTexture,在Pass 2中我们得到每个像素内的透明物体片段链表(无序)。
在Pass 3中我们首先对链表中的节点(片段),根据片段的深度进行排序。可以将链表中的节点复制到一个数组中,然后对数组进行排序。
然后将opaqueTexture视作背景,从远到近便利链表中的所有透明物体的片段。使用OVER运算,将透明物体的片段叠加到背景上。这样就可以得到最终的渲染结果。
下面是实现的vertex shader和fragment shader代码:
// vertex shader#version 430 corelayout(location = 0) in vec3 aPos;layout(location = 1) in vec3 aNor;layout(location = 2) in vec2 aTexCoord;out vec2 textureCoord;void main() { textureCoord = aTexCoord; // 裁剪空间坐标系 (clip space) 中 点的位置 gl_Position = vec4(aPos, 1.0f);}
#version 430 core#define MAX_FRAGMENTS 75layout (location = 0) out vec4 FragColor;in vec2 textureCoord;uniform sampler2D texture_opaque; // opaqueTexturestruct NodeType{ vec4 color; // float * 4 float depth; // float * 1 uint next; // uint * 1};layout(binding = 0, r32ui) uniform uimage2D headPointers; // head_ptr_bufferlayout(binding = 0, offset = 0) uniform atomic_uint nextNodeCounter; // atomic_bufferlayout( binding = 0, std430 ) buffer linkedLists { NodeType nodes[];};// linked_list_bufferuniform uint MaxNodes;const float EPSILON = 0.0001;void main(){ // 定义一个数组 用于将链表中的节点复制到该数组中 NodeType frags[MAX_FRAGMENTS]; // 计数当前像素 (x,y) 处的 list 有多少个 节点 int count = 0; // 获取 当前像素 (x,y) 处的 headpointer uint idx = imageLoad(headPointers, ivec2(gl_FragCoord.xy)).r; // 将 当前 headpointer 对应的 链表 复制到 一个 数组中 // Copy the linked list for this fragment into an array while(idx!=0xffffffff && count < MAX_FRAGMENTS) { NodeType node = nodes[idx]; bool isDuplicate = false; for (int i = 0; i < count; i++) { // 检查 nodes[idx] 是否和已有的fragment接近 // 如果接近,说明是同一个面片中,两个相邻三角形的边界 片段, 那么只保留一个 片段 即可 if (abs(frags[i].depth - node.depth) < EPSILON) { isDuplicate = true; break; } } if (!isDuplicate) { frags[count] = node; count++; } idx = node.next; } // 排序 (使用插入排序) // 排序完成后: depth从大到小排序(从远到近排序) for(int i=1; i<count; i++){ int j=i; NodeType toInsertNode = frags[i]; while(j>0 && toInsertNode.depth > frags[j-1].depth){ frags[j] = frags[j-1]; j --; } frags[j] = toInsertNode; } // back-to-front // 从远到近使用 over 运算混合 vec4 color = texture(texture_opaque, textureCoord); // 令color 初始化为背景颜色 for(int i=0; i<count; i++){ color.rgb = color.rgb * (1.0 - frags[i].color.a) + frags[i].color.rgb * frags[i].color.a; color.a = color.a + frags[i].color.a * (1.0 - color.a); } FragColor = color;}
四、全部代码及模型文件
使用OpenGL实现基于链表的OIT的全部代码以及模型文件可以在[OpenGL]基于链表的OIT中下载。
下载源代码后使用以下命令编译运行:
mkdir buildcd buildcmake ..make./OpenGL_OIT_Linked_list
五、参考
[1].Transparency (or Translucency) Rendering
[2].【论文复现】Real-Time Concurrent Linked List Construction on the GPU
[3].nvpro-samples/vk_order_independent_transparency
[4].CPU无序透明 (Order Independent Transparency)