在Unity中实现屏幕空间阴影(2)
参考文章: https://www.imgtec.com/blog/implementing-fast-ray-traced-soft-shadows-in-a-game-engine/
完成的工程: https://github.com/yangrc1234/ScreenSpaceShadow

(一个例子,注意靠近立柱的部分的阴影较为锐利,远离的部分更加模糊)
Penumbra
现实生活中,距离遮挡物越近的地方,其阴影会更加锐利;反之则更加模糊。这一片介于被照亮和阴影中间的区域被称为Penumbra。我们用如下公式去计算Penumbra的大小:

对于太阳光,我们可以认为Light Size / Total Distance是一个常数。当这个常数大时,模糊效果就会更大。
在实现过程中,我们生成两张贴图,一张是阴影贴图,保存某个点上是否有阴影;另一个保存一个点到其遮挡物的距离。我们可以在一张贴图里的不同通道保存这两个信息。
生成贴图完毕后,我们在第二个pass里对阴影贴图做模糊。其中模糊的半径根据该点上的遮挡物距离决定。如果一个点不在阴影中,则去搜索最靠近的阴影中的点(如果没搜到,说明该点被完全照亮),然后用这个点的遮挡物距离进行模糊。搜索时我们直接在该点的水平方向或者垂直方向上的点进行搜索。
之后将模糊得到的结果和原有的屏幕阴影做Blend即可。
具体实现
我们使用2个CommandBuffer去实现这个效果。脚本都附在Directional Light下。
第一个CommandBuffer在LightEvent.BeforeScreenspaceMask执行。在这一步我们渲染上面提到的阴影贴图+遮挡物距离。
第二个CommandBuffer在LightEvent.AfterScreenspaceMask执行。在这一步中,我们的渲染目标由Unity设置为了屏幕空间阴影贴图。此时我们可以同时进行模糊以及Blend的操作。
这里分为两个CommandBuffer有两个原因,
- 除了这两个事件中,默认的RenderTarget是屏幕空间阴影贴图,我们似乎没有办法在其他地方将RenderTarget设置为这个屏幕空间阴影贴图。
- 在第一个CommandBuffer渲染完阴影贴图+遮挡物距离之后,此时RenderTarget已经改变了。而我们没有办法将RenderTarget重置成屏幕空间阴影贴图。所以等到LightEvent.AfterScreenspaceMask的时候切换回RenderTarget吧。
听起来有点捉鸡,不知道是我太菜还是CommandBuffer太菜……
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
[RequireComponent(typeof(Light))]
public class ScreenSpaceShadow : MonoBehaviour {
public Shader sssShader;
private Light dlight { get {
return GetComponent<Light>();
} }
private Material _mat;
private Material mat {
get {
return _mat ?? (_mat = new Material(sssShader));
}
}
private CommandBuffer cmdBuf; //executed before shadow map.
private CommandBuffer afterCmdBuf;
private RenderTexture tempScreenSpaceShadow;
private void UpdateCommandBuffers() {
if (tempScreenSpaceShadow != null)
DestroyImmediate(tempScreenSpaceShadow);
tempScreenSpaceShadow = new RenderTexture(Screen.width, Screen.height, 0,RenderTextureFormat.RGFloat); //R channel stands for dentisy, G stands for occluder distance.
tempScreenSpaceShadow.filterMode = FilterMode.Point;
cmdBuf = new CommandBuffer();
cmdBuf.Blit(null, tempScreenSpaceShadow, mat, 0); //first pass. render a dentisy/occluder distance buffer.
afterCmdBuf = new CommandBuffer();
afterCmdBuf.Blit(tempScreenSpaceShadow, BuiltinRenderTextureType.CurrentActive, mat, 1); //second pass. blur the dentisy and blend final result into current screen shadow map.
}
//doesn't work for multi camera.
//I can't find a way to make it compatitable with multi camera.
//if you know how to access correct view matrix inside shader(Currently UNITY_MATRIX_V doesn't work in Unity 2017.1, or I misunderstand the meaning of UNITY_MATRIX_V? ), you can remove this, and multi camera will work.
private void Update() {
mat.SetMatrix("_WorldToView", Camera.main.worldToCameraMatrix);
}
private void OnEnable() {
UpdateCommandBuffers();
dlight.AddCommandBuffer(LightEvent.BeforeScreenspaceMask, cmdBuf);
dlight.AddCommandBuffer(LightEvent.AfterScreenspaceMask, afterCmdBuf);
}
private void OnDisable() {
dlight.RemoveCommandBuffer(LightEvent.BeforeScreenspaceMask, cmdBuf);
dlight.RemoveCommandBuffer(LightEvent.AfterScreenspaceMask, afterCmdBuf);
}
}
接下来是Shader的实现。之前的文章中我们已经提过了屏幕空间光线追踪的原理以及实现。这里我直接贴出相关函数的代码。
#ifndef _YRC_SCREEN_SPACE_RAYTRACE_
#define _YRC_SCREEN_SPACE_RAYTRACE_
//convenient function.
bool RayIntersect(float raya, float rayb, float2 sspt,float thickness) {
if (raya > rayb) {
float t = raya;
raya = rayb;
rayb = t;
}
#if 1 //by default we use fixed thickness.
float screenPCameraDepth = -LinearEyeDepth(tex2Dlod(_CameraDepthTexture, float4(sspt / 2 + 0.5, 0, 0)).r);
return raya < screenPCameraDepth && rayb > screenPCameraDepth - thickness;
#else
float backZ = tex2Dlod(_BackfaceTex, float4(sspt / 2 + 0.5, 0, 0)).r;
return raya < backZ && rayb > screenPCameraDepth;
#endif
}
bool traceRay(float3 start, float3 direction, float jitter, float4 texelSize,float maxRayLength, float maxStepCount, float pixelStride, float pixelThickness, out float2 hitPixel, out float marchPercent,out float hitZ,out float rayLength) {
//clamp raylength to near clip plane.
rayLength = ((start.z + direction.z * maxRayLength) > -_ProjectionParams.y) ?
(-_ProjectionParams.y - start.z) / direction.z : maxRayLength;
float3 end = start + direction * rayLength;
float4 H0 = mul(unity_CameraProjection, float4(start, 1));
float4 H1 = mul(unity_CameraProjection, float4(end, 1));
float2 screenP0 = H0.xy / H0.w;
float2 screenP1 = H1.xy / H1.w;
float k0 = 1.0 / H0.w;
float k1 = 1.0 / H1.w;
float Q0 = start.z * k0;
float Q1 = end.z * k1;
if (abs(dot(screenP1 - screenP0, screenP1 - screenP0)) < 0.00001) {
screenP1 += texelSize.xy;
}
float2 deltaPixels = (screenP1 - screenP0) * texelSize.zw;
float step; //the sample rate.
step = min(1 / abs(deltaPixels.y), 1 / abs(deltaPixels.x)); //make at least one pixel is sampled every time.
//make sample faster.
step *= pixelStride;
float sampleScaler = 1.0 - min(1.0, -start.z / 100); //sample is slower when far from the screen.
step *= 1.0 + sampleScaler;
float interpolationCounter = step; //by default we use step instead of 0. this avoids some glitch.
float4 pqk = float4(screenP0, Q0, k0);
float4 dpqk = float4(screenP1 - screenP0, Q1 - Q0, k1 - k0) * step;
pqk += jitter * dpqk;
float prevZMaxEstimate = start.z;
bool intersected = false;
UNITY_LOOP //the logic here is a little different from PostProcessing or (casual-effect). but it's all about raymarching.
for (int i = 1;
i <= maxStepCount && interpolationCounter <= 1 && !intersected;
i++,interpolationCounter += step
) {
pqk += dpqk;
float rayZMin = prevZMaxEstimate;
float rayZMax = ( pqk.z) / ( pqk.w);
if (RayIntersect(rayZMin, rayZMax, pqk.xy - dpqk.xy / 2, pixelThickness)) {
hitPixel = (pqk.xy - dpqk.xy / 2) / 2 + 0.5;
marchPercent = (float)i / maxStepCount;
intersected = true;
}
else {
prevZMaxEstimate = rayZMax;
}
}
#if 1 //binary search
if (intersected) {
pqk -= dpqk; //one step back
UNITY_LOOP
for (float gapSize = pixelStride; gapSize > 1.0; gapSize /= 2) {
dpqk /= 2;
float rayZMin = prevZMaxEstimate;
float rayZMax = (pqk.z) / ( pqk.w);
if (RayIntersect(rayZMin, rayZMax, pqk.xy - dpqk.xy / 2, pixelThickness)) { //hit, stay the same.(but ray length is halfed)
}
else { //miss the hit. we should step forward
pqk += dpqk;
prevZMaxEstimate = rayZMax;
}
}
hitPixel = (pqk.xy - dpqk.xy / 2) / 2 + 0.5;
}
#endif
hitZ = pqk.z / pqk.w;
rayLength *= (hitZ - start.z) / (end.z - start.z);
return intersected;
}
#endif
在两个pass中,我们的vertex program是相同的。代码如下:
struct appdata {
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f {
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
float3 csRay : TEXCOORD1; //unused during second pass.
};
v2f vert(appdata v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
float4 cameraRay = float4(v.uv * 2.0 - 1.0, 1.0, 1.0);
cameraRay = mul(unity_CameraInvProjection, cameraRay);
o.csRay = cameraRay / cameraRay.w;
return o;
}
第一个pass的fragment program如下:
#define RAY_LENGTH 40.0 //maximum ray length.
#define STEP_COUNT 256 //maximum sample count.
#define PIXEL_STRIDE 4 //sample multiplier. it's recommend 16 or 8.
#define PIXEL_THICKNESS (0.04 * PIXEL_STRIDE) //how thick is a pixel. correct value reduces noise.
float4 fragDentisyAndOccluder(v2f i) : SV_Target //we return dentisy in R, distance in G
{
float decodedDepth = Linear01Depth(tex2D(_CameraDepthTexture, i.uv).r);
float3 csRayOrigin = decodedDepth * i.csRay;
float3 wsNormal = tex2D(_CameraGBufferTexture2, i.uv).rgb * 2.0 - 1.0;
float3 csNormal = normalize(mul((float3x3)_WorldToView, wsNormal));
float3 wsLightDir = -_LightDir;
float3 csLightDir = normalize(mul((float3x3)_WorldToView, wsLightDir));
float2 hitPixel;
float marchPercent;
float3 debugCol;
float atten = 0;
float2 uv2 = i.uv * float2(1024,1024);
float c = (uv2.x + uv2.y) * 0.25;
float hitZ;
float rayBump = max(-0.010*csRayOrigin.z, 0.001);
float rayLength;
bool intersectd = traceRay(
csRayOrigin + csNormal * rayBump,
csLightDir,
0, //don't need jitter here.
float4(1 / 991.0, 1 / 529.0, 991.0, 529.0), //texel size.
RAY_LENGTH,
STEP_COUNT,
PIXEL_STRIDE,
PIXEL_THICKNESS,
hitPixel,
marchPercent,
hitZ,
rayLength
);
return intersectd ? float4(1 , rayLength, 0, 1) : 0;
}
当我们没有命中时,我们返回全0的结果,否则将R设置为1,同时在G通道上保存距离。
在我上面贴出的原文中,阴影贴图可以在光线追踪到透明物体时设置为0.x这样的数值,但是我们的deferred rendering肯定没有这样的操作。(原文中的硬件是专门用于光线追踪的硬件,它的光线追踪也不是屏幕空间光线追踪)所以这里的贴图应该可以压缩成一个通道,为0时表示没有阴影,非0时表示到阻挡者的距离。
生成的贴图:

第二个pass如下:
#define BLURBOX_HALFSIZE 8
#define PENUMBRA_SIZE_CONST 4
#define MAX_PENUMBRA_SIZE 8
#define DEPTH_REJECTION_EPISILON 1.0
fixed4 fragBlur(v2f i) :SV_TARGET{
float2 dentisyAndOccluderDistance = tex2D(_MainTex,i.uv).rg;
fixed dentisy = dentisyAndOccluderDistance.r;
float occluderDistance = dentisyAndOccluderDistance.g;
float maxOccluderDistance = 0;
float3 uvOffset = float3(_MainTex_TexelSize.xy, 0); //convenient writing here.
for (int j = 0; j < BLURBOX_HALFSIZE; j++) { //search on vertical and horizontal for nearest shadowed pixel.
float top = tex2D(_MainTex, i.uv + j * uvOffset.zy).g;
float bot = tex2D(_MainTex, i.uv - j * uvOffset.zy).g;
float lef = tex2D(_MainTex, i.uv + j * uvOffset.xz).g;
float rig = tex2D(_MainTex, i.uv - j * uvOffset.xz).g;
if (top != 0 || bot != 0 || lef != 0 || rig != 0) {
maxOccluderDistance = max(top, max(bot, max (lef, rig)));
break;
}
}
float penumbraSize = maxOccluderDistance * PENUMBRA_SIZE_CONST;
float camDistance = LinearEyeDepth(tex2D(_CameraDepthTexture, i.uv));
float projectedPenumbraSize = penumbraSize / camDistance;
projectedPenumbraSize = min(1 + projectedPenumbraSize, MAX_PENUMBRA_SIZE);
float depthtop = LinearEyeDepth(tex2D(_CameraDepthTexture, i.uv + j * uvOffset.zy));
float depthbot = LinearEyeDepth(tex2D(_CameraDepthTexture, i.uv - j * uvOffset.zy));
float depthlef = LinearEyeDepth(tex2D(_CameraDepthTexture, i.uv + j * uvOffset.xz));
float depthrig = LinearEyeDepth(tex2D(_CameraDepthTexture, i.uv - j * uvOffset.xz));
float depthdx = min(abs(depthrig - camDistance), abs(depthlef - camDistance));
float depthdy = min(abs(depthtop - camDistance), abs(depthbot - camDistance));
float counter = 0;
float accumulator = 0;
UNITY_LOOP
for (int j = -projectedPenumbraSize; j < projectedPenumbraSize; j++) { //xaxis
for (int k = -projectedPenumbraSize; k < projectedPenumbraSize; k++) { //yaxis
float depth = LinearEyeDepth(tex2Dlod(_CameraDepthTexture, float4(i.uv + uvOffset.xy * float2(j, k),0,0)));
if (depthdx * abs(j) + depthdy * abs(k) + DEPTH_REJECTION_EPISILON < abs(camDistance - depth)) //depth rejection
break;
counter += 1;
accumulator += tex2Dlod(_MainTex, float4(i.uv + uvOffset.xy * float2(j, k),0,0)).r;
}
}
return (1 - saturate(accumulator / counter));
}
大体内容还是很好理解的。首先我们根据遮挡距离计算penumbra的大小,然后除以z值,获得屏幕空间下的penumbra的大小。然后根据这个大小去采样同样大小的屏幕空间的像素即可。
其中有一个depth rejection的操作,用于将跟当前点不是同一个平面上的采样剔除掉。具体的操作是比较采样点和当前点的深度,如果差值超过了一个阈值,则认为不在一个平面。这个阈值是通过当前点和其上下左右四个点的深度值计算得到的。具体的就不细说了。
这里返回的是光照值。此时生成的贴图是这样的:

因为我们希望原有的屏幕空间阴影和我们计算的结果结合。此时我们可以想到可以通过将两张贴图的值相乘得到最终结果。(两张贴图保存的都是光照度,阴影为0,光照为1,相乘后两张贴图的阴影融合)
我们设置Blend选项为Blend Zero SrcAlpha,此时公式为Src * Zero + Dst * SrcAlpha,即Dst * SrcAlpha,我们就得到了最终结果。
(注意后面的山的阴影,来自原本的屏幕空间阴影贴图)
总结
以上就是我的屏幕空间阴影的实现。这个实现有诸多不完善的地方,比如
不支持多相机。
这个问题的根本原因是UNITY_MATRIX_V在shader中不知道为什么无效了。因此我们需要在脚本中手动设置世界到相机的矩阵。在之前的屏幕空间反射中,这样的设置还没有太大的问题,但是在这里我们使用CommandBuffer注入了Directional Light的流程进行渲染,这个CommandBuffer在所有相机渲染光照的时候都会执行一次。为了保证正确,我们需要在每次渲染的时候都设置一次对应相机的矩阵。但是我现在还没有找到正确的,可以设置对应相机矩阵的方法。(OnRenderObject里使用Camera.current的矩阵进行设置并不work,不知道为什么)。不支持Shadow Strength。
在Light的选项里有一个Shadow Strength。在渲染屏幕空间阴影贴图时,根据这个值,渲染出来的阴影部分强度也是不同的。但是我们渲染出来的阴影会以相乘的方式融合进去,此时整张阴影贴图的强度就变得不均匀了。也许有修改Blend选项之类的办法来解决这个问题,我暂时没想到。

(注意山上的阴影,原贴图的部分是较浅,我们的部分是黑色)只支持Deferred rendering。
这个效果中需要Deferred rendering的点只有屏幕光线追踪时,需要用到法线做一个起始点偏移。事实上我们是可以是可以通过深度贴图来一定程度上重建一个屏幕空间的法线信息的,但是我还没有尝试过。因此理论上我们可以将该效果改造为同时适应forward rendering与deferred rendering的一个效果。
在Unity中实现屏幕空间阴影(2)的更多相关文章
- 在Unity中实现屏幕空间阴影(1)
接着上篇文章,我们实现了SSR效果. 其中的在屏幕空间进行光线追踪的方法是通用的.借此我们再实现一种屏幕空间的效果,即屏幕空间阴影. 文中的图片来自Catlike coding http://catl ...
- 在Unity中实现屏幕空间反射Screen Space Reflection(4)
第四部分讲一下如何在2D屏幕空间步进光线. http://casual-effects.blogspot.com/2014/08/screen-space-ray-tracing.html 中的代码感 ...
- 在Unity中实现屏幕空间反射Screen Space Reflection(1)
本篇文章我会介绍一下我自己在Unity中实现的SSR效果 出发点是理解SSR效果的原理,因此最终效果不是非常完美的(代码都是够用就行),但是从学习的角度来说足以学习到SSR中的核心算法. 如果对核心算 ...
- 在Unity中实现屏幕空间反射Screen Space Reflection(3)
本篇讲一下相交检测的优化.有两个措施. 线段相交检测 之前的检测都是检测光线的终点是否在物体内.我们可以尝试检测光线的线段是否与物体相交. 比如说有一个非常薄的物体,光线差不多垂直于它的表面.如果用普 ...
- 在Unity中实现屏幕空间反射Screen Space Reflection(2)
traceRay函数 在上一篇中,我们有如下签名的traceRay函数 bool traceRay(float3 start, float3 direction, out float2 hitPixe ...
- 关于Unity中的屏幕适配
一.Game视图的屏幕分辨率可以先自定义添加,供以后选择,以下是手游经常用到的分辨率: 1.1136X640,iPhone5 2.1920X1080,横屏,主流游戏都是这个分辨率 3.1080X192 ...
- 浅谈unity中gamma空间和线性空间
转载请标明出处:http://www.cnblogs.com/zblade/ 一.概述 很久没有写文章了,今天写一篇对gamma空间和线性空间的个人理解总结,在查阅和学习了各个资料后,算是一个个人笔记 ...
- 【Unity技巧】Unity中的优化技术
http://blog.csdn.net/candycat1992/article/details/42127811 写在前面 这一篇是在Digital Tutors的一个系列教程的基础上总结扩展而得 ...
- Unity中的优化技术
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明.本文链接:https://blog.csdn.net/candycat1992/article/ ...
随机推荐
- HDU4790_Just Random
这个题目我只能说我一看就知道是这么做的,但是由于实现能力略水,Wa了3发. 题意为给你两个区间[a,b]和[c,d],两个区间分别任取一个数,现在要你求出这个数模p的值为m的概率有多大. 其实是这么做 ...
- Struts创建流程
1.启动服务,加载web.xml 并实例化StrutsPrepareAndExecuteFilter过滤器 2.在实例化StrutsPrepareAndExecuteFilter的时候会执行过滤器中的 ...
- 辣鸡蒟蒻Klaier的一些计划
需要熟练的东西:cdq分治,堆,树链剖分,tarjan及其它一些图论算法,网络流,kmp,字符串哈希,线段树主席树,树状数组,斜率优化dp 需要学的东西:lct,后缀数组,AC自动机,平衡树 球队收益 ...
- MethodHandle
JDK7为间接调用方法引入新的API,在java.lang.invoke包下,可以看作为反射的升级版,但它不像反射API那样显得冗长.繁重 主要的类 MethodHandle 方法句柄.对可直接执行的 ...
- BZOJ4953 Wf2017Posterize(动态规划)
设f[i][j]为前i种强度选了j种且其中第i种选时前i个的最小误差.转移枚举上个选啥前缀和优化即可. #include<iostream> #include<cstdio> ...
- OSPF虚连接简单配置
实验实例:<华为路由器学习指南>P715 本实例的拓扑结构如图:Area 2没有直接与骨干区域直接相连,Area 1被用作传输区域来连接Area 0与Area2.为了使Area2与骨干区域 ...
- https的通信过程
https的特点 1. https有 握手阶段 和 请求阶段2. 握手阶段 使用 非对称加密算法 请求阶段 使用 对称加密算法3. 保证数据的完整性使用数字签名4. 握手阶段有两组非对称加密,数字证书 ...
- 【数学】【背包】【NOIP2018】P5020 货币系统
传送门 Description 在网友的国度中共有 \(n\) 种不同面额的货币,第 \(i\) 种货币的面额为 \(a[i]\),你可以假设每一种货币都有无穷多张.为了方便,我们把货币种数为 \(n ...
- 《剑指offer》— JavaScript(4)重建二叉树
重建二叉树 题目描述 输入某二叉树的前序遍历和中序遍历的结果,请重建出该二叉树.假设输入的前序遍历和中序遍历的结果中都不含重复的数字.例如输入前序遍历序列{1,2,4,7,3,5,6,8}和中序遍历序 ...
- 在 Ubuntu16.04上安装anaconda+Spyder+TensorFlow(支持GPU)
TensorFlow 官方文档中文版 http://www.tensorfly.cn/tfdoc/get_started/introduction.html https://zhyack.github ...