// Shader uniforms uniform extern sampler2D textureReflectionSource; uniform extern sampler2D blurredTextureReflectionSource; uniform extern float3 cameraPosition; uniform extern float roughness; uniform extern float4x4 viewProjectionMatrixForReflectionSource; uniform extern float3 cameraPositionForReflectionSource; uniform extern float4 reflectionBackgroundColour; // The colour we fall back to when there's no hit or when the ray travels too far. // Random functions // Based on the implementation by David Hoskins at https://www.shadertoy.com/view/4djSRW // MIT License for these hash functions: /* Copyright (c)2014 David Hoskins. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.*/ float3 hash33(float3 p3) // 3D input, 3D output { p3 = frac(p3 * float3(.1031, .1030, .0973)); p3 += dot(p3, p3.yxz + 33.33); return frac((p3.xxy + p3.yxx) * p3.zyx); } float4 correctProjectionSpaceCoordinate(float4 projectionSpaceCoordinate) { // perform perspective divide (only relevant for perspective lights: for orthographic lights this does nothing) projectionSpaceCoordinate /= projectionSpaceCoordinate.w; // transform to [0,1] range projectionSpaceCoordinate = projectionSpaceCoordinate * 0.5f + 0.5f; #begin:DX|GNM# projectionSpaceCoordinate.y = 1 - projectionSpaceCoordinate.y; #end# return projectionSpaceCoordinate; } struct ReflectionSampleData { float4 sampleColour; float pointAlongRayDistanceToCamera; float2 uv; }; ReflectionSampleData calculateReflectionSampleData(float4 worldPosition, float3 reflectionDirection, float distanceAlongRay) { float3 pointAlongRayInWorldSpace = worldPosition.xyz + reflectionDirection * distanceAlongRay; #begin:DX# float4 pointAlongRayInScreenSpace = mul(viewProjectionMatrixForReflectionSource, float4(pointAlongRayInWorldSpace, 1)); #end# #begin:!DX# float4 pointAlongRayInScreenSpace = mul(viewProjectionMatrixForReflectionSource, float4(pointAlongRayInWorldSpace, 1) * float4(1, 1, -1, 1)); #end# ReflectionSampleData returnData; returnData.uv = correctProjectionSpaceCoordinate(pointAlongRayInScreenSpace).xy; returnData.pointAlongRayDistanceToCamera = distance(pointAlongRayInWorldSpace.xyz, cameraPositionForReflectionSource); returnData.sampleColour = tex2D(textureReflectionSource, returnData.uv); return returnData; } // Call this to change the colour of the material without reflections into a colour that includes reflections. // Whether reflection is visible at this pixel, depends on the reflectionStrength, which is read from a texture that defines the material of this object. float3 applyReflections(float4 originalPositionInProjectionSpace, float4 worldPosition, float3 normal, float4 colourBefore, float reflectionStrength) { // We're going to march over the reflection ray, but we're going to do so in screen space. // As soon as we find a pixel that's on the other side of the ray, we've found a hit and know what we're reflecting. // This implementation assumes that depth if stored in the alpha channel of the renderTexture that also contains the colours. #begin:!DX# worldPosition *= float4(1, 1, -1, 1); #end# // First calculate the reflectionRay. float3 viewDirectionTowardsPixel = worldPosition.xyz - cameraPosition; float3 reflectionDirection = normalize(reflect(viewDirectionTowardsPixel, normalize(normal))); // Randomise the reflection direction based on the roughtness of the surface. // The magic number multiplication is based on what looks good in combination with the usage of roughness below for blending with the blurred texture. reflectionDirection += (hash33(worldPosition.xyz * 10) - float3(0.5f, 0.5f, 0.5f)) * roughness * 0.02; // Step along the ray and check whether the ray goes behind whatever is on the screen at the ray's position float4 reflectionColour = reflectionBackgroundColour; float previousPointAlongRayDistanceToCamera = distance(worldPosition.xyz, cameraPosition); float previousDistanceAlongRay = 0; float currentDistanceAlongRay = 0; float currentStepDistance = 0.07f; float2 currentUV = float2(0, 0); bool collisionFound = false; for (int i = 1; i <= 35; ++i) { previousDistanceAlongRay = currentDistanceAlongRay; // We take steps of increasing distance, so that we get more precision near the surface and less in the distance. // This works nicely with that the characters are usually standing directly on the reflection surface and are thus near and reflected with most precision. // Also, we fade out reflections in the distance and blur them in the distance, so loss of precision far away is less problematic. currentDistanceAlongRay += currentStepDistance; ReflectionSampleData sampleData = calculateReflectionSampleData(worldPosition, reflectionDirection, currentDistanceAlongRay); if (sampleData.sampleColour.a <= sampleData.pointAlongRayDistanceToCamera && sampleData.sampleColour.a > previousPointAlongRayDistanceToCamera - currentStepDistance) // 'thickness': is the ray flying underneath-and-then-behind the object (no hit), or is the ray hitting it? { reflectionColour = sampleData.sampleColour; currentUV = sampleData.uv; collisionFound = true; break; } previousPointAlongRayDistanceToCamera = sampleData.pointAlongRayDistanceToCamera; if (currentDistanceAlongRay > 1.4f) currentStepDistance += 0.05f; } // We've found a collision, but because we took big steps above it's not very precise yet. // Therefore we now improve precision using binary search. if (collisionFound) { for (int i = 0; i < 2; ++i) { // Read what's in between the max and min samples that we have so far float midDistanceAlongRay = 0.5f * (previousDistanceAlongRay + currentDistanceAlongRay); ReflectionSampleData midSampleData = calculateReflectionSampleData(worldPosition, reflectionDirection, midDistanceAlongRay); if (midSampleData.sampleColour.a <= midSampleData.pointAlongRayDistanceToCamera) { // The midpoint is also a hit, which means that the real hit must be in front of it (or on it but we ignore that edgecase to keep the shader simpler and hopefully faster) currentDistanceAlongRay = midDistanceAlongRay; reflectionColour = midSampleData.sampleColour; currentUV = midSampleData.uv; } else { // The midpoint isn't a hit. This means the real hit must be behind it. previousDistanceAlongRay = midDistanceAlongRay; } } // Use a blurred version of the reflection the further we get from the surface float4 blurredReflectionColour = tex2D(blurredTextureReflectionSource, currentUV); reflectionColour = lerp(reflectionColour, blurredReflectionColour, saturate(currentDistanceAlongRay * roughness)); // Fade reflection out towards the max distance towards which we trace. // The maximum distance here was calculated based on the numbers set above. Note that we use the distance at the // second-to-last iteration instead of the last one because the binary search can bring us back forward that far. reflectionColour = lerp(reflectionColour, reflectionBackgroundColour, saturate(currentDistanceAlongRay / 8.45f)); } // Combine with the colour of the object without reflections return lerp(colourBefore.rgb, reflectionColour.rgb, reflectionStrength); }