65.9K
CodeProject 正在变化。 阅读更多。
Home

在Android*上使用Fragment Shader Ordering实现高效的独立于顺序的透明度

2015年1月20日

CPOL

9分钟阅读

viewsIcon

11112

本示例演示了 GL_INTEL_fragment_shader_ordering 扩展的使用,该扩展是根据 OpenGL* 4.4 核心配置文件和 GLES 3.1 规范编写的。

Intel® 开发者区 提供跨平台应用开发工具和操作指南、平台和技术信息、代码示例以及同行专业知识,帮助开发者创新和成功。加入我们的社区,面向 Android物联网Intel® RealSense™ 技术Windows,下载工具、访问开发套件、与志同道合的开发者交流想法,并参与黑客松、竞赛、路演和本地活动。

引言

本示例演示了 GL_INTEL_fragment_shader_ordering 扩展的使用,该扩展是根据 OpenGL* 4.4 核心配置文件和 GLES 3.1 规范编写的。所需的最低 OpenGL 版本为 4.2 或 ARB_shader_image_load_store。该扩展引入了一个新的 GLSL 内置函数 beginFragmentShaderOrderingINTEL(),它会阻塞片段着色器调用的执行,直到映射到相同 xy 窗口坐标的先前图元(primitive)的调用完成。该示例利用此行为,为典型的 3D 场景中的独立于顺序的透明度 (OIT) 提供实时解决方案。

独立于顺序的透明度

透明度是实时渲染中的一个基本挑战,因为很难以正确的顺序合成任意数量的透明层。本示例建立在 Marco Salvi、Jefferson Montgomery、Karthik Vaidyanathan 和 Aaron Lefohn 所著的 自适应透明度多层 alpha 混合 文章中所述工作的 Mì-té。这些文章展示了透明度如何能非常接近 A-buffer 合成获得的真实结果,但通过使用各种有损压缩技术来压缩透明度数据,速度可以提高 5 倍到 40 倍。本示例演示了一种基于这些压缩算法的算法,适用于包含在游戏等实时应用中。

透明度挑战

使用正常 alpha 透明度混合渲染测试场景的示例如图 1 所示。

图 1:OIT 示例

几何图形按固定顺序渲染:先是地面,然后是圆顶内的物体,接着是圆顶本身,最后是外面的植物。实心物体首先绘制并更新深度缓冲区,然后透明物体按相同顺序绘制,但不更新深度缓冲区。放大区域突出了由此产生的一个视觉伪影:树叶在圆顶内,但却位于几层玻璃的前面。不幸的是,渲染顺序意味着所有玻璃层,即使是那些在树叶后面的,也都被绘制在了上面。让透明对象更新深度缓冲区会产生另一组问题。传统上,这只能通过将对象分解成更小的块,并根据相机视点从前到后排序来解决。即使这样,它也不是完美的,因为对象可能会相交,并且随着更多对象的排序和绘制,渲染成本会增加。

图 2 和图 3 放大了视觉伪影,图 2 显示的是未排序的情况,所有玻璃层都在树叶之前绘制,而图 3 显示的是已正确排序的情况。

图 2:未排序

图 3:已排序

实时独立于顺序的透明度

已经有几种尝试可以解决任意顺序的几何图元的合成问题,而无需在 CPU 上进行排序或将几何图形分解为不相交的元素。这些方法包括深度剥离(depth-peeling),它需要多次提交几何图形,以及 A-buffer 技术,其中所有对给定像素有贡献的片段都存储在一个链表中,然后进行排序和按正确顺序混合。尽管 A-buffer 在离线渲染器中取得了成功,但由于其无界内存需求和普遍较低的性能,尚未被实时渲染社区采纳。

一种新方法

与 A-buffer 方法(在每像素列表中存储所有颜色和深度数据,然后进行排序和合成)不同,本示例利用了 Marco Salvi 的工作,并重构了 alpha 混合方程,以避免递归和排序,并生成一个“可见性函数”(图 4)。

图 4:可见性函数

可见性函数中的步数对应于在渲染阶段按每像素级别存储可见性信息所使用的节点数量。当添加像素时,它们会存储在节点结构中,直到节点被填满。然后,当尝试插入更多像素时,算法会计算可以合并哪些先前节点,以在保持数据集大小的同时创建可见性函数的最小变化。最后一步是评估可见性函数 vis() 并使用公式 final_color= 混合片段。

该示例按以下阶段渲染场景:

  1. 在第一次传递中,将着色器存储缓冲区对象 (Shader Storage Buffer Object) 清除为默认值。
  2. 将所有实心几何图形渲染到主帧缓冲区对象 (Frame Buffer Object),并更新深度缓冲区。
  3. 渲染所有透明几何图形,同时读取深度缓冲区而不更新;最终片段数据将从帧缓冲区丢弃。片段数据存储在着色器存储缓冲区对象中的一组节点中。
  4. 解析着色器存储缓冲区对象中的数据,并将最终结果混合到主帧缓冲区对象中。
图 5:渲染路径

考虑到在解析阶段读取着色器存储缓冲区对象可能成本很高,因为所需的带宽很大,本示例使用的一个优化是利用模板缓冲区 (stencil buffer) 来掩盖透明像素将混合到帧缓冲区中的区域。这会将渲染更改为图 6 所示。

  1. 清除模板缓冲区。
  2. 在第一次传递中,将着色器存储缓冲区对象 (Shader Storage Buffer Object) 清除为默认值。
  3. 设置以下模板操作:
    1. glDisable(GL_STENCIL_TEST);
  4. 将所有实心几何图形渲染到主帧缓冲区对象,并更新深度。
  5. 设置以下模板操作:
    1. glEnable(GL_STENCIL_TEST);
    2. glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
    3. glStencilFunc(GL_ALWAYS, 1, 0xFF);
  6. 渲染所有透明几何图形,同时读取深度缓冲区而不更新;最终片段数据以 alpha 值为 0 混合到主帧缓冲区。模板缓冲区会为绘制到帧缓冲区的每个片段进行标记。片段数据存储在着色器存储缓冲区对象中的一组节点中。丢弃片段是不可能的,因为这会阻止模板被更新。
  7. 设置以下模板操作:
    1. glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
    2. glStencilFunc(GL_EQUAL, 1, 0xFF);
  8. 解析着色器存储缓冲区对象中的数据,仅针对通过模板测试的片段,并将最终结果混合到主帧缓冲区对象中。
  9. 设置以下模板操作:
    1. glStencilFunc(GL_ALWAYS, 1, 0xFF);
    2. glDisable(GL_STENCIL_TEST);
图 6:模板渲染路径

使用模板缓冲区的收益可以在解析阶段的新成本中看到,该成本降低了 80%,尽管这很大程度上取决于屏幕上被透明几何图形覆盖的百分比。屏幕上被透明对象覆盖的百分比越高,性能提升就越小。

void PSOIT_InsertFragment_NoSync( float surfaceDepth, vec4 surfaceColor )
{	
	ATSPNode nodeArray[AOIT_NODE_COUNT];    

	// Load AOIT data
	PSOIT_LoadDataUAV(nodeArray);

	// Update AOIT data
	PSOIT_InsertFragment(surfaceDepth,		
		1.0f - surfaceColor.w,  // transmittance = 1 - alpha
		surfaceColor.xyz,
		nodeArray);
	// Store AOIT data
	PSOIT_StoreDataUAV(nodeArray);
}
图 7:GLSL 着色器存储缓冲区代码

上述算法可以在支持着色器存储缓冲区对象的任何设备上实现,但目前描述的算法有一个非常重要的缺陷:可能存在多个映射到相同窗口 xy 坐标的片段在同时进行。

如果多个片段同时在相同的 xy 坐标上运行,它们将读取 PSOIT_LoadDataUAV 中的相同起始数据,但会得到不同的值,它们尝试并存储在 PSOIT_StoreDataUAV 中,最后一个完成的将覆盖其他已处理的。其效果是压缩例程在帧之间可能会有所不同,并且通过禁用 Pixel Sync 可以在示例中看到这一点。用户应该会看到重叠透明区域出现微妙的闪烁。实现缩放功能是为了让这一点更容易看到。GPU 可以并行执行的片段越多,出现闪烁的可能性就越大。

默认情况下,该示例通过使用新的 GLSL 内置函数 beginFragmentShaderOrderingINTEL() 来避免此问题。当硬件显示扩展字符串 GL_INTEL_fragment_shader_ordering 时,可以使用此函数。beginFragmentShaderOrderingINTEL() 函数会阻塞片段着色器执行,直到映射到相同窗口 xy 坐标的先前图元的(primitive)所有着色器调用完成。当此函数返回时,先前映射到相同 xy 窗口坐标的所有内存事务都对当前片段着色器调用可见。这使得可以以确定性的方式合并先前片段以创建可见性函数。beginFragmentShaderOrderingINTEL 函数对具有不重叠窗口 xy 坐标的片段的着色器执行没有影响。

如何调用 beginFragmentShaderOrderingINTEL 的示例显示在图 8 中。

GLSL code example
    -----------------

    layout(binding = 0, rgba8) uniform image2D image;

    vec4 main()
    {
        ... compute output color 
        if (color.w > 0)        // potential non-uniform control flow
        {
            beginFragmentShaderOrderingINTEL();
            ... read/modify/write image         // ordered access guaranteed 
        }
        ... no ordering guarantees (as varying branch might not be taken) 
 
        beginFragmentShaderOrderingINTEL();

        ... update image again                  // ordered access guaranteed 
    }
图 8:beginFragmentShaderOrderingINTEL

请注意,没有显式的内置函数来指示应该排序的区域的结束。相反,将要排序的区域在逻辑上延伸到片段着色器执行的结束。

在 OIT 示例中,它只是按图 9 所示添加:

void PSOIT_InsertFragment( float surfaceDepth, vec4 surfaceColor )
{	
    // from now on serialize all UAV accesses (with respect to other fragments shaded in flight which map to the same pixel)
#ifdef do_fso
    beginFragmentShaderOrderingINTEL();
#endif
    PSOIT_InsertFragment_NoSync( surfaceDepth, surfaceColor );
}
图 9:向着色器存储缓冲区访问添加片段排序

当任何可能写入透明片段的片段着色器调用它时,如图 10 所示。

out vec4 fragColor;// -------------------------------------
void main( )
{
    vec4 result = vec4(0,0,0,1);

    // Alpha-related computation
    float alpha = ALPHA().x;
    result.a =  alpha;
    vec3 normal = normalize(outNormal);

    // Specular-related computation
    vec3 eyeDirection  = normalize(outWorldPosition - EyePosition.xyz);
    vec3 Reflection    = reflect( eyeDirection, normal );
    float  shadowAmount = 1.0;

    // Ambient-related computation
    vec3 ambient = AmbientColor.rgb * AMBIENT().rgb;
    result.xyz +=  ambient;
    vec3 lightDirection = -LightDirection.xyz;

    // Diffuse-related computation
    float  nDotL = max( 0.0 ,dot( normal.xyz, lightDirection.xyz ) );
    vec3 diffuse = LightColor.rgb * nDotL * shadowAmount  * DIFFUSE().rgb;
    result.xyz += diffuse;
    float  rDotL = max(0.0,dot( Reflection.xyz, lightDirection.xyz ));
    vec3 specular = pow(rDotL,  8.0 ) * SPECULAR().rgb * LightColor.rgb;
    result.xyz += specular;
    fragColor =  result;

#ifdef dopoit	
   if(fragColor.a > 0.01)
   {
	PSOIT_InsertFragment( outPositionView.z, fragColor );
	fragColor = vec4(1.0,1.0,0.0,0.0);
   }
#endif
}
图 10:典型的材质片段着色器

只有 alpha 值高于阈值的片段才会被添加到着色器存储缓冲区对象中,从而有效地剔除任何不会为场景贡献有意义数据的片段。

构建示例

构建要求

安装最新的 Android SDK 和 NDK

将 NDK 和 SDK 添加到您的路径中

export PATH=$ANDROID_NDK/:$ANDROID_SDK/tools/:$PATH

构建方法

  1. cd 到 OIT_2014\OIT_Android 文件夹
  2. 仅首次需要,您可能需要初始化您的项目
    android update project –path . --target android-19.
  3. 构建 NDK 组件
    NDK-BUILD
  4. 构建 APK
    ant debug
  5. 安装 APK
    adb install -r bin\NativeActivity-debug.apk 或 ant installd
  6. 运行它:

结论

该示例演示了 Marco Salvi、Jefferson Montgomery、Karthik Vaidyanathan 和 Aaron Lefohn 在高端独立显卡上使用 DirectX 11* 进行的研究,关于自适应独立于顺序的透明度,如何使用 GLES 3.1 和片段着色器排序在 Android 平板电脑上进行实时实现。该算法在一个固定的内存占用空间内运行,该空间可以根据所需的视觉保真度进行调整。模板缓冲区等优化使得该技术可以在各种硬件上以可接受的性能实现,为实时渲染中最具挑战性的问题之一提供了实用的解决方案。独立于顺序的透明度示例中展示的原理可以应用于各种其他通常会创建每像素链表的算法,包括体积阴影技术和后处理抗锯齿。

相关文章和参考资料

https://www.opengl.org/registry/specs/INTEL/fragment_shader_ordering.txt
https://software.intel.com/en-us/articles/adaptive-transparency
https://software.intel.com/en-us/articles/multi-layer-alpha-blending

© . All rights reserved.