traceRay函數
在上一篇中,我們有如下簽名的traceRay函數
bool traceRay(float3 start, float3 direction, out float2 hitPixel, out float3 debugCol ) {
}
其中的參數意義都很明了。start和direction是相機空間下的光線起點,以及光線方向。
traceRay的核心代碼並不復雜,如下:
#define RAY_LENGTH 2.0
#define STEP_COUNT 64 //maximum sample count.
UNITY_LOOP //強制使用循環結構,不然就會代碼5秒鍾,編譯1小時
for (int i = 1; i <= STEP_COUNT; i++) {
float3 p = start + (float)i/STEP_COUNT * RAY_LENGTH * direction ; //p是當前的光線的空間位置
float pDepth = p.z / -_ProjectionParams.z; //_ProjectionParams.z是far clip plane的值。又因為viewspace下正前方z值是負的,所以加個負號。
float4 screenCoord = mul(_Projection, float4(p,1)); //將光線投影到screen space中。
screenCoord /= screenCoord.w;
if (screenCoord.x < -1 || screenCoord.y < -1 || screenCoord.x > 1 || screenCoord.y > 1)
return false;
float camDepth = Linear01Depth(tex2Dlod(_CameraDepthTexture, float4(screenCoord.xy / 2 + 0.5,0,0))); //獲取當前像素的深度。為了使用循環結構,這里必須用tex2Dlod而不是tex2D。
if (Intersect(pDepth,camDepth) ) { //相交檢測
hitPixel = screenCoord.xy / 2 + 0.5;
debugCol = float3(hitPixel, 0);
return true;
}
}
相交檢測
最簡單的方式
最簡單的,如果該像素的深度大於當前光線的深度(離相機更遠),此時我們認為這是一個命中。
if (pDepth > camDepth) {
...
}
該種方法如上圖所示,可以看到物體的下方會有明顯的“拖影”。
加入厚度
為了改進效果,我們加入一個像素厚度的考量。當光線位於像素后面,並且不超出該像素的厚度時,才算命中。我們往往給像素一個固定的厚度。
if (pDepth > camDepth && pDepth < camDepth + 0.001 ) { //0.001是厚度
...
}
如圖,拖影不見了。
獲取像素實際的厚度
這種方法一般情況下就已經足夠好了。如果要進一步改進的話,我們可以通過backface渲染,得到第二張深度貼圖。通過將兩張深度貼圖的采樣相減,得到一個像素的“厚度”。再按照這個厚度去做相交測試。
后處理腳本:
private void OnRenderImage(RenderTexture source, RenderTexture destination) {
RenderBackface();
mat.SetTexture("_BackfaceTex", GetBackfaceTexture());
mat.SetMatrix("_WorldToView", GetComponent<Camera>().worldToCameraMatrix);
Graphics.Blit(source, destination, mat,0);
}
private void RenderBackface() {
if (backfaceCamera == null) {
var t = new GameObject();
var mainCamera = Camera.main;
t.transform.SetParent(mainCamera.transform);
t.hideFlags = HideFlags.HideAndDontSave;
backfaceCamera = t.AddComponent<Camera>();
backfaceCamera.CopyFrom(mainCamera);
backfaceCamera.enabled = false;
backfaceCamera.clearFlags = CameraClearFlags.SolidColor;
backfaceCamera.backgroundColor = Color.white;
backfaceCamera.renderingPath = RenderingPath.Forward;
backfaceCamera.SetReplacementShader(backfaceShader, "RenderType");
backfaceCamera.targetTexture = GetBackfaceTexture();
}
backfaceCamera.Render();
}
private RenderTexture backfaceText;
private RenderTexture GetBackfaceTexture() {
if (backfaceText == null) {
backfaceText = new RenderTexture(Screen.width, Screen.height, 24, RenderTextureFormat.RFloat);
backfaceText.filterMode = FilterMode.Point; //VERY IMPORTANT!
}
return backfaceText;
}
渲染背面深度的shader(來自kode80):
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'
Shader "Unlit/BackfaceShader"
{
Properties
{
}
SubShader
{
Tags { "RenderType"="Opaque" }
LOD 100
Cull Front
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct v2f {
float4 position : POSITION;
float4 linearDepth : TEXCOORD0;
};
v2f vert(appdata_base v) {
v2f output;
output.position = UnityObjectToClipPos(v.vertex);
output.linearDepth = float4(0.0, 0.0, COMPUTE_DEPTH_01, 0.0);
return output;
}
float4 frag(v2f input) : COLOR
{
return float4(input.linearDepth.z, 0.0, 0.0, 0.0);
}
ENDCG
}
}
}
float camDepth = Linear01Depth(tex2Dlod(_CameraDepthTexture, float4(screenCoord.xy / 2 + 0.5, 0, 0)));
float backZ = tex2Dlod(_BackfaceTex, float4(screenCoord.xy / 2 + 0.5, 0, 0)).r;
if (pDepth > camDepth && pDepth < backZ) {
hitPixel = screenCoord.xy / 2 + 0.5;
debugCol = float3(hitPixel, 0);
return true;
}
如圖
注意我在C#腳本中標注的IMPORTANT一行。少了這一行導致了一個非常難debug的bug。具體原因是相機的深度貼圖是Point filter的,而自己創建的rendertexture是默認Bilinear filter的;如果不修改的話,我們用同一個坐標去采樣會導致實際上是不同位置的采樣進行相減。
要注意的是,這種獲取物體厚度的辦法並不萬能。比如一個物體是只有單面的,此時厚度計算就會出問題(可以想想為什么),類似的,如果相機在一個物體內部(其實也相當於單面)也會出問題。
對於這些單面物體,如果是透明物體,可以設置為Transparent,不寫入z緩沖,並且RenderType設置為非Opqaue,此時背面渲染shader就會忽視這個物體。
同時,此時光線有可能和物體的“背面“相交,但是毫無疑問我們只能獲得物體“正面”的顏色信息。此時反射出現的內容依然是物體的正面,對於純色物體這沒什么問題,但是對於其他物體就會顯得很weird了。