The projection of the source positions into the reflection is an injective transformation, which means that two different pixels from the main view can be merged in the reflection and become “reflective concurrents” without knowing which one of them should prevail. Hence the blinking pixels.
There are gaps in the reflection caused by the occlusion in the main view that prevents valid pixels in the reflection to be projected.
•源位置投影到反射中是里射变换,这意味着来自主视图的两个不同像素可以合并到反射中,并成为“反射同线”,而不知道其中哪一个应该占优势。因此出现了闪烁的像素。
That’s where the Projection hash texture and the hashing function we chose eventually make sense. By using the intrinsic InterlockedMax when writing on the UAV, two hashes are going to be sorted first by their high bytes and so by their PixelY value.
这就是我们最终选择的投影散列纹理和散列函数的意义所在。通过在UAV上写入时使用内部InterlockedMax,两个哈希将首先按其高字节排序,然后按其像素值排序。
// Read-write max when accessing the projection hash UAV
uint projectionHash = SrcPosPixel.y << 16 | SrcPosPixel.x;
InterlockedMax(ProjectionHashUAV[ReflPosPixel], projectionHash, dontCare);
The concurrent projection is now sorted “from-bottom-to-top” and the source pixel locations stored in the projection hash are now the ones closest to the water plane thus the closest to the camera in the reflection view. The projection is now stable.
现在,并发投影被“从下到上”排序,并且存储在投影散列中的源像素位置现在是最接近水平面的位置,因此在反射视图中是最接近相机的位置。预测现在是稳定的。
Filling the gaps
The missing geometry is the #1 issue with the SSR approach and we need to find a way to fill it or else the reflection effect would be disastrously broken.
First we’ll deal with the missing reflection on the screen borders. This is due to geometry absent from the main view but needed in the reflection. There’s no actual solution besides rendering a bigger out-of-screen main frame so we’d be able to fetch it to fill the borders. But in a real world game where every microsecond counts, this is not a option.
Instead we’ll add some stretch on the projected location based on the distance between the source pixel and the water plane.
填补空白
缺少的几何体是SSR方法的#1问题,我们需要找到一种方法来填充它,否则反射效果将被破坏。
首先,我们将处理屏幕边框上缺少的反射。这是由于主视图中缺少几何体,但反射中需要几何体。除了渲染一个更大的屏幕外主框架,我们没有实际的解决方案,所以我们可以获取它来填充边框。但在一个每微秒都很重要的真实游戏中,这不是一个选项。
相反,我们将根据源像素和水平面之间的距离在投影位置上添加一些拉伸。
float HeightStretch = (PosWS.z – WaterHeight);
float AngleStretch = saturate(- CameraDirection.z);
float ScreenStretch = saturate(abs(ReflPosUV.x * 2 - 1) – Threshold);
ReflPosUV.x *= 1 + HeightStretch * AngleStretch * ScreenStretch * Intensity;
Reflection stretching to fill the missing pixels on the borders(反射拉伸以填充边界上缺失的像素)
Then, it is time to deal with the holes in the projection, which were created by the geometry occluded by closer pixels, and which couldn’t have been projected.
A classic temporal reprojection helps a lot and very little movement actually suffices to almost completely fill the cracks.
As a fallback for pixels that still couldn’t be filled with relevant information but which could still be valid (i.e not in the sky), we’ll just make the reflection surface we generated the frame before “bleed” on the current one. It surely isn’t correct but it gracefully avoids any discontinuity and fills the remnant gaps with a coherent color/luminosity.
然后,是时候处理投影中的孔了,这些孔是由被更接近的像素遮挡的几何体创建的,并且不可能被投影。
一个经典的时间重投影有很大的帮助,实际上很少的移动就足以几乎完全填满裂缝。
对于仍然无法填充相关信息但仍然有效(即不在天空中)的像素,作为回退,我们将在当前帧上“出血”之前生成反射面。这肯定是不正确的,但它优雅地避免了任何不连续性,并用连贯的颜色/亮度填充了剩余的间隙。
The reprojection and bleeding fill the gaps with coherent values(重新投影和出血用一致的值填补了空白)
Optimizations
We have a nice real-time solution at this point but let’s try to get some extra bits of performance. We are going to use an empty additional stencil and see how it can drastically change the cost of the SSPR.
We discard the pixels whose height is below the water plane as they have no chance to participate in the reflection. The successful pixels are marked in the additional stencil.
The hash resolve pass uses this mask to only resolve the reflection on the pixels which have been previously discarded (thus whose stencil value has not been marked), as they are the only ones able to be reflective.
With these optimizations, the SSPR cost is linearly dependant on the percentage of the screen where pixels aren’t the sky and are located below the water plane.
On Ghost Recon Wildlands, we were then able to generate any water reflection surface for a cost of 0.3~0.4 ms on consoles at 1/4 resolution.
优化
在这一点上,我们有一个很好的实时解决方案,但让我们尝试获得一些额外的性能。我们将使用一个空的额外模具,看看它如何能极大地改变SSPR的成本。
我们丢弃高度低于水平面的像素,因为它们没有机会参与反射。成功的像素将标记在附加模具中。
散列解析过程使用此掩码仅解析先前丢弃的像素上的反射(因此其模具值未标记),因为它们是唯一能够反射的像素。
通过这些优化,SSPR成本与屏幕百分比成线性关系,其中像素不是天空,而是位于水平面下方。
在幽灵侦察荒地上,我们能够以1/4分辨率在控制台上以0.3~0.4毫秒的成本生成任何水反射表面。
Multiple water planes
As we achieved our goal of an affordable reflection technology, we’re able to compute it several times in a single frame, allowing us to handle multiple water surfaces.
To achieve this, we’ll have to know which planes need the SSPR rendered.
When rendering the water, each water pixel increments a counter with its plane ID
These counters are then processed to know which planes are actually visible, and we keep the N planes which are the most present on the screen.
We generate N reflection layers in a texture array using SSPR.
In the next frame, the water will use its ID to fetch the array and retrieve the reflection.
多水平面
当我们实现了一种价格合理的反射技术的目标时,我们能够在一个帧中多次计算它,使我们能够处理多个水面。
为了实现这一点,我们必须知道哪些飞机需要渲染SSPR。
渲染水时,每个水像素使用其平面ID增加一个计数器
然后对这些计数器进行处理,以了解哪些平面实际可见,并保留屏幕上显示最多的N个平面。
我们使用SSPR在纹理阵列中生成N个反射层。
在下一帧中,水将使用其ID获取阵列并检索反射。
Multiplanar reflection on the pool and the lake surfaces(水池和湖面上的多平面反射)
A complete example
一个完整的例子
Lit color buffer
SSPR stencil + Projection Hash
Resolved hash
Water rendering