Best practice when creating shaders for mutliple object types, materials, rays

Hi all, I’d like my OptiX renderer to accept different types of objects (mesh, implicit sphere,…) and materials (metal, dielectric,…). What is the best practice for creating shaders? Should I implement CH program for each combination of object type and material (and probably ray type)? Or is it better to have one CH per object type and choose within the shader different paths depending on the material? Or the other way round?

Assuming OptiX 7 API:
The hit record consists of the intersection, anyhit and closesthit programs.
Means for a triangle mesh and implicit sphere primitives (or any other custom primitive type, and built-in curve primitives) you would need separate hit records inside the shader binding table (SBT) since the intersection program is different.
That also implies that you cannot have different geometric primitives inside one geometry acceleration structure (GAS).

Depending on the shading you’re intending to implement, the respective hit programs could be shared, because the optixGetHitKind() function allows to determine which primitive type has been intersected dynamically and then calculate the final vertex attributes from the respective intersection attribute registers accordingly inside the hit programs.

If the shading is very different, like a hair shader which would specifically require curve primitives to work, then it’s also fine if you program dedicated hit programs for each primitive and assign the matching programs inside the hit record for that geometry individually.

So each geometric primitive type needs its own hit record due the intersection program, but if you want to share the anyhit and closest hit programs among different primitive types, use the optixGetHitKind() information to determine how to calculate the required vertex attributes for the shading.

It’s also possible to handle all material types with a single closest hit program by using direct callable programs, as I’m showing in my OptiX 7 example programs (links in the sticky posts), but runtime performance is going to be better if implementing the material behavior in separate closest hit programs without callable programs. (OK, depending on the number of light types, direct callables for the light sampling would still be beneficial for code size.)

It’s basically a matter of reusing or duplicating code for different geometric primitives. Your choice.

I would recommend making the closest hit programs as small as possible. Convoluted über-shaders might be slower because they could require more local data and registers. Depends in the use case.

Reduce the number of optixTrace calls inside the code to a minimum (my renderers have only two calls, one inside the raygen, one inside the closesthit for shadows.). Do not nest them deep inside conditionals when possible.

For ray types, you would usually not need more than two in a renderer one for radiance and one for visibility tests.
If the scene consists of only solid objects and no cutout opacity, you don’t even need anyhit and closest hit programs for that because you can implement a binary visibility test with a specific ray flag and a miss program.
Explained here: https://forums.developer.nvidia.com/t/anyhit-program-as-shadow-ray-with-optix-7-2/181312/2

If you think of adding specific ray types just for picking, for example, I wouldn’t do that but add a special case into the radiance ray’s closest hit program to only return the necessary hit information and flag that picking mode via a launch parameter. Mind that only 16 different ray types are possible in an SBT because the ray’s sbtStride and sbtOffset values are only 4 bit wide.

There are also different ways to architect the SBT, esp. when using one top-level instance acceleration structure (IAS).
You could for example have an SBT entry per instance, that would mean the instance field sbtOffset determines the hit record and the instanceId field is free for other uses. You can hold additional information per instance in custom SBT data you can access with optixGetSbtDataPointer() inside the hit programs. (I’m using that in my examples.)
Or you could have one SBT entry per material per geometric primitive type and the instance sbtOffset would again pick the primitive type and material hit record inside the SBT, but the instanceId field selects the additional instance information like the mesh data and material parameters. The latter would be the smaller SBT.

2 Likes

thanks for the answer!

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.