I’m learning custom SRP following Catlike Coding 's directional light shadow tutorial, but ended up with the shadow texture returns “shadow” result in the fragment where should be lighted up, shown on the figure blow on the first row left; the right is the depth map in frame debugger.
The plane uses the same shader as the Cube and Sphere in the scene but without a “ShadowCaster” Pass; because once the “ShadowCaster” pass is added for the plane all scene returns black, as shown in the second row.
Exactly, I understand how the shadowmap works (comparing depths), and also know the PCF or Bias or some other techs to improve the quality, but I have been struggling debugging this for 2~3 days with no exact understanding of why would this happen.
My codes of shader and Shadow class is listed at the bottom. I have been hand-craftly checking every variables to my knowledge, the shadow map is rightly pass to Shader because I designed some location xyz in the shadow map an it turned out with non black values; and the projection matrix is also right because I made L2 distance check with desinged location xyz; and the cascadeID or something else are also right passed to Shader.
So what is the problem exactly? I guess there may be some API usage issues with my code, but I’ve no idea.
My Unity version is 2022.3.28f1, Apple’s Metal platform, and I tested the same issue on Windows too. I really want to solve this problem.
Shadow.hlsl
#ifndef CUSTOM_SHADOWS_INCLUDED
#define CUSTOM_SHADOWS_INCLUDED
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Shadow/ShadowSamplingTent.hlsl"
#if defined(_DIRECTIONAL_PCF3)
#define DIRECTIONAL_FILTER_SAMPLES 4
#define DIRECTIONAL_FILTER_SETUP SampleShadow_ComputeSamples_Tent_3x3
#elif defined(_DIRECTIONAL_PCF5)
#define DIRECTIONAL_FILTER_SAMPLES 9
#define DIRECTIONAL_FILTER_SETUP SampleShadow_ComputeSamples_Tent_5x5
#elif defined(_DIRECTIONAL_PCF7)
#define DIRECTIONAL_FILTER_SAMPLES 16
#define DIRECTIONAL_FILTER_SETUP SampleShadow_ComputeSamples_Tent_7x7
#endif
#define MAX_SHADOWED_DIRECTIONAL_LIGHT_COUNT 4
#define MAX_CASCADE_COUNT 4
TEXTURE2D_SHADOW(_DirectionalShadowAtlas);
#define SHADOW_SAMPLER sampler_linear_clamp_compare
SAMPLER_CMP(SHADOW_SAMPLER);
CBUFFER_START(_CustomShadows)
int _CascadeCount;
float4 _ShadowAtlasSize;
float4 _CascadeCullingSpheres[MAX_CASCADE_COUNT];
float4x4 _DirectionalShadowMatrices[MAX_SHADOWED_DIRECTIONAL_LIGHT_COUNT * MAX_CASCADE_COUNT];
float4 _ShadowDistanceFade;
float4 _CascadeData[MAX_CASCADE_COUNT];
CBUFFER_END
struct ShadowData {
int cascadeIndex;
float cascadeBlend;
float strength;
};
float FadedShadowStrength (float distance, float scale, float fade) {
return saturate((1.0 - distance * scale) * fade);
}
ShadowData GetShadowData (Surface surfaceWS) {
ShadowData data;
data.cascadeBlend = 1.0;
data.strength = FadedShadowStrength(
surfaceWS.depth, _ShadowDistanceFade.x, _ShadowDistanceFade.y
);
int i;
for (i = 0; i < _CascadeCount; i++) {
float4 sphere = _CascadeCullingSpheres[i];
float distanceSqr = DistanceSquared(surfaceWS.position, sphere.xyz);
if (distanceSqr < sphere.w) {
float fade = FadedShadowStrength(
distanceSqr, _CascadeData[i].x, _ShadowDistanceFade.z
);
if (i == _CascadeCount - 1) {
data.strength *= fade;
}
else {
data.cascadeBlend = fade;
}
break;
}
}
if (i == _CascadeCount) {
data.strength = 0.0;
}
#if !defined(_CASCADE_BLEND_SOFT)
data.cascadeBlend = 1.0;
#endif
data.cascadeIndex = i;
return data;
}
float SampleDirectionalShadowAtlas (float3 positionSTS) {
return SAMPLE_TEXTURE2D_SHADOW(
_DirectionalShadowAtlas, SHADOW_SAMPLER, float3(positionSTS.x, positionSTS.y, positionSTS.z)
);
}
struct DirectionalShadowData {
float strength;
int tileIndex;
float normalBias;
};
float FilterDirectionalShadow (float3 positionSTS) {
#if defined(DIRECTIONAL_FILTER_SETUP)
float weights[DIRECTIONAL_FILTER_SAMPLES];
float2 positions[DIRECTIONAL_FILTER_SAMPLES];
float4 size = _ShadowAtlasSize.yyxx;
DIRECTIONAL_FILTER_SETUP(size, positionSTS.xy, weights, positions);
float shadow = 0;
for (int i = 0; i < DIRECTIONAL_FILTER_SAMPLES; i++) {
shadow += weights[i] * SampleDirectionalShadowAtlas(
float3(positions[i].xy, positionSTS.z)
);
}
return shadow;
#else
return SampleDirectionalShadowAtlas(positionSTS);
#endif
}
float GetDirectionalShadowAttenuation (
DirectionalShadowData directional,
ShadowData global,
Surface surfaceWS) {
#if !defined(_RECEIVE_SHADOWS)
return 1.0;
#endif
if (directional.strength <= 0.0) {
return 1.0;
}
float3 normalBias = surfaceWS.normal * (directional.normalBias * _CascadeData[global.cascadeIndex].y);
float3 positionSTS = mul(
_DirectionalShadowMatrices[directional.tileIndex], // The matrix is right
float4(surfaceWS.position + normalBias, 1) //surfaceWS right
).xyz;
float shadow = FilterDirectionalShadow(positionSTS);
if (global.cascadeBlend < 1.0) {
normalBias = surfaceWS.normal *
(directional.normalBias * _CascadeData[global.cascadeIndex + 1].y);
positionSTS = mul(
_DirectionalShadowMatrices[directional.tileIndex + 1],
float4(surfaceWS.position + normalBias, 1.0)
).xyz;
shadow = lerp(
FilterDirectionalShadow(positionSTS), shadow, global.cascadeBlend
);
}
return lerp(1.0, shadow, directional.strength);
}
#endif
Shadow.cs
using Palmmedia.ReportGenerator.Core;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.UI;
public class Shadows {
const string bufferName = "Shadows";
const int maxShadowedDirectionalLightCount = 4, maxCascades = 4;
CommandBuffer buffer = new CommandBuffer {
name = bufferName
};
struct ShadowedDirectionalLight {
public int visibleLightIndex;
public float slopeScaleBias;
public float nearPlaneOffset;
}
ShadowedDirectionalLight[] ShadowedDirectionalLights =
new ShadowedDirectionalLight[maxShadowedDirectionalLightCount];
ScriptableRenderContext context;
CullingResults cullingResults;
ShadowSettings settings;
int ShadowedDirectionalLightCount;
static string[] directionalFilterKeywords = {
"_DIRECTIONAL_PCF3",
"_DIRECTIONAL_PCF5",
"_DIRECTIONAL_PCF7",
};
public Vector3 ReserveDirectionalShadows (Light light, int visibleLightIndex) {
if (ShadowedDirectionalLightCount < maxShadowedDirectionalLightCount &&
light.shadows != LightShadows.None && light.shadowStrength > 0f &&
cullingResults.GetShadowCasterBounds(visibleLightIndex, out Bounds b)) {
ShadowedDirectionalLights[ShadowedDirectionalLightCount] =
new ShadowedDirectionalLight {
visibleLightIndex = visibleLightIndex,
slopeScaleBias = light.shadowBias,
nearPlaneOffset = light.shadowNearPlane
};
return new Vector3(
light.shadowStrength,
settings.directional.cascadeCount * ShadowedDirectionalLightCount++,
light.shadowNormalBias
);
}
return Vector3.zero;
}
static int dirShadowAtlasId = Shader.PropertyToID("_DirectionalShadowAtlas");
static int dirShadowMatricesId = Shader.PropertyToID("_DirectionalShadowMatrices");
static int cascadeCountId = Shader.PropertyToID("_CascadeCount"),
cascadeCullingSpheresId = Shader.PropertyToID("_CascadeCullingSpheres"),
cascadeDataId = Shader.PropertyToID("_CascadeData"),
shadowDistanceFadeId = Shader.PropertyToID("_ShadowDistanceFade"),
shadowAtlasSizeId = Shader.PropertyToID("_ShadowAtlasSize");
static Matrix4x4[]
dirShadowMatrices = new Matrix4x4[maxShadowedDirectionalLightCount * maxCascades];
static string[] cascadeBlendKeywords = {
"_CASCADE_BLEND_SOFT",
"_CASCADE_BLEND_DITHER"
};
static Vector4[]
cascadeCullingSpheres = new Vector4[maxCascades],
cascadeData = new Vector4[maxCascades];
public void Render () {
if (ShadowedDirectionalLightCount > 0) {
RenderDirectionalShadows();
}
else {
buffer.GetTemporaryRT(
dirShadowAtlasId, 1, 1,
32, FilterMode.Bilinear, RenderTextureFormat.Shadowmap
);
}
}
void RenderDirectionalShadows () {
int atlasSize = (int)settings.directional.atlasSize;
SetKeywords(
directionalFilterKeywords, (int)settings.directional.filter - 1
);
SetKeywords(
cascadeBlendKeywords, (int)settings.directional.cascadeBlend - 1
);
buffer.SetGlobalVector(
shadowAtlasSizeId, new Vector4(atlasSize, 1f / atlasSize)
);
buffer.GetTemporaryRT(
dirShadowAtlasId,
atlasSize,
atlasSize, 32,
FilterMode.Bilinear,
RenderTextureFormat.Shadowmap);
buffer.SetRenderTarget(
dirShadowAtlasId,
RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store
);
buffer.ClearRenderTarget(true, false, Color.clear);
buffer.BeginSample(bufferName);
ExecuteBuffer();
int tiles = ShadowedDirectionalLightCount * settings.directional.cascadeCount;
int split = tiles <= 1 ? 1 : tiles <= 4 ? 2 : 4;
int tileSize = atlasSize / split;
for (int i = 0; i < ShadowedDirectionalLightCount; i++) {
RenderDirectionalShadows(i, split, tileSize);
}
buffer.SetGlobalInt(cascadeCountId, settings.directional.cascadeCount);
buffer.SetGlobalVectorArray(
cascadeCullingSpheresId, cascadeCullingSpheres
);
buffer.SetGlobalMatrixArray(dirShadowMatricesId, dirShadowMatrices);
Debug.Log("cull" + cascadeCullingSpheres[2][3]);
float f = 1f - settings.directional.cascadeFade;
buffer.SetGlobalVectorArray(cascadeDataId, cascadeData);
Debug.Log(cascadeData[0]);
buffer.SetGlobalVector(
shadowDistanceFadeId,
new Vector4(
1f / settings.maxDistance,
1f / settings.distanceFade,
1f / (1f - f * f))
);
buffer.EndSample(bufferName);
ExecuteBuffer();
}
void SetKeywords (string[] keywords, int enabledIndex) {
for (int i = 0; i < keywords.Length; i++) {
if (i == enabledIndex) {
buffer.EnableShaderKeyword(keywords[i]);
}
else {
buffer.DisableShaderKeyword(keywords[i]);
}
}
}
Matrix4x4 ConvertToAtlasMatrix (Matrix4x4 m, Vector2 offset, int split) {
if (SystemInfo.usesReversedZBuffer) {
m.m20 = -m.m20;
m.m21 = -m.m21;
m.m22 = -m.m22;
m.m23 = -m.m23;
}
float scale = 1f / split;
m.m00 = (0.5f * (m.m00 + m.m30) + offset.x * m.m30) * scale;
m.m01 = (0.5f * (m.m01 + m.m31) + offset.x * m.m31) * scale;
m.m02 = (0.5f * (m.m02 + m.m32) + offset.x * m.m32) * scale;
m.m03 = (0.5f * (m.m03 + m.m33) + offset.x * m.m33) * scale;
m.m10 = (0.5f * (m.m10 + m.m30) + offset.y * m.m30) * scale;
m.m11 = (0.5f * (m.m11 + m.m31) + offset.y * m.m31) * scale;
m.m12 = (0.5f * (m.m12 + m.m32) + offset.y * m.m32) * scale;
m.m13 = (0.5f * (m.m13 + m.m33) + offset.y * m.m33) * scale;
return m;
}
void RenderDirectionalShadows (int index, int split, int tileSize) {
ShadowedDirectionalLight light = ShadowedDirectionalLights[index];
var shadowSettings =
new ShadowDrawingSettings(
cullingResults,
light.visibleLightIndex,
BatchCullingProjectionType.Orthographic);
int cascadeCount = settings.directional.cascadeCount;
int tileOffset = index * cascadeCount;
Vector3 ratios = settings.directional.CascadeRatios;
float cullingFactor =
Mathf.Max(0f, 0.8f - settings.directional.cascadeFade);
for (int i = 0; i < cascadeCount; i++) {
cullingResults.ComputeDirectionalShadowMatricesAndCullingPrimitives(
light.visibleLightIndex, i, cascadeCount, ratios, tileSize, light.nearPlaneOffset,
out Matrix4x4 viewMatrix, out Matrix4x4 projectionMatrix,
out ShadowSplitData splitData
);
splitData.shadowCascadeBlendCullingFactor = cullingFactor;
shadowSettings.splitData = splitData;
if (index == 0) {
SetCascadeData(i, splitData.cullingSphere, tileSize);
}
int tileIndex = tileOffset + i;
dirShadowMatrices[tileIndex] = ConvertToAtlasMatrix(
projectionMatrix * viewMatrix,
SetTileViewport(tileIndex, split, tileSize), split
);
buffer.SetViewProjectionMatrices(viewMatrix, projectionMatrix);
ExecuteBuffer();
buffer.SetGlobalDepthBias(0f, light.slopeScaleBias);
context.DrawShadows(ref shadowSettings);
buffer.SetGlobalDepthBias(0f, 0f);
}
}
void SetCascadeData (int index, Vector4 cullingSphere, float tileSize) {
float texelSize = 2f * cullingSphere.w / tileSize;
float filterSize = texelSize * ((float)settings.directional.filter + 1f);
cascadeData[index].x = 1f / cullingSphere.w;
cullingSphere.w -= filterSize;
cullingSphere.w *= cullingSphere.w;
cascadeCullingSpheres[index] = cullingSphere;
cascadeData[index] = new Vector4(
1f / cullingSphere.w,
filterSize * 1.4142136f
);
}
Vector2 SetTileViewport (int index, int split, float tileSize) {
Vector2 offset = new Vector2(index % split, index / split);
buffer.SetViewport(new Rect(
offset.x * tileSize, offset.y * tileSize, tileSize, tileSize
));
return offset;
}
public void Cleanup () {
buffer.ReleaseTemporaryRT(dirShadowAtlasId);
ExecuteBuffer();
}
public void Setup (
ScriptableRenderContext context, CullingResults cullingResults,
ShadowSettings settings
) {
this.context = context;
this.cullingResults = cullingResults;
this.settings = settings;
ShadowedDirectionalLightCount = 0;
}
void ExecuteBuffer () {
context.ExecuteCommandBuffer(buffer);
buffer.Clear();
}
}
Vertex, Fragment, and Shader pass
Vertex and Frag, named Shadow_usage.hlsl#ifndef CUSTOM_SHADOW_CASTER_PASS_INCLUDED
#define CUSTOM_SHADOW_CASTER_PASS_INCLUDED
#include "../ShaderLibrary/Usage.hlsl"
TEXTURE2D(_BaseMap);
SAMPLER(sampler_BaseMap);
UNITY_INSTANCING_BUFFER_START(UnityPerMaterial)
UNITY_DEFINE_INSTANCED_PROP(float4, _BaseMap_ST)
UNITY_DEFINE_INSTANCED_PROP(float4, _BaseColor)
UNITY_DEFINE_INSTANCED_PROP(float, _Cutoff)
UNITY_INSTANCING_BUFFER_END(UnityPerMaterial)
struct Attributes {
float3 positionOS : POSITION;
float2 baseUV : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct Varyings {
float4 positionCS : SV_POSITION;
float2 baseUV : VAR_BASE_UV;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
Varyings ShadowCasterPassVertex (Attributes input) {
Varyings output;
UNITY_SETUP_INSTANCE_ID(input);
UNITY_TRANSFER_INSTANCE_ID(input, output);
float3 positionWS = TransformObjectToWorld(input.positionOS);
output.positionCS = TransformWorldToHClip(positionWS);
#if UNITY_REVERSED_Z
output.positionCS.z =
min(output.positionCS.z, output.positionCS.w * UNITY_NEAR_CLIP_VALUE);
#else
output.positionCS.z =
max(output.positionCS.z, output.positionCS.w * UNITY_NEAR_CLIP_VALUE);
#endif
float4 baseST = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseMap_ST);
output.baseUV = input.baseUV * baseST.xy + baseST.zw;
return output;
}
void ShadowCasterPassFragment (Varyings input) {
UNITY_SETUP_INSTANCE_ID(input);
float4 baseMap = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.baseUV);
float4 baseColor = UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _BaseColor);
float4 base = baseMap * baseColor;
#if defined(_SHADOWS_CLIP)
clip(base.a - UNITY_ACCESS_INSTANCED_PROP(UnityPerMaterial, _Cutoff));
#elif defined(_SHADOWS_DITHER)
float dither = InterleavedGradientNoise(input.positionCS.xy, 0);
clip(base.a - dither);
#endif
}
#endif
Shader
Shader "CustomRP/Lit"
{
Properties
{
_BaseMap ("Base Map", 2D) = "white" {}
_BaseColor ("Base Color", Color) = (1,1,1,1)
_Metallic ("Metallic", Range(0, 1)) = 0
[Toggle(_PREMULTIPLY_ALPHA)] _PremulAlpha ("Premultiply Alpha", Float) = 0
_Smoothness ("Smoothness", Range(0, 1)) = 0.5
_Cutoff ("Alpha Cutoff", Range(0.0, 1.0)) = 0.5
[Toggle(_CLIPPING)] _Clipping ("Alpha Clipping", Float) = 1
[Enum(UnityEngine.Rendering.BlendMode)] _SrcBlend ("Src Blend", Float) = 1
[Enum(UnityEngine.Rendering.BlendMode)] _DstBlend ("Dst Blend", Float) = 0
[Enum(Off, 0, On, 1)] _ZWrite ("Z Write", Float) = 1
[KeywordEnum(On, Clip, Dither, Off)] _Shadows ("Shadows", Float) = 0
[Toggle(_RECEIVE_SHADOWS)] _ReceiveShadows ("Receive Shadows", Float) = 1
}
SubShader
{
Pass
{
Tags {
"LightMode" = "CustomLit"
}
Blend [_SrcBlend] [_DstBlend]
ZWrite [_ZWrite]
HLSLPROGRAM
#pragma target 3.5
#pragma shader_feature _PREMULTIPLY_ALPHA
#pragma shader_feature _CLIPPING
#pragma shader_feature _RECEIVE_SHADOWS
#pragma multi_compile _ _DIRECTIONAL_PCF3 _DIRECTIONAL_PCF5 _DIRECTIONAL_PCF7
#pragma multi_compile _ _CASCADE_BLEND_SOFT _CASCADE_BLEND_DITHER
#pragma multi_compile_instancing
#pragma vertex Litvert
#pragma fragment Litfrag
#include "Lit_usage.hlsl"
ENDHLSL
}
Pass{
Tags { "LightMode" = "ShadowCaster" }
ColorMask 0
HLSLPROGRAM
#pragma target 3.5
#pragma shader_feature _ _SHADOWS_CLIP _SHADOWS_DITHER
#pragma multi_compile_instancing
#pragma vertex ShadowCasterPassVertex
#pragma fragment ShadowCasterPassFragment
#include "Shadow_usage.hlsl"
ENDHLSL
}
}
CustomEditor "CustomShaderGUI"
}
