Index each gltf imported triangle with a unique primitiveID

I want to import my geometry from a GLTF file and have OptiX assign each primitive a unique ID.

I am starting from the optixMeshviewer example and am using the tinygltfloader by syoyo.
I proceeded as follows: I export a scene from blender as GLTF (in my case just two cubes) and then import them in OptiX just as done in the example. Since GLTF reuses the same mesh in the two nodes (am I understanding this correctly) the triangles of the two cubes share the same primitive IDs.
More specifically, My hitcounter buffer only registres hits in the first 12 entries. The primitives 12-23, which should correspond to the second cube, stay at 0 hits even if I move around the camera.

I tried to work around this by adding the two cubes to the same mesh, but still they share the same IDs as if they were two separate GLTF primitives.

Now I was wondering if this an exporting problem, or if I have to modify the importing routine. In the documentation I did find the OptixBuildInputTriangleArray Struct with the primitiveIndexOffset value. In the code I couldn’t find a reference to this struct, but I think if I can pass the number of previously imported triangles to this struct, it should start incrementally indexing the triangles of the next processed node/GLTF primitive.

Could you point me in the right direction where I have to change the code, as right now I am completely clueless where to start. Your help is very much appreciated!

Hi martinhwehr,

To make sure I understand – is the problem that you can see two cubes, but your closest hit shader gives you back only indexes 0-11? Or are you only seeing a single cube?

The primitiveIndexOffset is described in the programming guide here: https://raytracing-docs.nvidia.com/optix7/guide/index.html#acceleration_structures#5092

I think the most relevant tidbit is the sentence “The primitiveIndexOffset is only used when reporting the intersection primitive.” This means that primitiveIndexOffset can’t be used as a way to adjust your input data’s indexing, but it can be used to identify which mesh you hit.

The way to make use of primitiveIndexOffset is when you have multiple meshes in your GAS, and you want the primitive ID that you receive in your shader to be able to uniquely identify individual triangles across multiple meshes. You can use the offset primitive ID to lookup primvars in your own down without needing an indirection, assuming you’ve packed the primvars for multiple meshes in a single buffer sequentially. Does that make sense?

Are you using one build input for both meshes, or two different build inputs? If you used one build input with two meshes in it that are described by a single vertex buffer and a single index buffer, then the vertex indexing for 2 cubes needs to go from 0-23.

If you use two build inputs with one mesh each, and you pass two separate pairs of vertex and index buffers, then the index buffers should each go from 0-11. (Note you can use two build inputs but still cudaMalloc only one index buffer, by passing pointers to the interior of the vertex & index buffers.) If you want to know whether you hit triangles in mesh #2, you could use the primitiveIndexOffset feature to make sure you get triangle IDs 12-23 in your hit shaders.

If the problem is that you can’t even see both cubes, then the issue is more likely related to your SBT. In that case, I would suspect two build inputs are used, but the two SBT entries are pointing at the first cube’s data only.


David.

Hi David!

Thank for your thorough response.
No I am able to see both Cubes in all cases I tried out. I never messed with the gltfloader apart from it adding me up the number of triangles it loaded in total.

In my approach I wanted to be able to use optixGetPrimitiveIndex() inside my ch() programm. I then assumed that each triangle (i am not using the term primitive as it means something else in GLTF files) would get assigned a unique ID by OptiX during the loading of the scene. From that ID I would write to a float buffer with an entry for each unique triangle and thus can read out the hit statistics.
It seems this is not how OptiX works?

I maybe didn’t describe it clear enough, I didn’t mess with the loader at all, I just though that I could work around this problem by putting all the triangles into one mesh with one gltf primitive, which should work for my case, I guess?

A more robust method would be to use the primitiveIndexOffset with the build inputs. Did I understand you correctly, that even if I use the offset my second cube would also be indexed starting from 0, but somehow I can distinguish it from the first cube in my ch()?
This is where I am struggling right now.

I am currently not at my work machine, but I will definitely check this out tomorrow. I can also upload the gltf files I am using, which might help you to understand my problems?

Thank you very much for your help again!

Martin

Ah, so it sounds like this all depends on exactly what it means to put all the triangles into one mesh with one gltf primitive. I don’t know how GLTF does that, but if you get an index buffer out of GLTF that has indices in the range 0-11, and the triangles appear to be repeated twice, it means you’ll need to either fix the indices before passing them to OptiX, or use multiple build inputs if you don’t want to translate the data.

To clarify, primitiveIndexOffset is a number that affects what optixGetPrimitiveIndex() returns. If you have two build inputs with cubes, and they both have an index buffer with values in the range 0-11, you can use a primitiveIndexOffset value of 0 for the first mesh, and 12 for the second mesh. That way, in your CH shader, optixGetPrimitiveIndex() will return 0-11 when hitting triangles on the first mesh, and 12-23 when hitting triangles on the second mesh.


David.

Yeah I already asked on stack overflow if and how this can be achieved when exporting from blender.
By multiple build inputs you mean, that I load several files into the same scene?

Okay, so primitiveIndexOffset works the way I thought. Today I wasn’t really sure where and how the information from the gltfloader get passed to OptiX in the meshviewer example.
Are you familiar with way it’s handled in that example and could point me were i should start looking`in the code?

Thank you very much again!

Martin

If there is only one mesh with a single cube inside your glTF but two are displayed, that would mean that cube mesh is instanced twice.
To verify that, you can build the OptiX SDK examples as debug and put a break-point onto Scene::buildInstanceAccel() when running your scene.

In that case there will only be 12 triangles as geometry and therefore 12 primitive indices inside a single geometry acceleration structure (GAS) and an instance acceleration structure (IAS) on top with two different transforms placing the cubes in the world.

Can also be that each mesh is actually disjunct and still placed under a transform with an OptixInstance. In that case there will also be only the primitive indices 0-11 but on different meshes, which you can still distinguish by looking at the instance ID or index.

Primitive index offset wouldn’t apply in either case.

With OptiX 7 it’s easy to distinguish the two instances though.
You have a user defined OptixInstance::instanceId and the instance child index at the bottom-most IAS.
With a two-level hierarchy the child index is unique.
Now inside the device code you can query either using the functions optixGetInstanceId and optixGetInstanceIndex.
https://raytracing-docs.nvidia.com/optix7/guide/index.html#device_side_functions#device-side-functions

Using that information you can have as many instanced meshes and still be able to identify which primitive ID in which mesh you actually hit.

If you still want to flatten any scene to a single GAS to get unique primitive IDs, you would need to change the glTF loader to pre-transform the matrices onto the vertex attributes to get everything into world coordinates and change the scene generation to not use instancing.

Thanks for your answer!

When I combined the two cubes to one mesh and gave them the same material and reexported it from blender and it worked! It’s still a workaround, but I guess it’s good enough for right now.

I am using Optix 7. So I will look into the optixGetInstanceIndex. I still don’t quite get what this function returns. Is the Instance ID just a node in a GAS? Is this identifier unique to each mesh, or is it just a bounding box around some near (OptiX) primitives?

If I want to be able the refer to the material of the primitive in the ch() program to determine parameters such as reflectivity, can I pass the material from the gltf file to OptiX, or would it be smarter to have an external referencefile in which look up my values from inside the ch()?

I am still not that familiar with the whole acceleration structure building. Just to see if I understand you correctly: Before I pass the data to OptiX I would have to “manually” add all the triangles into one mesh using world coordinates and then use the modified mesh to build my scene in OptiX?
Does the Acceleration structure building rely on the gltf input (which, if I didn’t misunderstand it, already features some kind of bounding boxes/acceleration structures?) or will OptiX build acceleration structures for the triangles anyway independent of the input?

Please read the whole OptiX 7 Programming Guide and look into the API Reference for individual functions.
https://raytracing-docs.nvidia.com/
https://raytracing-docs.nvidia.com/optix7/guide/index.html#introduction#

The OptiX 7 render graph setup with the different acceleration structure nodes (there are only two) is explained with diagrams here:
https://raytracing-docs.nvidia.com/optix7/guide/index.html#basic_concepts_and_definitions#acceleration-structures

optixGetInstanceId: https://raytracing-docs.nvidia.com/optix7/api/html/group__optix__device__api.html#ga4d70272186506b1a5cb4813f1047021f
optixGetInstanceIndex: https://raytracing-docs.nvidia.com/optix7/api/html/group__optix__device__api.html#gab0c73e334441d76fc2bbcf4c4cc4184c

Not sure I understood that. You cannot access files from device programs. All data must reside in memory the device can access.
The glTF loader inside the utility scene functions of the OptiX SDK examples already handles materials defined inside the glTF file, but you’re free to pass any parameter to the hit programs you like.

The usual way to communicate data to specific programs is via the Shader Binding Table (SBT) record which effectively defines which program is executed on what ray tracing event (ray generation, any hit, closest hit, miss, exception).
Understanding the Shader Binding Table (SBT) is absolutely crucial when designing applications with OptiX 7, to be able to control which data is available to what program.
You must master this part: https://raytracing-docs.nvidia.com/optix7/guide/index.html#shader_binding_table#shader-binding-table
It looks more difficult than it is. The flexibility is quite astounding. There are multiple different ways to setup a SBT to achieve the same result and it depends on the application’s requirements what fits best.

Not in general.
This would only be required if you want to have all triangles uniquely defined inside a single geometry acceleration structure (GAS) with no instancing (aka. a completely flattened scene hierarchy.)

This is not always feasible because there is a limit of 2^29 primitives in one GAS and you will most likely run out of VRAM before that.
If you can instance identical geometry (means reusing the same GAS traversable handle in multiple OptixInstance children of an IAS) then it’s highly recommended to do that to save on AS build time and VRAM.
The instance transform in a two-level hierarchy is basically free on RTX boards because that is handled in hardware.

OptiX doesn’t know about any scene file formats.
You are responsible to provide the data for the acceleration build.
Your input defines how OptiX builds the acceleration structure.

There are two acceleration build inputs in OptiX 7.0.: For triangle primitives and for custom primitives.
The first gets triangle data, the second gets axis aligned bounding box (AABB) data you need to calculate on your own custom primitives. The custom primitives require a developer-defined intersection program, the triangles use a built-in one, on RTX board that’s in hardware.

Explained in more detail here: https://raytracing-docs.nvidia.com/optix7/guide/index.html#acceleration_structures#acceleration-structures

Thank you for your in-depth answer.

The set up of the SBT and even what it’s supposed to do in OptiX still remains a mystery to me. I will work again thoroughly through the documentation and try to get to the bottom of this.

Is there (exept for the memory usage) any downside to using a flat GAS? I feel like it would be easier for an inexperienced programmer like me, to avoid to have to dwell to deep into the complete AS building and hierarchy. We likely won’t be even coming close to the 2^29 limit with what we are trying to do.