Ray-traced procedural geometries in HDRP

Hi,

I have been playing around with HDRP raytracing features, and wanted to include a ray-traced procedural geometry in the scene.

I think the way to go is to implement an “intersection” shader (Intersection Shader - Win32 apps | Microsoft Learn).

I have found few elements about this:

  1. On the “Mesh Renderer” component, there is a checkbox “Ray Trace Procedurally” which seem to serve this exact purpose:
    7405772--905342--upload_2021-8-10_18-36-2.png
  2. The Unity documentation mentions the “intersection” shaders with a small example, which makes me think the engine supports them:
    Unity - Scripting API: Experimental.Rendering.RayTracingShader.SetShaderPass
    Unity - Scripting API: Rendering.CommandBuffer.SetRayTracingShaderPass

However, I couldn’t figure out how to actually make it work (I naively tried to include such an “intersection” shader in the material shader while checking the box, without success).

Does someone know if this is actually supported, and how to do it?

Hi again community,

I actually was on the right trails but had made some mistakes on the way.

For those who might be interested, here is how I included ray-traced procedural geometries in the HDRP, so that those procedural geometries correctly interact with other ray-traced geometries (reflexion, shadows, …):

Unity editor preparation steps:
0) Configure the project for HDRP with ray-tracing enabled
1) Create a 3D object, for example a cube.
2) Create a material using the HDRP/lit shader, and assign the material to the object.
3) On the Material, check the “Recursive rendering” box so that this geometry is rendered through ray-tracing.
4) On the Mesh Renderer, check the box “Ray Trace Procedurally”. Unity will automatically configure the corresponding Acceleration Structure to leverage its “intersection” shader instead of the mesh triangles. The object will disappear for now since we haven’t specify the “intersection” shader yet.

Shader steps:
5) We need to modify the shader: create a new shader which is a copy of the HDRP/lit shader, rename it, and assign it to the material.
6 - quick and dirty) To check the configuration so far or just validate it can work, add below code at the end of the HLSL program of the “ForwardDXR” pass.

   [shader("intersection")]
    void IntersectionMain()
    {
        float3 or = ObjectRayOrigin();
        float3 dir = normalize(ObjectRayDirection());
        AttributeData attr;
        attr.barycentrics = float2(0, 0);
        float r = 0.35;
        float3 toCenter = float3(0., 0., 0.)-ObjectRayOrigin();
        float dirCos = dot(normalize(toCenter), dir);
        float minCos = cos(asin(r / length(toCenter)));
        if (dirCos > minCos) {
            ReportHit(1, 0, attr);
        }
    }

A sphere should be displayed in the center of the object, and it should be reflected by other ray-traced objects. However, since the shader expects a ray / triangle intersection, the shade is messed up (normals in particular are completely wrong).
6 - alternative “cleaner” way) Actually only the intersection code of the shader should be updated, so create a .hlsl file and replace below “include” line by your new file:
#include "Packages/com.unity.render-pipelines.high-definition/Runtime/RenderPipeline/Raytracing/Shaders/RaytracingIntersection.hlsl"
Then in this file you can use the content of the initial “RaytracingIntersection.hlsl” as basis, add your intersection shader and update the rest of the file accordingly (in particular the normal computation).
Here is an example to get a simple sphere:

    // Modify AttributeData struct like this:
    struct AttributeData
    {
        // Barycentric value of the intersection
        float2 barycentrics; // Makes sense in ray / triangle intersection, not really there
        float3 normalOS;
    };
  
    // Modify GetCurrentIntersectionVertex like this:
    void GetCurrentIntersectionVertex(AttributeData attributeData, out IntersectionVertex outVertex) {
        outVertex.normalOS   = attributeData.normalOS;
        float3 rayDir = float3(0,1,0); // The HDRP does not provide RayIntersection to this function :(
        outVertex.tangentOS  = float4(normalize(cross(cross(attributeData.normalOS, rayDir), attributeData.normalOS)), 1);
        outVertex.texCoord0  = 0.0;
        outVertex.texCoord1  = 0.0;
        outVertex.texCoord2  = 0.0;
        outVertex.texCoord3  = 0.0;
        outVertex.color      = 0.0;
    }
  
    // Add the intersection shader:
    float HitSphere(float3 sphereCenter, float sphereRadius, float3 rayOrigin, float3 rayDirection) {
        float3 oc = rayOrigin - sphereCenter;
        float a = dot(rayDirection, rayDirection);
        float b = 2.0 * dot(oc, rayDirection);
        float c = dot(oc,oc) - sphereRadius*sphereRadius;
        float discriminant = b*b - 4*a*c;
        if (discriminant < 0.0) {
            return -1.0;
        }
        else {
            float numerator = -b - sqrt(discriminant);
            return numerator > 0.0 ? numerator / (2.0 * a) : -1;
        }
    }
    [shader("intersection")]
    void IntersectionMain()
    {
        float3 or = ObjectRayOrigin();
        float3 dir = normalize(ObjectRayDirection());
        AttributeData attr;
        attr.barycentrics = float2(0, 0);
        float3 sphereCenter = 0.;
        float sphereRadius = 0.15 * abs(frac(_Time.y/2)-0.5) + 0.2; // Make the radius vary with time just for fun (the procedural geometry is raytraced each frame)
        float hitDist = HitSphere(sphereCenter, sphereRadius, or, dir);
        if (hitDist >= 0.) {
            attr.normalOS = normalize(or + hitDist * dir - sphereCenter);
            ReportHit(hitDist, 0, attr);
        }
    }

Note that shadow ray-tracing is disabled on this screenshot, thus we see a cube shadow on the floor :wink: (I don’t have an RTX graphic card :'()

Of course it only gets interesting when using a less trivial intersection shader, like a ray-march based on Signed Distance Fields:
7412756--906551--upload_2021-8-12_18-16-44.png
Nothing impressive but that’s the idea…

I hope it can help some people, since such a trivial example would have been greatly appreciated!
Maybe there could be a specific scene in the SmallOfficeRayTracing project example?

Anyway, let me know if there is a better or cleaner way to do this!
In particular it would be great if there was an easier way to override the RaytracingIntersection.hlsl code, through the ShaderGraph for example (which seems to be the prefered way to edit shaders in HDRP).

10 Likes

Hello,

Thanks for you post.

Currently custom intersection shaders are only officially supported for SRP. We do not have any official support for HDRP. So technically you can make it work, but you’re gonna hit a bunch of blockers to make it work properly.

It is on our road map, but not ETA to share.

Best,

1 Like

Thanks for sharing this information, good to know it is on the HDRP roadmap! :slight_smile:

For what I want to do it was still much easier and faster to fork the HDRP than building a SRP from scratch.
(and for a quick test there is still the solutions I described)

Any updates on this topic? having the same problem…

1 Like

@Veig can you share the full project with your solution?

1 Like

Hi @auzaiffe

Is this suppose to be working now in HDRP (14.0.4)? When I turn on Raytrace Procedurally on a MeshRenderer with a material that has Recursive Rendering enabled, the reflection of the object disappears (as reflected by other objects).

EDIT: Should have followed the instructions above more carefully. I got it working now, but with a strange depth offset issue.

It looks like RaytracingIntersection.hlsl is referenced many places throughout HDRP so simply changing the path in the Lit shader may not be enough.

I am getting there, but the ray traced shadows are still using the vertices. Could this be because RaytracingShadow.raytrace still refers to Unity’s RaytracingIntersection.hlsl instead of mine*?* Oddly enough ray traced reflections seem to work fine, even though RaytracingReflections.raytrace also refers to Unity’s RaytracingIntersection.hlsl.

HDRP 12.1.8.

RaytracingIntersection.hlsl

#ifndef UNITY_RAYTRACING_INTERSECTION_INCLUDED
#define UNITY_RAYTRACING_INTERSECTION_INCLUDED

// Engine includes
#include "UnityRayTracingMeshUtils.cginc"

// TEST INPUT
#define sphereCenter 0.0
#define    sphereRadius 0.49


float HitSphere( float3 center, float radius, float3 ro, float3 rd )
{
    float3 oc = ro - center;
    float a = dot(rd, rd);
    float b = 2.0 * dot(oc, rd);
    float c = dot(oc,oc) - radius*radius;
    float discriminant = b*b - 4*a*c;
    if( discriminant < 0.0 ) {
        return -1.0;
    } else {
        float numerator = -b - sqrt(discriminant);
        return numerator > 0.0 ? numerator / (2.0 * a) : -1;
    }
}


// Raycone structure that defines the stateof the ray
struct RayCone
{
    float width;
    float spreadAngle;
};

// Structure that defines the current state of the intersection
struct RayIntersection
{
    // Origin of the current ray -- FIXME: can be obtained by WorldRayPosition(), should we remove it?
    float3  origin;
    // Distance of the intersection
    float t;
    // Value that holds the color of the ray
    float3 color;
    // Cone representation of the ray
    RayCone cone;
    // The remaining available depth for the current Ray
    uint remainingDepth;
    // Current sample index
    uint sampleIndex;
    // Ray counter (used for multibounce)
    uint rayCount;
    // Pixel coordinate from which the initial ray was launched
    uint2 pixelCoord;
    // Velocity for the intersection point
    float velocity;
};


// Structure to fill for intersections
struct IntersectionVertex
{
    float3 normalOS;
    float4 tangentOS;
    float4 texCoord0;
    float4 texCoord1;
    float4 texCoord2;
    float4 texCoord3;
    float4 color;

#ifdef USE_RAY_CONE_LOD
    // Value used for LOD sampling
    float  triangleArea;
    float  texCoord0Area;
    float  texCoord1Area;
    float  texCoord2Area;
    float  texCoord3Area;
#endif
};

// Fetch the intersetion vertex data for the target vertex
void FetchIntersectionVertex( uint vertexIndex, out IntersectionVertex outVertex )
{
    float3 ro = ObjectRayOrigin();
    float3 rd = ObjectRayDirection();

    float hitDist = HitSphere( sphereCenter, sphereRadius, ro, rd );
    outVertex.normalOS = 0;
    if( hitDist >= 0.0 ) outVertex.normalOS = normalize( ro + hitDist * rd - sphereCenter );

    outVertex.tangentOS  = 0.0;
    outVertex.texCoord0  = 0.0;
    outVertex.texCoord1  = 0.0;
    outVertex.texCoord2  = 0.0;
    outVertex.texCoord3  = 0.0;
    outVertex.color  = 0.0;
}


struct AttributeData
{
    float2 barycentrics; // Barycentric value of the intersection. Makes sense in ray / triangle intersection, not really there
    float3 normalOS;
};
void GetCurrentIntersectionVertex( AttributeData attributeData, out IntersectionVertex outVertex )
{
    outVertex.normalOS = attributeData.normalOS;

    //float3 rd = ObjectRayOrigin();
    float3 rd = float3( 0, 1, 0 ); // The HDRP does not provide RayIntersection to this function :(

    outVertex.tangentOS  = float4( normalize( cross( cross( attributeData.normalOS, rd ), attributeData.normalOS ) ), 1 );
    outVertex.texCoord0  = 0.0;
    outVertex.texCoord1  = 0.0;
    outVertex.texCoord2  = 0.0;
    outVertex.texCoord3  = 0.0;
    outVertex.color      = 0.0;
}


[shader("intersection")]
void IntersectionMain()
{
    float3 ro = ObjectRayOrigin();
    float3 rd = ObjectRayDirection();
  
    float hitDist = HitSphere( sphereCenter, sphereRadius, ro, rd );
    if( hitDist >= 0.0 ) {
        AttributeData attr;
        attr.barycentrics = 0.0;
        attr.normalOS = normalize( ro + hitDist * rd - sphereCenter );
        ReportHit( hitDist, 0, attr );
    }
}


// Compute the proper world space geometric normal from the intersected triangle
void GetCurrentIntersectionGeometricNormal( AttributeData attr, out float3 geomNormalWS )
{
    geomNormalWS = mul( (float3x3) UNITY_MATRIX_I_M, attr.normalOS ); // World to object attr.normalOS;
}


#endif // UNITY_RAYTRACING_INTERSECTION_INCLUDED

1 Like