解析,改进,批评一个国外免费透明水面Shader,进一步了解Shader背后的物理原理。
菲涅尔反射
我将原水面Shader一再简化,从中抽取最主要的部分,忽略细枝末节,并改掉缺陷,指出不足。
简化版:
其中,顶点偏移,也就是波的造型,是用贴图制造的,一目了然:
就是一张高度图经过4方向扰动,乘以波高度。然后它再乘上顶点法线在世界坐标系上的Z轴高度,这个莫名奇妙的操作会使得Normal朝上的顶点的波高更加高,而偏的更加低。我认为编者脑子里也不清楚,所以我修改了,直接乘以(0,0,1)。如果你的高度图只有b通道,这个(0,0,1)都不用乘。
再观原材质图,你一定会注意到我们的主角,一个叫Fresnel_Function的节点。
UE4这样起名字,也真是大言不惭,一会我们会说到。
这个节点,涉及到一个现象,菲涅尔效应。国外一般不称为效应,搜索Fresnel equations。
现象倒是很简单:
对着玻璃看,越斜着看,它反射越多,越不透明,越像镜子;
正着看,反射越少,就越透明。
我们在生活中常常见到过这种现象:
(我怀疑夏天远方道路反光,也是这个原因)
总之,这是个在各个材质上都很普遍发生的现象,包括水面。
如果说我们之前的不透明水面本质就是镜子,那我们这次的透明水面本质就是玻璃。
真正的Fresnel方程比较复杂,可脱胎于麦克斯韦电磁学方程。
对于透明物体:
然而UE4的Fresnel_Function节点的方程并不根据实际。它利用视线与平面越垂直,反射越少这一点,进行了粗略模拟。这个方程也是图形学中常见的:
其中V向量是像素点到相机位置的向量,也就是光照模型中常见的View向量,与法线N起点一致。power用来控制效果,显然由于1-V·N在[0,1]间,power越大,那么越垂直于N的V的F才越大。在下图中,也就是当power越来越大,黑色会扩大,而白环会减少,慢慢收缩到最边缘那些法线十分垂直于视线的顶点上(白色为1,黑色为0):
显然这个函数可以用来做边缘光等其他特效。
那么,我们回过头来看透明水波材质。它的Fresnel节点的power赋予了5,十分接近边缘:
注意:
以下简称Fresnel_Fuction(...)为Fresnel(power)。
以下称V垂直于N的情况为“垂视”
原材质给金属度设置的是0。可以理解,因为金属材质不发生菲涅尔效应,而且金属度越高,透明金属材质越丢失BaseColor。但金属度太低,水的透明和反光也就低了一个层次。看下金属度为1的水面,按需求和风格调整即可:
(油腻的反光又出现了)
反光直接给1,没什么好说的。给0就失去了波光粼粼的效果。
回看透明水波材质。它的粗糙度被设置为:
也就是越垂视的地方粗糙度越为0,完全光滑;其他地方为0.1,也很光滑,但较边缘差了点。
对于对面,从直觉上来说是这样,而且Roughness影响反射光计算,越光滑反射越多,符合菲涅尔效应。
实际效果上对比不明显。
越垂视,越不透明。也符合菲涅尔效应。
在介质密度不同的表明,光发生折射。从低密度到高密度,折射角小于入射角。
回忆初高中物理,折射率定律。国外更一般叫成斯涅尔定律(Snell's Law):
由于真空密度最低,或者说真空光速最快,所以所有的材质折射率都大于1。
从真空射入某材质:
透明水波Shader认为越垂视,折射率越高,这是对的。首先直觉上,越弯曲的玻璃,折射越厉害。
其次,你可以认为空气与水折射率不变,当入射角变大,那么折射率一定要变大。
或者你可以认为折射率=c/v,在垂视的地方,光路更长,相当于v更小,所以折射率更大了。
你可能会发现我们这个水波像是三角透明玻璃组成的,十分风格化。放大之后是这样的:
原因是这个透明水波Shader的法线。
图中用了一个函数称为Shine_func,打开后发现实现十分简单,效果也和迪厅反光球一样:
关键是DDX和DDY是什么?
这个蛋疼的起名让我一开始以为是二阶导数,但明显不对。经过搜索我发现它就是屏幕空间x方向偏导数和y方向偏导数。
问题在于为什么结果它是这样的,而不是我们之前那么顺滑的连续函数。
以DDX为例,就是检查当前像素点(Pixel)在屏幕水平方向上离得最近的左右两个顶点(Vertex),计算从左顶点到右顶点的空间向量,再归一化。
我将这个结果赋予BaseColor,这样看的准一点。
当屏幕向右是世界系y方向时,其值偏向为(0,0,1)。因为屏幕上的顶点从左到右,顶点向y轴方向变化(注意左下角世界坐标系的方向):
当我大致面朝(1,1,0)方向:
注意到方块红面由于在往屏幕右方向走时,顶点世界位置x增大(导数大于0),y不变(导数为0),而z变小(导数为负,负数clamp后为0),其ddx为(1,0,0)
方块上面由于在往屏幕右方向走时,顶点的x,y方向都有增加且一样,ddx为(1,1,0)
当我的屏幕右方向更接近x轴时,方块黄面开始变红:
尽管我们的相机位置一直在变,同一个顶点的DDX和DDY也在变,但其都保持在切平面上,叉乘出来也一定是法线方向。
这里可以看出UE4材质系统的2个缺点:
1.明明与真正的WP无关,但却必须引用这个节点,给读者以误导
2.程序执行顺序对用户不清楚。这里的WP明显是已经经过WPOffset以后的WP了。WPOffset中的WP节点和这里的WP节点明明一样,出来的值却不一样,若不是我们知晓渲染管线流程,就会被搞得怀疑人生。
上面看出来了,叉乘出来的法线应该属于世界坐标系,但我们的材质默认是切线空间法线。原材质连这个都没注意到,所以我说编者脑子不清楚。
你可能记得,材质有个选项可以修改。但因为一个蛋疼的原因,我们不得不手动计算,之后会讲到。我们需要手动将世界坐标系的法线转到切线坐标系。
(上图从左到右分别是原法线,编者原材质水波法线,和我修改后的法线。我猜由于透明水波既没什么阴影,水波的折射是否正确也很难有人看出来,所以这个问题没人注意。)
所幸UE4有节点轻松解决坐标系转换:
看到没有,球下方边缘有很奇怪的折射。
本来是水面折射,怎么会和球有关?既然有“屏幕空间反射”,那么这个折射是不是也是基于屏幕空间算的,然后出的问题呢?
在材质detail中搜索Refraction,我们发现引擎提供2种算法,默认算法是这样的:
简单地来说它警告你不要用于像水面一样的大面积折射面。关于为什么会有这个bug它也没说细说,只说这个是物理模拟。
我选择第二种:
它说这个折射是个假模拟,值给1就是没折射,2就是offset为1的折射。这个offset是啥意思它也没说清楚。还告诉我们必须用切线空间法线,这就是为什么我们之前要手动转换。
蛋疼吗?蛋疼。UE4辣鸡吗?辣鸡。
我更改"折射"为:
因为这个假折射率要比较大,折射效果才明显。没办法,谁叫真折射有Bug呢?咱目前这种情况,就是我写本系列教程的缘由,了解原理,运用原理,避免这种情况。因为弱势,所以被牵着鼻子走,不知道在干什么。很不幸,我因为辣鸡,又碰上了这种情况,成为了一个"能用就行"的乞讨者。
以后有机会尝试改写引擎代码,改写这个折射。
本篇解析,改进,批评一个国外免费透明水面Shader,对简单的物理和材质系统进行初步了解。
由于能力的不足,在折射问题上最终又成为了一个被拦在原理和实现之外的人。之后提升自己的能力,改变这一状况。
水波模拟系列到此告一段落。