Generation and propagation

The below provides annotated extracts (a readers digest) of crucial parts of some of the NVIDIA OptiX programs used by Opticks. The intention is to show the broad outlines of how the programs are used to implement the optical photon simulation. For details consult the sources.

NVIDIA OptiX programs

RG : Ray Generation
entry point into the ray tracing pipeline, invoked by the system in parallel for each user-defined work assignment
EX : Exception
invoked for conditions such as stack overflow and other errors
CH : Closest hit
Called when a traced ray finds the closest intersection point, such as for material shading
AH : Any hit
Called when a traced ray finds a new, potentially closest, intersection point, such as for shadow computation not used by Opticks, disfavored for performance reasons
IN : Intersection
Implements a ray-primitive intersection test, invoked during traversal
BB : Bounding box
Computes a primitive’s world space bounding box, called when the system builds a new acceleration structure over the geometry
MI : Miss
Called when a traced ray misses all scene geometry
AT : Attribute
Called after intersection with a built-in triangle. Used to provide triangle-specific attributes to the any-hit and closest-hit program. not used by Opticks

Ray Generation

optixrap/cu/generate.cu:

412 RT_PROGRAM void generate()
413 {

///
///   Associate photon with its genstep, via seed buffer
///

414     unsigned long long photon_id = launch_index.x ;
421     unsigned int genstep_id = seed_buffer[photon_id] ;
427     unsigned int genstep_offset = genstep_id*GNUMQUAD ;
428
429     union quad ghead ;           // union f:float4/i:int4/u:uint4
430     ghead.f = genstep_buffer[genstep_offset+0];
431     int gencode = ghead.i.x ;   // integer bits from float buffer
...
443     curandState rng = rng_states[photon_id];
444
445     State s ;
446     Photon p ;
449

///
///  Load CerenkovStep or ScintillationStep param from genstep buffer and generates a photon
///

450     if(gencode == CERENKOV)
451     {
452         CerenkovStep cs ;
453         csload(cs, genstep_buffer, genstep_offset, genstep_id);
457         generate_cerenkov_photon(p, cs, rng );
458         s.flag = CERENKOV ;
459     }
460     else if(gencode == SCINTILLATION)
461     {
462         ScintillationStep ss ;
463         ssload(ss, genstep_buffer, genstep_offset, genstep_id);
467         generate_scintillation_photon(p, ss, rng );
468         s.flag = SCINTILLATION ;
469     }
470     else if(gencode == TORCH)
...
480     else if(gencode == EMITSOURCE)
...

///
///  Bounce loop : propagating around geometry
///

514     int bounce = 0 ;
...
544     PerRayData_propagate prd ;
545
546     while( bounce < bounce_max )
547     {
552         bounce++;   // increment at head, not tail, as CONTINUE skips the tail
553
554         // closest hit program sets these, see material1_propagate.cu:closest_hit_propagate
555         prd.distance_to_boundary = -1.f ;
558         prd.identity.z = 0 ; // boundaryIndex, 0-based
560         prd.boundary = 0 ;   // signed, 1-based
561
564         rtTrace(top_object, optix::make_Ray(p.position, p.direction, propagate_ray_type, propagate_epsilon, RT_DEFAULT_MAX), prd );


///         Closest hit program (material1_propagate.cu:closest_hit_propagate) invoked by the ray trace
///         communicates back here to the ray generation program via the prd (PerRayData_propagate).


565
566         if(prd.boundary == 0)
567         {
568             s.flag = MISS ;  // overwrite CERENKOV/SCINTILLATION for the no hitters
574             break ;
575         }

///         fill_state
///              uses the boundary index to lookup wavelength dependent material and surface properties
///              (eg scattering_length, absorption_length, reemission_prob, reflectivity) from the boundary texture.
///
///         NB the above rtTrace is the only geometry query :
///         this works as all properties necessary for the propagation
///         are "hung" on the boundaries.
///

579         fill_state(s, prd.boundary, prd.identity, p.wavelength );
580
581         s.distance_to_boundary = prd.distance_to_boundary ;
582         s.surface_normal = prd.surface_normal ;
583         s.cos_theta = prd.cos_theta ;
...
607         command = propagate_to_boundary( p, s, rng );
608         if(command == BREAK)    break ;           // BULK_ABSORB
609         if(command == CONTINUE) continue ;        // BULK_REEMIT/BULK_SCATTER
610         // PASS : survivors will go on to pick up one of the below flags,
611
612

///
///         s.optical.x > 0 indicates there are surface properties (eg detect "EFFICIENCY")
///         for this boundary
///
613         if(s.optical.x > 0 )       // x/y/z/w:index/type/finish/value
614         {
615             command = propagate_at_surface(p, s, rng);
616             if(command == BREAK)    break ;       // SURFACE_DETECT/SURFACE_ABSORB
617             if(command == CONTINUE) continue ;    // SURFACE_DREFLECT/SURFACE_SREFLECT
618         }
619         else
620         {
622             propagate_at_boundary_geant4_style(p, s, rng);     // BOUNDARY_RELECT/BOUNDARY_TRANSMIT
624         }
625
626     }   // bounce < bounce_max

optixrap/cu/propagate.h:

///
///   Choosing history is simple when only a few possibilites.
///   The ray trace to find closest boundary is done at every step in order
///   to get the current material/surface properties in this material m1 and
///   the next material m2.
///


078 __device__ int propagate_to_boundary( Photon& p, State& s, curandState &rng)
...
112     float scattering_distance = -s.material1.z*logf(curand_uniform(&rng));   // .z:scattering_length
113     float absorption_distance = -s.material1.y*logf(curand_uniform(&rng));   // .y:absorption_length
...
123     if (absorption_distance <= scattering_distance)
124     {
125         if (absorption_distance <= s.distance_to_boundary)
126         {
127             p.time += absorption_distance/speed ;
128             p.position += absorption_distance*p.direction;
129
130             const float& reemission_prob = s.material1.w ;
131             float u_reemit = reemission_prob == 0.f ? 2.f : curand_uniform(&rng);  // avoid consumption at absorption when not scintillator
132
133             if (u_reemit < reemission_prob)
134             {
135                 // no materialIndex input to reemission_lookup as both scintillators share same CDF
136                 // non-scintillators have zero reemission_prob
137                 p.wavelength = reemission_lookup(curand_uniform(&rng));
138                 p.direction = uniform_sphere(&rng);
139                 p.polarization = normalize(cross(uniform_sphere(&rng), p.direction));
140                 p.flags.i.x = 0 ;   // no-boundary-yet for new direction
141
142                 s.flag = BULK_REEMIT ;
143                 return CONTINUE;
144             }
145             else
146             {
147                 s.flag = BULK_ABSORB ;
148                 return BREAK;
149             }
150         }
151         //  otherwise sail to boundary
152     }
153     else
154     {
...

Boundary assignment during X4PhysicalVolume::convertStructure

Boundaries are central to the Opticks geometry model which is boundary based, unlike Geant4 which is volume based. Boundaries are formed during the X4PhysicalVolume::convertStructure recursive traversal from a physical volume PV and its parent PV.

A GBnd instance holds four indices (omat, osur, isur, imat) representing.

  • outer material
  • outer surface
  • inner surface
  • inner material

Adding GBnd to GBndLib only returns a new boundary index if that GBnd has not been added previously. All structural volumes (GVolume) have a boundary index assigned, and this boundary is passed through to the GPU geometry via the identityBuffer.

The upshot is that at any ray trace intersect the boundary index is retrieved which allows rapid access to material and surface properties via lookups on the 2d boundary texture, the dimensions being the wavelength and the boundary index.

0880 void X4PhysicalVolume::convertStructure()
 881 {
 882     LOG(info) << "[ creating large tree of GVolume instances" ;
 ...
 889     const G4VPhysicalVolume* pv = m_top ;
 890     GVolume* parent = NULL ;
 891     const G4VPhysicalVolume* parent_pv = NULL ;
 892     int depth = 0 ;
 893     bool recursive_select = false ;
 ...
 898     m_root = convertStructure_r(pv, parent, depth, parent_pv, recursive_select );
 899


 954 GVolume* X4PhysicalVolume::convertStructure_r(const G4VPhysicalVolume* const pv, GVolume* parent, int depth, const G4VPhysicalVolume* const parent_pv, bool& recursive_select )
 955 {
 ...
 960      GVolume* volume = convertNode(pv, parent, depth, parent_pv, recursive_select );
 961
 967      m_ggeo->add(volume); // collect in nodelib
 968
 969      const G4LogicalVolume* const lv = pv->GetLogicalVolume();
 970
 971      for (int i=0 ; i < lv->GetNoDaughters() ;i++ )
 972      {
 973          const G4VPhysicalVolume* const child_pv = lv->GetDaughter(i);
 974          convertStructure_r(child_pv, volume, depth+1, pv, recursive_select );
 975      }
 976
 977      return volume   ;
 978 }


1151 GVolume* X4PhysicalVolume::convertNode(const G4VPhysicalVolume* const pv, GVolume* parent, int depth, const G4VPhysicalVolume* const pv_p, bool& recursive_select )
1152 {
....
1159     unsigned boundary = addBoundary( pv, pv_p );
...
1292     GVolume* volume = new GVolume(ndIdx, gtransform, mesh );
...
1305     volume->setBoundary( boundary );
1309
1310     volume->setLocalTransform(ltriple);
1311     volume->setGlobalTransform(gtriple);
....
1320     volume->setPVName( pvName.c_str() );
1321     volume->setLVName( lvName.c_str() );
....
1326     if(parent)
1327     {
1328          parent->addChild(volume);
1329          volume->setParent(parent);
1330     }
...
1339     return volume ;
1340 }



0989 unsigned X4PhysicalVolume::addBoundary(const G4VPhysicalVolume* const pv, const G4VPhysicalVolume* const pv_p )
 990 {
 991     const G4LogicalVolume* const lv   = pv->GetLogicalVolume() ;
 992     const G4LogicalVolume* const lv_p = pv_p ? pv_p->GetLogicalVolume() : NULL ;
 993
 994     const G4Material* const imat_ = lv->GetMaterial() ;
 995     const G4Material* const omat_ = lv_p ? lv_p->GetMaterial() : imat_ ;  // top omat -> imat
 996
 997     const char* omat = X4::BaseName(omat_) ;
 998     const char* imat = X4::BaseName(imat_) ;
 ...
1002     // look for a border surface defined between this and the parent volume, in either direction
1003     bool first_priority = true ;
1004     const G4LogicalSurface* const isur_ = findSurface( pv  , pv_p , first_priority );
1005     const G4LogicalSurface* const osur_ = findSurface( pv_p, pv   , first_priority );
...
1088     unsigned boundary = 0 ;
1089     if( g_sslv == NULL && g_sslv_p == NULL  )   // no skin surface on this or parent volume, just use bordersurface if there are any
1090     {
1091         const char* osur = X4::BaseName( osur_ );
1092         const char* isur = X4::BaseName( isur_ );
1093         boundary = m_blib->addBoundary( omat, osur, isur, imat );
1094     }
....
1112     return boundary ;

/// m_blib (ggeo/GBndLib)
///     GBndLib::addBoundary
///          looks up indices of material and surfaces from the names,
///          and stores 4 integers (omat,osur,isur,imat) returning a boundary
///          index for each unique quadruplet of indices
///

Intersection

Calls to rtTrace traverse the BVH acceleration structure to find bounding boxes that are intersected by the ray. For the closest of these the intersection

Closest Hit

optixrap/cu/material1_propagate.cu:

20 #include <optix.h>
21 #include "PerRayData_propagate.h"
22 #include "wavelength_lookup.h"
23
24 //attributes set by TriangleMesh.cu:mesh_intersect
25
26 rtDeclareVariable(float3,  geometricNormal, attribute geometric_normal, );
27 rtDeclareVariable(uint4,  instanceIdentity, attribute instance_identity, );
28
29 rtDeclareVariable(PerRayData_propagate, prd, rtPayload, );
30 rtDeclareVariable(optix::Ray,           ray, rtCurrentRay, );
31 rtDeclareVariable(float,                  t, rtIntersectionDistance, );
32
33
34 RT_PROGRAM void closest_hit_propagate()
35 {
36      const float3 n = normalize(rtTransformNormal(RT_OBJECT_TO_WORLD, geometricNormal)) ;
37      float cos_theta = dot(n,ray.direction);
38
39      prd.cos_theta = cos_theta ;
40      prd.distance_to_boundary = t ;   // huh: there is an standard attrib for this
41      unsigned int boundaryIndex = instanceIdentity.z ;
42      prd.boundary = cos_theta < 0.f ? -(boundaryIndex + 1) : boundaryIndex + 1 ;
43      prd.identity = instanceIdentity ;
44      prd.surface_normal = cos_theta > 0.f ? -n : n ;
45 }
46
47 // prd.boundary
48 //    * 1-based index with cos_theta signing, 0 means miss
49 //    * sign identifies which of inner/outer-material is material1/material2
50 //    * by virtue of zero initialization, a miss leaves prd.boundary at zero
51 //
52 //  cos_theta > 0.f
53 //        outward going photons, with p.direction in same hemi as the geometry normal
54 //
55 //  cos_theta < 0.f
56 //        inward going photons, with p.direction in opposite hemi to geometry normal
57 //
58 // surface_normal oriented to point from material2 back into material1
59 //

optixrap/OGeo.cc:

506 optix::Material OGeo::makeMaterial()
507 {
...
513     optix::Material material = m_context->createMaterial();
514     material->setClosestHitProgram(OContext::e_radiance_ray, m_ocontext->createProgram("material1_radiance.cu", "closest_hit_radiance"));
515     material->setClosestHitProgram(OContext::e_propagate_ray, m_ocontext->createProgram("material1_propagate.cu", "closest_hit_propagate"));
516     return material ;
517 }

Opticks uses only a single optix::Material, that is associated to the closest hit program in OGeo::makeMaterial. Renderers typically use optix::Material is to “shade” the appearance of different geometry depending on material type, eg wood, metal, plastic, etc..

As Opticks needs only the distance to the intersection and surface normal at the intersection, there is no need for multiple optix::Material. The different properties of materials and surfaces are carried in the boundary index.