原文:penumbra shadows in raymarched SDFs
Intro - 介绍
许多步进用的距离场(distance fields)函数,他们都是本身提供了全局信息。这意味着,当要着色着色某一点时,它可以使用距离场函数(distance function)很简单就探索出周围几何体。这不像传统的光栅器,它得使用一个预先烘焙好的全局全局光照数据给后续的使用(如shadowmap阴影图,depthmap深度图,pointcloud点云图),或是在一个光线追踪要查找全局信息就必须通过光线投射来采样几何体信息,而在distance field距离场中是着色期间就可以取到的,这简直就是免费的(“免费”是加了双引号)。这意味着许多逼真的着色器和光照技术使用距离场都是很简单就可以实现的。并且raymarcher射线步进器在采样/渲染时会更真实。此文将运用光线步进的这些良好的特性来免费渲染半影的软阴影。
免费的计算软阴影与半影
经典的光线投射阴影
The trick - 技巧
那么,假设你有一个距离场编码函数 float map(vec3 p)
。你可以在这里
查看构建一些基础的距离函数。为了简便起见,假设 map()
函数包含了场景中所有需要渲染的对象,并且所有对象都允许投射阴影给其他对象。那么,在着色一个点的阴影计算是很简单的,沿着光方向的向量来射线步进,直到从光源到着色点的距离有碰撞到东西就好了。你可能需要处理类似这样的代码:
float shadow( in vec3 ro, in vec3 rd, float mint, float maxt )
{
for( float t=mint; t<maxt; )
{
float h = map(ro + rd*t);
if( h<0.001 )
return 0.0;
t += h;
}
return 1.0;
}
这代码运作得真的优美,并且生成良好且精确的锐利阴影,如上面的 经典的光线投射阴影 图一样。现在我们可以仅仅只添加一句代码就可以让结果看起来更好!
这个技巧就是,处理那些阴影射线没有碰撞到任何对象的,但射线在步进过程中又非常靠近碰撞对象的处理。那么,这些着色点就很可能是处于penumbra半影的点。那么就很有可能越是近靠近的已碰撞对象的点,就是越黑的点。同样的,你着色的点最靠近碰撞对象但没有碰撞的点,也会越黑。那么,这些都是发生在我们的阴影射线步进处理的,这些距离对于我们来说都是有效的阴影距离!上面的代码中第一个距离是h
,接着第二个是t
。所以,我们可以简单的为每一步的步进点计算半影因子,并取半影中最暗的。在2019年,一些Shadertoy用户注意到,可以通过一个内半影来偏移阴影计算。最终代码看起来是这样的:
float softshadow( in vec3 ro, in vec3 rd, float mint, float maxt, float w )
{
float s = 1.0;
for( float t=mint; t<maxt; )
{
float h = map(ro + rd*t);
s = min( s, 0.5+0.5*h/(w*t) );
if( s<0.0 ) break;
t += h;
}
s = max(s,0.0);
return s*s*(3.0-2.0*s); // smoothstep
}
译者jave.lin:千万别在Unity的shaderLab跑这段softShadow,我跑了3次,Unity都无响应,我还以为其他地方有死循环了,后来发现是此段程序有问题。
你可以通过将0到1的值偏移到-1到1,来优化0.5乘数与加数(这里有点没看懂)。你可以这找到参考的实现:https://www.shadertoy.com/view/WdyXRD。
w=1/2
w = 1/8
w = 1/32
w = 1/128
这一简单的修改就足以生成本页面开头的那张更好效果的软阴影图。如你所见,提升是巨大的:不单只得到了软阴影,还让阴影更真实了,当阴影阻挡对象与被阻挡对象连接处(注意桥与地板接触的地方)时阴影是锐利的,而当阴影阻挡对象远离被阻挡变越远时,半影则更加软。这里,消耗点在于每一次步进点的一次除法,零消耗是相对map()
的处理内容来衡量的。
w
参数是光源的大小,用于控制硬/软阴影的。可以看到不同的w
值对应的渲染阴影的效果。
所以,基本上,如果你会处理典型的光线步进阴影,你也可以免费的实现半影的软阴影。
下图展示了一个使用光线步进的距离场的一个示例:
半影的软阴影示例
而你可以查看更多的 使用距离场来光线步进的文章示例。
An improvement - 改进
在这项技术公布7年后,Sebastian Aaltonen 在他的GDC上发布了一项改进,这对于在使用这项技术时出现带条瑕疵是很有帮助的,尤其是锐利的角投射出来的阴影。
为了这个算法的稳定性,我们应该精准地沿着射线找到半影位置。然而,由于我们的步进中,可能会错过沿着射线生成最暗的半影点。那可以说明这种步进的步长方式有漏光。特别是锐利的角投射阴影时通常会错过最暗半影的。Sebastian的技巧有助于在h/t
计算半影时,不仅仅只采样步进点的情况,他也考量了每次迭代步进射线点到表面最近的点。或是说,使用当前的与前一个的采样点,他的技巧是使用了三角化信息来计算考量最近距离的。下图展示了这种几何情况:
白色箭头是我们步进用的射线。绿点是当前沿着射线步进到的点,而红点是之前一个步进的点。绿圈与红圈代表当前与之前SDF
的场景最近距离。其一可表示最近表面将落在两个圆形相交的位置(黄线那两个点)。最近点将会是沿着射线的,与黄线有交点的的距离。
y
就是当前点(绿点)到沿着射线点最近点(黄点)之间的距离,而d
就是最近的距离(黄色线段中一半的那个点)。那么,用代码来计算这两个量是很简单的:
译者jave.lin:上面的描述有可能不太好理解,下面我再话一个更详细的图,从图中可知:y=|BE|, d = |CE|
float y = r2*r2/(2.0*r1);
float d = sqrt(r2*r2-y*y);
r1
和r2
分别是红圈和绿圈的半径,或这说是,SDFs 估算出前一个步进点与当前步进点的距离。根据这两个量,我们可以改进半影的估算:
float softshadow( in vec3 ro, in vec3 rd, float mint, float maxt, float k )
{
float res = 1.0;
float ph = 1e20;
for( float t=mint; t<maxt; )
{
float h = map(ro + rd*t);
if( h<0.001 )
return 0.0;
float y = h*h/(2.0*ph);
float d = sqrt(h*h-y*y)
res = min( res, k*d/max(0.0,t-y) );
ph = h;
t += h;
}
return res;
}
这将会在复杂情况下生成更好的阴影,可以参考下图对比:
原来的方法
改进后的方法
你可以查看这里已改进过的实现:https://www.shadertoy.com/view/lsKcDD