HLSL TraceRay always return no intersection
03:13 14 Jul 2026

I am implementing GPU path tracing using RTXGI and NVRHI.
The problem:
Intersection queries always fails when there are obvious occluders.

Here my shader code adapted from the RTXGI sample shader.

#pragma pack_matrix(column_major)

#include "TinyUniformSampleGenerator.hlsli"

cbuffer PerFrameCameraCB : register(b0, space0)
{
    float3 CameraWs;
    uint StartSampleIndex;
    uint NbSamplePerPixel;
    float AspectRatio;
    float FovyScale;
    float FocalDistance;
        float LensRadius;
    float4x4 ViewToWorld;
};

RaytracingAccelerationStructure SceneBVH : register(t0, space0);
RWTexture2D Output : register(u0, space0);

struct [raypayload] PrimaryRayPayload
{
    float hitDistance : read(caller) : write(closesthit, miss);
};

struct [raypayload] ShadowRayPayload
{
    bool hit : read(caller, anyhit) : write(caller, closesthit, anyhit);
};

struct Attributes
{
    float2 uv;
};

[shader("closesthit")]
void ClosestHitPrimary(inout PrimaryRayPayload payload : SV_RayPayload, in Attributes attrib : SV_IntersectionAttributes)
{
    uint packedDistance = asuint(RayTCurrent()) & (~0x1u);
    packedDistance |= HitKind() == HIT_KIND_TRIANGLE_FRONT_FACE ? 0x1 : 0x0;
    payload.hitDistance = asfloat(packedDistance);
}

[shader("anyhit")]
void AnyHitPrimary(inout PrimaryRayPayload payload : SV_RayPayload, in Attributes attrib : SV_IntersectionAttributes)
{
    // AcceptHit but continue looking for the closest hit
}

[shader("miss")]
void MissPrimary(inout PrimaryRayPayload payload : SV_RayPayload)
{
    payload.hitDistance = -1.0f;
}

[shader("closesthit")]
void ClosestHitShadow(inout ShadowRayPayload payload : SV_RayPayload, in Attributes attrib : SV_IntersectionAttributes)
{
    payload.hit = true;
}

[shader("anyhit")]
void AnyHitShadow(inout ShadowRayPayload payload : SV_RayPayload, in Attributes attrib : SV_IntersectionAttributes)
{
    payload.hit = true;
}

[shader("miss")]
void MissShadow(inout ShadowRayPayload payload : SV_RayPayload)
{
}

inline float2 uniformSampleUnitCircle(const float r)
{
    float theta = 6.2831853f * r;
    return float2(cos(theta), sin(theta));
}

RayDesc spawnRay(inout TinyUniformSampleGenerator sg, uint2 pixel, uint2 viewportRes)
{
    RayDesc ray;
    ray.Origin = CameraWs;

    float2 randFloat2 = sampleNext2D(sg);

    const float xReal = pixel.x + randFloat2.x;
    const float yReal = pixel.y + randFloat2.y;

    const float xNorm = xReal / (float) viewportRes.x;
    const float yNorm = yReal / (float) viewportRes.y;

    const float x = (2.0f * xNorm - 1.0f) * AspectRatio * FovyScale;
    const float y = (1.0f - 2.0f * yNorm) * FovyScale;

    const float4 targetPointVec4 = mul(float4(x * FocalDistance, y * FocalDistance, -FocalDistance, 1.0f), ViewToWorld);
    const float3 targetPoint = targetPointVec4.xyz;

    if (LensRadius > 1e-6f)
    {
        randFloat2 = sampleNext2D(sg);

        const float2 lensSampleCameraSpace = uniformSampleUnitCircle(randFloat2.x)
            * LensRadius * randFloat2.y;

        const float4 lensSampleWorld = mul(ViewToWorld, float4(lensSampleCameraSpace.x, lensSampleCameraSpace.y, 0.0, 1.0f));
        ray.Origin = lensSampleWorld.xyz;
    }

    ray.Direction = normalize(targetPoint - ray.Origin);
    ray.TMin = 1e-8f;
    ray.TMax = 1e8f;
    return ray;
}

[shader("raygeneration")]
void RayGen()
{
    const uint2 pixel = DispatchRaysIndex().xy;
    const uint2 viewportRes = DispatchRaysDimensions().xy;

    if (pixel.x >= viewportRes.x || pixel.y >= viewportRes.y)
        return;
    
    for (uint i = 0u; i < NbSamplePerPixel; ++i)
    {
        TinyUniformSampleGenerator usg;
        usg.init(pixel, StartSampleIndex + i);

        RayDesc ray = spawnRay(usg, pixel, viewportRes);
        PrimaryRayPayload hitData;
        TraceRay(SceneBVH, 0, 0xFF, 0, 0, 0, ray, hitData);

        if (hitData.hitDistance > 0.f)
        {
            Output[pixel] = float4(1, 1, 1, 1);
        }
        else
        {
            Output[pixel] = float4(1, 0, 0, 1);
        }
    }
}


This shader will generate this image. I expect white when there is an intersection:
enter image description here

While the rasterization shaders will output this:

enter image description here

Sky in blue means there is no intersection there! And only there!

Here my per frame constant buffer

    __declspec(align(16)) struct PerFrameCameraCB
    {
        Math::Mat4 ViewToWorld;
        DirectX::XMFLOAT3 CameraWs;
        UINT StartSampleIndex;
        UINT NbSamplePerPixel;
        FLOAT AspectRatio;
        FLOAT FovyScale;
        FLOAT FocalDistance;
        FLOAT LensRadius;
    };

Checked in PIX and it looks fine:
enter image description here

CPP side it is updated like this:


    void PathTracer::updateCameraConstantBuffer()
    {
        PerFrameCameraCB constants = {};

        constants.ViewToWorld = m_renderStartCameraProperties.viewToWorld;
        constants.CameraWs = *reinterpret_cast(&m_renderStartCameraProperties.position);
        constants.StartSampleIndex = m_currentSample;
        constants.NbSamplePerPixel = s_nbSamplePerFrame;
        constants.AspectRatio = m_settings.m_aspectRatio;
        constants.FovyScale = m_renderStartCameraProperties.fovyScale;
        constants.FocalDistance = m_renderStartCameraProperties.focalDistance;
        constants.LensRadius = m_renderStartCameraProperties.lensRadius;

        m_commandList->writeBuffer(m_cameraConstantuffer, &constants, sizeof(PerFrameCameraCB));
    }

FovyScale is tan(fovy * 0.5f);

Here how the raytracing shader is created according to the RTXGI reference


    void PathTracer::initShader()
    {
        BindingLayouts bindingLayouts = BuildBindingLayouts();

        ID3DBlob* shaderBlob = Graphics::Renderer::Realtime::Dx12::Effect::readShader(_STRING("Pathtracing"));
        _ASSERT(shaderBlob);

        nvrhi::ShaderLibraryHandle shaderLibrary = m_device->createShaderLibrary(shaderBlob->GetBufferPointer(), shaderBlob->GetBufferSize());
        _ASSERT(shaderLibrary);

        m_permutation.shaderLibrary = shaderLibrary;

        nvrhi::rt::PipelineDesc pipelineDesc;
        for (int i = 0; i < DescriptorSetIDs::COUNT; ++i)
            pipelineDesc.globalBindingLayouts.push_back(bindingLayouts.dummy[i]);

        pipelineDesc.globalBindingLayouts[DescriptorSetIDs::Globals] = bindingLayouts.global;
        //pipelineDesc.globalBindingLayouts[DescriptorSetIDs::Bindless] = layouts.bindless;
        //pipelineDesc.globalBindingLayouts[DescriptorSetIDs::Denoiser] = layouts.denoiser;

        pipelineDesc.shaders = {
            { "", shaderLibrary->getShader("RayGen", nvrhi::ShaderType::RayGeneration), nullptr },
            { "", shaderLibrary->getShader("MissPrimary", nvrhi::ShaderType::Miss), nullptr },
            { "", shaderLibrary->getShader("MissShadow", nvrhi::ShaderType::Miss), nullptr },
        };

        pipelineDesc.hitGroups = {
            {
                "HitGroup",
                shaderLibrary->getShader("ClosestHitPrimary", nvrhi::ShaderType::ClosestHit),
                shaderLibrary->getShader("AnyHitPrimary", nvrhi::ShaderType::AnyHit),
                nullptr, // intersectionShader
                nullptr, // bindingLayout
                false    // isProceduralPrimitive
            },
            {
                "HitGroupShadow",
                shaderLibrary->getShader("ClosestHitShadow", nvrhi::ShaderType::ClosestHit),
                shaderLibrary->getShader("AnyHitShadow", nvrhi::ShaderType::AnyHit),
                nullptr, // intersectionShader
                nullptr, // bindingLayout
                false    // isProceduralPrimitive
            },
        };

        pipelineDesc.maxPayloadSize = sizeof(float) * 6;

#if ENABLE_NRC
        if (macroDefined("NRC_"))
            pipelineDesc.globalBindingLayouts[DescriptorSetIDs::Nrc] = layouts.nrc;
#endif // ENABLE_NRC

#if ENABLE_SHARC
        if (macroDefined("SHARC_"))
            pipelineDesc.globalBindingLayouts[DescriptorSetIDs::Sharc] = layouts.sharc;
#endif // ENABLE_SHARC

        m_permutation.pipeline = m_device->createRayTracingPipeline(pipelineDesc);
        m_permutation.shaderTable = m_permutation.pipeline->createShaderTable();

        m_permutation.shaderTable->setRayGenerationShader("RayGen");
        m_permutation.shaderTable->addHitGroup("HitGroup");
        m_permutation.shaderTable->addHitGroup("HitGroupShadow");
        m_permutation.shaderTable->addMissShader("MissPrimary");
        m_permutation.shaderTable->addMissShader("MissShadow");
    }

The ViewToWorld matrix has GLM layout so it is column major as expected by my path tracing shader!

Strange thing? PIX report that my acceleration structure are properly created. So the problem is not there:
https://youtu.be/JdSTel-CMug

Here how path tracing is launched:

void PathTracer::performPathTracing()
{
    ID3D12GraphicsCommandList* d3dCmdList =
        m_commandList->getNativeObject(nvrhi::ObjectTypes::D3D12_GraphicsCommandList);
    AutoRenderEventTracker autoTracker(d3dCmdList, _STRING("HybridRenderer::performPathTracing"));


    // Transition pathTracerOutput
    m_commandList->setTextureState(m_renderingTexture.Get(), nvrhi::TextureSubresourceSet(0, 1, 0, 1), nvrhi::ResourceStates::UnorderedAccess);
    m_commandList->commitBarriers();

    nvrhi::rt::State state;
    for (int i = 0; i < DescriptorSetIDs::COUNT; ++i)
        state.bindings.push_back(m_dummyBindingSets[i]); // Unified Binding

    state.bindings[DescriptorSetIDs::Globals] = m_globalBindingSet;
    state.shaderTable = m_permutation.shaderTable;

    m_commandList->setRayTracingState(state);

    nvrhi::rt::DispatchRaysArguments args;
    args.width = this->getFinalWidth();
    args.height = this->getFinalHeight();
    m_commandList->dispatchRays(args);
}

What Am I missing?

Help Needed!

c++ hlsl directx-12