Square/Planar Artifacts While Volume Rendering NVDB Files Using Optix


Operating System: Ubuntu
Version: Optix 7.7.0

Bug Description

In the sample provided by OptiX, optixVolumeViewer, I am getting square artifacts while raymarching using HDDA that correspond to the vdb grid subdivisions. The volume I am using is wdas_cloud_half from here. I converted it to .nvdb using the shipped nanovdb_convert tool. I also replaced the nanovdb library in optixVolumeViewer with the newer one to handle the new volume.
These are the artifacts I am talking about:

They are only visible from one side and disappear when viewed from the opposite side.
The code used in optixVolumeViewer’s volume.cu that does volume rendering is (comments and other parts omitted):

template<typename AccT>
inline __device__ float transmittanceHDDA(
    const nanovdb::Vec3f& start,
    const nanovdb::Vec3f& end,
    AccT& acc, const float opacity )
    float transmittance = 1.f;
    auto dir = end - start;
    auto len = dir.length();
    nanovdb::Ray<float> ray( start, dir / len, 0.0f, len );
    nanovdb::Coord ijk = nanovdb::RoundDown<nanovdb::Coord>( ray.start() ); // first hit of bbox

    nanovdb::HDDA<nanovdb::Ray<float> > hdda( ray, acc.getDim( ijk, ray ) );

    float t = 0.0f;
    float density = acc.getValue( ijk ) * opacity;
    while( hdda.step() )
        float dt = hdda.time() - t; // compute length of ray-segment intersecting current voxel/tile
        transmittance *= expf( -density * dt );
        t = hdda.time();
        ijk = hdda.voxel();

        density = acc.getValue( ijk ) * opacity;
        hdda.update( ray, acc.getDim( ijk, ray ) ); // if necessary adjust DDA step size

    return transmittance;
extern "C" __global__ void __closesthit__radiance_volume()
    const HitGroupData* sbt_data = reinterpret_cast<HitGroupData*>( optixGetSbtDataPointer() );

    const auto* grid = reinterpret_cast<const nanovdb::FloatGrid*>(
        sbt_data->geometry_data.volume.grid );
    const auto& tree = grid->tree();
    auto        acc  = tree.getAccessor();

    const float3 ray_orig = optixGetWorldRayOrigin();
    const float3 ray_dir  = optixGetWorldRayDirection();

    const float t0 = optixGetRayTmax();
    const float t1 = __uint_as_float( optixGetPayload_0() );

    PayloadRadiance payload = {};
        params.solid_objects, // visibility mask - limit intersections to solid objects

    const auto ray = nanovdb::Ray<float>( reinterpret_cast<const nanovdb::Vec3f&>( ray_orig ), reinterpret_cast<const nanovdb::Vec3f&>( ray_dir ) );
    auto start = grid->worldToIndexF( ray( t0 + 1e-3) );
    auto end   = grid->worldToIndexF( ray( fminf( payload.depth, t1 ) ) );

    auto bbox = grid->indexBBox();
    confine( bbox, end );

    const float opacity = sbt_data->material_data.volume.opacity;
    float  transmittance = transmittanceHDDA( start, end, acc, opacity );

    float3 result = payload.result * transmittance;

    optixSetPayload_0( __float_as_uint( result.x ) );
    optixSetPayload_1( __float_as_uint( result.y ) );
    optixSetPayload_2( __float_as_uint( result.z ) );
    optixSetPayload_3( __float_as_uint( 0.0f ) );

Complete code can be found in the OptiX-Samples shipped with their SDK

To Reproduce

Steps to reproduce the behavior:

  1. Download and build OpenVDB with NanoVDB
  2. Download Optix 7.7.0
  3. Replace the nanovdb folder in optixVolumeViewer with the include/nanovdb folder from the directory where openvdb was installed. And then build it.
  4. Download wdas_cloud_half.vdb from here.
  5. Use nanovdb_convert in nanovdb to convert wdas_cloud_half.vdb to wdas_cloud_half.nvdb.
  6. Run optixVolumeViewer executable with argument --volume wdas_cloud_half.nvdb.

Additional Context

The sample smoke volume provided by Optix does not show these artifacts.

Hi Kaavaygupta,

I’m having trouble reproducing. When I compile a nanovdb_convert from OpenVDB master, it produces a converted file-format that is incompatible with the one baked into the Optix 7.7 SDK volume viewer example. OpenVDB repo is at 32.6.0, sample from old 7.7 Optix SDK on 28.0.0.

When I try loading the converted Disney cloud volume into the sample, it barfs a version incompatibility.

Could you clarify the repro steps?

Oh, my bad. I forgot one step.
After building OpenVDB along with NanoVDB and downloading optix 7.7.0, you need to replace the nanovdb folder in optixVolumeViewer with the include/nanovdb folder from the directory where openvdb was installed. And then build it.
I’ll update the repro steps.

Without testing, I would guess that the new scene has a different size and the hardcoded ray offset in
auto start = grid->worldToIndexF( ray( t0 + 1e-3) );
is not the right value for this scene to prevent self-intersection effects.

The original code doesn’t do that, but the confine() function is also using a hardcoded epsilon float eps = 1e-7f; to move start and end points into the bounding box. That would also be scene size dependent and in case your scene is bigger, might not be the correct value either.

Yeah, I was just seeing if offseting the start point would work or not. The results are the same in the orignal code (which confines both start and end points) nonetheless

OK, but is the scene bigger and have you experimented with the float eps = 1e-7f; as well?

Yes, I have tried changing the values of eps. The more I increase it, the noisier the rendering gets but the artifacts are still visible. Making it smaller has no noticeable differences.
The volume dimensions are surely bigger than the smoke volume provided with the sample.
I am not so sure about the scene as I am fairly new to OptiX.

After alot tinkering around, I’ve finally figured out the solution.
The problem was how the example code was accessing the volume coordinates in the raymarching loop.

In the function template<typename AccT> inline __device__ float transmittanceHDDA(const nanovdb::Vec3f& start, const nanovdb::Vec3f& end, AccT& acc, const float opacity ):

It used ijk = hdda.voxel() to get the current coordinate. This was the main root of the problem.
Replacing it to ijk = nanovdb::RoundDown<nanovdb::Coord>(ray(hdda.time() + 1.0001f)); solved this issue.

The rest of the confine logic worked fine without any modifactions.
I request the team to fix the sample code to avoid further confusions.