Solving Self-Intersection Artifacts in DirectX Raytracing

Originally published at:

Learn an efficient and robust method for avoiding self-intersections for the DXR ray tracing API.

Is this method suitable for transparent object?
I have implemented a bias method originnated from UE4, which involves offsetting the ray origin along the ray direction. The code snippet below illustrates this, with MinBias set to 5e-6 and MaxBias set to 1e-5. This method generally produces satisfactory results in most cases.
However, when I attempted to apply the method in this blog to a transparent object, I encountered an issue. The resulting image exhibited shadow acne that was not present when using the previous UE method, as shown in the image below.

// WorldNormal is the vector towards which the ray position will be offseted.
void ApplyPositionBias(inout float3 RayOrigin, float3 RayDirection, const float3 WorldNormal, const float MaxNormalBias)
	// Apply normal perturbation when defining ray to:
	// * avoid self intersection with current underlying triangle
	// * hide mismatch between shading surface & geometric surface
	// While using shading normal is not correct (we should use the 
	// geometry normal, but it is not available atm/too costly to compute), 
	// it is good enough for a cheap solution
	const float MinBias = 5e-6f;
	const float MaxBias = max(MinBias, MaxNormalBias);
	const float NormalBias = lerp(MaxBias, MinBias, saturate(dot(WorldNormal, RayDirection)));

	RayOrigin += RayDirection * NormalBias;

new method:
UE Method:

In my implementation of the new method, I used the calculated ‘safeOffset’ to bias the ray origin. I am uncertain if I have correctly applied the method. My w2o matrix is just the inverse of o2w matrix, will this operation introduce extra error? In my test, I just need to scale the safeOffset by 1.1 to avoid this shadow acne.

  mat4x3 o2w = mat4x3(transMatrix);
  mat4 inverseTrans = inverse(transMatrix);
  mat4x3 w2o = mat4x3(inverseTrans);

  vec3 objPosition, wldPosition, objNormal, wldNormal;
  float wldOffset;
  safeSpawnOffset(objPosition, wldPosition, objNormal, wldNormal, wldOffset, v0.Position, 
                  v1.Position, v2.Position, hitResult.barycentrics, o2w, w2o);
  position = wldPosition;
  gBuffer.safeOffset = wldOffset;
  gBuffer.Normal = wldNormal;
  ray = CreateRay(gBuffer.WorldPos, OutDir, 0, DEFAULT_TMAX);
  float safeOffset = NotSameSide ? -gBuffer.safeOffset : gBuffer.safeOffset;
  ray.origin = safeSpawnPoint(ray.origin, gBuffer.Normal, safeOffset); 

The computation of the inverse may indeed add extra rounding errors. The error bounds in the blog post assume that the matrix inverse has at most 1 ulp of error in each element. When done in double precision this is achievable for most ‘reasonable’ transformation matrices, but when using single precision floats for the inversion (which is what likely happens here) the error may very well be larger. Does the issue also appear when you query the w2o and o2w matrices using intrinsics? That said, the computed safe offset will be quite tight so adding a small margin of error like you did shouldn’t be a problem.

Note that if the ray direction is unaffected, for example when passing through cutouts or transparent geometry with equal IOR, an alternative stable approach is to leave the ray origin and direction as is and only advance the tmin of the ray to the current hit t of the transparent surface. This will also skip the current surface, is very cheap and very accurate. This however only works if the direction is not altered by the transparent surface interaction.