在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/ ...
随机推荐
- 复利计算C转java版
import java.util.Scanner; public class Compound_int { public static void main(String[] args) { tip() ...
- CDN问题
名称解释:正反向解析 主辅服务器 domain zone 记录:SOA.NS.A.CNAME.PRT.MX DNS配置文件中各字段作用,如TTL DNS端口号? TCP53和UDP53使用场合 Lin ...
- 第六周PSP &进度条
团队项目PSP 一.表格: C类型 C内容 S开始时间 E结束时间 I时间间隔 T净时间(mins) 预计花费时间(mins) 讨论 讨论alpha完成情况并总结 9:40 11:20 17 ...
- 第181天:HTML5——视频、音频
一.HTML5新增的video.source标签 <video width="320" height="240" controls="contr ...
- Xmind8破解,以及相关的流程和破解包
一.下载XMindCrack.jar文件:(传的貌似被屏蔽了:如果需要请留下邮箱,抽空会发给你) 百度云 ,里面破解文件,安装包都给了,但Xmind安装包不一定是最新的,有需求的可自行去官网下载 . ...
- [洛谷P5137]polynomial
题目大意:求:$$\sum\limits_{i=0}^na^{n-i}b^i\pmod{p}$$$T(T\leqslant10^5)$组数据,$a,b,n,p\leqslant10^{18}$ 题解 ...
- 利用Runloop优化流畅度
我们可以对runloop添加观察者,当观察到状态为kCFRunLoopExit,kCFRunLoopBeforeWaiting的时候,做一些耗时的处理,废话不说,直接上代码 - (void)viewD ...
- BZOJ1997 [Hnoi2010]Planar 【2-sat】
题目链接 BZOJ1997 题解 显然相交的两条边不能同时在圆的一侧,\(2-sat\)判一下就好了 但这样边数是\(O(m^2)\)的,无法通过此题 但是\(n\)很小,平面图 边数上界为\(3n ...
- 70路小报:用PV和UV作为网站衡量指标已经过时
方法]投资人呼吁:PV和UV不应该再作为产品衡量指标 风险投资机构Andreessen Horowitz近日一直反对再用传统的网站衡量指标去评价互联网产品,比如PV和UV,甚至包括应用的下载量. 他们 ...
- [CodeVs3196]黄金宝藏(DP/极大极小搜索)
题目大意:给出n(≤500)个数,两个人轮流取数,每次可以从数列左边或者右边取一个数,直到所有的数被取完,两个人都以最优策略取数,求最后两人所得分数. 显然这种类型的博弈题,第一眼就是极大极小搜索+记 ...