Skip to content

Instantly share code, notes, and snippets.

@runevision
Last active May 3, 2024 13:32
Show Gist options
  • Save runevision/0bde31108e140bb019bfc197236b5177 to your computer and use it in GitHub Desktop.
Save runevision/0bde31108e140bb019bfc197236b5177 to your computer and use it in GitHub Desktop.
Water Foam Particle Shader
UNITY_DECLARE_SHADOWMAP(_SunCascadedShadowMap);
float4 _SunCascadedShadowMap_TexelSize;
#define GET_CASCADE_WEIGHTS(wpos, z) getCascadeWeights_splitSpheres(wpos)
#define GET_SHADOW_FADE(wpos, z) getShadowFade_SplitSpheres(wpos)
#define GET_SHADOW_COORDINATES(wpos,cascadeWeights) getShadowCoord(wpos,cascadeWeights)
/**
* Gets the cascade weights based on the world position of the fragment and the poisitions of the split spheres for each cascade.
* Returns a float4 with only one component set that corresponds to the appropriate cascade.
*/
inline fixed4 getCascadeWeights_splitSpheres(float3 wpos)
{
float3 fromCenter0 = wpos.xyz - unity_ShadowSplitSpheres[0].xyz;
float3 fromCenter1 = wpos.xyz - unity_ShadowSplitSpheres[1].xyz;
float3 fromCenter2 = wpos.xyz - unity_ShadowSplitSpheres[2].xyz;
float3 fromCenter3 = wpos.xyz - unity_ShadowSplitSpheres[3].xyz;
float4 distances2 = float4(dot(fromCenter0,fromCenter0), dot(fromCenter1,fromCenter1), dot(fromCenter2,fromCenter2), dot(fromCenter3,fromCenter3));
fixed4 weights = float4(distances2 < unity_ShadowSplitSqRadii);
weights.yzw = saturate(weights.yzw - weights.xyz);
return weights;
}
/**
* Returns the shadow fade based on the world position of the fragment, and the distance from the shadow fade center
*/
inline float getShadowFade_SplitSpheres( float3 wpos )
{
float sphereDist = distance(wpos.xyz, unity_ShadowFadeCenterAndType.xyz);
half shadowFade = saturate(sphereDist * _LightShadowData.z + _LightShadowData.w);
return shadowFade;
}
/**
* Returns the shadowmap coordinates for the given fragment based on the world position and z-depth.
* These coordinates belong to the shadowmap atlas that contains the maps for all cascades.
*/
inline float4 getShadowCoord( float4 wpos, fixed4 cascadeWeights )
{
float3 sc0 = mul (unity_WorldToShadow[0], wpos).xyz;
float3 sc1 = mul (unity_WorldToShadow[1], wpos).xyz;
float3 sc2 = mul (unity_WorldToShadow[2], wpos).xyz;
float3 sc3 = mul (unity_WorldToShadow[3], wpos).xyz;
return float4(sc0 * cascadeWeights[0] + sc1 * cascadeWeights[1] + sc2 * cascadeWeights[2] + sc3 * cascadeWeights[3], 1);
}
/**
* Combines the different components of a shadow coordinate and returns the final coordinate.
*/
inline float3 combineShadowcoordComponents (float2 baseUV, float2 deltaUV, float depth, float2 receiverPlaneDepthBias)
{
float3 uv = float3( baseUV + deltaUV, depth );
uv.z += dot (deltaUV, receiverPlaneDepthBias); // apply the depth bias
return uv;
}
/**
* PCF shadowmap filtering based on a 3x3 kernel (optimized with 4 taps)
*
* Algorithm: http://the-witness.net/news/2013/09/shadow-mapping-summary-part-1/
* Implementation example: http://mynameismjp.wordpress.com/2013/09/10/shadow-maps/
*/
half sampleShadowmap_PCF3x3 (float4 coord, float2 receiverPlaneDepthBias)
{
const float2 offset = float2(0.5,0.5);
float2 uv = (coord.xy * _SunCascadedShadowMap_TexelSize.zw) + offset;
float2 base_uv = (floor(uv) - offset) * _SunCascadedShadowMap_TexelSize.xy;
float2 st = frac(uv);
float2 uw = float2( 3-2*st.x, 1+2*st.x );
float2 u = float2( (2-st.x) / uw.x - 1, (st.x)/uw.y + 1 );
u *= _SunCascadedShadowMap_TexelSize.x;
float2 vw = float2( 3-2*st.y, 1+2*st.y );
float2 v = float2( (2-st.y) / vw.x - 1, (st.y)/vw.y + 1);
v *= _SunCascadedShadowMap_TexelSize.y;
half shadow;
half sum = 0;
sum += uw[0] * vw[0] * UNITY_SAMPLE_SHADOW( _SunCascadedShadowMap, combineShadowcoordComponents( base_uv, float2(u[0], v[0]), coord.z, receiverPlaneDepthBias) );
sum += uw[1] * vw[0] * UNITY_SAMPLE_SHADOW( _SunCascadedShadowMap, combineShadowcoordComponents( base_uv, float2(u[1], v[0]), coord.z, receiverPlaneDepthBias) );
sum += uw[0] * vw[1] * UNITY_SAMPLE_SHADOW( _SunCascadedShadowMap, combineShadowcoordComponents( base_uv, float2(u[0], v[1]), coord.z, receiverPlaneDepthBias) );
sum += uw[1] * vw[1] * UNITY_SAMPLE_SHADOW( _SunCascadedShadowMap, combineShadowcoordComponents( base_uv, float2(u[1], v[1]), coord.z, receiverPlaneDepthBias) );
shadow = sum / 16.0f;
shadow = lerp (_LightShadowData.r, 1.0f, shadow);
return shadow;
}
/**
* PCF shadowmap filtering based on a 5x5 kernel (optimized with 9 taps)
*
* Algorithm: http://the-witness.net/news/2013/09/shadow-mapping-summary-part-1/
* Implementation example: http://mynameismjp.wordpress.com/2013/09/10/shadow-maps/
*/
half sampleShadowmap_PCF5x5 (float4 coord, float2 receiverPlaneDepthBias)
{
const float2 offset = float2(0.5,0.5);
float2 uv = (coord.xy * _SunCascadedShadowMap_TexelSize.zw) + offset;
float2 base_uv = (floor(uv) - offset) * _SunCascadedShadowMap_TexelSize.xy;
float2 st = frac(uv);
float3 uw = float3( 4-3*st.x, 7, 1+3*st.x );
float3 u = float3( (3-2*st.x) / uw.x - 2, (3+st.x)/uw.y, st.x/uw.z + 2 );
u *= _SunCascadedShadowMap_TexelSize.x;
float3 vw = float3( 4-3*st.y, 7, 1+3*st.y );
float3 v = float3( (3-2*st.y) / vw.x - 2, (3+st.y)/vw.y, st.y/vw.z + 2 );
v *= _SunCascadedShadowMap_TexelSize.y;
half shadow;
half sum = 0.0f;
half3 accum = uw * vw.x;
sum += accum.x * UNITY_SAMPLE_SHADOW( _SunCascadedShadowMap, combineShadowcoordComponents( base_uv, float2(u.x,v.x), coord.z, receiverPlaneDepthBias) );
sum += accum.y * UNITY_SAMPLE_SHADOW( _SunCascadedShadowMap, combineShadowcoordComponents( base_uv, float2(u.y,v.x), coord.z, receiverPlaneDepthBias) );
sum += accum.z * UNITY_SAMPLE_SHADOW( _SunCascadedShadowMap, combineShadowcoordComponents( base_uv, float2(u.z,v.x), coord.z, receiverPlaneDepthBias) );
accum = uw * vw.y;
sum += accum.x * UNITY_SAMPLE_SHADOW( _SunCascadedShadowMap, combineShadowcoordComponents( base_uv, float2(u.x,v.y), coord.z, receiverPlaneDepthBias) );
sum += accum.y * UNITY_SAMPLE_SHADOW( _SunCascadedShadowMap, combineShadowcoordComponents( base_uv, float2(u.y,v.y), coord.z, receiverPlaneDepthBias) );
sum += accum.z * UNITY_SAMPLE_SHADOW( _SunCascadedShadowMap, combineShadowcoordComponents( base_uv, float2(u.z,v.y), coord.z, receiverPlaneDepthBias) );
accum = uw * vw.z;
sum += accum.x * UNITY_SAMPLE_SHADOW( _SunCascadedShadowMap, combineShadowcoordComponents( base_uv, float2(u.x,v.z), coord.z, receiverPlaneDepthBias) );
sum += accum.y * UNITY_SAMPLE_SHADOW( _SunCascadedShadowMap, combineShadowcoordComponents( base_uv, float2(u.y,v.z), coord.z, receiverPlaneDepthBias) );
sum += accum.z * UNITY_SAMPLE_SHADOW( _SunCascadedShadowMap, combineShadowcoordComponents( base_uv, float2(u.z,v.z), coord.z, receiverPlaneDepthBias) );
shadow = sum / 144.0f;
shadow = lerp (_LightShadowData.r, 1.0f, shadow);
return shadow;
}
/**
* Samples the shadowmap at the given coordinates.
*/
half unity_sampleShadowmap( float4 coord )
{
half shadow = UNITY_SAMPLE_SHADOW(_SunCascadedShadowMap,coord);
shadow = lerp(_LightShadowData.r, 1.0, shadow);
return shadow;
}
///////////////////
/**
* Gets the shadows attenuations at world positions.
*/
half GetSunShadowsAttenuation(float3 worldPositions, float screenDepth)
{
fixed4 cascadeWeights = GET_CASCADE_WEIGHTS(worldPositions.xyz, screenDepth);
return unity_sampleShadowmap(GET_SHADOW_COORDINATES(float4(worldPositions, 1), cascadeWeights));
}
half GetSunShadowsAttenuation_PCF3x3(float3 worldPositions, float screenDepth, float receiverPlaneDepthBias)
{
fixed4 cascadeWeights = GET_CASCADE_WEIGHTS(worldPositions.xyz, screenDepth);
return sampleShadowmap_PCF3x3(GET_SHADOW_COORDINATES(float4(worldPositions, 1), cascadeWeights), receiverPlaneDepthBias);
}
half GetSunShadowsAttenuation_PCF5x5(float3 worldPositions, float screenDepth, float receiverPlaneDepthBias)
{
fixed4 cascadeWeights = GET_CASCADE_WEIGHTS(worldPositions.xyz, screenDepth);
return sampleShadowmap_PCF5x5(GET_SHADOW_COORDINATES(float4(worldPositions, 1), cascadeWeights), receiverPlaneDepthBias);
}
Shader "Custom/WaterFoamParticle" {
Properties {
_BumpSphereScale ("Sphere Normal Scale", Float) = 1.0
_BumpNoiseScale ("Noise Normal Scale", Float) = 1.0
_ShadowOffsetMult ("Shadow Offset Mult", Range(0, 1)) = 0.5
_MainTex ("Noise", 2D) = "white" {}
_EdgeToNoiseFreq ("Edge To Noise Freq", Float) = 5.0
_NoiseStrength ("Noise Strength", Range(0, 1)) = 1.0
_Radius ("Radius", Range(0, 1)) = 1.0
_Sharpness ("Sharpness", Range(1, 20)) = 1.0
_Scroll ("Scroll", Range(0, 1)) = 1.0
//[Header(Colors)]
//_FoamTint ("Foam Tint", Color) = (1,1,1,1)
[Header(Lighting Hacks)]
_FoamIndirectLightMultiplier ("Foam Ambient Multiplier", Range(0, 20)) = 7
[Header(Material Properties)]
_Glossiness ("Smoothness", Range(0,1)) = 0.5
_Metallic ("Metallic", Range(0,1)) = 0.0
}
SubShader {
Tags { "RenderType"="Opaque" }
LOD 200
CGPROGRAM
#pragma surface surf Water vertex:vert alpha:premul
#pragma target 3.0
#define _ALPHAPREMULTIPLY_ON 1
#include "Lighting.cginc"
#include "Shadows.cginc"
#include "UnityPBSLighting.cginc"
// Textures
sampler2D _MainTex;
// Colors
fixed3 _FoamTint;
// Values
half _BumpSphereScale, _BumpNoiseScale;
half _NoiseStrength;
half _ShadowOffsetMult;
half _EdgeToNoiseFreq;
half _Radius;
half _Sharpness;
half _FoamIndirectLightMultiplier;
float _Scroll;
fixed _Glossiness;
fixed _Metallic;
struct appdata_t {
float4 vertex : POSITION;
float4 tangent : TANGENT;
float3 normal : NORMAL;
float4 texcoord : TEXCOORD0;
float4 texcoord1 : TEXCOORD1;
float4 texcoord2 : TEXCOORD2;
float4 texcoord3 : TEXCOORD3;
fixed4 color : COLOR;
};
struct Input {
float2 uv_MainTex;
float2 rawUV;
float4 color : COLOR;
float4 screenPos;
float3 worldPos;
float3 worldNormal; INTERNAL_DATA
half size;
};
struct SurfaceOutputWater {
fixed3 Albedo; // base (diffuse or specular) color
float3 Normal; // tangent space normal, if written
half3 Emission;
half Metallic; // 0=non-metal, 1=metal
// Smoothness is the user facing name, it should be perceptual smoothness but user should not have to deal with it.
// Everywhere in the code you meet smoothness it is perceptual smoothness
half Smoothness; // 0=rough, 1=smooth
half Occlusion; // occlusion (default 1)
fixed Alpha; // alpha for transparencies
// Added:
fixed ShadowAttenuation;
};
void vert (inout appdata_t v, out Input o) {
UNITY_INITIALIZE_OUTPUT(Input,o);
o.rawUV = v.texcoord.xy;
o.size = v.texcoord.z;
}
inline half4 LightingWater (SurfaceOutputWater s, float3 viewDir, UnityGI gi) {
s.Normal = normalize(s.Normal);
// shader relies on pre-multiply alpha-blend (_SrcBlend = One, _DstBlend = OneMinusSrcAlpha)
// this is necessary to handle transparency in physically correct way - only diffuse component gets affected by alpha
s.Albedo *= s.Alpha;
// Apply shadow to light contribution
gi.light.color *= s.ShadowAttenuation;
// Boost indirect lighting in shadow
gi.indirect.diffuse *= _FoamIndirectLightMultiplier;
// Remove specular/reflection from foam
gi.indirect.specular = 0;
half4 c = UNITY_BRDF_PBS (s.Albedo, 0, 0, s.Smoothness, s.Normal, viewDir, gi.light, gi.indirect);
c.a = s.Alpha;
return c;
}
inline void LightingWater_GI (SurfaceOutputWater s, UnityGIInput data, inout UnityGI gi) {
#if defined(UNITY_PASS_DEFERRED) && UNITY_ENABLE_REFLECTION_BUFFERS
gi = UnityGlobalIllumination(data, s.Occlusion, s.Normal);
#else
Unity_GlossyEnvironmentData g = UnityGlossyEnvironmentSetup(s.Smoothness, data.worldViewDir, s.Normal, lerp(unity_ColorSpaceDielectricSpec.rgb, s.Albedo, s.Metallic));
gi = UnityGlobalIllumination(data, s.Occlusion, s.Normal, g);
#endif
}
float3 GetNormalFromPattern (float pattern, float scale, float2 uv) {
float4 xyzw = float4(ddx(uv), ddy(uv));
float determinant = 1 / (xyzw.x * xyzw.w - xyzw.y * xyzw.z);
float4 inverse = float4(xyzw.w, -xyzw.y, -xyzw.z, xyzw.x) * determinant;
float2 deriv = float2(-ddx(pattern), -ddy(pattern));
float2 transformed = float2(dot(deriv, inverse.xy), dot(deriv, inverse.zw));
return normalize(float3(transformed, 1.0 / scale));
}
void surf (Input IN, inout SurfaceOutputWater o) {
fixed2 dirFromCenter = IN.rawUV - 0.5;
float radial = 1 - 2 * length(dirFromCenter);
float2 noiseUV = IN.uv_MainTex + IN.color.xy;
noiseUV += frac(float2(_Time.z, _Time.w) * _Scroll);
float noise = tex2D(_MainTex, noiseUV).a - 0.5;
// Shape
float val = lerp (radial - 0.5 + _Radius, 0.5 + (noise) * _EdgeToNoiseFreq, _NoiseStrength);
val = lerp(0.5, val, _Sharpness);
// Reduce based on particle alpha
half sizeOverLifetime = IN.color.a;
half maxPossibleValue = lerp(0.5 + _Radius, 0.5 + 0.4 * _EdgeToNoiseFreq, _NoiseStrength);
maxPossibleValue = lerp(0.5, maxPossibleValue, _Sharpness);
val -= (1 - sizeOverLifetime) * maxPossibleValue;
// Restrict to circular area and smoothen
val *= max(radial, smoothstep(0, 1, radial * _Sharpness * 0.5));
val = sqrt(val);
val = smoothstep(0, 1, val);
// Calculate normal
float heightForNormal = _BumpSphereScale / length(float3(dirFromCenter, 1))
+ noise * _EdgeToNoiseFreq * _BumpNoiseScale;
float3 normal = GetNormalFromPattern(heightForNormal, 1, IN.rawUV);
// World space variables
fixed offset = (1 - val) * IN.size * _ShadowOffsetMult;
float3 positionWS = IN.worldPos;
float3 viewDirWS = normalize(UnityWorldSpaceViewDir(positionWS));
float3 normalRawWS = WorldNormalVector (IN, float3(0, 0, 1));
float headOn = 0.1 + 0.9 * saturate(dot(viewDirWS, normalRawWS));
// Shadow
float3 shadowOffset = -viewDirWS;
float shadowAttenuation =
GetSunShadowsAttenuation_PCF5x5(positionWS + shadowOffset * offset, IN.screenPos.z, 0).x;
// Output
o.Albedo = 0.5;
o.Alpha = val;
o.Normal = normal;
//o.Fade = val;
o.ShadowAttenuation = shadowAttenuation;
o.Metallic = _Metallic;
o.Smoothness = _Glossiness;
}
ENDCG
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment