Restir的实现中存在明显的鬼影

编程语言 2026-07-09

我最近实现了Restir算法。该算法只是基于概率来计算直接光照。源码在GitHub上: https://github.com/Trylz/Restir_DI_CPP
所有Restir代码在这里: Source\Samples\Restir

烦人的问题是我的实现存在重投影问题(我称之为鬼影)。

下面是在开启时间滤波时我得到的结果
https://www.youtube.com/watch?v=bYaBjdURGxU

向前移动时鬼影很明显:
转置的

下面是在关闭时间滤波时的结果:
https://www.youtube.com/watch?v=uZVAdYT8F9w
鬼影消失,收敛也得到了改善。

时间滤波着色器代码位于这里: Source\Samples\Restir\TemporalFilteringPass.slang

#include "Light.slangh"
#include "Reservoir.slangh"

import Utils.Sampling.TinyUniformSampleGenerator;

cbuffer PerFrameCB
{
    uint2 viewportDims;
    float3 cameraPositionWs;
    float4x4 previousFrameViewProjMat;
    uint nbReservoirPerPixel;
    uint sampleIndex;
    uint motion;
    float temporalLinearDepthThreshold;
    float temporalWsRadiusThreshold;
    float temporalNormalThreshold;
};

RWStructuredBuffer<RestirReservoir> gCurrentFrameReservoirs;
StructuredBuffer<RestirReservoir> gPreviousFrameReservoirs;

Texture2D<float4> gCurrentPositionWs;
Texture2D<float4> gPreviousPositionWs;

Texture2D<float4> gCurrentNormalWs;
Texture2D<float4> gPreviousNormalWs;

Texture2D<float4> gAlbedo;
Texture2D<float4> gSpecular;

int2 getPreviousFramePixelPos(float4 P, float width, float height)
{
    float4 ndc = mul(P, previousFrameViewProjMat);
    ndc = ndc / ndc.w;
    float2 s = (ndc.xy + float2(1, 1)) * 0.5f;
    s = float2(s.x, 1.f - s.y);
    return (int2)(s * float2(width, height));
}

[numthreads(16, 16, 1)] void TemporalFilteringPass(uint3 threadId
                                                   : SV_DispatchThreadID)
{
    const uint2 pixel = threadId.xy;
    if (any(pixel >= viewportDims))
        return;

    if (gCurrentPositionWs[pixel].w == 0.0f)
        return;

    const float3 currP = gCurrentPositionWs[pixel].xyz;

    int2 previousPixelPos = getPreviousFramePixelPos(float4(currP, 1.0f), (float)viewportDims.x, (float)viewportDims.y);
    if (previousPixelPos.x < 0 || previousPixelPos.x >= (int)viewportDims.x)
        return;
    if (previousPixelPos.y < 0 || previousPixelPos.y >= (int)viewportDims.y)
        return;
    if (gPreviousPositionWs[previousPixelPos].w == 0.0f)
        return;

    const float3 prevP = gPreviousPositionWs[previousPixelPos].xyz;
    if (length(prevP - currP) > temporalWsRadiusThreshold)
        return;

    const float3 currN = gCurrentNormalWs[pixel].xyz;
    const float3 prevN = gPreviousNormalWs[previousPixelPos].xyz;

    if (length(prevN - currN) > temporalNormalThreshold)
        return;

    const float currlinearDepth = gCurrentNormalWs[pixel].w;
    const float prevlinearDepth = gPreviousNormalWs[pixel].w;

    if (abs(currlinearDepth - prevlinearDepth) > temporalLinearDepthThreshold)
        return;

    const float3 V = normalize(cameraPositionWs - currP);
    const float3 diffuse = gAlbedo[pixel].xyz;
    const float3 specular = gSpecular[pixel].xyz;
    const float roughness = gSpecular[pixel].w;

    // Current pixel
    const uint currentPixelLinearIndex = pixel.y * viewportDims.x + pixel.x;
    const uint currentPixelReservoirsStart = currentPixelLinearIndex * nbReservoirPerPixel;

    // Previous pixel
    const uint previousPixelLinearIndex = previousPixelPos.y * viewportDims.x + previousPixelPos.x;
    const uint previousPixelReservoirsStart = previousPixelLinearIndex * nbReservoirPerPixel;

    // Init rng
    TinyUniformSampleGenerator rng = TinyUniformSampleGenerator(pixel.xy, sampleIndex);

    // Combine reservoirs
    for (uint i = 0; i < nbReservoirPerPixel; ++i)
    {
        RestirReservoir currentReservoir = gCurrentFrameReservoirs[currentPixelReservoirsStart + i];

        RestirReservoir previousReservoir = gPreviousFrameReservoirs[previousPixelReservoirsStart + i];
        if (motion > 0)
        {
            // Clamp M according to paper. But use a smaller value(5) since it gives better results.
            previousReservoir.mM = min(5 * currentReservoir.mM, previousReservoir.mM);
        }

        gCurrentFrameReservoirs[currentPixelReservoirsStart + i] =
            combineReservoirs(currentReservoir, previousReservoir, currP, currN, V, diffuse, specular, roughness, rng);
    }
}

要理解实现,可以从阅读README https://github.com/Trylz/Restir_DI_CPP/blob/main/README.md

//---------------------------------------------------------------------

我认为我做错的地方!
我觉得我的运动向量是错的。我不应该需要对OptixDenoiserPass使用的视图投影矩阵进行转置:

转置的

这点是通过这个PR修复的:
https://github.com/Trylz/Restir_DI_CPP/pull/3/changes
所有修复鬼影的尝试都发生在这个 an_attempt_to_fix_motion_vectors 分支上。

不幸的是这些改动是有道理的,但并不能解决问题。

正如我之前所说,关闭时间滤波时的状况鬼影消失:
https://www.youtube.com/watch?v=uZVAdYT8F9w

当时间滤波开启且去噪开启时,问题更加明显。我认为这与去噪器滤波的累积特性有关。伪影在帧之间传播。

如果你提高相机速度并关闭去噪,你可以清楚地看到鬼影。

mpScene->setCameraSpeed(radius * 4.0f);

所以这不是去噪器的问题!


因此我相信问题绝对是在这里引入的:


引入位置

有谁有想法吗?为什么向后移动时鬼影不会出现?这太奇怪了。

谢谢!

解决方案

这是因为对复用的时间样本的可见性从未重新测试

RestirApp.cpp:226-235的传递顺序:

GBuffer → RIS → Visibility → Temporal → Spatial → Shading

VisibilityPass会对当前帧的reservoir相对于当前BVH跟踪一条阴影射线,然后再也不会运行。在combineReservoirs中,上一帧的reservoir r2携带一个光样本,其遮挡是在上一帧从前一着色点对BVH进行评估的。combineReservoirs在新的P/N处重新评估BRDF和 NdotL,但不会重新测试可见性。

前向相机运动会产生非遮挡区:前景移动导致新暴露的表面。复用的样本在前一个前景表面是未遮挡的,但在新可见的背景表面上却是被遮挡的。使用min(5 * M_curr, M_prev) 时,陈旧的样本会持续存在5–20帧,从而产生观察到的模糊。向后运动则产生遮挡而非非遮挡。将当前背景像素重新投影到上一帧的同一表面,因此reservoir保持一致且不会出现伪影。

要修复,在合并之前,从当前着色点对上一帧reservoir的光样本跟踪一条阴影射线(ReSTIR DI,无偏变体):

float  Ll = length(L);
RayDesc ray = { currP, 0.001, L / Ll, Ll };
ShadowRayData sd; sd.hit = true;
TraceRay(gScene.rtAccel, RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH,
         0xFF, 1, rayTypeCount, 1, ray, sd);
if (sd.hit) previousReservoir.m_W = 0.0f;

此外

const float prevlinearDepth = gPreviousNormalWs[pixel].w;               // incorrect
const float prevlinearDepth = gPreviousNormalWs[previousPixelPos].w;    // correct
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章