实时渲染:屏幕空间反射SSR

从上次面试之后,就寻思着用Unity做一些实时的渲染效果,这个时候就想起了GAMES202作业中的实时屏幕空间反射。

屏幕空间反射效果可以产生微妙的反射,模拟潮湿的地板表面或水坑。这种技术产生的反射质量低于使用反射探针或平面反射(后者可以产生完美平滑的反射)。屏幕空间反射是用于限制镜面反射光泄漏量的理想效果。

屏幕空间反射的本质是利用GBuffer中的Normal, Albedo, Depth几个分量进行屏幕空间(以及view space)的RayMarching,简单的做法是,从摄像机发射一条光线到每个Shader fragment,再根据fragment的法线计算出reflect方向,这样就相当于得到了一条ray的origin和dir。这个时候我们再将步进的射线的z深度与depth texture的值进行比较,即可得到是否intersect,进而进行反射颜色的计算。

实现

首先给Unity的摄像机加上一个material和shader,用类似于opengl的blit方法将渲染目标更改为一个texture传输给shader,这里因为Unity好像没有提供WorldToView矩阵,我们需要自己传给Shader,同时由于需要使用GBuffer中的信息,需要将渲染模式改为deferred(其实也不是必须的,但是使用前向渲染模式需要自己手动额外渲染所需的信息,会造成资源的浪费)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

public class CamToTexture : MonoBehaviour
{
private Material mat;

public Shader shader;
public bool useSSR = true;
// Start is called before the first frame update
void Start()
{
GetComponent<Camera>().depthTextureMode = DepthTextureMode.Depth;
}
public Material material
{
get
{
mat = CheckShaderAndCreateMaterial(shader, mat);
return mat;
}
}
private void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (useSSR)
Graphics.Blit(src, dest, material);
else
Graphics.Blit(src, dest);
Shader.SetGlobalMatrix("_NormalWorldToView", GetComponent<Camera>().worldToCameraMatrix.inverse.transpose);
}
}

在顶点着色器中,首先通过uv坐标计算出裁剪坐标,z坐标设置为远裁剪平面的位置\(1\),再用投影逆变换把每个fragment的view space坐标计算出来(\(P^{-1} \cdot P \cdot V \cdot M \cdot \vec{p}\)),然后同样进行透视除法(逆矩阵同样会对\(w\)分量造成影响,很好理解)

1
2
3
4
5
// uv -> clip space
float4 origin = float4(v.uv * 2 - 1, 1, 1);
// clip space -> view space
origin = mul(unity_CameraInvProjection, origin);
o.rayOrigin = origin / origin.w;

接下来,在片段着色器中得到已经插值的rayOrigin。众所周知透视投影得到的深度是非线性的,因此需要一个Linear01Depth的线性转换。同时把远裁剪平面的z1转换到对应的深度中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
float depth = Linear01Depth(tex2D(_CameraDepthTexture, i.uv).r);
// view space ray origin
float3 rayOrigin = depth * i.rayOrigin;
float3 worldSpaceNormal = tex2D(_CameraGBufferTexture2, i.uv).rgb * 2 - 1;
float3 viewSpaceNormal = normalize(mul((float3x3)(_NormalWorldToView), worldSpaceNormal));
float4 color = tex2D(_MainTex, i.uv);

// raymarching hit uv position
float2 hituv;
float4 reflectColor = float4(0, 0, 0, 0);
if(rayMarch(rayOrigin, normalize(reflect(rayOrigin, viewSpaceNormal)), hituv)){
reflectColor = _RefInt * tex2D(_MainTex, hituv);
}

return color + tex2D(_CameraGBufferTexture1, i.uv) * reflectColor;

接下来就是屏幕空间的RayMarching,在每个步骤中求得对应的光线深度pDepth(因为从view space来到了clip space,需要除以远裁剪平面的z值),随后很简单地将view space的ray乘上投影矩阵,得到平面空间坐标(之后转换到uv坐标需要除以2加0.5)。这里intersect的算法很简单地使用了rayDepth > objDepth,也就是物体比光线更远的时候视为一次命中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bool rayMarch(in float3 rayOrigin, in float3 rayDir, out float2 hit){
hit = float2(0, 0);
float3 ray = rayOrigin;
UNITY_LOOP
for(int i = 0; i <= MARCH_STEPS; i++){
ray += STEP_SIZE * rayDir;
// ray depth, divide far clip space plane
float rayDepth = ray.z / -_ProjectionParams.z;
float4 screenSpaceCoord = mul(unity_CameraProjection, float4(ray, 1));
// projection division
screenSpaceCoord /= screenSpaceCoord.w;
if(screenSpaceCoord.x < -1 || screenSpaceCoord.x > 1 || screenSpaceCoord.y < -1 || screenSpaceCoord.y > 1){
// outside screen space
return false;
}
float objDepth = Linear01Depth(tex2Dlod(_CameraDepthTexture, float4(screenSpaceCoord.xy / 2 + 0.5, 0, 0)));
if(rayDepth > objDepth){
hit = screenSpaceCoord.xy / 2 + 0.5;
return true;
}
}
return false;
}

优化

To be continue...

Reference

作者

Carbene Hu

发布于

2022-03-16

更新于

2022-05-12

许可协议

评论