Shader "Hidden/NiloToon/NiloToonMotionBlur" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { // require Unity2022.3 or above PackageRequirements { "com.unity.render-pipelines.universal": "14.0.0" } Tags { "RenderType"="Opaque" "RenderPipeline" = "UniversalPipeline"} ZWrite Off ZTest Always Blend Off Cull Off HLSLINCLUDE #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/UnityInput.hlsl" #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Common.hlsl" TEXTURE2D_X(_MainTex); SAMPLER(sampler_MainTex); TEXTURE2D_X(_TileMaxTex); SAMPLER(sampler_TileMaxTex); TEXTURE2D_X(_NeighborMaxTex); SAMPLER(sampler_NeighborMaxTex_linear_clamp); TEXTURE2D_X(_MotionVectorTexture); SAMPLER(sampler_MotionVectorTexture); TEXTURE2D_X(_CameraDepthTexture); SAMPLER(sampler_CameraDepthTexture); #if UNITY_VERSION <= 202310 SAMPLER(sampler_LinearClamp); SAMPLER(sampler_PointClamp); #endif TEXTURE2D_X(_CopyTex); SAMPLER(sampler_CopyTex); //float4 _MainTex_TexelSize; float _NumSamples; float _TileSize; float _Intensity; float _SoftZExtent; float4 _SourceMV_TexelSize; float _MaxBlurRadius; struct Attributes { float4 positionOS : POSITION; float2 texcoord : TEXCOORD0; }; struct Varyings { float4 positionHCS : SV_POSITION; float2 texcoord : TEXCOORD0; }; Varyings Vert(Attributes input) { Varyings output; output.positionHCS = TransformObjectToHClip(input.positionOS.xyz); output.texcoord = input.texcoord; return output; } float2 GetMotionVector(float2 uv) { // Unity doc to sample URP's motion vector // https://docs.unity3d.com/6000.0/Documentation/Manual/urp/features/motion-vectors-sample.html return SAMPLE_TEXTURE2D_X(_MotionVectorTexture, sampler_LinearClamp, uv).xy; } // return = is A at the back relative to B float SoftDepthCompare(float depthA, float depthB) { // Soft depth comparison function return saturate(1.0 - (depthA - depthB) / _SoftZExtent); } float ConeWeight(float2 X, float2 Y, float2 v) { // Cone function: linear falloff based on distance along the motion vector return saturate(1.0 - length(X - Y) / (length(v) + 1e-4)); } float CylinderWeight(float2 X, float2 Y, float2 v) { // Cylinder function for simultaneous blurriness float dist = length(X - Y); float vLen = length(v) + 1e-4; return saturate(1.0 - smoothstep(0.95 * vLen, 1.05 * vLen, dist)); } float Random(float2 uv) { // Simple hash-based random function return frac(sin(dot(uv , float2(12.9898,78.233))) * 43758.5453); } // copied from URP's CameraMotionBLur.shader float2 ClampVelocity(float2 velocity, float maxVelocity) { float len = length(velocity); return (len > 0.0) ? min(len, maxVelocity) * (velocity * rcp(len)) : 0.0; } float4 TileMaxFilter(Varyings input) : SV_Target { // Source texture dimensions float sourceWidth = _SourceMV_TexelSize.z; float sourceHeight = _SourceMV_TexelSize.w; // The size of the tile in pixels int tileSize = _TileSize; // Compute number of tiles (ensure integer values) float numTilesX = ceil(sourceWidth / tileSize); float numTilesY = ceil(sourceHeight / tileSize); // Compute tile indices in TileMax RT float2 tileCoord = input.texcoord * float2(numTilesX, numTilesY); int2 tileIndex = int2(tileCoord); // Compute the origin of the tile in source texture pixel coordinates float2 tileOrigin = float2(tileIndex) * tileSize; // Initialize variables float2 maxVelocity = float2(0, 0); float maxLength = 0; // Loop over the pixels in the tile for (int y = 0; y < tileSize; y++) { int py = int(tileOrigin.y) + y; //if (py >= sourceHeight) // break; // Avoid accessing beyond texture height for (int x = 0; x < tileSize; x++) { int px = int(tileOrigin.x) + x; //if (px >= sourceWidth) // break; // Avoid accessing beyond texture width float2 pixelCoord = float2(px, py); // Convert pixel coordinates to UVs in the source texture float2 uv = pixelCoord / float2(sourceWidth, sourceHeight); uv = clamp(uv, 0.0, 1.0); // Ensure UVs are within [0,1] // Sample the motion vector at this UV float2 velocity = GetMotionVector(uv); float velocityLength = length(velocity); if (velocityLength > maxLength) { maxVelocity = velocity; maxLength = velocityLength; } } } return float4(maxVelocity, 0, 1); } float4 NeighborMaxFilter(Varyings input) : SV_Target { float2 maxVelocity = float2(0, 0); float maxLength = 0; // Compute the texel size of TileMaxTex in UV space float2 texelSize = _TileSize * _SourceMV_TexelSize.xy; // Equivalent to tile size in UVs for (int y = -1; y <= 1; y++) { for (int x = -1; x <= 1; x++) { float2 offset = float2(x, y) * texelSize; float2 uv = input.texcoord + offset; // Clamp UVs to prevent sampling outside the texture uv = clamp(uv, 0.0, 1.0); float2 velocity = SAMPLE_TEXTURE2D_X(_TileMaxTex, sampler_LinearClamp, uv).xy; float velocityLength = length(velocity); if (velocityLength > maxLength) { maxVelocity = velocity; maxLength = velocityLength; } } } return float4(maxVelocity, 0, 1); } float4 NaiveMotionBlur(Varyings input) : SV_TARGET { float2 velocity = GetMotionVector(input.texcoord); float4 color = 0; for (int i = -32; i < 32; i++) { color += SAMPLE_TEXTURE2D_X(_CopyTex, sampler_LinearClamp, input.texcoord + velocity * i * 0.005); } return color * (1.0/65.0); } float4 MotionBlur(Varyings input) : SV_Target { float _ExposureTime = (1000/60) * _Intensity; float _SampleCount = 15; // atleast 3, better to be odd number float _EarlyExitThreshold = 1e-4; //--------------------------- float2 uv = input.texcoord; float2 pixelPos = uv * _ScaledScreenParams.xy; // Retrieve the dominant motion vector from NeighborMax texture float2 vN = SAMPLE_TEXTURE2D_X(_NeighborMaxTex, sampler_LinearClamp, uv).xy; vN *= _ExposureTime; vN = ClampVelocity(vN, _MaxBlurRadius); float vNLength = length(vN); float4 color = SAMPLE_TEXTURE2D_X(_MainTex, sampler_MainTex, uv); // Check for significant motion if (vNLength <= _EarlyExitThreshold) { // No significant motion, return original color return color; } // Initialize accumulation variables float weightSum = 0.5; float3 colorSum = color.rgb * weightSum; // Get per-pixel motion vector float2 velocity = SAMPLE_TEXTURE2D_X(_MotionVectorTexture, sampler_LinearClamp, uv).xy; velocity *= _ExposureTime; // Clamp velocity velocity = ClampVelocity(velocity, _MaxBlurRadius); // Get depth of current pixel float depth = Linear01Depth(SAMPLE_TEXTURE2D_X(_CameraDepthTexture, sampler_CameraDepthTexture, uv).r,_ZBufferParams); // DX: 1 at near, 0 at far. Linear // Jitter to prevent ghosting float jitter = Random(uv) - 0.5; //jitter *= 0.5; // Number of samples along motion vector int S = _SampleCount; for (int i = 0; i < S; i++) { // Fraction along the motion vector (-0.5 to 0.5) float t = (i + jitter) / S - 0.5; // Sample position along the motion vector float2 sampleOffset = vN * t; float2 sampleUv = uv + sampleOffset * _SourceMV_TexelSize.xy; // Ensure sampleUv is within texture bounds sampleUv = saturate(sampleUv); // Retrieve sample color and depth float3 sampleColor = SAMPLE_TEXTURE2D_X_LOD(_MainTex, sampler_LinearClamp, sampleUv, 0).rgb; float sampleDepth = Linear01Depth(SAMPLE_TEXTURE2D_X_LOD(_CameraDepthTexture, sampler_CameraDepthTexture, sampleUv, 0).r, _ZBufferParams); // DX: 1 at near, 0 at far. Linear float2 sampleVelocity = GetMotionVector(sampleUv); sampleVelocity *= _ExposureTime; // Clamp the length of velocity to _MaxBlurRadius sampleVelocity = ClampVelocity(sampleVelocity, _MaxBlurRadius); // [copy directly according to McGuire12Blur paper] // Fore- vs. background classification of Y relative to X // f means front // b means back // depth input is 0~1 (we assume linear is better) // Note: we swap f&b's value, it is different to paper's design, but it is the correct way in unity float f = SoftDepthCompare(sampleDepth, depth); // f = is offseted pixel(Y) at the front of original pixel(X) float b = SoftDepthCompare(depth, sampleDepth); // b = is offseted pixel(Y) at the back of original pixel(X) float alphaY = 0.0; // [copy directly according to McGuire12Blur paper] float2 X = pixelPos; // original pixel coord index float2 Y = pixelPos + sampleOffset; // offseted pixel coord index float2 VX = velocity; // original pixel velocity float2 VY = sampleVelocity; // offseted pixel velocity // TODO: length(X-Y) and Velocity should be using the same unit alphaY += f * ConeWeight(Y, X, VY); // case 1: Y contributes when Y is moving+blurry and passes in front of X alphaY += b * ConeWeight(X, Y, VX); // case 2: X itself is moving+blurry so we should be able to see through alphaY += CylinderWeight(Y, X, VY) * CylinderWeight(X, Y, VX) * 2.0; // case 3: 2 moving blur together // test alphaY *= 1.0 / max(1,distance(X,Y)/_TileSize); // Accumulate color and weight colorSum += sampleColor * alphaY; weightSum += alphaY; } // Normalize accumulated color float3 finalColor = colorSum / max(weightSum, 1e-4); return float4(finalColor, 1.0); } ENDHLSL Pass // Tile max { Name "Tile Max" HLSLPROGRAM #pragma vertex Vert #pragma fragment TileMaxFilter ENDHLSL } Pass // Neighbor max { Name "Neighbor Max" HLSLPROGRAM #pragma vertex Vert #pragma fragment NeighborMaxFilter ENDHLSL } Pass // Motion blur { Name "Motion Blur" HLSLPROGRAM #pragma vertex Vert //#pragma fragment NaiveMotionBlur #pragma fragment MotionBlur ENDHLSL } } }