哈Ha! 本文介绍了
反射阴影贴图的简单实现(该算法在
上一篇文章中进行了
介绍 )。 接下来,我将解释我是如何做到的以及存在的陷阱。 还将考虑一些可能的优化。
图1:从左到右:没有RSM,有RSM,差异结果
在
图1中,您可以看到使用
RSM获得的结果。 为了创建这些图像,使用了“斯坦福兔子”和三个彩色四边形。 在左侧的图像中,您可以看到仅使用
聚光灯而不使用
RSM进行渲染的结果。 阴影中的所有内容均为黑色。 中间的图像显示了
RSM的结果。 以下差异是显而易见的:到处都有较亮的颜色,粉红色,淹没地板和兔子的阴影不是完全黑色。 最后一张图片显示了第一张图片和第二张图片之间的差异,因此显示了
RSM的贡献。 在中间图像中可以看到更紧的边缘和伪影,但这可以通过调整核心的大小,间接照明的强度和样本数量来解决。
实作
该算法是在其自己的引擎上实现的。 着色器是用HLSL编写的,渲染是用DirectX 11编写的。在撰写本文之前,我已经为定向光(定向光源)设置了
延迟着色和
阴影映射 。 首先,仅在添加了对
阴影贴图和点光源的
RSM支持之后,我
才对定向光实现了
RSM 。
阴影贴图扩展
传统上,
阴影贴图 (
Shadow Maps ,SM)只不过是深度图。 这意味着您甚至不需要像素/片段着色器来填充SM。 但是,对于
RSM,您将需要一些额外的缓冲区。 您需要存储世界空间
位置 ,世界空间
法线和
光通量 (光输出)。 这意味着您需要一个具有多个渲染目标的像素/片段着色器。 请记住,对于这种技术,您需要切断
脸部剔除 ,而不是前端
剔除 。
使用
脸部剔除前边缘是避免阴影伪影的广泛使用方法,但这不适用于
RSM 。
您将世界空间位置和法线传递给像素着色器,并将其写入适当的缓冲区。 如果使用
法线贴图 ,则还要在像素着色器中计算它们。 通过将反照率材料乘以光源的颜色
来计算
通量 。 对于
聚光灯,您需要将结果值乘以入射角。 对于
定向光,可以获得不
遮挡的图像。
准备照明计算
对于主要段落,您需要做一些事情。 您必须将阴影遍历中使用的所有缓冲区绑定为纹理。 您还需要随机数。
官方文章说,您需要预先计算这些数字并将其保存在缓冲区中,以减少
RSM采样过程中的操作数量。 由于该算法在性能方面很繁琐,因此我完全同意官方文章。 还建议在那里保持时间一致性(所有间接照明计算都使用相同的采样模式)。 当每个帧使用不同的阴影时,这将防止闪烁。
每个样本需要两个随机浮点数,其范围为[0,1]。 这些随机数将用于确定样品的坐标。 您还将需要用于将位置从世界空间(世界空间)转换为阴影空间(光源空间)的矩阵。 您还将需要此类参数进行采样,如果您采样超出纹理的边界,则该参数将呈现黑色。
通过照明
现在是很难理解的部分。 我建议您在计算特定光源的直接照明之后再计算间接照明。 这是因为您需要全屏四边形以获得
定向光 。 但是,对于
点光源和
点光源,通常需要使用带有
剔除的特定形状的网格来填充较少的像素。
在下面的代码中,将为像素计算间接照明。 接下来,我将解释那里发生了什么。
float3 DoReflectiveShadowMapping(float3 P, bool divideByW, float3 N) { float4 textureSpacePosition = mul(lightViewProjectionTextureMatrix, float4(P, 1.0)); if (divideByW) textureSpacePosition.xyz /= textureSpacePosition.w; float3 indirectIllumination = float3(0, 0, 0); float rMax = rsmRMax; for (uint i = 0; i < rsmSampleCount; ++i) { float2 rnd = rsmSamples[i].xy; float2 coords = textureSpacePosition.xy + rMax * rnd; float3 vplPositionWS = g_rsmPositionWsMap .Sample(g_clampedSampler, coords.xy).xyz; float3 vplNormalWS = g_rsmNormalWsMap .Sample(g_clampedSampler, coords.xy).xyz; float3 flux = g_rsmFluxMap.Sample(g_clampedSampler, coords.xy).xyz; float3 result = flux * ((max(0, dot(vplNormalWS, P – vplPositionWS)) * max(0, dot(N, vplPositionWS – P))) / pow(length(P – vplPositionWS), 4)); result *= rnd.x * rnd.x; indirectIllumination += result; } return saturate(indirectIllumination * rsmIntensity); }
该函数的第一个参数是
P ,它是特定像素的世界空间位置(在世界空间中)。
DivideByW用于获得正确
Z值所需的预期分割。
N是世界空间法线。
float4 textureSpacePosition = mul(lightViewProjectionTextureMatrix, float4(P, 1.0)); if (divideByW) textureSpacePosition.xyz /= textureSpacePosition.w; float3 indirectIllumination = float3(0, 0, 0); float rMax = rsmRMax;
在代码的这一部分中,将计算光空间(相对于光源)的位置,并初始化间接照明变量,其中将从每个样本计算出的值相加,并根据
官方文章中的照明方程式
设置rMax变量,我将在下一节中解释其值。
for (uint i = 0; i < rsmSampleCount; ++i) { float2 rnd = rsmSamples[i].xy; float2 coords = textureSpacePosition.xy + rMax * rnd; float3 vplPositionWS = g_rsmPositionWsMap .Sample(g_clampedSampler, coords.xy).xyz; float3 vplNormalWS = g_rsmNormalWsMap .Sample(g_clampedSampler, coords.xy).xyz; float3 flux = g_rsmFluxMap.Sample(g_clampedSampler, coords.xy).xyz;
在这里,我们开始循环并为方程式准备变量。 出于优化目的,我计算出的随机样本已经包含坐标偏移,即要获得UV坐标,我只需要在光空间坐标上添加
rMax * rnd即可。 如果产生的UV超出[0.1]范围,则样品应为黑色。 这是合乎逻辑的,因为它们超出了照明范围。
float3 result = flux * ((max(0, dot(vplNormalWS, P – vplPositionWS)) * max(0, dot(N, vplPositionWS – P))) / pow(length(P – vplPositionWS), 4)); result *= rnd.x * rnd.x; indirectIllumination += result; } return saturate(indirectIllumination * rsmIntensity);
这是计算间接照明方程式的部分(
图2 ),并根据从光空间坐标到样本的距离进行加权。 该方程式看起来令人生畏,并且代码并不能帮助您理解所有内容,因此我将更详细地进行解释。
变量
Φ (phi)是
光束 ,即辐射强度。
上一篇文章更详细地描述了
通量 。
助焊剂鳞片带有两个标量艺术品。 第一个在光源法线(texel)和从光源到当前位置的方向之间。 第二个介于当前法线和从当前位置到光源(纹理像素)位置的方向向量之间。 为了不对照明产生负面影响(如果像素不发光,事实证明),标量积限制在[0,∞]范围内。 我想在这个方程式中,出于性能考虑,最后要进行归一化。 在执行标量积之前对方向向量进行归一化同样可以接受。
图2:位置为x且法向为n方向像素光源p的点的照度方程此过程的结果可以与后缓冲(直接照明)混合,结果
如图1所示 。
陷阱
在实施此算法时,我遇到了一些问题。 我将谈论这些问题,以免您踩到相同的耙子。
采样器错误
我花了大量时间弄清楚为什么间接照明看起来很重复。 Crytek Sponza的纹理是隐藏的,因此您需要包装的采样器。 但是对于
RSM来说,它不是很合适。
OpenglOpenGL将RSM纹理设置为GL_CLAMP_TO_BORDER
自订值
为了改善工作流程,重要的是能够通过按一下按钮来更改一些变量。 例如,间接照明的强度和采样范围(
rMax )。 必须为每个光源调整这些参数。 如果采样范围较大,则可以从任何地方获得间接照明,这对于大型场景很有用。 对于更多的本地间接照明,您将需要较小的范围。
图3显示了全局和局部间接照明。
图3: rMax依赖关系的演示。分开通过
起初,我以为可以在着色器中进行间接照明,因此我认为直接照明是可以的。 对于
定向光,这是可行的,因为您仍然绘制了全屏四边形。 但是,对于
点光源和
点光源,您需要优化间接照明的计算。 因此,我考虑了间接照明一个单独的通道,如果您还想进行
屏幕空间插值 ,则这是必需的。
快取
该算法与缓存根本不友好。 它在几个纹理的随机点执行采样。 没有优化的样本数量也无法接受。 分辨率为1280 * 720,
RSM 400样本数,您将为每个光源制作1.105.920.000样本。
利弊
我将列出这种间接照明计算算法的优缺点。
对于 | 反对 |
易于理解的算法 | 根本不是缓存的朋友 |
与延迟渲染器很好地集成 | 需要变量设置 |
可以用于其他算法( LPV ) | 在本地和全局间接照明之间进行强制选择 |
最佳化
我做了几次尝试来提高该算法的速度。 如
官方文章中所述,您可以实现
屏幕空间插值 。 我这样做了,渲染速度更快了。 下面,我将描述一些优化,并使用具有3面墙和一只兔子的场景在以下实现之间进行比较(以每秒帧数为单位):没有
RSM ,则是朴素的
RSM实现,由
RSM内插。
Z检查
我的
RSM工作效率低下的原因之一是因为我还为天空盒中的像素计算了间接照明。 Skybox绝对不需要它。
CPU随机采样
样本的初步计算不仅可以提供更大的时间连贯性,而且还使您不必在着色器中重新计算这些样本。
屏幕空间插值
官方文章建议使用低分辨率渲染目标来计算间接照明。 对于具有许多平滑法线和直墙的场景,可以轻松在较低分辨率的点之间插值照明信息。 我将不详细介绍插值,以便使本文简短一些。
结论
以下是不同数量样本的结果。 关于这些结果,我有一些评论:
- 从逻辑上讲,当不执行RSM计算时,对于不同数量的样本,FPS保持在700左右。
- 插值会带来一些开销,并且对于少量样本来说不是很有用。
- 即使有100个样本,最终图像看起来也不错。 这可能是由于插值导致的,“模糊”间接照明。
样本数 | 无RSM的FPS | 适用于天真的RSM的FPS | 内插RSM的FPS |
100 | 〜700 | 152 | 264 |
200 | 〜700 | 89 | 179 |
300 | 〜700 | 62 | 138 |
400 | 〜700 | 44 | 116 |