在Unity中实现屏幕空间反射Screen Space Reflection(2)

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了。

posted @ 2017-09-22 13:50  yangrc1234  阅读(4460)  评论(0编辑  收藏  举报