zeux / niagara

A Vulkan renderer written from scratch on stream
MIT License
1.26k stars 72 forks source link

Fauly occlusion with camera #17

Closed Eichenherz closed 1 year ago

Eichenherz commented 3 years ago

Hello Zeux ! I've been dealing with a bug in the "fast/compact" sphere projection doc1 doc2

vec4 ProjectedSphereToAABB( vec3 viewSpaceCenter, float r, float projWidth, float projHeight )
{
    vec2 cXZ = viewSpaceCenter.xz;
    vec2 vXZ = vec2( sqrt( dot( cXZ, cXZ ) - r * r ), r );
    vec2 minX = mat2( vXZ.x, vXZ.y, -vXZ.y, vXZ.x ) * cXZ;
    vec2 maxX = mat2( vXZ.x, -vXZ.y, vXZ.y, vXZ.x ) * cXZ;

    vec2 cYZ =viewSpaceCenter.yz;
    vec2 vYZ = vec2( sqrt( dot( cYZ, cYZ ) - r * r ), r );
    vec2 minY = mat2( vYZ.x, vYZ.y, -vYZ.y, vYZ.x ) * cYZ;
    vec2 maxY = mat2( vYZ.x, -vYZ.y, vYZ.y, vYZ.x ) * cYZ;

    // NOTE: quick and dirty projection
    vec4 aabb = vec4( ( minX.x / minX.y ) * projWidth,
                      ( minY.x / minY.y ) * projHeight,
                      ( maxX.x / maxX.y ) * projWidth,
                      ( maxY.x / maxY.y ) * projHeight );

    // NOTE: from NDC to texture UV space 
    aabb = aabb.xwzy * vec4( 0.5, -0.5, 0.5, -0.5 ) + vec4( 0.5 );

    return aabb;
}

Basically , I was getting occlusion culling on "the object in question" but only in a "Bermuda Tiangle" region, and it shouldn't have happen because it was visible. The issue is produced by the quick and dirty projection, namely it lacks the 1/zNear as a factor:

vec4 aabb = vec4( ( minX.x / minX.y ) * projWidth / zNear,
                      ( minY.x / minY.y ) * projHeight/ zNear,
                      ( maxX.x / maxX.y ) * projWidth / zNear,
                      ( maxY.x / maxY.y ) * projHeight / zNear );

Took me a while to figure out :) Cheers !

zeux commented 3 years ago

In your renderer, how do you define projWidth? Typically znear shouldn't affect the projection results, but if you define projWidth/projHeight as the view-space bounds of the camera slice at znear (so the size of the front clipped plane of the viewing frustum), then yeah - you'd need to divide.

I prefer the formulation where projection width/height define the size of the image plane at Z=1; this definition doesn't depend on znear, which means you can vary znear to adjust for clipping without changing the projection math.

Eichenherz commented 3 years ago
inline DirectX::XMMATRIX PerspInvDepthInfFovLH( float fovYRads, float aspectRatioWH, float zNear )
{
    float sinFov;
    float cosFov;

    DirectX::XMScalarSinCos( &sinFov, &cosFov, fovYRads * 0.5f );

    float h = cosFov / sinFov;
    float w = h / aspectRatioWH;

    DirectX::XMMATRIX proj;
    proj.r[ 0 ] = DirectX::XMVectorSet( w, 0, 0, 0 );
    proj.r[ 1 ] = DirectX::XMVectorSet( 0, h, 0, 0 );
    proj.r[ 2 ] = DirectX::XMVectorSet( 0, 0, 0, zNear );
    proj.r[ 3 ] = DirectX::XMVectorSet( 0, 0, 1, 0 );

    return proj;
}

projWidth would be w, projHeight would be h From the nvidia paper I figured it out. They explicitly multiply the extreme points on the sphere by proj and it would result in 1/znear as being a scaling coef.

I'm not sure I know how to toggle between Z=1 formulation and what I currently have.

if( visible && LATE_PASS ){
        vec3 viewSpaceCenter = ( globs.view * vec4( center.x, center.y, center.z, 1.0f ) ).xyz;

        if( viewSpaceCenter.z > radius + cullInfo.zNear ){
            vec4 aabb = ProjectedSphereToAABB( viewSpaceCenter, radius,
                                          cullInfo.projWidth / cullInfo.zNear,
                                              cullInfo.projHeight / cullInfo.zNear );
......

and ProjectedSphereToAABB is identical to what niagara does.