2D光影系统-阴影

接上一篇2D光影系统-光源二Sprite光

截屏2024-02-26 11.26.41

前言

在前两篇文章中,我系统介绍了2d的光照,完善了全局光和sprite光,在这一篇中,我们要用到全局方向光的方向属性,来投射阴影,比较硬核,咱们徐徐道来。

阴影(Shadow)

在3D世界,太阳就是平行光,物体会在平行光的背面,按照平行光的方向投射出阴影,而在2D场景中,我们只能模拟这种现象,按照指定图片的轮廓,在指定的位置,以指定的方向,绘制在接收投射的地方,一般就是地面

原理

道理我们都懂,但具体怎么做呢,想想前面两篇文章,我们得到了一个叫光照图的东西,有了这个图,我们场景的所有物体都可以通过采样它来改变自己的颜色。阴影其实也一样,我们需要一张阴影图,把这个阴影图和光照图合并在一起,得到一张光影图,在这张图里,有阴影的地方是暗色调的,没有阴影的地方就是光照图的原色,然后注意了,我只希望地面接收阴影,所以这个光影图是留给地面采样的,而场景中其他物体,不需要接收阴影,所以它们还采样没有阴影的光照图。

阴影

所以第一步,就是绘制阴影。在本案例中,我们的树就是投射阴影的物体,首先,我在树的Sprite下挂了一个cocos自带的mesh–Quad,起名shadow,如下图

WeChat857d0be1574dd6bff2d7b9953ca9bf73

WeChat3365ae6b164fdf6ea18ab7d0886acf9c

之所以用Quad,而不用Sprite,是考虑到drawcall问题,也就是合批,树的材质和阴影的材质不一样,这样就会打断Sprite的合批,树的sprite会走动态合批,而Quad,我会让它们走gpuInstancing合批。然后给shadow换个Layer,我这里起名SHADOW,如上图,同时,增加一个Camera,只照阴影,所以它对应的Visibility也是SHADOW。

下面重点来了,就是我们的阴影材质,而投影的核心算法就在这里,给我们的阴影一个shader

vs部分:

WeChatf2f31c9a78399022909403f6724ea1a6

大致解释一下,_LightAngle是方向光的角度,也就是阴影投射的角度;_ShadowStartOffset是阴影的起始偏移,因为原点是中心点,我们必须偏移到物体脚底下显示才正确;_ShadowScale是阴影的尺寸,想想昼夜变化,早晚时阴影会比较长,中午了阴影会变短,这个尺寸就是干这个的。

顶点着色器的核心作用是把要投影的物体的每个顶点,根据投影起始坐标和投影方向,计算得到新的位置。而核心算法,就是已知投影平面(空间一点和法线构成平面方程),投影方向,求某点在这个投影平面按投影方向投影的点

图片 1 图片 2

所以是3d空间映射到了2d空间的图像。这个虚拟的投影平面,就是点和法线构成的,点就是树根的世界坐标,如上的planePoint,法线是自定义的向量,如上的planeNormal,而投影方向向量就是projDir,最后用每个顶点到planePoint的向量planeVec,根据投影方程得到新的世界坐标即可。

fs部分:

WeChat2fad6003e8312aba1e6945addd1a0919

fs部分比较简单,其实就是要图的轮廓,毕竟阴影就是个纯黑嘛,所以我只关心alpha部分就行了,col.a就是轮廓了,而_ShadowIntensity是阴影的透明度。

材质部分如图,除了参数部分的设置,可以看到USE INSTANCING也勾选了,可以做到gpuInstancing合批

WeChat9cd2543c173514721ed4a8659f2b5a7a

光影图

有了阴影,我们就要生成阴影光照图了。上面已经提到了,我们要给阴影一个独立的Camera,照那些Shadow的mesh,就自然得到了阴影图,然后通过自定义后处理,结合我们之前得到的光照图,最终输出到一张rt中,也就是光影图了。

先看看Camera的设置

WeChat8c0e6e619a5b1aaa2a2fb4192c434102

如图,我把最终结果输出到light-shadow-rt中,这就是我的光影图,在自定义的后处理中,我要把之前得到的光照rt传进来

WeChat48e944ad884c5c4c2e90626af80b0b86

代码很简单

WeChat0b6a39c479867ff03c0b9faee68c7bd2

没啥好解释的,就是把Camera的阴影图传给shader的_ShadowPPRt这个变量中

WeChat908991679ce1c1cf4375d047d90d7f37

shader也很简单,采样阴影图_ShadowPPRt,在传进来的光照图_lightRt上,有阴影的地方做颜色的衰减而已,用的就是之前阴影shader的alpha值,只是要用1减去它,所以阴影越浓,光照色越暗

输出光影图

到了这一步,该得到的我们都已经得到了,场景物体对光影图的采样与第一篇无异,只是注意,地面的光影图要用我们这里新生成的光影图,而场景物体,比如树木篝火等,要采样之前的光照图,这样不会被阴影遮住。效果就如开篇所示了。

昼夜变换

题外话,之前光照我们做了昼夜变化,阴影自然也不能落下,前面我们提供了阴影的角度,尺寸,强度,就是干这个用的,想象一下,太阳东升西落,阴影除了角度变化,阴影的长短,浓淡都在变化。参数都有了,做个脚本支持一下而已。

在第一篇里面,我们做了个简单的昼夜编辑器,模拟阳光颜色的变化,现在我们再把上面的参数也都加进去。

WeChat90208cd839f222d6f9d631de3f17f28b

WeChat1174aa57e323c6623ec1594b84bdd1c6

startSunAngle和endSunAngle是方向光的起始角度和结束角度,也就是阴影的角度,在update中根据一天的时间在这两个角度中做插值,shadowIntensity是阴影强度,用了cocos的curve,也就是曲线编辑器,但吐槽一下,这东西感觉不太好用,不如unity好使,就是做了个曲线,模拟阴影强度的变化,早晚是0,中午达到最大。shadowScale是尺寸,也用曲线模拟,跟强度相反,早晚最长,中午最短。编辑器如下

WeChat8e16a49277dab44e6e182e67b008ce49

最终上个动画效果

113

打完收工!!!

17赞

深度好文,666

大佬,有demo看下吗

求分享下源码,看看能做出来吗

真棒,希望能分享源码

牛掰克拉斯

大佬牛逼!
还有点疑惑:
火把的光照亮一部分阴影效果 和 树多了阴影的层级关系 该怎么办?

火把照亮一部分阴影效果是啥意思,是说有些阴影不希望照亮吗,如果是这样,那就得分光影图了,不希望被照亮的单独分一张光影图出来;树多了阴影层级关系,你是指阴影之间有重叠会加深吗,可以用在阴影shader里加模版缓冲区,有相同模版的就不用绘制了

1赞

本系列教程已介绍的比较清楚,如果实在有同学想要看源码,本工程已上架Cocos Store,研发不易,敬请赞助顿饭钱,万分感谢:2D昼夜光影系统

性能太差啦!!

好文帮顶。

怎么讲,麻烦细说一下,本方案并未用复杂的计算,cpu端基本不费,并且用了GPU Instancing,同种物体的阴影一次绘制成型,光照也是一次绘制成型,因为用了mesh,不会打断sprtie的动态合批,截图上10个drawcall搞完所有事情,Shader上也未用复杂的计算,期待留下您宝贵的意见,我也好改进

我想是不是在shader里面有频繁的三角函数计算,反正ios微信不满帧

哦哦,谢谢反馈,2d游戏gpu一般不是卡点,就这点计算量如果都不行,那3d的就疯了,卡点一般会出在cpu,后面我排查一下,看看代码哪里有性能问题,再一次感谢 :pray:

这个项目在编辑器层面还要设置什么吗?为什么我新建一个项目,把脚本和材质拖进去,场景节点也设置成一样,为什么会发生异常?我把你项目的 settings复制替换进去就好啦。

因为要用到自定义后处理,所以要开自定义管线,不知道这个你开了没

WeChat3c4d080e2f60af7ef44253f6cd4d2d21
WeChat9702478b8cbe140696b16ae590987d77

明白!!!!!

能出一个教程讲解一下源码吗?我买了,但是我实在不清楚,你的项目里都是2d,为啥会讲到3d投影

~M1E8DSE7ZHA%TEOWVQQC
1和2 之间是什么关系,怎么联系的

实话说,我用三篇文章在讲解源码啊,你跟着看下来就足够了,有些知识领域可能需要自己去扩展,比如你说的2d项目为啥会用到3d投影,其实都是算法而已,3d投影也只是投在了2d图像上,额外的知识可能需要去了解平面方程,投影算法,而图2中,1是后处理,是为了处理阴影相机照的阴影图的相关操作的,2是树的阴影片,是阴影摄像机照的东西,从而生成阴影图给1的后处理操作用的,扩展知识是你需要了解后处理是什么,RenderTexture是什么等等,阴影这篇文章已经把怎么做讲的很清楚了