#include "$ENGINE$/PerCameraData.bslinc" mixin RayMarch { mixin PerCameraData; code { #ifndef NUM_STEPS #define NUM_STEPS 16 #endif #ifndef HI_Z #define HI_Z 0 #endif // Note this is a very high number of iterations, but it is expected only a small number of pixels will use // this full range. Generally those are pixels that keep having false positives when intersecting a higher // Z level. #define MAX_HIZ_ITERATIONS 64 #define HIZ_START_LEVEL 2 float3 viewToNDC(float3 view) { float4 projected = mul(gMatProj, float4(view, 1)); projected.xyz /= projected.w; return projected.xyz; } bool linearSearch(Texture2D depth, SamplerState samp, float3 rayStart, float3 rayStep, int numSteps, float stepIncrement, float compareTolerance, inout float t) { float lastDiff = 0.0f; [unroll] for(int i = 0; i < numSteps; ++i) { float3 rayPos = rayStart + rayStep * t; #if HI_Z float sampleDepth = depth.Sample(samp, rayPos.xy).r; #else float sampleDepth = depth.SampleLevel(samp, rayPos.xy, 0).r; #endif // Check if ray is behind an object, but not too much behind otherwise we'll have false positives. // Instead we treat "compareTolerance" as an approximate thickness of the object. Proper // thickness should be calculated by rendering depth buffer for backfaces. float depthDiff = rayPos.z - sampleDepth; bool hit = abs(depthDiff - compareTolerance) < compareTolerance; if(hit) { // Refine hit using line segment intersection float tt = lastDiff / (depthDiff - lastDiff); t += tt * stepIncrement + stepIncrement; return true; } lastDiff = depthDiff; t += stepIncrement; } return false; } bool hiZSearch(Texture2D depth, SamplerState samp, int2 bufferSize, int maxMipLevel, float3 rayPos, float3 rayStep, out float3 hitPos) { float iterationCount = 0.0f; int mipLevel = HIZ_START_LEVEL; bufferSize >>= mipLevel; // Get the ray equation, in the form so that t == Z float3 D = rayStep / rayStep.z; // Scale vector so Z is moved into [0, 1] range float3 O = rayPos + D * -rayPos.z; // Get point where Z equals 0 (on near plane) // Our ray is now O + D * t, where t = 0 = near plane, and t = 1 = far plane // Avoid division by zero D.x = abs(D.x) < 0.00001f ? 0.00001f : D.x; D.y = abs(D.y) < 0.00001f ? 0.00001f : D.y; while(rayPos.z < 0.9999f && iterationCount < MAX_HIZ_ITERATIONS) { // Get depth of the current cell float cellZ = depth.SampleLevel(samp, rayPos.xy, mipLevel).r; // Get pixel coordinates of the current cell float2 curCellIdx = trunc(rayPos.xy * bufferSize); // Find intersection with the cell floor plane float3 newRay = O + D * max(cellZ, rayPos.z); // max() so we can't hit the ceiling (ray going backwards) // Get pixel coordinates of the new ray's cell float2 newCellIdx = trunc(newRay.xy * bufferSize); // If we moved to another cell, no intersection with floor if(any(curCellIdx != newCellIdx)) { float2 cellStart = (curCellIdx / bufferSize); float2 cellEnd = ((curCellIdx + 1) / bufferSize); float2 intersectStart = (cellStart - rayPos.xy) / D.xy; float2 intersectEnd = (cellEnd - rayPos.xy) / D.xy; // Only care about positive t float maxIntersectX = max(intersectStart.x, intersectEnd.x); float maxIntersectY = max(intersectStart.y, intersectEnd.y); // Closest t is the one at the boundary float minIntersect = min(maxIntersectX, maxIntersectY); // Little extra to ensure the boundary is crossed. max() to ensure the value isn't too // small to prevent it ever leaving a cell. Note that this clamping results in a quality loss, // you want to keep the clamp value as low as possible, but not too low to avoid artifacts. minIntersect = max(minIntersect * 1.05f, 1.0f / (bufferSize * 512)); // 1/512th of a pixel // Move the ray past the boundary rayPos = O + D * (rayPos.z + minIntersect); if(mipLevel < maxMipLevel) { ++mipLevel; bufferSize >>= 1; } } else // Intersection with floor, move to higher quality mip { rayPos = newRay; --mipLevel; if(mipLevel < 0) { hitPos = rayPos; return true; } bufferSize <<= 1; } ++iterationCount; } hitPos = rayPos; return false; } struct RayMarchParams { int2 bufferSize; int numMips; float4 NDCToHiZUV; // From NDC to HiZ UV. .xy - multiply, .zw - add float2 HiZUVToScreenUV; // From HiZ UV to screen UV. .xy - multiply float3 rayOrigin; // World space float3 rayDir; // World space float jitterOffset; }; float4 rayMarch(Texture2D depth, SamplerState samp, RayMarchParams params) { float3 viewOrigin = mul(gMatView, float4(params.rayOrigin, 1)); float3 viewDir = mul(gMatView, float4(params.rayDir, 0)); float3 ndcStart = viewToNDC(viewOrigin); float3 ndcEnd = viewToNDC(viewOrigin + viewDir); float3 ndcStep = ndcEnd - ndcStart; // Resize ray so it reaches screen edge //// We want: start + |step| * t = 1 //// Solve for t: t = (1 - start) / |step| //// This has two solutions, but we can handle them both in a single equation by flipping sign depending on "step", on only one of the components: //// t = 1/|step| - start/step float epsilon = 0.00001f; // Handle div by zero float2 stepScale = 1.0f / abs(ndcStep.xy + epsilon) - ndcStart.xy/(ndcStep.xy + epsilon); ndcStep *= min(stepScale.x, stepScale.y); float deviceZStart = NDCZToDeviceZ(ndcStart.z); float deviceZEnd = NDCZToDeviceZ(ndcStart.z + ndcStep.z); #if HI_Z float3 uvStart; uvStart.xy = ndcStart.xy * params.NDCToHiZUV.xy + params.NDCToHiZUV.zw; uvStart.z = deviceZStart; float3 uvStep; uvStep.xy = ndcStep.xy * params.NDCToHiZUV.xy; uvStep.z = deviceZEnd - deviceZStart; #else float3 uvStart = float3(NDCToUV(ndcStart.xy), deviceZStart); float3 uvStep = float3(ndcStep.xy * gClipToUVScaleOffset.xy, deviceZEnd - deviceZStart); #endif float stepIncrement = 1.0f / NUM_STEPS; // Offset starting position to avoid self-intersection. Use random values to avoid // staircase artifacts. float t = stepIncrement + stepIncrement * params.jitterOffset; // Note: Perhaps tweak this value float compareTolerance = uvStep.z * stepIncrement * 2.0f; #if HI_Z // Note: Perhaps do a few steps of linear search first to handle nearby surfaces // Hierarchical search float3 rayPos = uvStart + uvStep * t; float3 hitPos; if(hiZSearch(depth, samp, params.bufferSize, params.numMips, rayPos, uvStep, hitPos)) return float4(hitPos.xy * params.HiZUVToScreenUV.xy, hitPos.z, 0); #else // Plain linear search if(linearSearch(depth, samp, uvStart, uvStep, NUM_STEPS, stepIncrement, compareTolerance, t)) return float4(uvStart + uvStep * t, t); #endif // Hit not found return float4(0, 0, 0, 1); } }; };