我用于渲染_CameraDepthTexture的相机是一个透视相机。正交的情况没试,估计差不多。
unity的image effect的机制我们大致都了解:它是画了一个覆盖全屏的quad(具体尺寸和位置未知)。
要注意的是image effect使用的投影矩阵并非相机的投影矩阵,而是使用了一个正交投影矩阵(具体形式未知),这一点我卡了好久才意识到,不过一想也很正常合理,毕竟只是想画个全屏quad,直接push个正交矩阵(即切换到2d模式)去画就好了。
虽然image effect使用的投影矩阵和相机不一致,但两者之间的联系是投影到2d屏幕的结果是相同的,这是关键。
下面开始重建:
对于image effect下的当前像素p,可以获得其屏幕坐标sPos_p=(screenX_p,screenY_p,0),这也是p在相机下的屏幕坐标,由于_CameraDepthTexture恰好铺满相机屏幕,所以p在_CameraDepthTexture上的纹理坐标sUV_p=sPos_p.xy/screenSize,即p点处深度值为
depth=_CameraDepthTexture(sUV_p)
设p点处深度值为depth的点为p‘,则p’在相机空间的z值为
linearDepth=LinearEyeDepth(depth)
如图,即|QP‘|=linearDepth:
现在我们要求的是P‘的世界坐标,计算如下:
1,先求P点的世界坐标,分两步:
(1)求p的归一化设备空间坐标ndcPos_p=screenToNDC*sPos_p.
(2)求p的世界坐标wPos_p:
temp=clipToWorldMatrix*ndcPos_p. (clipToWorldMatrix须从外部传入)
wPos_p=temp/temp.w
2,设由eyeWorldPos指向P的单位向量为dir,则dir=normalize(wPos_p-eyeWorldPos).
3,distance(eyeWorldPos,P‘)=|QP‘|/cos(theta)=linearDepth/dot(eyeWorldDir,dir)
4,最终P‘点世界坐标为wPos_p‘=eyeWorldPos+dir*distance(eyePos,P‘)
重建完成。
说明:
1,eyeWorldPos可在shader中通过内置变量获取。eyeWorldDir我试过直接用内置变量UNITY_MATRIX_V [2].xyz,结果似乎不对,于是改用了从外部传入camera.tranform.forward。
2,这里只是为说明原理方便,使用了“image effect和相机两种渲染模式下屏幕空间坐标相等”这个条件来解题,而实际上若利用“两种模式下归一化设备空间坐标相等”来做更简单些,至少不用把screenSize牵扯进来了。
3,clipToWorldMatrix必须从外部传入,因为
clipToWorldMatrix=camera.cameraToWorldMatrix*camera.ProjectionMatrix.inverse
其中camera.ProjectionMatrix必须从外部传入才能获得正确的值,因为shader内部unity已经把它偷粮换柱成2d正交矩阵了。
4,Projection或unProjection的过程一定不要忘了做透视除法。
5,linearDepth的确切含义是“相机空间中的z坐标”,按此含义,它表示的是前面图中|QP‘|的长度,而非eye到P‘的距离。
6,如何验证上面方法重建出的世界坐标是正确呢?
我采用的方法是把distance(eyeWorldPos,P‘)/farPlane作为颜色值渲染出来。然后再另搞一个非重建的测试用例,也把distance(eyeWorldPos,worldPos)/farPlane作为颜色值渲染出来(使用完全相同的测试场景,完全相同的相机角度),然后看两幅图是否一模一样。我测试的结果是一样的。
----补充:
1,如果嫌unity自带的image effect机制有太多黑箱,比如全屏quad位置和尺寸不详,所使用的投影矩阵不详,也可以自己搞custom的image effect机制来替换unity自带机制:自己用GL指令画全屏quad,自己push投影矩阵。参考:http://flafla2.github.io/2016/10/01/raymarching.html(这是一篇讲在unity中搭建raymarching环境的文章,其中采用了自定义image effect机制的做法)。
2,在网上搜到一些其它重建源码:
https://github.com/zezba9000/UnityMathReference/tree/master/Assets/Shaders/DepthBuffToWorldPos
(这个我仔细测试了一下,效果是正确的,但其DepthBuffToWorldPosDemo.cs中计算clipToWorld的方法有点儿怪,我没理解)
https://github.com/keijiro/DepthToWorldPos
(这个没细测,感觉也是正确的)