使用Compute Shader加速Irradiance Environment Map的计算
- Irradiance Environment Map基本原理
Irradiance Environment Map(也叫Irradiance Map或Diffuse Environment Map),属于Image Based Lighting技术中的一种。
Irradiance Map的详细定义可参考GPU Gems 2 Chapter 10.“Real-Time Computation of Dynamic Irradiance Environment Maps”。简单说来就是一种用于近似Environment Diffuse Lighting的方法。想象一个场景中有k个方向光,方向分别为d1…dk,光照强度为i1…ik,对于一个法线和Diffuse Color分别为n和c的Lambert表面,其光照强度为:
对于Environment Lighting,我们可以用一个Cube Map来表示,Cube Map里的每一个texel就是一个方向光,光强度为texel的值,方向为texel的location。这样就能通过一个Cube Map来表示任意的Environment Lighting。一般把这个Cube Map叫做Light Probe。
对于Lambert表面,其光照强度只和法线n和光照方向l相关,所以给定一个Light Probe,可以计算出所有可能的法线方向的光照,然后存储到一个Cube Map里,渲染时,只需要使用法线n去这个Cube Map里索引就能得到Environment Lighting,这个存储着光照的Cube Map就叫Irradiance Map。计算的伪代码如下:
- diffuseConvolution(outputEnvironmentMap, inputEnvironmentMap)
- {
- for_all {T0: outputEnvironmentMap}
- sum =
- N = envMap_direction(T0)
- for_all {T1: inputEnvironmentMap}
- L = envMap_direction(T1)
- I = inputEnvironmentMap[T1]
- sum += max(, dot(L, N)) * I
- T0 = sum
- return outputEnvironmentMap
- }
对于每一个法线n都需要去遍历所有的光线方向,算法复杂度为O(NM),N为Light Probe的大小,M为Irradiance Map的大小。
- Spherical Harmonics
由于Diffuse光照本身是变化很缓慢的低频数据,所以可以使用SH来加速计算。把算法分为两步:
1. 把Light Probe投影到SH上,求解出SH系数存储下来。
2. 将Light Probe的SH和Diffuse Transfer的SH做卷积即可求出Irradiance Map。
具体做法如下:
- //将LightProbe投影到SH上
- for each texel of the lightProbe
- {
- lightSample = texelRadiance;
- weight += texelSolidAngle;
- //计算光照方向
- l = texelDirection;
- //根据光照方向求出SH基函数
- SHBasis = calculateSHBasis(l);
- //累加SH系数
- lightSH += lightSample*SHBasis*texelSolidAngle;
- }
- lightSH = lightSH**PI/weight;
- for each texel of the irradianceMap
- {
- //法线方向
- n = texelDirection;
- // 求出cosine lobe的SH
- diffuseSH = calculateDiffuseSH(n);
- // 用cosine lobe的SH和light probe的SH做卷积
- irradiance = dotSH(diffuseSH, lightSH);
- // lambert brdf
- irradiance *= /PI;
- texelValue = irradiance;
- }
使用SH来计算的话,Light Probe和Irradiance Map只需要分别遍历一遍,所以算法复杂度为O(KN+KM),N为Light Probe的大小,M为Irradiance Map的大小,其中K为SH系数的个数,对于Diffuse光照,使用3阶的SH函数就能获得不错的近似结果,3阶的SH有9个系数,所以K远小于N和M。
因为Diffuse光照本身是低频的,所以输出的Irradiance Map可以使用较小的分辨率,那么整个算法的开销主要是在第一步——把Light Probe投影到 SH上。
- 使用GPU计算Light Probe SH
GPU Gems 2 的Chapter 10介绍了使用pixel shader来计算Light Probe SH的方法,使用SM5.0的Compute Shader来计算可以获得更大的加速比。
观察求解Light Probe SH的过程——遍历所有的texel,对于每个texel,求解出SH,然后累加,最后累加的结果就是SH系数。如果使用并行的算法,伪代码如下:
- g_mutex;
- for each texel of the lightProbe
- {
- lightSample = texelRadiance;
- //计算光照方向
- l = texelDirection;
- //根据光照方向求出SH基函数
- SHBasis = calculateSHBasis(l);
- //累加SH系数
- g_mutex.lock();
- lightSH += lightSample*SHBasis*texelSolidAngle;
- weight += texelSolidAngle;
- g_mutex.unlock();
- }
- lightSH = lightSH**PI/weight;
但是对于GPU的Compute Shader,只能同步一个Group里的thread,对于不同Group的thread无法同步,所以无法使用一个加锁的全局变量不断累加的方法。
既然无法一次求出所有的累加结果,那么就先求出每个Group的累加结果,然后根据GroupID写入到输出的Buffer,然后把这个Buffer作为输入,重复之前的操作,直到输出Buffer的Size为1时就求出了结果。比如对于一个512x512的Cube Map,固定Thread Group大小为8x8,那么我们分配64x64个Group,其输出Buffer大小为64x64(每一个Group输出一个结果),运行Compute Shader计算结果输出到Buffer,这时数据就缩小到了64x64,然后重复之前的操作,下一轮的数据大小就变成了8x8(64/8)。一直重复这个操作,直到输出的Buffer大小为1时就求解出了结果。
这个算法的思路和HDR渲染中求解场景的平均亮度是一样的,在求解平均亮度时,每次把Texture的Size缩小到1/4做Down Sample,直到Texture大小为1x1时就求出了平均亮度。
求解每个Group的SH累加结果的算法参考Nvidia的 “Optimizing Parallel Reduction in CUDA”, Parallel Reduction的思路就是一个递归的tree-based approach,如下图
对于Shader代码,使用循环来模拟这个过程,具体做法是设置一个步长step,把相隔step个步长的数据相加,然后step乘以2,重复这个过程,直到step大于N,N为输入数据的大小,循环累加的代码如下:
- for (uint s = ; s < groupthreads; s *= ) // stride: 1, 2, 4, 8, 16,32, 64, 128
- {
- int index = * s * GI;
- if (index < (groupthreads))
- sharedMem[index] += sharedMem[index + s];
- GroupMemoryBarrierWithGroupSync();
- }
算法的流程如下图:
完整的Shader代码如下:
- #define THREAD_SIZE_X 8
- #define THREAD_SIZE_Y 8
- #define GROUP_THREADS THREAD_SIZE_X*THREAD_SIZE_Y
- Texture2D<float> g_InputBuffer : register(t0);
- RWTexture2D<float> g_OutputBuffer : register(u0);
- groupshared float g_ShareMem[GROUP_THREADS];
- [numthreads(THREAD_SIZE_X, THREAD_SIZE_Y, )]
- void ReductionCS(uint3 Gid : SV_GroupID,
- uint3 DTid : SV_DispatchThreadID,
- uint3 GTid : SV_GroupThreadID,
- uint GI : SV_GroupIndex)
- {
- //加载数据到share memory中
- uint Idx = DTid.y*g_InputBuffer.Length.x + DTid.x;
- g_ShareMem[GI.xy] = g_InputBuffer[Idx];
- GroupMemoryBarrierWithGroupSync();
- // 循环累加所有数据
- [unroll]
- for (uint s = ; s < GROUP_THREADS; s *= ) // stride: 1, 2, 4, 8, 16, 32, 64, 128
- {
- int index = * s * GI;
- if (index < GROUP_THREADS)
- g_ShareMem[index] += g_ShareMem[index + s];
- GroupMemoryBarrierWithGroupSync();
- }
- if (GI == )
- {
- //写入结果
- g_OutputBuffer[Gid.xy] = g_ShareMem[];
- }
- }
NV的paper中还提到了可以进一步优化,GPU在运行Thread Group时,会把Thread划分为Warp,Nvidia的GPU中一个Warp包含32个Thread,这些线程是由SIMD32处理器同步运行的,所以如果线程数目小于32时,可以去掉GroupMemoryBarrierWithGroupSync() 的调用来提升性能。(对于AMD的GPU,把线程划分为Wavefronts,和NV的Warp对应,AMD的每个Wavefronts中包含64个同步执行的Thread)
- 运行结果
运行的参考对象是DirectX ToolKit中的SHProjectCubeMap,这个函数使用CPU来计算Light Probe SH。测试使用的Light Probe是一个512x512的R16G16B16A16_FLOAT格式的HDR Cube Map,测试结果发现在Release模式下使用Compute Shader可以比DXTK的CPU版本快10倍以上,加速比很高。
- 其他
1. Light Probe一般都是HDR格式的,所以生成的Irradiance Map也是HDR格式的,那么使用Irradiance Map计算光照时需要使用HDR渲染。
2. Diffuse Lighting一般用3阶的SH足矣,对于HDR的Light Probe,5阶的SH会更准确(4阶的Diffuse Transer SH系数为0,所以无需使用)。当然也意味着更多的计算量。
3. 使用GPU计算的时候,计算的结果参考了DXTK的函数,但是发现和DXTK的结果有些许偏差。仔细调试后发现是浮点累加误差导致的,DXTK的结果并不准确,在其SHProjectCubeMap源码实现中,是使用float类型不断累加,比如对于求解solid angle weight,最后的结果会累加到几百万,但是对于单个的weight数据,其值可能只有1左右,而float有效数字只有6位,所以在累加的过程中会有误差,数据到几百万时,误差可能会达到几十。理论上来讲,solid angle weight是使用texture uv求解的,那么对于Cube Map的每一个face,其累加结果应该都是相同的,DXTK因为使用float累加,浮点误差导致其每个face的solid angle weight累加结果并不相同,所以DXTK的结果并不准确,只能作为参考值。
实际上相比于DXTK,GPU算法的准确度更高,因为调试的时候发现GPU求解出来的结果,每一个face的solid angle累加结果都是相同的,不会像DXTK那样有误差。因为在GPU算法中,累加是分层求解的(参考前面的算法图解),每一层中的节点数据范围都差不多,这样浮点的误差就不会像直接累加那么大,所以GPU求解的速度和精度都好于DXTK的SHProjectCubeMap。
参考资料:
http://http.developer.nvidia.com/GPUGems2/gpugems2_chapter10.html
http://developer.download.nvidia.com/assets/cuda/files/reduction.pdf
http://diaryofagraphicsprogrammer.blogspot.com/2014/03/compute-shader-optimizations-for-amd.html
https://seblagarde.wordpress.com/2012/06/10/amd-cubemapgen-for-physically-based-rendering/
使用Compute Shader加速Irradiance Environment Map的计算的更多相关文章
- GraphicsLab Project之Diffuse Irradiance Environment Map
作者:i_dovelemon 日期:2020-01-04 主题:Rendering Equation,Irradiance Environment Map,Spherical Harmonic 引言 ...
- OpenGL 之 Compute Shader(通用计算并行加速)
平常我们使用的Shader有顶点着色器.几何着色器.片段着色器,这几个都是为光栅化图形渲染服务的,OpenGL 4.3之后新出了一个Compute Shader,用于通用计算并行加速,现在对其进行介绍 ...
- Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十三章:计算着色器(The Compute Shader)
原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十三章:计算着色器(The Compute Shader) 代码工程 ...
- 【原创翻译】初识Unity中的Compute Shader
一直以来都想试着自己翻译一些东西,现在发现翻译真的很不容易,如果你直接把作者的原文按照英文的思维翻译过来,你会发现中国人读起来很是别扭,但是如果你想完全利用中国人的语言方式来翻译,又怕自己理解的不到位 ...
- 聊聊如何正确向Compute Shader传递数组
0x00 前言 前一段时间去英国出差,发现Unity Brighton 办公室的手绘地图墙很漂亮,在这里分享给大家. 在这篇文章中,我们选择了过去几周Unity官方社区交流群以及UUG社区群中比较有代 ...
- OpenGL Compute Shader靠谱例子及读取二进制Shader,SPIR-V
学OpenGL以来一直苦恼没有像DX那样可以读取二进制Shader使用的方法,除去有时不想公开自己写的牛逼Shader的心理(虽然目前还从没写过什么牛逼的Shader), 主要是不用现场编译,加快读取 ...
- Compute Shader
[Compute Shader] 1.Similar to regular shaders, compute shaders are Asset files in your project, with ...
- Compute Shader基础
ComputeShader: GPGPU:General Purpose GPU Programming,GPU通用计算,利用GPU的并行特性.大量并行无序数据的少分支逻辑适合GPGPU.平台 ...
- Vulkan在Android使用Compute shader
oeip 相关功能只能运行在window平台,想移植到android平台,暂时选择vulkan做为图像处理,主要一是里面有单独的计算管线且支持好,二是熟悉下最新的渲染技术思路. 这个 demo(git ...
随机推荐
- 7 款华丽的 HTML5 Loading 动画特效
我们在进行大数据的传输或者复杂操作的等待时,最好能有一个Loading等待的小动画提示用户.本文将为大家分享一些超华丽的基于HTML5的Loading加载动画特效,希望你会喜欢. 1.HTML5 Ca ...
- PHP函数 mysql_real_escape_string 与 addslashes 的区别
addslashes 和 mysql_real_escape_string 都是为了使数据安全的插入到数据库中而进行的过滤,那么这两个函数到底是有什么区别呢? 首先,我们还是从PHP手册入手: 手册上 ...
- 编写快速、高效的JavaScript代码
许多Javascript引擎都是为了快速运行大型的JavaScript程序而特别设 计的,例如Google的V8引擎(Chrome浏览器,Node均使用该引擎).在开发过程中,如果你关心你程序的内存和 ...
- Activity的生命周期与加载模式——Activity的生命周期演示
当Activity处于Android应用中运行时,它的活动状态由Android以Activity栈的形式管理.当前活动的Activity位于栈顶.随着不同应用的运行,每个Activity都有可能从活动 ...
- ThinkPHP URL伪静态、路由规则、重写、生成
一.URL规则 1.默认是区分大小写的 2.如果我们不想区分大小写可以改配置文件 'URL_CASE_INSENSITIVE'=>true,//url不区分大小写 ...
- jQuery内容过滤器
jQuery内容过滤器 <h1>this is h1</h1> <div id="p1"> <h2>this is h2</h ...
- removeEventListener('2016');
2016----最后一天工作日要快结束了,趁剩下的一点时间写篇博客玩玩,想到啥就写啥.总结下来就一句---累并快乐着... 先祝大家新年快乐!万事如意发大财. 一年跳了三家公司,上半年在家小公司干着整 ...
- Java日期工具类,Java时间工具类,Java时间格式化
Java日期工具类,Java时间工具类,Java时间格式化 >>>>>>>>>>>>>>>>>&g ...
- 数据库--iOS
1.创建表 @"create table if not exists Person(id integer primary key autoincrement,name text,gender ...
- Spring 中使用Quartz实现任务调度
前言:Spring中使用Quartz 有两种方式,一种是继承特定的基类:org.springframework.scheduling.quartz.QuartzJobBean,另一种则不需要,(推荐使 ...