zoukankan      html  css  js  c++  java
  • 在Unity中实现屏幕空间反射Screen Space Reflection(4)

    第四部分讲一下如何在2D屏幕空间步进光线。
    http://casual-effects.blogspot.com/2014/08/screen-space-ray-tracing.html 中的代码感觉不太好理解,这里的代码是按照我自己的理解去重新实现的简单版,在效率上可能不如这个网址中的代码。

    3D空间的光线步进

    原本的实现中,我们得到光线后,将其在3D空间中进行步进,再投影到2D空间上。在投影过后,3D空间中均匀的采样点在2D空间中就不是均匀分布的了。

    (图来自 http://casual-effects.blogspot.com/2014/08/screen-space-ray-tracing.html)
    这种不均匀导致了采样效率的低下。特别是光线的z方向的步进较大时,这种不均匀采样更为严重。此时有大量的采样被消耗在同一个像素上,或者一大片区域只有少数几个采样,这种情形是很多见的。
    为了改进这个情况,我们将算法改进为在2D的屏幕上,按照像素步进光线。

    相比于原来的在3D空间中步进光线,再将光线投影回2D屏幕,采样对应点的深度,做相交检测;在2D空间中步进光线,我们可以免去投影回2D屏幕这一步骤,但是我们也无法知道2D屏幕上的点的深度是多少了。
    解决办法很简单,我们用2D空间中的光线起点和终点(这两个点的z值是知道的)做插值,得到中间的点的z值即可。
    另外需要注意的是,为了获取透视校正的插值,我们需要做的是用1/z做线性插值,而不是直接对z做插值,如果对光栅化有了解的话应该可以理解这一段。

    首先我们将起点和终点投影到屏幕空间里。

    				float4 H0 = mul(unity_CameraProjection, float4(start, 1));		//H0.xy / H0.w is in [-1,1]
    				float4 H1 = mul(unity_CameraProjection, float4(end, 1));
    
    				float2 screenP0 = H0.xy / H0.w;
    				float2 screenP1 = H1.xy / H1.w;		//。屏幕空间的采样坐标。
    

    接着我们算出两个点在屏幕上的距离,以及对应的采样步长。

                                    float4 texelSize = _MainTex_TexelSize;
    				if (abs(dot(screenP1 - screenP0, screenP1 - screenP0)) < 1.0) {
    					screenP1 += texelSize.xy;
    				}
    				float2 deltaPixels = (screenP1 - screenP0) * texelSize.zw;	//屏幕上两点的像素间隔。
    				float step;	//线性插值的步长。
    				step = min( 1 / abs(deltaPixels.y), 1 / abs(deltaPixels.x)); // 使每次采样都会恰好间隔一个像素
    

    这里的step是采样的步长。当step == 1时,一步就从start采样到end了。
    step = min( 1 / abs(deltaPixels.y), 1 / abs(deltaPixels.x)); 使得每次步进都会在较长的轴上步进一个像素的距离

    				step *= PIXEL_STRIDE;		//加大采样距离(加快插值进度)。 
    				float sampleScaler = 1.0 - min(1.0, -start.z / 100);	
    				step *= 1.0 + sampleScaler;				//距离较近时(不容易采偏),插值进度更快
    

    PIXEL_STRIDE是一个大于1的数,用于加大采样像素的间隔。
    接着我们加入一个samplerScale。当一个像素实际距离镜头较远时,我们需要相对较小的采样步长(因为远处物体的像素要更少,需要更精确的采样),对于近处的物体我们可以用相对宽松的步长。

    下面是采样的部分。

    
    				float interpolationCounter = step;	//记录当前插值的进度。采样进度计数,大于等于1时采样就会结束。初始值为step可以避免一些奇怪的情况。
    
    				float oneOverzCurrent = 1 / start.z;  //当前采样点的1/z,
    				float2 screenPCurrent = screenP0; //是当前采样点的屏幕坐标。
    
    				float dOneOverZCurrent = step * (1 / end.z - 1 / start.z);        //1/z的每两个采样的差值
    				float2 dScreenPCurrent = step * (screenP1 - screenP0);        //同上
    
    				oneOverzCurrent += jitter * dOneOverZCurrent;
    				screenPCurrent += jitter * dScreenPCurrent;
    				float intersect = 0;
    				float prevDepth = 1 / (oneOverzCurrent + 0.1 * dOneOverZCurrent) / -_ProjectionParams.z;	//上一个采样的z值,用于线段求交;+ 0.1 * dOneOverZCurrent可以防止因为精度问题导致光线在起点自交的问题。
    #if 1
    				UNITY_LOOP
    				for (int i = 1; i <= STEP_COUNT && interpolationCounter <= 1; i++) {
    					oneOverzCurrent += dOneOverZCurrent;
    					screenPCurrent += dScreenPCurrent;
    					interpolationCounter += step;
    					float screenPTrueDepth = 1 / oneOverzCurrent/ -_ProjectionParams.z;    //求出当前光线终点实际的z值
    					if (RayIntersect(screenPTrueDepth,prevDepth, screenPCurrent)){        //求交
    #if 1  //binary search
    						... 
    #endif
    						hitPixel = (screenPCurrent) / 2 + 0.5;
    						intersect = 1;
    						alpha *= 1 - (float)i / STEP_COUNT;
    						break;
    					}
    					prevDepth = screenPTrueDepth;
    				}
    

    jitter是抖动值,可以优化最终效果。可以通过

    
    				float2 uv2 = i.uv * _MainTex_TexelSize.zw;
    				float c = (uv2.x + uv2.y) * 0.25;
    				float jitter = fmod(c,1.0);
    

    计算得到。

    其他的

    到这里,实现SSR的核心方法应该就差不多了。剩下的就是一些通用的优化方法了,比如降采样、加模糊等等,就不再说了。

    需要一提的是,这里的屏幕空间光线追踪,其实是一个很通用的算法,可以基于它实现其他的更多的需要光线追踪的效果。

    另外,当场景中有必须用forward rendering渲染的物体存在时,会出现一些bug。具体原因是unity的绘制顺序是Deferred -> Foward -> Image Effect,我们在Image Effect绘制SSR时,此时屏幕上已经有Forward物体被绘制了,这时候根据GBuffer算出来的(虽然depth
    buffer是正确的)反射颜色就会直接叠加到Forward物体上。

    这个bug可以通过改用command buffer解决。官方的实现中,SSR效果处于Deferred rendering结束之后进行(AfterFinalPass),即Deferred -> SSR -> Forward。此时就不会出现奇怪的问题了。但是这样也导致了Forward Object无法被反射(因为计算反射的时候Forward物体还没被绘制)。

    下一篇文章会讲一下,用这种光线追踪方法实现屏幕空间阴影。这种阴影作为一个后处理效果,效率极高,而且可以直接和Unity内置的Screen space shadow mask结合使用。

  • 相关阅读:
    Windows10:家庭版如何设置开机自动登录
    Windows10:常用快捷键(转载)
    Windows10:找回传统桌面的系统图标
    MyBatis之XML映射文件详解
    MyBatis之核心对象及配置文件详解
    Java用Jsoup登录网站,以JSON格式提交(POST)数据,处理JSON格式返回结果,并将处理结果保存到系统剪贴板
    (转)总结.Net下后端的几种请求方式(WebClient、WebRequest、HttpClient)
    jvisualvm.exe 查看堆栈分配
    Hutool 常用的工具类和方法
    StringRedisTemplate 常用方法
  • 原文地址:https://www.cnblogs.com/yangrouchuan/p/7582433.html
Copyright © 2011-2022 走看看