RayMarch.bslinc 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. #include "$ENGINE$/PerCameraData.bslinc"
  2. mixin RayMarch
  3. {
  4. mixin PerCameraData;
  5. code
  6. {
  7. #ifndef NUM_STEPS
  8. #define NUM_STEPS 16
  9. #endif
  10. #ifndef HI_Z
  11. #define HI_Z 0
  12. #endif
  13. // Note this is a very high number of iterations, but it is expected only a small number of pixels will use
  14. // this full range. Generally those are pixels that keep having false positives when intersecting a higher
  15. // Z level.
  16. #define MAX_HIZ_ITERATIONS 64
  17. #define HIZ_START_LEVEL 2
  18. float3 viewToNDC(float3 view)
  19. {
  20. float4 projected = mul(gMatProj, float4(view, 1));
  21. projected.xyz /= projected.w;
  22. return projected.xyz;
  23. }
  24. bool linearSearch(Texture2D depth, SamplerState samp, float3 rayStart, float3 rayStep, int numSteps, float stepIncrement, float compareTolerance, inout float t)
  25. {
  26. float lastDiff = 0.0f;
  27. [unroll]
  28. for(int i = 0; i < numSteps; ++i)
  29. {
  30. float3 rayPos = rayStart + rayStep * t;
  31. #if HI_Z
  32. float sampleDepth = depth.Sample(samp, rayPos.xy).r;
  33. #else
  34. float sampleDepth = depth.SampleLevel(samp, rayPos.xy, 0).r;
  35. #endif
  36. // Check if ray is behind an object, but not too much behind otherwise we'll have false positives.
  37. // Instead we treat "compareTolerance" as an approximate thickness of the object. Proper
  38. // thickness should be calculated by rendering depth buffer for backfaces.
  39. float depthDiff = rayPos.z - sampleDepth;
  40. bool hit = abs(depthDiff - compareTolerance) < compareTolerance;
  41. if(hit)
  42. {
  43. // Refine hit using line segment intersection
  44. float tt = lastDiff / (depthDiff - lastDiff);
  45. t += tt * stepIncrement + stepIncrement;
  46. return true;
  47. }
  48. lastDiff = depthDiff;
  49. t += stepIncrement;
  50. }
  51. return false;
  52. }
  53. bool hiZSearch(Texture2D depth, SamplerState samp, int2 bufferSize, int maxMipLevel, float3 rayPos, float3 rayStep, out float3 hitPos)
  54. {
  55. float iterationCount = 0.0f;
  56. int mipLevel = HIZ_START_LEVEL;
  57. bufferSize >>= mipLevel;
  58. // Get the ray equation, in the form so that t == Z
  59. float3 D = rayStep / rayStep.z; // Scale vector so Z is moved into [0, 1] range
  60. float3 O = rayPos + D * -rayPos.z; // Get point where Z equals 0 (on near plane)
  61. // Our ray is now O + D * t, where t = 0 = near plane, and t = 1 = far plane
  62. // Avoid division by zero
  63. D.x = abs(D.x) < 0.00001f ? 0.00001f : D.x;
  64. D.y = abs(D.y) < 0.00001f ? 0.00001f : D.y;
  65. while(rayPos.z <= 1.0f && iterationCount < MAX_HIZ_ITERATIONS)
  66. {
  67. // Get depth of the current cell
  68. float cellZ = depth.SampleLevel(samp, rayPos.xy, mipLevel).r;
  69. // Get pixel coordinates of the current cell
  70. float2 curCellIdx = trunc(rayPos.xy * bufferSize);
  71. // Find intersection with the cell floor plane
  72. float3 newRay = O + D * max(cellZ, rayPos.z); // max() so we can't hit the ceiling (ray going backwards)
  73. // Get pixel coordinates of the new ray's cell
  74. float2 newCellIdx = trunc(newRay.xy * bufferSize);
  75. // If we moved to another cell, no intersection with floor
  76. if(any(curCellIdx != newCellIdx))
  77. {
  78. float2 cellStart = (curCellIdx / bufferSize);
  79. float2 cellEnd = ((curCellIdx + 1) / bufferSize);
  80. float2 intersectStart = (cellStart - rayPos.xy) / D.xy;
  81. float2 intersectEnd = (cellEnd - rayPos.xy) / D.xy;
  82. // Only care about positive t
  83. float maxIntersectX = max(intersectStart.x, intersectEnd.x);
  84. float maxIntersectY = max(intersectStart.y, intersectEnd.y);
  85. // Closest t is the one at the boundary
  86. float minIntersect = min(maxIntersectX, maxIntersectY);
  87. // Little extra to ensure the boundary is crossed. max() to ensure the value isn't too
  88. // small to prevent it ever leaving a cell. Note that this clamping results in a quality loss,
  89. // you want to keep the clamp value as low as possible, but not too low to avoid artifacts.
  90. minIntersect = max(minIntersect * 1.05f, 1.0f / (bufferSize * 512)); // 1/512th of a pixel
  91. // Move the ray past the boundary
  92. rayPos = O + D * (rayPos.z + minIntersect);
  93. if(mipLevel < maxMipLevel)
  94. {
  95. ++mipLevel;
  96. bufferSize >>= 1;
  97. }
  98. }
  99. else // Intersection with floor, move to higher quality mip
  100. {
  101. rayPos = newRay;
  102. --mipLevel;
  103. if(mipLevel < 0)
  104. {
  105. hitPos = rayPos;
  106. return true;
  107. }
  108. bufferSize <<= 1;
  109. }
  110. ++iterationCount;
  111. }
  112. hitPos = rayPos;
  113. return false;
  114. }
  115. struct RayMarchParams
  116. {
  117. int2 bufferSize;
  118. int numMips;
  119. float4 NDCToHiZUV; // From NDC to HiZ UV. .xy - multiply, .zw - add
  120. float2 HiZUVToScreenUV; // From HiZ UV to screen UV. .xy - multiply
  121. float3 rayOrigin; // World space
  122. float3 rayDir; // World space
  123. float jitterOffset;
  124. };
  125. float4 rayMarch(Texture2D depth, SamplerState samp, RayMarchParams params)
  126. {
  127. float3 viewOrigin = mul(gMatView, float4(params.rayOrigin, 1));
  128. float3 viewDir = mul(gMatView, float4(params.rayDir, 0));
  129. float3 ndcStart = viewToNDC(viewOrigin);
  130. float3 ndcEnd = viewToNDC(viewOrigin + viewDir);
  131. float3 ndcStep = ndcEnd - ndcStart;
  132. // Resize ray so it reaches screen edge
  133. //// We want: start + |step| * t = 1
  134. //// Solve for t: t = (1 - start) / |step|
  135. //// 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:
  136. //// t = 1/|step| - start/step
  137. float epsilon = 0.00001f; // Handle div by zero
  138. float2 stepScale = 1.0f / abs(ndcStep.xy + epsilon) - ndcStart.xy/(ndcStep.xy + epsilon);
  139. ndcStep *= min(stepScale.x, stepScale.y);
  140. float deviceZStart = NDCZToDeviceZ(ndcStart.z);
  141. float deviceZEnd = NDCZToDeviceZ(ndcStart.z + ndcStep.z);
  142. #if HI_Z
  143. float3 uvStart;
  144. uvStart.xy = ndcStart.xy * params.NDCToHiZUV.xy + params.NDCToHiZUV.zw;
  145. uvStart.z = deviceZStart;
  146. float3 uvStep;
  147. uvStep.xy = ndcStep.xy * params.NDCToHiZUV.xy;
  148. uvStep.z = deviceZEnd - deviceZStart;
  149. #else
  150. float3 uvStart = float3(NDCToUV(ndcStart.xy), deviceZStart);
  151. float3 uvStep = float3(ndcStep.xy * gClipToUVScaleOffset.xy, deviceZEnd - deviceZStart);
  152. #endif
  153. float stepIncrement = 1.0f / NUM_STEPS;
  154. // Offset starting position to avoid self-intersection. Use random values to avoid
  155. // staircase artifacts.
  156. float t = stepIncrement + stepIncrement * params.jitterOffset;
  157. // Note: Perhaps tweak this value
  158. float compareTolerance = uvStep.z * stepIncrement * 2.0f;
  159. #if HI_Z
  160. // Note: Perhaps do a few steps of linear search first to handle nearby surfaces
  161. // Hierarchical search
  162. float3 rayPos = uvStart + uvStep * t;
  163. float3 hitPos;
  164. if(hiZSearch(depth, samp, params.bufferSize, params.numMips, rayPos, uvStep, hitPos))
  165. return float4(hitPos.xy * params.HiZUVToScreenUV.xy, hitPos.z, 0);
  166. #else
  167. // Plain linear search
  168. if(linearSearch(depth, samp, uvStart, uvStep, NUM_STEPS, stepIncrement, compareTolerance, t))
  169. return float4(uvStart + uvStep * t, t);
  170. #endif
  171. // Hit not found
  172. return float4(0, 0, 0, 1);
  173. }
  174. };
  175. };