本文承接上文,实现了阴影灯光相机与feature来渲染深度图的主框架,同时也留一下一些问题,比如视角变化的问题,呈现出来的依然是主相机的视角,第二个问题是原来的代码只支持只有一个submesh的情况,在mesh有多个材质球的情况下会导致只绘制第一个submesh,会导致绘制出来的深度图不完整。除了要解决上面的两个问题,这次还要实现RenderTexture的回读,包括同步回读与异步回读的比较,从GPU回读深度图后,可以将回读数据保存到图片中,为后面的一些工作做铺垫。
一:问题修复
1、灯光相机视口变换问题
上次的shader中,使用的是注释部分的代码,导致渲染的RenderTexture中的视角变化是主相机的视角,深度值虽然是对的,这次进行了更正,同时也注意到了这次的变量有用CBUFFER_START标签包含起来,这是为了能融入URP本身的SRPBatch优化。同时我们新增了相机空间的投影矩阵,然后通过脚本外部传入,这样我们就能得到正确的显示方向了。

这里有个地方要注意,在脚本中传入相机矩阵时需要做一次转换,通过GL.GetGPUProjectionMatrix来将矩阵转换为对应的平台,这是因为DX平台和OPENGL平台的NDC空间坐标范围不同,所以要做一次转换,第二个参数是是否写入纹理,我们要写入RT中,所以这里为true。和false的区别是y轴方向的翻转。

2、mesh有多个材质球的问题
图中的树为例,我们同一个mesh,但是拥有多个材质球,而我们之前的代码是drawMesh了一个submeshIndex为0的情况,这里我们要根据mesh的submesh的数量来进行一次循环操作。


到这里我们的问题就解决了,接下来我们在在URP管线默认的LitShader进行改造,来插入我们的pass。
二:LitShader改造

首先我们把Lit这个Shader从目录里面复制出来,将名称改成自己的名字,比如我这里改成了standardDepth,然后我们根据需要去除一些不必要的pass,比如MetaPass和Universal2D这两个pass,然后在其中插入我们的pass,我放在了第二个。接着我们要原来的顶点着色器做一些改造,让他保持和Lit一致,比如说支持实例化,

下图中的2个部分,上面是实例化相关的宏,从其他pass复制过来就行,顶点函数部分需要用UNITY_SETUP_INSTANCE_ID和UNITY_TRANSFER_INSTANCE_ID来包裹。然后我们将材质的shader更换为这个新的shader,兼容了lit的处理方式,同时也支持了我们的pass。

经过前面的步骤,我们终于得到了灯光相机渲染的深度图,现在我们要来从GPU里面把这些深度数据读取出来了,下面主要介绍2种方式来从RenderTexture中来读取数据:同步和异步。
三:同步回读
和其他语言的差异不大,同步回读就是指cpu从GPU回读数据时,需要等待GPU的返回,GPU可以看成时后端,cpu可以看成时前端,这样在GPU返回数据之前,我们cpu将会阻塞,后面的代码需要等返回后才会继续执行。实现的方式时拿到渲染好的RT后,将RT复制一份,同时将复制的临时RT设置为激活状态,这样就能通过Texture2D来读取屏幕缓冲区的像素数据,读取完成后,将RT的激活状态设置为null,同时调用api来释放临时RT。
下面给出代码。

四:异步回读
异步回读的话,基本原来类似,只不过将在commandBuffer中以命令的形式告诉gpu,通过回调函数返回request对象,然后通过GetData这个接口来获取color数据,这样操作能避免IO带来的阻塞,cpu能继续干其他时间,通过回调来获取最终得数据。注意这里的关键点是在cb中进行RequestAsyncReadback调用,将这个命令加入到了当前feature的命令队列。

五:保存深度图
拿到GPU中的数据之后我们只需要将数据整理好然后通过API来写入到图片文件中进行输出,代码如下。通过调用原生的文件IO,我们将Color先存入Texture2D中,然后将纹理编码为png进行2进制写入。这里我们是放在了Asset文件夹中,运行项目后,我们将在Asset文件夹下出现一张名为shdowmap.png的深度图,即为本文开始部分的图片。

到这里我们已经可以从内存和文件的形式保存我们的深度数据了,接下来的工作中,我们将更近一步,利用深度图来完成具体的阴影逻辑,例如CSM技术,本次到此结束,敬请期待我们后续的内容。