--------------更新
更簡單的方法:
//depth: raw depth, nonlinear, 0 at near plane, 1 at far plan
float4 screenUVwithDepth=float4(screenUV,depth,1);//now x,y,z are in [0,1]
ndcPos=screenUVwithDepth*2-1;//now x,y,z are in [-1,1] and w=1
float4 worldPos=mul(clipToWorld,ndcPos);
worldPos/=worldPos.w;//important
--------------舊帖
我用於渲染_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必須從外部傳入才能獲得正確的值,因為image effect shader內部已經被unity偷糧換柱成正交矩陣了。
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
(這個沒細測,感覺也是正確的)
----補充2
進一步驗證,用重建的世界坐標(與一張shadow map配合)生成的陰影:
由此進一步證明了重建結果的正確性。