Embree: Performance issue when ray casting User Defined Geometry

I am working on the integration of Intel's Embree library into our home renderer to accelerate its rendering time. Our renderer supports various geometry types, such as quads, triangles, but also spheres, hyperbolas and tori.

For ray casting triangles, triangle meshes and quads I am using Embree's build-in triangle- and quad-primitives. However, for the more complicated geometries I am using what's called User Defined Geometries (one passes functions for calculating bounding boxes, for intersection calculation and occlusion testing to Embree).

For scenes with triangles, triangle meshes and quads, Embree really is faster (about twice as fast). My problem is that for scenes with User Defined Geometries, Embree takes about the same amount of time and often is even slower (if only for seconds).

As an example, I have this scene:

Here is the output of the renderer when rendering this scene with 200 spp and without Embree support:

sebastian@sebastian-N551JX ~/ART_Gallery/BRDF_Showcase $artist OrenNayarSpheres.arm -DSAMPLES=200 -o normal200 artist - photorealistic renderer ART version 2.0.4-dev, 8 cores detected (c) 1996-2020 by the ART development team reading scene description 0.82 sec optimising scene graph for raycasting 0.00 sec identifying and analysing lightsources 0.89 sec --- interactive mode on, goal are 200 spp --- image sampling: path tracing, 200 spp 328.34 sec --- interactive mode off --- converting raw image to colour space 0.09 sec applying interactive calibration tone mapping operator 0.13 sec luminance and chroma reduction for L > 100.0 0.11 sec converting to 8 bpc sRGB TIFF 0.08 sec opening result image in external viewer  ... and here with Embree support: sebastian@sebastian-N551JX ~/ART_Gallery/BRDF_Showcase$ artist OrenNayarSpheres.arm -DSAMPLES=200 -o embree200 -e

artist - photorealistic renderer
ART version 2.0.4-dev, 8 cores detected
(c) 1996-2020 by the ART development team

embree support enabled
optimising scene graph for raycasting                               0.03 sec
identifying and analysing lightsources                              0.90 sec
---   interactive mode on, goal are 200 spp   ---
image sampling: path tracing, 200 spp                             322.82 sec
---   interactive mode off   ---
converting raw image to colour space                                0.09 sec
applying interactive calibration tone mapping operator              0.12 sec
luminance and chroma reduction for L > 100.0                        0.12 sec
converting to 8 bpc sRGB TIFF                                       0.08 sec
opening result image in external viewer


As you can see, the rendering time with Embree is technically faster, but only about 4 seconds. Now, I wouldn't call myself an experienced programmer. I followed the provided Embree "User Geometry Tutorial" as good as I could. What I am asking myself is, what can I do to make this faster.

I am quite aware of the fact that the information I am providing is probably too vague for a definite solution (and I am apologizing for that). My problem is that I don't know how to debug this issue and to determine, whether I am doing something wrong or if an improvement is not possible because of the internal workings of our renderer. The Renderer used (for everyone interested, its name is ART (Advanced Rendering Toolkit), it is Open Source and available here), does some things differently than other "mainstream" renderers.

What I did do far was counting the intersection function calls and I have the suspicion that the renderer does more intersection calculations with Embree than without.

Does anybody know any cases where Embree did not speed-up the rendering process? Does someone have any hints or tips for me, despite the little information I give?

Thank you very much in advance for your help and have a nice day!

PS: I am not sure if it helps, but here is my intersection calculation function that I pass to Embree as a callback function. The renderer itself is written in C and Objective-C and so is the intersect function.


// intersection callback function
void embree_intersect_geometry(const int * valid,
void * geometryUserPtr,
unsigned int geomID,
unsigned int instID,
struct RTCRay * rtc_ray,
struct RTCHit * rtc_hit)
{
if(!valid[0])
return;

// retreive raycaster and geometry data
ArnEmbree * embree = [ArnEmbree embreeManager];
ArnRayCaster * rayCaster = [embree getRayCasterFromRayCasterArray];
UserGeometryData * geometryData = (UserGeometryData *) geometryUserPtr;

// perform the intersection
ArIntersectionList intersectionList = ARINTERSECTIONLIST_EMPTY;
[geometryData->_combinedAttributes
getIntersectionList
: rayCaster
: RANGE( ARNRAYCASTER_EPSILON(rayCaster), MATH_HUGE_DOUBLE)
: &intersectionList
];

// if no intersection is found, return
return;

// if the prev intersection is closer, free intersection list and return
if(prevIsect && prevIsect->t < intersectionList.head->t) {
arintersectionlist_free_contents(&intersectionList,
ARNRAYCASTER_INTERSECTION_FREELIST(rayCaster));
return;
}

// update embree components
rtc_hit->geomID = geomID;
rtc_hit->primID = 0;

// since this function is called mutlple times in one go, clear out already existing
// intersection list, since we are only interested in nearest hit
arintersectionlist_free_contents(ARNRAYCASTER_EMBREE_INTERSECTIONLIST(rayCaster),
ARNRAYCASTER_INTERSECTION_FREELIST(rayCaster));

// set intersection list
*rayCaster->embreeIntersectionList = intersectionList;
}



Some explanation of the code above: The line "ArnEmbree * embree = [ArnEmbree embreeManager];" returns a singleton object with functionality concerning Embree. "ArnRayCaster * rayCaster" is an object that is used for traversing the scene graph. Since the renderer is multithreaded, one of such ArnRayCaster object is created for each thread. I am storing the relevant ArnRayCaster in an array and I am using the thread ID as the key. Finally, "UserGeometryData" is a struct that holds apart from the representation of the shape in memory, other information that is needed for the intersection calculation:

typedef struct UserGeometryData {
ArNode * _shape;
ArTraversalState _traversalState;
AraCombinedAttributes * _combinedAttributes;
BOOL _isUserGeometry;
}
UserGeometryData;


My best guess at why you are not seeing a speed up with your user geometry, is that your example does not contain much BVH traversal.

With traversing a triangle/quad mesh more time is spent traversing the BVH to decide which triangle you want to test than actually testing triangles. This is where embree really shines.

It looks like your user geometry is just one primitive, compared with a triangle mesh which can be divided into many primitives/triangles (which the BVH can reduce to just the primitives you need to test).

Considering the image you shared above, each ray will intersect the bounding box of your shape, and then call the same intersection function your non-embree version was calling. Comparing the two the non-embree version really isn't doing much more work.

You will probably get better speedups using embree if you try scattering hundreds of your shapes or if you can find a way to divide your work into primitives.

• Hi @Peter, thank you for your reply. It is very helpful. Indeed, my user geometries are single primitives. In the Paper "Embree: A Kernel Framework for Efficient CPU Ray Tracing", the authors say that Embree achieves high performance in complex scenes and (in average) our scenes are quite simple, speaking of primitives. Thanks also for the tip considering subdividing the geometries. I will try to find a way to do so. Have a nice day! May 18 at 9:07
• @sschimper A quick question. Do you know how much computation is needed for your procedural objects? Given that there is a chance of multiple intersections (>2) with a ray, I'm guessing it might be relatively expensive. Could you consider "logically" dicing your objects into smaller pieces, generate a smaller AABB around each piece and then reference your original object and/or include hints to reduce the test cost to be limited to just the portion inside the box? (FWIW This is based on a ray tracer I did about >3 decades ago that used a interval maths + newton iteration solver.) May 18 at 16:46
• Hi @SimonF, thanks for your comment. I experienced the issue you are describing when I was rendering scenes with environment lighting. Our renderer uses a kd-tree while Embree is making use of BVHs. Intersections with the infinite lighting sphere would be calculated for a pixel, even though it was occluded by another geometry. I solved this by only intersecting the infinite sphere when nothing else was hit. May 19 at 14:52
• @SimonF "Slicing up" my geometry into smaller parts would be possible I think, however, since some geometries are of implicit form, and to be honest, I don't know how to do it in a good way (I am still a student and far from being a professional). May 19 at 14:57