szellmann / visionaray

A C++-based, cross platform ray tracing library

Home Page:https://vis.uni-koeln.de/visionaray.html

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Handling Multiple BVHs

opened this issue · comments

Not really a bug or anything, but I was wondering what the best approach is for handling multiple BVHs. I'd like to continue to use builtin the Whitted/path tracing kernels, if that's possible. Sifting through the code, it seems like you could treat each BVH as a whole primitive and write an intersector for it. I'm a little unsure though and was hoping to get a suggestion.

In case it's relevant, the purpose of having multiple BVHs is to have one for static meshes and one for dynamic ones.

In case you know there will only ever be two BVHs then yea that's what I'd do. An alternative might be instancing. Here are two mini examples that show both. Note that in both cases I eventually build a list of bvh_ref objects as the default kernels expect lists, and as it's the recommended approach to pass ref objects around.

#include <visionaray/math/triangle.h>
#include <visionaray/bvh.h>

using namespace visionaray;

using Triangle = basic_triangle<3, float>;

template <typename BVHs>
void render(BVHs& refs) {}

int main()
{
    std::vector<Triangle> static_tris(10);
    std::vector<Triangle> dynamic_tris(10);

    index_bvh<Triangle> static_bvh;
    index_bvh<Triangle> dynamic_bvh;

    binned_sah_builder builder;
    builder.enable_spatial_splits(true);

    static_bvh = builder.build(
        index_bvh<Triangle>{},
        static_tris.data(),
        static_tris.size()
        );

    dynamic_bvh = builder.build(
        index_bvh<Triangle>{},
        dynamic_tris.data(),
        dynamic_tris.size()
        );

    for (;;)
    {
        // Assume tris have changed here..
        bvh_refitter refitter;
        refitter.refit(dynamic_bvh, dynamic_tris.data(), dynamic_tris.size());

        // With list of BVH refs
        aligned_vector<index_bvh<Triangle>::bvh_ref> refs;
        refs.push_back(static_bvh.ref());
        refs.push_back(dynamic_bvh.ref());
        render(refs);

        // With instancing
        aligned_vector<index_bvh<Triangle>::bvh_inst> insts;
        insts.push_back(static_bvh.inst(mat4::identity())); // doesn't need to be identity
        insts.push_back(dynamic_bvh.inst(mat4::identity()));
        builder.enable_spatial_splits(false); // instances and spatial splits doesn't work
        index_bvh<index_bvh<Triangle>::bvh_inst> tlas = builder.build(
            index_bvh<index_bvh<Triangle>::bvh_inst>{},
            insts.data(),
            insts.size()
            );
        // Again build a vector of refs as that's what the kernels expect
        aligned_vector<index_bvh<index_bvh<Triangle>::bvh_inst>::bvh_ref> inst_refs;
        inst_refs.push_back(tlas.ref());
    }
}

Note that I recently added a fast LBVH builder but that is only useful in CUDA mode. There you'd have to be careful to build the BVH on the GPU in the first place. There's some code that does this in viewer.cpp but I can also try to put this into an example that's a bit shorter.

Wondering if the above would be useful to be conserved somehow. Maybe in the wiki or as a standalone example (but with render() implemented and the triangles initialized).

Ah, I think I misunderstood the BVH refs. I thought at first those were node references, not entire BVH references. I may just use that instead since I wasn't planning on having just two BVHs (I just meant two types.)

More examples or more wiki entries are always nice! The ones that are there now have been pretty helpful.

I'll check out the LBVH builder. I'm not quite sure about that algorithm, though. I've come across a lot of broken LBVH algorithms and even the one I wrote a few months ago had terrible traversal performance. That's not to say anything about the one in this library, just that I tend to avoid them now.

The ref objects (bvh_ref, texture_ref, ...) are similar to array_ref; same interface as bvh or texture, but internally store pointers to some storage object (usually the one they were created from.

I think the LBVH builder is correct as I recently put quite a lot of time into that. It uses Karras' radix tree algorithm and I tried it on a bunch of objects and used it for a recent research paper project (where I didn't use it for ray tracing though).

LBVH uses the middle (a.k.a. "spatial median") heuristic, i.e. nodes are literally split in the middle, no SAH or stuff. That can be pretty bad. As a rule of thumb I saw traversal to be like 30% worse than binned SAH BVHs. Might perform much worse in special cases though (I guess).

I'm also currently trying to integrate PLOC by Meister and Bittner (as I need that for yet another project), I might need another week (or four) for that though :-)

A note on "BVHs are primitives": they are special kinds of primitives. You can pass lists of BVH refs to the closest_hit, any_hit and multi_hit functions and to the default kernels. They have special intersect() functions though, as when you traverse into the BVH you can also say that you want closest, any, or multiple hits. Also, they don't have prim_ids (which is actually a bit brittle internally unfortunately).

Using the LBVH builder would be as easy as exchanging binned_sah_builder with lbvh_builder (and you can't call enable_spatial_splits() on that). So might be easy to try. Construction on the CPU works but is single-threaded. Trees are the same though. So if traversal performance is ok for you, you could later switch to CUDA. Refitting is an option too (see the example above :-) )

Okay, thanks for all the help. This is a pretty awesome library. I'll close the issue because I think I've got the help I need.

Also, the only thing that I actually found broken with the other LBVH implementations is the handling of duplicate Morton codes. So it's easy to test using models with more than 2^11 primitives.

Thank you :-)

I may just use that instead since I wasn't planning on having just two BVHs (I just meant two types.)

Oh one quick note on the instancing approach from above (but probably no real news to you): instancing is used here to implement a two level BVH, i.e., you don't have to use it like real instancing. It's more about the TLAS being a BVH, whereas in the first case you basically have a linear list as TLAS, which might impact performance. Yet idk where the sweet spot would be, I guess you can have a list with tens of BVHs and would still perform ok.

Actually I didn't quite realize until just now that a two-level BVH was being made. That's really cool. I'll use this approach, as opposed to the linear list. Thanks for pointing that out!