Skip to content

Instantly share code, notes, and snippets.

@QuadStorm
Last active January 10, 2021 06:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save QuadStorm/483535c3394f8ec7336efc8b1806e16c to your computer and use it in GitHub Desktop.
Save QuadStorm/483535c3394f8ec7336efc8b1806e16c to your computer and use it in GitHub Desktop.
CHSS Shader for Blockland, version 460
#version 460
// Stealth Commander/QuadStorm's Ulimate Shader (2021-01-10)
// Built from Port's Poisson Disc (Optimized) Soft Shadow Shader (2015)
// Includes a Contact-Hardening/Percentage-Closer/Perceptually-Correct Soft Shadows (CHSS/PCSS) implementation
/* Relevant Files
CSM
https://developer.download.nvidia.com/SDK/10.5/opengl/src/cascaded_shadow_maps/doc/cascaded_shadow_maps.pdf
CHSS/PCSS
https://web.archive.org/web/20050903002824/http://www.randima.com:80/MastersThesis.pdf
https://http.download.nvidia.com/developer/presentations/2005/SIGGRAPH/Percentage_Closer_Soft_Shadows.pdf
https://developer.download.nvidia.com/presentations/2008/GDC/GDC08_SoftShadowMapping.pdf
https://developer.download.nvidia.com/shaderlibrary/docs/shadow_PCSS.pdf
https://www.realtimeshadows.com/sites/default/files/sig2013-course-softshadows.pdf
https://maxest.gct-game.net/content/chss.pdf
*/
// Default Shadows
// Ignore all soft shadow options and use default (simplest) shadowing
bool defaultShadows = false;
// Shadow Opacity Adjustment
// https://forum.blockland.us/index.php?topic=289446.0
bool shadowOpacity = true;
// Shadow Opacity (1.0f = default full opacity; 0.0f = fully transparent shadows ('minimum' shaders but much slower))
float occlusionBlend = 0.9f;
// Blending Factor
// "This is blending. Shadows are rendered to separate layers based on distance."
// "This may cause shadows to suddenly change appearance. Use this to change how long a distance they will "fade" between the two versions over."
float blendAlpha = 0.9f; // bl default is 0.9f
float blendBeta = 1.0f - blendAlpha;
// Fudge Factors (Shadow Bias)
// "These values are very important. If they're too low, you will see weird patterns and waves everywhere."
// "If they're too high, shadows will be disconnected from their objects. They need to be adjusted carefully."
// "These are set specifically for Max quality with max drawing distance. You'll need to change them based on your shader quality (and if you changed the Poisson disk below.. probably)."
const float fudgeFactor1 = 0.125f; //0.15f
const float fudgeFactor2 = 0.125f; //0.375f
const float fudgeFactor3 = 0.125f; //1.05f
const float fudgeFactor4 = 0.125f; //4.0f
/*
const float fudgeFactor1 = 0.1f;
const float fudgeFactor2 = 0.25f;
const float fudgeFactor3 = 0.7f;
const float fudgeFactor4 = 2.66f;
*/
// Dynamic Fudge Factor
// https://learnopengl.com/Advanced-Lighting/Shadows/Shadow-Mapping
// Disable if shadow peter-panning is worse to you instead of acne
bool dynamicFF = true;
// How much more the fudge factor changes with angle
float dynamicFudgeFactor = 5.0f;
// Sample Distance
// "How soft should the shadows be? (how far out does the edge go)"
float sampleDistance = 1.0f / 1000.0f;
// Cascaded Shadow Map Adjustment
// Changes shadow softness to be roughly equal for each shadow cascade
bool csmAdjustmentEnabled = true;
// Contact-Hardening Soft Shadows (Percentage-Closer/Perceptually-Correct Soft Shadows)
// Varies shadow penumbra by distance (sampleDistance ignored)
bool CHSS = true;
// Vogel Disk Blocker Search Sample Count
int VogelDiskSearchSampleCount = 5;
// Use nearPlane to calculate the CHSS Blocker Search distance rather than inputting a distance
bool searchDistanceNearPlane = true;
// Distance for the CHSS Blocker Search if nearPlane is not used
float blockerSearchDistance = 1.0f / 250.0f;
// Near plane for similar triangles blocker search
float nearPlane = 0.4f;
// Whether to adjust the blocker search distances by shadow cascades (similar to csmAdjustmentEnabled)
bool blockerSearchAdjustment = true;
// How large the light source is -- affects how quickly shadows become soft with distance; change as needed with context (0.005f - 0.25f)
float lightSize = 0.05f;
// CHSS penumbra size maximum, that will only allows shadows to match the size of the blocker search
bool chssMaxDistance = false;
// Vogel Disk Sample Count for Shadow Filtering
int VogelDiskFilterSampleCount = 5;
// Use default shadows in particles - h/t Hata's Shader Toggle
bool particleOptimization = true;
// Fancy Fog: somewhat more realistic fog falloff + height fog
bool fancyFog = true;
// Decay of fog with height
float fogHeightFactor = 0.01f;
// Superglow: Off-like glow effect h/t Pah1023 (https://forum.blockland.us/index.php?topic=316738.msg9781138#msg9781138)
bool superGlow = true;
// Specular Power: control the size of the specular highlight; larger values -> smaller specular highlight
float specularPower = 12.0f;
// Adjust the brightness of the specular effect
float dirLightSpecularMultiplier = 0.5f;
// Light Brightness
float lightRatio = 0.4f;
// Varying.
in vec4 vPos;
in vec3 worldNormal;
in vec3 worldPos;
// Global directional light uniforms.
uniform vec4 dirLightDir;
uniform vec4 dirLightColor;
uniform vec4 dirLightAmbient;
uniform vec4 dirShadowColor;
// Misc uniforms.
uniform vec3 camPos;
uniform mat4 obj2World;
uniform mat4 world2Cam;
uniform mat4 gl_TextureMatrix[4];
uniform bool isParticle;
uniform int doColorMultiply;
uniform int glow;
uniform sampler2DArray stex;
uniform sampler2D tex;
// Surface calculations, including specular power.
in vec2 texCoord;
vec4 viewDelta;
float specular;
float NdotL;
vec3 reflectVec;
in vec4 gl_Color;
void calculateSurface(vec4 color, inout vec4 albedo)
{
viewDelta.xyz = worldPos - camPos;
viewDelta.w = length(viewDelta.xyz);
viewDelta.xyz = -normalize(viewDelta.xyz);
vec4 texAlbedo = texture(tex, texCoord);
albedo.rgb = mix(color.rgb, texAlbedo.rgb, texAlbedo.a);
if(doColorMultiply == 1)
albedo *= gl_Color;
albedo.a = color.a;
NdotL = max(dot(worldNormal, dirLightDir.xyz), 0.0f);
reflectVec = normalize(reflect(-dirLightDir.xyz, worldNormal));
specular = pow(max(dot(reflectVec, viewDelta.xyz), 0.0f), specularPower) * length(texAlbedo.rgb);
}
// Fogging.
uniform vec4 fogBaseColor;
uniform vec4 fogConsts;
uniform sampler2D fogTex;
in vec2 fogCoords;
void applyFog(inout vec4 albedo, in float occlusionFactor)
{
// Calculate fog.
vec4 fogColor = texture(fogTex, fogCoords) * fogBaseColor;
// Blend it.
if (fancyFog)
{
albedo = mix(albedo, fogColor, (0.5f + 0.5f * sin(fogColor.a * 3.141592654f - 1.570796327f)) * (1 - min(worldPos.z * fogHeightFactor, 1.0f)));
}
else
{
albedo = mix(albedo, fogColor, fogColor.a);
}
}
// Shadowing
uniform vec4 far_d;
uniform vec2 texSize; // x - size, y - 1/size
uniform vec4 zScale;
uniform int shadowSplitCount;
void calculateShadowCoords(inout vec4 shadow_coordA, inout vec4 shadow_coordB, out float blend)
{
int index = 3;
float fudgeFactorA = 0.0f;
float fudgeFactorB = 0.0f;
fudgeFactorA = fudgeFactor4 / zScale.w;
fudgeFactorB = fudgeFactor4 / zScale.w;
blend = 0.0f;
// find the appropriate depth map to look up in based on the depth of this fragment
if(vPos.y < far_d.x)
{
index = 0;
if(shadowSplitCount > 1)
blend = clamp( (vPos.y - (far_d.x * blendAlpha)) / (far_d.x * blendBeta), 0.0f, 1.0f);
fudgeFactorA = fudgeFactor1 / zScale.x;
fudgeFactorB = fudgeFactor2 / zScale.y;
}
else if(vPos.y < far_d.y)
{
index = 1;
if(shadowSplitCount > 2)
blend = clamp( (vPos.y - (far_d.y * blendAlpha)) / (far_d.x * blendBeta), 0.0f, 1.0f);
fudgeFactorA = fudgeFactor2 / zScale.y;
fudgeFactorB = fudgeFactor3 / zScale.z;
}
else if(vPos.y < far_d.z)
{
index = 2;
if(shadowSplitCount > 3)
blend = clamp( (vPos.y - (far_d.z * blendAlpha)) / (far_d.x * blendBeta), 0.0f, 1.0f);
fudgeFactorA = fudgeFactor3 / zScale.z;
fudgeFactorB = fudgeFactor4 / zScale.w;
}
// transform this fragment's position from view space to scaled light clip space
// such that the xy coordinates are in [0;1]
// note there is no need to divide by w for orthogonal light sources
shadow_coordA = gl_TextureMatrix[index] * vPos;
if (dynamicFF)
{
shadow_coordA.w = shadow_coordA.z - max(dynamicFudgeFactor * fudgeFactorA * (1.0 - dot(worldNormal, dirLightDir.xyz)), fudgeFactorA); // Figure the input coordinate for PCF sampling if appropriate.
}
else
{
shadow_coordA.w = shadow_coordA.z - fudgeFactorA; // Figure the input coordinate for PCF sampling if appropriate.
}
shadow_coordA.z = float(index); // Encode the layer to sample.
//don't have to set second shadow coord if we're not blending
if(blend > 0.0f)
{
shadow_coordB = gl_TextureMatrix[index + 1] * vPos;
if (dynamicFF)
{
shadow_coordB.w = shadow_coordB.z - max(dynamicFudgeFactor * fudgeFactorB * (1.0 - dot(worldNormal, dirLightDir.xyz)), fudgeFactorB);
}
else
{
shadow_coordB.w = shadow_coordB.z - fudgeFactorB;
}
shadow_coordB.z = float(index + 1);
}
}
// Point lighting
uniform vec4 pointLightPos0;
uniform vec4 pointLightColor0;
uniform float pointLightRadius0;
uniform vec4 pointLightPos1;
uniform vec4 pointLightColor1;
uniform float pointLightRadius1;
uniform vec4 pointLightPos2;
uniform vec4 pointLightColor2;
uniform float pointLightRadius2;
uniform vec4 pointLightPos3;
uniform vec4 pointLightColor3;
uniform float pointLightRadius3;
uniform vec4 pointLightPos4;
uniform vec4 pointLightColor4;
uniform float pointLightRadius4;
uniform vec4 pointLightPos5;
uniform vec4 pointLightColor5;
uniform float pointLightRadius5;
uniform vec4 pointLightPos6;
uniform vec4 pointLightColor6;
uniform float pointLightRadius6;
uniform vec4 pointLightPos7;
uniform vec4 pointLightColor7;
uniform float pointLightRadius7;
vec4 accumulatePointLights()
{
vec4 pointLightTotal = vec4(0.0f);
vec3 lightDelta = vec3(0.0f);
float lightDot = 0.0f;
float ratio = 0.0f;
// Calculate effects of the 8 point lights.
lightDelta = worldPos.xyz - pointLightPos0.xyz;
lightDot = max(dot(-normalize(lightDelta), worldNormal), 0.0f);
ratio = 1.0f - (length(lightDelta) / pointLightRadius0);
ratio = pow(ratio, 3) * lightRatio;
ratio = max(ratio, 0.0f);
pointLightTotal.xyz += ratio * lightDot * pointLightColor0.xyz;
lightDelta = worldPos.xyz - pointLightPos1.xyz;
lightDot = max(dot(-normalize(lightDelta), worldNormal), 0.0f);
ratio = 1.0f - (length(lightDelta) / pointLightRadius1);
ratio = pow(ratio, 3) * lightRatio;
ratio = max(ratio, 0.0f);
pointLightTotal.xyz += ratio * lightDot * pointLightColor1.xyz;
lightDelta = worldPos.xyz - pointLightPos2.xyz;
lightDot = max(dot(-normalize(lightDelta), worldNormal), 0.0f);
ratio = 1.0f - (length(lightDelta) / pointLightRadius2);
ratio = pow(ratio, 3) * lightRatio;
ratio = max(ratio, 0.0f);
pointLightTotal.xyz += ratio * lightDot * pointLightColor2.xyz;
lightDelta = worldPos.xyz - pointLightPos3.xyz;
lightDot = max(dot(-normalize(lightDelta), worldNormal), 0.0f);
ratio = 1.0f - (length(lightDelta) / pointLightRadius3);
ratio = pow(ratio, 3) * lightRatio;
ratio = max(ratio, 0.0f);
pointLightTotal.xyz += ratio * lightDot * pointLightColor3.xyz;
lightDelta = worldPos.xyz - pointLightPos4.xyz;
lightDot = max(dot(-normalize(lightDelta), worldNormal), 0.0f);
ratio = 1.0f - (length(lightDelta) / pointLightRadius4);
ratio = pow(ratio, 3) * lightRatio;
ratio = max(ratio, 0.0f);
pointLightTotal.xyz += ratio * lightDot * pointLightColor4.xyz;
lightDelta = worldPos.xyz - pointLightPos5.xyz;
lightDot = max(dot(-normalize(lightDelta), worldNormal), 0.0f);
ratio = 1.0f - (length(lightDelta) / pointLightRadius5);
ratio = pow(ratio, 3) * lightRatio;
ratio = max(ratio, 0.0f);
pointLightTotal.xyz += ratio * lightDot * pointLightColor5.xyz;
lightDelta = worldPos.xyz - pointLightPos6.xyz;
lightDot = max(dot(-normalize(lightDelta), worldNormal), 0.0f);
ratio = 1.0f - (length(lightDelta) / pointLightRadius6);
ratio = pow(ratio, 3) * lightRatio;
ratio = max(ratio, 0.0f);
pointLightTotal.xyz += ratio * lightDot * pointLightColor6.xyz;
lightDelta = worldPos.xyz - pointLightPos7.xyz;
lightDot = max(dot(-normalize(lightDelta), worldNormal), 0.0f);
ratio = 1.0f - (length(lightDelta) / pointLightRadius7);
ratio = pow(ratio, 3) * lightRatio;
ratio = max(ratio, 0.0f);
pointLightTotal.xyz += ratio * lightDot * pointLightColor7.xyz;
return pointLightTotal;
}
vec4 accumulateParticlePointLights()
{
vec4 pointLightTotal = vec4(0.0f);
vec3 lightDelta = vec3(0.0f);
float ratio = 0.0f;
// Calculate effects of the 8 point lights.
lightDelta = worldPos.xyz - pointLightPos0.xyz;
ratio = 1.0f - (length(lightDelta) / pointLightRadius0);
ratio = pow(ratio, 3) * lightRatio;
ratio = max(ratio, 0.0f);
pointLightTotal.xyz += ratio * pointLightColor0.xyz;
lightDelta = worldPos.xyz - pointLightPos1.xyz;
ratio = 1.0f - (length(lightDelta) / pointLightRadius1);
ratio = pow(ratio, 3) * lightRatio;
ratio = max(ratio, 0.0f);
pointLightTotal.xyz += ratio * pointLightColor1.xyz;
return pointLightTotal;
}
// Combine specular and direct lighting terms.
// note: if we make combinedColor "out" only, it throws a potentially uninitialized value warning, so we've made it inout
void applyLighting(inout vec4 combinedColor, vec4 albedo, float occlusionFactor)
{
// large normal means glowing object
if(glow == 1 || (dot(worldNormal.xyz, vec3(1.0f))) > 2.0f)
{
combinedColor = superGlow ? vec4(albedo.xyz * 1.1f, albedo.a) : albedo;
return;
}
vec4 dirLightSpecular = occlusionFactor * specular * dirLightColor;
dirLightSpecular *= dirLightSpecularMultiplier; // arbitrary adjustment
vec4 dirLightDirect = mix(dirShadowColor, NdotL * dirLightColor + dirLightAmbient, occlusionFactor);
if(NdotL <= 0.04f)
{
dirLightDirect = dirShadowColor;
dirLightSpecular = vec4(0.0f);
}
else if(NdotL <= 0.1)
{
float val = (NdotL - 0.04f) / (0.1f - 0.04f);
dirLightDirect = mix(dirShadowColor, dirLightDirect, val);
dirLightSpecular = dirLightSpecular * val;
}
dirLightDirect += accumulatePointLights();
dirLightSpecular.a = length(dirLightSpecular.rgb);
dirLightDirect.a *= min(occlusionFactor + 0.75f, 1.0f);
combinedColor.rgb = dirLightDirect.rgb * albedo.rgb;
combinedColor.a = albedo.a;
combinedColor += dirLightSpecular;
}
// Below two functions adapted from https://www.gamedev.net/articles/programming/graphics/contact-hardening-soft-shadows-made-fast-r4906/
vec2 VogelDiskOffset(int sampleIndex, int samplesCount, float phi)
{
float GoldenAngle = 2.39996322972865332f;
float r = sqrt(sampleIndex + 0.5f) / sqrt(samplesCount);
float theta = sampleIndex * GoldenAngle + phi;
float sine, cosine;
sine = sin(theta);
cosine = cos(theta);
return r * vec2(cosine, sine);
}
float InterleavedGradientNoise(vec2 position_screen)
{
vec3 magic = vec3(0.06711056f, 0.00583715f, 52.9829189f);
return fract(magic.z * fract(dot(position_screen, magic.xy)));
}
float csmAdjustment(float index)
{
if (shadowSplitCount == 4)
{
if (index == 1)
return 0.333f;
else if (index == 2)
return 0.1f;
else if (index == 3)
return 0.033f;
}
else if (shadowSplitCount == 3)
{
if (index == 1)
return 0.333f;
else if (index == 2)
return 0.075f;
}
else if (shadowSplitCount == 2)
{
if (index == 1)
return 0.1f;
}
return 1.0f;
}
float basicShadow(vec4 shadow_coord)
{
return (texture(stex, vec3(shadow_coord)).x > shadow_coord.w) ? 1.0f : 0.0f;
}
float pcf(vec4 shadow_coord, float penumbraSize)
{
vec2 offset;
float index;
float sum = 0.0f;
if (csmAdjustmentEnabled)
{
index = shadow_coord.z;
penumbraSize *= bool(index) ? csmAdjustment(index) : 1.0f;
}
for (int i = 0; i < VogelDiskFilterSampleCount; i++)
{
offset = VogelDiskOffset(i, VogelDiskFilterSampleCount, InterleavedGradientNoise(round(fract(vec2(worldPos.xy * worldPos.yz)) * 2000)) * 6.28318530718f);
if (texture(stex, vec3(shadow_coord.xy + offset * penumbraSize, shadow_coord.z)).x > shadow_coord.w)
{
sum++;
}
}
return float(sum) / VogelDiskFilterSampleCount;
}
float shadowSampleCHSS(vec4 shadow_coord)
{
// Step 1: Blocker Search
float zBlocker;
float zReciever = shadow_coord.w;
float searchDistance;
float index = shadow_coord.z;
vec2 offset;
float blockerSum = 0;
int numBlockers = 0;
float avgBlockerDepth = 0;
float penumbraSize;
if (searchDistanceNearPlane)
searchDistance = lightSize * (zReciever - nearPlane) / zReciever * (blockerSearchAdjustment ? (bool(index) ? csmAdjustment(index) : 1.0f) : 1.0f);
else
searchDistance = blockerSearchDistance * (blockerSearchAdjustment ? (bool(index) ? csmAdjustment(index) : 1.0f) : 1.0f);
for (int i = 0; i < VogelDiskSearchSampleCount; i++)
{
offset = VogelDiskOffset(i, VogelDiskSearchSampleCount, InterleavedGradientNoise(fract(worldPos.xy * worldPos.yz) * 2000) * 6.28318530718f);
zBlocker = texture(stex, vec3(shadow_coord.xy + offset * searchDistance, shadow_coord.z)).x;
if (zBlocker < zReciever)
{
blockerSum += zBlocker;
numBlockers++;
}
}
avgBlockerDepth = blockerSum / numBlockers;
if (numBlockers == 0)
{
return basicShadow(shadow_coord);
}
// Step 2: Penumbra Size Calculation
penumbraSize = (zReciever - avgBlockerDepth) / avgBlockerDepth;
penumbraSize *= lightSize * (searchDistanceNearPlane ? nearPlane : 1.0f) / zReciever;
if (chssMaxDistance)
{
penumbraSize = min(penumbraSize, searchDistanceNearPlane ? (lightSize * (zReciever - nearPlane) / zReciever) : blockerSearchDistance);
}
// Step 3: Percentage-Closer Filtering
return pcf(shadow_coord, penumbraSize);
}
float shadowCoef(bool isParticle)
{
vec4 shadow_coordA = vec4(0.0f, 0.0f, 0.0f, 0.0f);
vec4 shadow_coordB = vec4(0.0f, 0.0f, 0.0f, 0.0f);
float blend = 0.0f;
calculateShadowCoords(shadow_coordA, shadow_coordB, blend);
float sampleA;
if (defaultShadows || (isParticle && particleOptimization))
{
sampleA = basicShadow(shadow_coordA);
}
else
{
if (CHSS)
{
sampleA = shadowSampleCHSS(shadow_coordA);
}
else
{
sampleA = pcf(shadow_coordA, sampleDistance);
}
}
if (blend > 0.0f)
{
float sampleB;
if (defaultShadows || (isParticle && particleOptimization))
{
sampleB = basicShadow(shadow_coordB);
}
else
{
if (CHSS)
{
sampleB = shadowSampleCHSS(shadow_coordB);
}
else
{
sampleB = pcf(shadow_coordB, sampleDistance);
}
}
return mix(sampleA, sampleB, blend);
}
else
{
return sampleA;
}
}
out vec4 gl_FragColor;
void main()
{
vec4 albedo = vec4(0.0f, 0.0f, 0.0f, 0.0f);
calculateSurface(gl_Color, albedo);
float occlusionFactor = 0.0f;
if(NdotL > -0.01f)
{
if(shadowSplitCount <= 0)
{
occlusionFactor = 1.0f;
}
else
{
occlusionFactor = shadowCoef(isParticle);
}
}
// Apply lighting and fog.
vec4 fragColor = vec4(0.0f, 0.0f, 0.0f, 0.0f);
if (isParticle)
{
vec4 texAlbedo = texture(tex, texCoord);
vec4 dirLightDirect = mix(dirShadowColor, dirLightColor + dirLightAmbient, occlusionFactor);
vec4 plt = accumulateParticlePointLights();
vec4 lightTotal = dirLightDirect + plt;
lightTotal.xyz = clamp(lightTotal.xyz, 0.0f, 1.2f);
fragColor = texAlbedo * gl_Color * lightTotal;
applyFog(fragColor, occlusionFactor);
fragColor.a = texAlbedo.a * gl_Color.a;
}
else
{
applyLighting(fragColor, albedo, shadowOpacity ? (1 - (1 - occlusionFactor) * occlusionBlend) : occlusionFactor);
applyFog(fragColor, occlusionFactor);
}
gl_FragColor = fragColor;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment