介绍
作业1在Unity上的复现, 实现了基于的硬阴影, PCF, PCSS
代码下载: https://github.com/Eagle104fred/Games202_Homework1
Games101 阴影课程笔记:https://smuwm007.feishu.cn/docs/doccnQopokLgMTVbIPsTSxow6He
注意事项
- 阴影的shader必须写在阴影的接受物体上例如墙壁等地方, 和模型本身的shader没啥关系
- 注意深度像机的参数设置
实现过程
1.构建shadowMap以及硬阴影
- 新建unity场景后建立一个Camera, 也可以在下面创建一个光源给模型打光不过不影响阴影效果。
- 新建一个产生阴影的Object和一个接受阴影的Object
2.设置阴影相机参数
需要增加三个文件, 一个是相机的脚本文件用于传递相机的参数和ShadowMap给Shader, 一个是ShadowMap需要新建一个RenderTexture文件来存储, 一个是用于绘制阴影的空Shader
LightCam.cs
public class LightCam : MonoBehaviour
{
// Start is called before the first frame update
public Shader shader;
Camera mCamera;
private void Awake()
{
mCamera = this.GetComponent<Camera>();
mCamera.SetReplacementShader(shader, "");//使用shader进行渲染
Shader.SetGlobalTexture("_ShadowMap", mCamera.targetTexture);//拿到shadowMap, 设置为全局供shader使用
}
// Update is called once per frame
void Update()
{
Shader.SetGlobalMatrix("_ShadowLauncherMatrix", transform.worldToLocalMatrix);//保存将世界坐标转换到光源坐标的矩阵
Shader.SetGlobalVector("_ShadowLauncherParam", new Vector4(mCamera.orthographicSize, mCamera.nearClipPlane, mCamera.farClipPlane));//存储相机内参
}
}
ShaderDepth.shader
Shader "Custom/ShaderDepth"
{
SubShader
{
Tags { "RenderType"="Opaque" }
Offset 1,1 //绘制深度时候偏移一点位置
Pass
{
}
}
}
3.配置接受物体的shader
ShaderRecieve.shader
Shader "Custom/ShadowRecieve"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_BaseColor ("BaseColor", Color) = (1,1,1,1)
}
SubShader
{
Tags { "RenderType"="Opaque" }
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
// make fog work
#pragma multi_compile_fog
#pragma enable_d3d11_debug_symbols
#include "UnityCG.cginc"
struct appdata{
float4 vertex : POSITION;
float2 shadowUV : TEXCOORD0;
};
struct v2f
{
float2 uv:TEXCOORD0;
UNITY_FOG_COORDS(1)
float4 vertex : SV_POSITION;
float4 shadowPos:TEXCOORD1;
};
sampler2D _MainTex;
float4 _MainTex_ST;
sampler2D _ShadowMap;
float4x4 _ShadowLauncherMatrix;
float3 _ShadowLauncherParam;
float4 _BaseColor;
//基于shadowmap的硬阴影
float HardShadow(v2f i)
{
float4 shadow = tex2Dproj(_ShadowMap,i.shadowPos);//拿到坐标在光源场景下的深度
float shadowAlpha = shadow.r;//拿到深度值
float2 clipalpha = saturate((0.5-abs(i.shadowPos.xy - 0.5))*20);//限定在0-1之间
shadowAlpha *= clipalpha.x * clipalpha.y;
float depth = 1-UNITY_SAMPLE_DEPTH(shadow);
shadowAlpha*=step(depth,i.shadowPos.z);//如果depth<shadowPos就没有被遮挡
return shadowAlpha;
}
v2f vert(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);//MVP矩阵
float4 worldPos = mul(unity_ObjectToWorld,v.vertex);//模型空间转换到世界空间,相当于进行了model矩阵
float4 shadowPos = mul(_ShadowLauncherMatrix,worldPos);//从世界坐标到光源坐标
shadowPos.xy = (shadowPos.xy/_ShadowLauncherParam.x+1)/2;//再将-1,1范围转换到0,1范围用于读取shadowMap中的深度
shadowPos.z = (shadowPos.z / shadowPos.w - _ShadowLauncherParam.y) / (_ShadowLauncherParam.z - _ShadowLauncherParam.y);//初始化深度
o.shadowPos = shadowPos;
o.uv = TRANSFORM_TEX(v.shadowUV, _MainTex);//读取uv
//UNITY_TRANSFER_FOG(o,o.vertex);
return o;
}
float4 frag(v2f i):SV_Target
{
float4 color = tex2D(_MainTex,i.uv);//拿到主颜色
float shadowAlpha=0.0;
shadowAlpha = HardShadow(i);
color.rgb *=(1-shadowAlpha)*_BaseColor.rgb;//阴影能见度加上材质本身的颜色
return color;
}
ENDCG
}
}
}
PCF
泊松盘采样原理
PCF的本质就是每个点的阴影值都取决于附近区域的shadowMap进行采样后,再做卷积的结果,但是如果对周围区域每一个像素都算一遍shaodowMap性能开销很大,因此需要用到随机采样。
泊松盘采样
//采样点个数(泊松盘)
#define NUM_SAMPLES 150
//采样的圈数(泊松盘)
#define NUM_RINGS 10
#define pi 3.141592653589793
#define pi2 6.283185307179586
float2 poissonDisk[NUM_SAMPLES];
float rand_2to1(float2 uv )
{
// 0 - 1
const float a = 12.9898, b = 78.233, c = 43758.5453;
float dt = dot( uv.xy, float2( a,b ) ), sn = fmod( dt, pi );
return frac(sin(sn) * c);
}
void poissonDiskSamples(const in float2 randomSeed )
{
float ANGLE_STEP = pi2 * float( NUM_RINGS ) / float( NUM_SAMPLES );
float INV_NUM_SAMPLES = 1.0 / float( NUM_SAMPLES );
float angle = rand_2to1( randomSeed ) * pi2;//随机初始角
float radius = INV_NUM_SAMPLES;
float radiusStep = radius;
for( int i = 0; i < NUM_SAMPLES; i ++ )
{
poissonDisk[i] = float2( cos( angle ), sin( angle ) ) * pow( radius, 0.75 );
radius += radiusStep;//递增半径
angle += ANGLE_STEP;//递增角度
}
}
PCF代码
float PCF(v2f i)
{
float4 shadowCoord = i.shadowPos;
poissonDiskSamples(shadowCoord.xy);
float textureSize = 2048.0;//shadowMap大小
float filterStride = 5.0;
float filterRange = filterStride/textureSize;
int unBlockCount = 0;
float shadowAlpha=0.0;
for(int i=0;i<NUM_SAMPLES;i++)
{
float2 sampleCoord = poissonDisk[i]*filterRange+shadowCoord.xy;//泊松偏移乘以采样间隔再加上原点位置
float shadow = tex2D(_ShadowMap,sampleCoord);
shadowAlpha = shadow.r;
float2 clipalpha = saturate((0.5-abs(sampleCoord - 0.5))*20);//限定在0-1之间
shadowAlpha *= clipalpha.x * clipalpha.y;//阴影区域裁剪
float depth = 1-UNITY_SAMPLE_DEPTH(shadow);
if(depth+0.005<shadowCoord.z)//避免自遮挡
{
unBlockCount++; //计算未被遮挡的采样点数量
}
}
return float(unBlockCount)/float(NUM_SAMPLES)*shadowAlpha;//阴影值为未遮挡概率
}
PCSS
PCSS主要分为三部:
1.查找每一个像素的平均深度, 这个过程和pcf很像只不过是统计的是Block而不是unBlock。
2.根据公式就是那个该像素点的Blocker平均深度,计算软阴影的大小。
w
P
e
n
u
m
b
r
a
=
(
d
R
e
c
e
i
v
e
r
−
d
B
l
o
c
k
e
r
)
∗
w
L
i
g
h
t
d
B
l
o
c
k
e
r
w_{Penumbra}=\frac{(d_{Receiver}-d_{Blocker})*w_{Light}}{d_{Blocker}}
wPenumbra=dBlocker(dReceiver−dBlocker)∗wLight
3.正常计算PCF。
findBlocker找到平均深度:
float _findBlocker(float4 shadowPos,float3 normal)
{
float4 shadowCoord = shadowPos;
poissonDiskSamples(shadowCoord.xy);
float textureSize = 2048.0;
//注意 block 的步长要比 PCSS 中的 PCF 步长长一些,这样生成的软阴影会更加柔和
float filterStride = 20.0;
float filterRange = 1.0 / textureSize * filterStride;
int shadowCount = 0;
float blockDepth = 0.0;
for(int i=0;i<NUM_SAMPLES;i++)
{
float2 sampleCoord = poissonDisk[i]*filterRange+shadowCoord.xy;
float shadow = tex2D(_ShadowMap,sampleCoord);
float depth = 1-UNITY_SAMPLE_DEPTH(shadow);
//计算动态bias(根据光源和法线的夹角动态调整bias)
float bias=min(0.009*(abs(1-dot(normal,_ShadowLightDirection))),0.0005);//将dot的0-1取值归一化到0-0.009
if(depth+bias<shadowCoord.z)
{
blockDepth+=depth;
shadowCount++; //计算未被遮挡的采样点数量
}
}
if(shadowCount==NUM_SAMPLES)return 3.0;
return blockDepth/float(shadowCount);
}
PCSS:
float PCSS(v2f i)
{
float4 shadowCoord = i.shadowPos;
// STEP 1: avgblocker depth
float3 normal = normalize(i.normal);//用于计算动态bias
float zBlocker = _findBlocker(shadowCoord,normal);
//if(zBlocker<EPS)return 0.0;
//if(zBlocker>1.0)return 1.0;
// STEP 2: penumbra size
float W_LIGHT=1.0;
float wPenumbra = (shadowCoord.z-zBlocker) * W_LIGHT / zBlocker;
// STEP 3: PCF
float textureSize = 1024.0;
float filterStride = 10.0;
float filterRange = filterStride/textureSize*wPenumbra;//用距离光源的距离控制软阴影大小
int unBlockCount = 0;
float shadowAlpha=0.0;
for(int i=0;i<NUM_SAMPLES;i++)
{
float2 sampleCoord = poissonDisk[i]*filterRange+shadowCoord.xy;
float shadow = tex2D(_ShadowMap,sampleCoord);
shadowAlpha = shadow.r;
float2 clipalpha = saturate((0.5-abs(sampleCoord - 0.5))*20);//限定在0-1之间
shadowAlpha *= clipalpha.x * clipalpha.y;//阴影区域裁剪
float depth = 1-UNITY_SAMPLE_DEPTH(shadow);
//计算动态bias(根据光源和法线的夹角动态调整bias)
float bias=max(0.01*(1-abs(dot(normal,_ShadowLightDirection))),0.005);//将dot的0-1取值归一化到0-0.009
if(depth+bias<shadowCoord.z)
{
unBlockCount++; //计算未被遮挡的采样点数量
}
}
return float(unBlockCount)/float(NUM_SAMPLES)*shadowAlpha;
}