这篇文章并不是谈代码里怎么实现,而是阴影效果的实现理念。先有概念上的理解,才有代码的实现。
现实里,阴影是怎么出来的?是光朝某个方向照射,有一部分照在了前面的物体上,造成在后面的物体上亮度出现差别。亮度低的地方就被我们认作是阴影。
可以说没有阴影这回事,只有光,只要物体表面的光是正确的,那么阴影就自然出来了。
在刚开始接触3D世界里的阴影的时候,我在想,光照不是已经算出来来了吗?那么阴影应该自动出现了。可实际上在Unity里计算物体的光照时,我们是认为光能够照到任何地方的。比如漫反射:
fixed3 diffuse = _LightColor0.rgb * saturate(dot(lightDir, i.worldNormal)) * albedo;
这里的_LightColor0
是光照,我们并没有做任何的判断,何时_LightColor0
有何时没有,拿来就用。
如果要按照现实同样的逻辑来计算阴影,那么就要判断光照何时没有,也就是在当前的基础上做减法。但实际并不是这么做,而是做加法。
理念1: 把阴影看做是一种特殊的光,在原来的物体表面覆盖上去。
如果你用电脑画过画就会很容易理解,在画图时给一个部分加阴影的做法是:新开一个图层,放在原色图层的上面,然后设置成正片叠底的模式,在这个图层用灰色画就可以达到阴影的效果。正片叠底其实就是颜色叠加,在一个颜色上面叠加灰色,原颜色性质保持,就是变暗了,跟阴影效果符合。
用代码解释:
color x = (a,b,c); //rgb分量分别是a b c
color shadow = (0.9, 0.9, 0.9); //灰色作阴影,每个通道值是在[0,1]
color y = x * shadow = (a * 0.9, b * 0.9 , c * 0.9); //颜色混合效果就是各通道分量相乘
这样做剩下的问题就是找到那些地方有阴影这种特殊的光,在计算物体颜色的时候,做一个乘法就搞定了。
哪些地方有阴影呢?先看一束光,假设位置A和B在同一束光里,那么谁在后面谁就有阴影。可以想象一下烤串的样子,只有第一个有光,后面的都在阴影里。
所以,如果我可以建立一个数据表,把一个光源所有方向第一个被照到的位置记录下来。那么对于新的位置A,就在这个数据表里找到同一束光第一个位置B,如果A比B近一些,那么A就变成了这束光第一个被照到的地方,且A就没阴影,反之就有A就在阴影里。
对于一个光源来说,我只要维持一个这样的数据表,就可以把阴影的问题搞定了,这个数据表就是阴影映射纹理。纹理这类东西,就是你给一个xy坐标,它返回给你一个特定的值,在这里,这个xy坐标就是用来定位这束光的坐标,而返回的值就是阴影的颜色,如 (0.9, 0.9, 0.9)。
这个逻辑其实跟深度纹理是类似的。
理念2:使用阴影映射纹理
那么问题就转化成了如何定位一束光?
解决方案就是建立一个光源自身的坐标体系(即光源空间),而这个坐标系的xy位置就是用来定位每一束光的。
关于光源空间怎么定义的,下面是我的猜测。
比如对于平行光,平行光没有光源位置概念,只有方向。那么我把光照射方向作为Z轴,与光照射方向垂直的面设为xy轴平面。那么是不是xy值就可以定位一束光的呢?我觉得是的。两个点如果xy值一样,那么他们肯定在同一束光里,因为Z轴方向就是光的方向。
对于点光源,光是球形的发散出去的。光的方向是变化的,那么xy轴该怎么搞?在3D图形空间变换里有一部分,是把顶点从相机空间转变到裁剪空间里去,也就是把照相机的那个有视锥体转变成xyz都是[-1,1]范围的一个立方体。那么光源这里可以同样采用这种变换手段,只是这时是360度的视角。
发散一下,假如光会拐弯,那么xy平面就取垂直于光线的一个平面,这个平面可能是一个凹凸不平的面,把这个不平的面展开成平面就好了,就跟不平的模型表面的纹理坐标一样。这样的截面只需要一个就可以,比如就取包含光源本身的那么面。
对于一个点A,如果不在这个截面里,就可以沿着光线的回流,直到找到同一束光线上的xy截面的点B,这个点B和A具有相同的xy值。
(突然隐约想到流体分析里是不是用了这样的想法。)