Last active
February 28, 2025 16:42
-
-
Save ValerioMarty/751086bb6642591de54361e3591f824c to your computer and use it in GitHub Desktop.
shader for the colored volumetric light tutorial
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Shader "Hidden/VolumetricLight" | |
{ | |
Properties | |
{ | |
//we need to have _MainTex written exactly like this because unity will pass the source render texture into _MainTex automatically | |
_MainTex ("Texture", 2D) = "white" {} | |
} | |
SubShader | |
{ | |
// No culling or depth | |
Cull Off ZWrite Off ZTest Always | |
Pass | |
{ | |
HLSLPROGRAM | |
#pragma vertex vert | |
#pragma fragment frag | |
#pragma multi_compile _ _MAIN_LIGHT_SHADOWS_CASCADE | |
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl" | |
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl" | |
//Boilerplate code, we aren't doind anything with our vertices or any other input info, | |
// because technically we are working on a quad taking up the whole screen | |
real4x4 _ClipToWorld; | |
struct appdata | |
{ | |
real4 vertex : POSITION; | |
real2 uv : TEXCOORD0; | |
}; | |
struct v2f | |
{ | |
real2 uv : TEXCOORD0; | |
real4 vertex : SV_POSITION; | |
}; | |
v2f vert (appdata v) | |
{ | |
v2f o; | |
o.vertex = TransformWorldToHClip(v.vertex); | |
o.uv = v.uv; | |
return o; | |
} | |
sampler2D _MainTex; | |
//regular raymarching variables | |
real _Scattering; | |
real3 _SunDirection; | |
const real _Steps; | |
real _JitterVolumetric; | |
real _MaxDistance; | |
//Color raymarching variables | |
TEXTURE2D(_CameraDepth2Texture); | |
SAMPLER(sampler_CameraDepth2Texture); | |
real _DepthSteps=8; | |
real _DepthMaxDistance=18; | |
real _Boost=4; | |
real _ColorJitterMultiplier=2; | |
//This function will tell us if a certain point in world space coordinates is in light or shadow of the main light | |
real ShadowAtten(real3 worldPosition) | |
{ | |
return MainLightRealtimeShadow(TransformWorldToShadowCoord(worldPosition)); | |
} | |
//Unity already has a function that can reconstruct world space position from depth | |
real3 GetWorldPos(real2 uv){ | |
#if UNITY_REVERSED_Z | |
real depth = SampleSceneDepth(uv); | |
#else | |
// Adjust z to match NDC for OpenGL | |
real depth = lerp(UNITY_NEAR_CLIP_VALUE, 1, SampleSceneDepth(uv)); | |
#endif | |
return ComputeWorldSpacePosition(uv, depth, UNITY_MATRIX_I_VP); | |
} | |
// Mie scaterring approximated with Henyey-Greenstein phase function. | |
real ComputeScattering(real lightDotView) | |
{ | |
real result = 1.0f - _Scattering * _Scattering; | |
result /= (4.0f * PI * pow(1.0f + _Scattering * _Scattering - (2.0f * _Scattering) * lightDotView, 1.5f)); | |
return result; | |
} | |
//standard hash | |
real random( real2 p ){ | |
return frac(sin(dot(p, real2(41, 289)))*45758.5453 )-0.5; | |
} | |
real random01( real2 p ){ | |
return frac(sin(dot(p, real2(41, 289)))*45758.5453 ); | |
} | |
//from Ronja https://www.ronja-tutorials.com/post/047-invlerp_remap/ | |
real invLerp(real from, real to, real value){ | |
return (value - from) / (to - from); | |
} | |
real remap(real origFrom, real origTo, real targetFrom, real targetTo, real value){ | |
real rel = invLerp(origFrom, origTo, value); | |
return lerp(targetFrom, targetTo, rel); | |
} | |
//There is probably a simpler way to do this | |
//get Screen Position from a world space coordinate | |
real2 WorldToScreenPos(real3 pos){ | |
pos = (pos - _WorldSpaceCameraPos)*(_ProjectionParams.y + (_ProjectionParams.z - _ProjectionParams.y))+_WorldSpaceCameraPos; | |
real2 uv =0; | |
real3 toCam = mul(unity_WorldToCamera, pos); | |
real camPosZ = toCam.z; | |
real height = 2 * camPosZ / unity_CameraProjection._m11; | |
real width = _ScreenParams.x / _ScreenParams.y * height; | |
uv.x = (toCam.x + width / 2)/width; | |
uv.y = (toCam.y + height / 2)/height; | |
return uv; | |
} | |
//we need to not use mipmaps so it works even if loops arent unrolled | |
real GetDepthLevel0(real2 uv){ | |
return _CameraDepthTexture.SampleLevel(sampler_CameraDepthTexture,uv,0); | |
} | |
//we need to not use mipmaps so it works even if loops arent unrolled | |
real3 GetWorldPosLoop(real2 uv){ | |
#if UNITY_REVERSED_Z | |
real depth = GetDepthLevel0( uv); | |
#else | |
real depth = GetDepthLevel0( uv); | |
// Adjust z to match NDC for OpenGL | |
depth = lerp(UNITY_NEAR_CLIP_VALUE, 1, depth); | |
#endif | |
return ComputeWorldSpacePosition(uv, depth, UNITY_MATRIX_I_VP); | |
} | |
//we need to not use mipmaps so it works even if loops arent unrolled | |
real GetEyeDepth(real2 uv){ | |
#if UNITY_REVERSED_Z | |
real depth = GetDepthLevel0( uv); | |
#else | |
real depth = GetDepthLevel0( uv); | |
// Adjust z to match NDC for OpenGL | |
depth = lerp(UNITY_NEAR_CLIP_VALUE, 1, depth); | |
#endif | |
return LinearEyeDepth(depth,_ZBufferParams); | |
} | |
//this implementation is loosely based on http://www.alexandre-pestana.com/volumetric-lights/ and https://fr.slideshare.net/BenjaminGlatzel/volumetric-lighting-for-many-lights-in-lords-of-the-fallen | |
#define MIN_STEPS 25 | |
real3 frag (v2f i) : SV_Target | |
{ | |
real3 worldPos = GetWorldPos(i.uv); | |
real3 startPosition = _WorldSpaceCameraPos; | |
real3 rayVector = worldPos- startPosition; | |
real3 rayDirection = normalize(rayVector); | |
real rayLength = length(rayVector); | |
if(rayLength>_MaxDistance){ | |
rayLength=_MaxDistance; | |
worldPos= startPosition+rayDirection*rayLength; | |
} | |
//We can limit the amount of steps for close objects | |
// steps= remap(0,_MaxDistance,MIN_STEPS,_Steps,rayLength); | |
// steps= remap(0,_MaxDistance,0,_Steps,rayLength); | |
// steps = max(steps,MIN_STEPS); | |
real stepLength = rayLength / _Steps; | |
real3 step = rayDirection * stepLength; | |
//to eliminate banding we sample at diffent depths for every ray, this way we obfuscate the shadowmap patterns | |
real rayStartOffset= random01( i.uv)*stepLength *_JitterVolumetric/100; | |
real3 currentPosition = startPosition + rayStartOffset*rayDirection; | |
startPosition=currentPosition; | |
real3 accumFog = 0; | |
//everything anout the color raymarch that can be calculated outside the loop | |
real3 depthRayDirection = -_SunDirection; | |
real depthStepLength = _DepthMaxDistance/_DepthSteps; | |
real3 depthStep= depthRayDirection*depthStepLength; | |
//we ask for the shadow map value at different depths, if the sample is in light we compute the contribution at that point and add it | |
for (real j = 0; j < _Steps-1; j++) | |
{ | |
real shadowMapValue = ShadowAtten(currentPosition); | |
//if it is in light | |
[branch] | |
if(shadowMapValue>0){ | |
real3 kernelColor = ComputeScattering(dot(rayDirection, _SunDirection)).xxxx ; | |
real3 depthRayPosition= currentPosition; | |
depthRayPosition+=rayStartOffset*_ColorJitterMultiplier*depthRayDirection; | |
for(real z=0;z<_DepthSteps;z++){ | |
real distanceToDepthRay = length( depthRayPosition-_WorldSpaceCameraPos); | |
real2 uvDepthPos = WorldToScreenPos(depthRayPosition); | |
[branch] | |
if(abs (uvDepthPos.x)>1 || abs(uvDepthPos.y)>1){ | |
break; | |
} | |
real depthInUV = _CameraDepth2Texture.SampleLevel(sampler_CameraDepth2Texture,uvDepthPos,0)*_ProjectionParams.z; | |
if(distanceToDepthRay>depthInUV){ | |
real3 color = (tex2Dlod(_MainTex,float4(uvDepthPos,0,0)))*2*_Boost; | |
kernelColor= kernelColor.x*color; | |
break; | |
} | |
depthRayPosition+=depthStep; | |
} | |
kernelColor= saturate(kernelColor); | |
accumFog += kernelColor; | |
// break; | |
} | |
currentPosition += step; | |
} | |
//we need the average value, so we divide between the amount of samples | |
accumFog /= _Steps; | |
return accumFog; | |
} | |
ENDHLSL | |
} | |
Pass | |
{ | |
Name "Gaussian Blur x" | |
HLSLPROGRAM | |
#pragma vertex vert | |
#pragma fragment frag | |
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl" | |
struct appdata | |
{ | |
real4 vertex : POSITION; | |
real2 uv : TEXCOORD0; | |
}; | |
struct v2f | |
{ | |
real2 uv : TEXCOORD0; | |
real4 vertex : SV_POSITION; | |
}; | |
v2f vert (appdata v) | |
{ | |
v2f o; | |
o.vertex = TransformWorldToHClip(v.vertex); | |
o.uv = v.uv; | |
return o; | |
} | |
sampler2D _MainTex; | |
int _GaussSamples; | |
real _GaussAmount; | |
static const real gauss_filter_weights[] = { 0.14446445, 0.13543542, 0.11153505, 0.08055309, 0.05087564, 0.02798160, 0.01332457, 0.00545096 ,0,0,0,0,0,0,0,0,0} ; | |
#define BLUR_DEPTH_FALLOFF 100.0 | |
#define BILATERAL_BLUR | |
real3 frag (v2f i) : SV_Target | |
{ | |
real3 col =0; | |
real3 accumResult =0; | |
real accumWeights=0; | |
//depth at the current pixel | |
real depthCenter; | |
#if UNITY_REVERSED_Z | |
depthCenter = SampleSceneDepth(i.uv); | |
#else | |
// Adjust z to match NDC for OpenGL | |
depthCenter = lerp(UNITY_NEAR_CLIP_VALUE, 1, SampleSceneDepth(i.uv)); | |
#endif | |
for(real index=-_GaussSamples;index<=_GaussSamples;index++){ | |
//we offset our uvs by a tiny amount | |
real2 uv= i.uv+real2( index*_GaussAmount/1000,0); | |
//sample the color at that location | |
real3 kernelSample = tex2D(_MainTex, uv); | |
//depth at the sampled pixel | |
real depthKernel; | |
#if UNITY_REVERSED_Z | |
depthKernel = SampleSceneDepth(uv); | |
#else | |
// Adjust z to match NDC for OpenGL | |
depthKernel = lerp(UNITY_NEAR_CLIP_VALUE, 1, SampleSceneDepth(uv)); | |
#endif | |
//weight calculation depending on distance and depth difference | |
real depthDiff = abs(depthKernel-depthCenter); | |
real r2= depthDiff*BLUR_DEPTH_FALLOFF; | |
real g = exp(-r2*r2); | |
real weight = g * gauss_filter_weights[abs(index)]; | |
//sum for every iteration of the color and weight of this sample | |
accumResult+=weight*kernelSample; | |
accumWeights+=weight; | |
} | |
//final color | |
col= accumResult/accumWeights; | |
return col; | |
} | |
ENDHLSL | |
} | |
Pass | |
{ | |
Name "Gaussian Blur y" | |
HLSLPROGRAM | |
#pragma vertex vert | |
#pragma fragment frag | |
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl" | |
struct appdata | |
{ | |
real4 vertex : POSITION; | |
real2 uv : TEXCOORD0; | |
}; | |
struct v2f | |
{ | |
real2 uv : TEXCOORD0; | |
real4 vertex : SV_POSITION; | |
}; | |
v2f vert (appdata v) | |
{ | |
v2f o; | |
o.vertex = TransformWorldToHClip(v.vertex); | |
o.uv = v.uv; | |
return o; | |
} | |
sampler2D _MainTex; | |
int _GaussSamples; | |
real _GaussAmount; | |
#define BLUR_DEPTH_FALLOFF 100.0 | |
static const real gauss_filter_weights[] = { 0.14446445, 0.13543542, 0.11153505, 0.08055309, 0.05087564, 0.02798160, 0.01332457, 0.00545096 } ; | |
#define BILATERAL_BLUR | |
real3 frag (v2f i) : SV_Target | |
{ | |
real3 col =0; | |
real3 accumResult =0; | |
real accumWeights=0; | |
//depth at the current pixel | |
real depthCenter; | |
#if UNITY_REVERSED_Z | |
depthCenter = SampleSceneDepth(i.uv); | |
#else | |
// Adjust z to match NDC for OpenGL | |
depthCenter = lerp(UNITY_NEAR_CLIP_VALUE, 1, SampleSceneDepth(i.uv)); | |
#endif | |
for(real index=-_GaussSamples;index<=_GaussSamples;index++){ | |
//we offset our uvs by a tiny amount | |
real2 uv= i.uv+real2(0, index*_GaussAmount/1000); | |
//sample the color at that location | |
real3 kernelSample = tex2D(_MainTex, uv); | |
//depth at the sampled pixel | |
real depthKernel; | |
#if UNITY_REVERSED_Z | |
depthKernel = SampleSceneDepth(uv); | |
#else | |
// Adjust z to match NDC for OpenGL | |
depthKernel = lerp(UNITY_NEAR_CLIP_VALUE, 1, SampleSceneDepth(uv)); | |
#endif | |
//weight calculation depending on distance and depth difference | |
real depthDiff = abs(depthKernel-depthCenter); | |
real r2= depthDiff*BLUR_DEPTH_FALLOFF; | |
real g = exp(-r2*r2); | |
real weight = g * gauss_filter_weights[abs(index)]; | |
//sum for every iteration of the color and weight of this sample | |
accumResult+=weight*kernelSample; | |
accumWeights+=weight; | |
} | |
//final color | |
col= accumResult/accumWeights; | |
return col; | |
} | |
ENDHLSL | |
} | |
Pass | |
{ | |
Name "Compositing" | |
HLSLPROGRAM | |
#pragma vertex vert | |
#pragma fragment frag | |
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl" | |
struct appdata | |
{ | |
real4 vertex : POSITION; | |
real2 uv : TEXCOORD0; | |
}; | |
struct v2f | |
{ | |
real2 uv : TEXCOORD0; | |
real4 vertex : SV_POSITION; | |
}; | |
v2f vert (appdata v) | |
{ | |
v2f o; | |
o.vertex = TransformWorldToHClip(v.vertex); | |
o.uv = v.uv; | |
return o; | |
} | |
sampler2D _MainTex; | |
TEXTURE2D (_volumetricTexture); | |
SAMPLER(sampler_volumetricTexture); | |
TEXTURE2D (_LowResDepth); | |
SAMPLER(sampler_LowResDepth); | |
real4 _SunMoonColor; | |
real _Intensity; | |
real _Downsample; | |
real3 frag (v2f i) : SV_Target | |
{ | |
real3 col = 0; | |
//based on https://eleni.mutantstargoat.com/hikiko/on-depth-aware-upsampling/ | |
int offset =0; | |
real d0 = SampleSceneDepth(i.uv); | |
/* calculating the distances between the depths of the pixels | |
* in the lowres neighborhood and the full res depth value | |
* (texture offset must be compile time constant and so we | |
* can't use a loop) | |
*/ | |
real d1 = _LowResDepth.Sample(sampler_LowResDepth, i.uv, int2(0, 1)).x; | |
real d2 = _LowResDepth.Sample(sampler_LowResDepth, i.uv, int2(0, -1)).x; | |
real d3 =_LowResDepth.Sample(sampler_LowResDepth, i.uv, int2(1, 0)).x; | |
real d4 = _LowResDepth.Sample(sampler_LowResDepth, i.uv, int2(-1, 0)).x; | |
d1 = abs(d0 - d1); | |
d2 = abs(d0 - d2); | |
d3 = abs(d0 - d3); | |
d4 = abs(d0 - d4); | |
real dmin = min(min(d1, d2), min(d3, d4)); | |
if (dmin == d1) | |
offset= 0; | |
else if (dmin == d2) | |
offset= 1; | |
else if (dmin == d3) | |
offset= 2; | |
else if (dmin == d4) | |
offset= 3; | |
switch(offset){ | |
case 0: | |
col = _volumetricTexture.Sample(sampler_volumetricTexture, i.uv, int2(0, 1)); | |
break; | |
case 1: | |
col = _volumetricTexture.Sample(sampler_volumetricTexture, i.uv, int2(0, -1)); | |
break; | |
case 2: | |
col = _volumetricTexture.Sample(sampler_volumetricTexture, i.uv, int2(1, 0)); | |
break; | |
case 3: | |
col = _volumetricTexture.Sample(sampler_volumetricTexture, i.uv, int2(-1, 0)); | |
break; | |
default: | |
col = _volumetricTexture.Sample(sampler_volumetricTexture, i.uv); | |
break; | |
} | |
real3 finalShaft =saturate (col)* normalize (_SunMoonColor)*_Intensity; | |
real3 screen = tex2D(_MainTex,i.uv); | |
return screen+finalShaft; | |
} | |
ENDHLSL | |
} | |
Pass | |
{ | |
Name "SampleDepth" | |
HLSLPROGRAM | |
#pragma vertex vert | |
#pragma fragment frag | |
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/DeclareDepthTexture.hlsl" | |
struct appdata | |
{ | |
real4 vertex : POSITION; | |
real2 uv : TEXCOORD0; | |
}; | |
struct v2f | |
{ | |
real2 uv : TEXCOORD0; | |
real4 vertex : SV_POSITION; | |
}; | |
v2f vert (appdata v) | |
{ | |
v2f o; | |
o.vertex = TransformWorldToHClip(v.vertex); | |
o.uv = v.uv; | |
return o; | |
} | |
real frag (v2f i) : SV_Target | |
{ | |
#if UNITY_REVERSED_Z | |
real depth = SampleSceneDepth(i.uv); | |
#else | |
// Adjust z to match NDC for OpenGL | |
real depth = lerp(UNITY_NEAR_CLIP_VALUE, 1, SampleSceneDepth(i.uv)); | |
#endif | |
return depth; | |
} | |
ENDHLSL | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment