`

SSAO详述

 
阅读更多

简介:

在所以的间接光照的方式中,AO(环境遮蔽)是最为简单的,它并不属于GI范畴,但是却能够渲染出GI相似的氛围。从性价比上来说,这种方式比真正的GI更为可取,比较GI是一种奢侈品。

 

原理:

以当前点P的位置为起点向空间内任意方向发射射线,并进行类似碰撞检测,碰撞到P1点,如果被遮挡对遮挡值进行累计。在这个过程中有两个限定的地方,第一个就是射线与当前点的法线的夹角值必须大于一个值(一个常量值),一次来判定P点是否被P1挡住。第二个就是采样的点数是固定的,随机的,并且要保障是均匀分布的。

 

实现思路:

1.第一个pass渲染场景的时候生产3个目标,第一个生成场景,第二个记录view空间的法向量值,第三个记录view空间的深度值。

2.第二个pass以法向量纹理和深度纹理为参数进行渲染:

(1)使用UV坐标获取随机向量,这个向量是3D的可以认为是view空间的向量。

(2)通过向量对采样点(采样点其实就是射线的方向,因为原点是P点)进行反射,为什么要反射呢?让原本固定的数组由于放射向量的随机性而产生随机的结果,所以这里的随机是相对于其它的点来说的,而不是当前的点的采样,当前点的采样是相对固定的。这里需要注意的是,反射的是方向,而不是点,这样最终采样的点始终在以R为半径的圆上面。

(3)射线方向向量*法向量,算出夹角值,这里设定夹角cos值的绝对值<0.5,过滤掉一些遮挡效果不明显的点。

(4)没有被过滤掉的点,会映射到屏幕空间,然后通过它的uv坐标获取到它的view空间的法向量和深度值,其实这个时候的法向量已经没有用了,所以可以直接获取深度值就行。为什么本来在view空间的点要映射到屏幕空间,之后有采样view空间的深度值呢?原因是,随机的那些在圆上面的采样点,并不一定在模型上,也就是说它不一定是个真实存在的点。映射会屏幕,然后获取深度值,这个时候其实相当于从eye发了一条射线穿过采样点然后落到模型上,这时候的深度值是有意义的。

(5)最后,比较P和采样点的深度值,如果比较大说明这个点比较远忽略掉,这里的比较规则可以根据具体场景进行控制。

 

 

  // 1.生成随机的反射向量,用这种方式代替旋转矩阵,减少计算量
  float3 Ref=normalize((tex2D(RandNorms,UV*20).xyz*2.0)-1);
  float AO=0;

  // 2.这里随机生成16条射线
  for (int i=0;i<16;i++)
  {
    // 3.数组的内容本身是固定的,但是通过随机向量进行反射后,整体随机了 
    float3 Ray=Rad*reflect(SphereNorms.xyz,Ref);
    float RayDot=dot(normalize(Ray),Normal);

    // 4.试想一下,如果射线与法线的夹角大于90度,即便射线找到了一个点,那也肯定是不会对这个点遮挡的,因为这个点是一个峰点而不是谷点
    if (abs(RayDot)<0.5)
      continue;

    if (RayDot<0)
      Ray=-Ray;

    // Calculate camera space postion at end of sampling ray
    // 5.SamplePos是以CamPos为圆心以R为半径的球面上面的点,这个点是view空间的
    float3 SamplePos=CamPos+Ray;
    
    // 6.把view空间的点转换到uv,最好把这里当做伪代码,认真你就输了
    float2 SampleUV=(SamplePos.xy/SamplePos.z)*0.5+0.5;
    SampleUV.y=1-SampleUV.y;

    float4 TestSample=tex2D(SampleMap,SampleUV);
    float TestDepth=(TestSample.z+TestSample.w/255)*Params.w;

    // 7.对结果进行累计,不太清楚这里为啥搞一个平滑差值,这个不是重点,即使你直接加也会有效果了
    float DepthDiff=(SamplePos.z-TestDepth);
    if (DepthDiff>0 && DepthDiff<Rad)
      AO+=1.0-smoothstep(0,Rad,DepthDiff);
  }

 上面这段代码算是很清晰的描述了整个SSAO的过程,其实想通了就不是很难。

 

HDAO:

// 1.这里是对当前进行测试,采样8个点然后计算出一个fDot 值来权衡是否要计算这个值。它才有的方式是要么对这个点进行全部采样计算,要么就不计算。
fDot = NormalRejectionTest(i2ScreenCoord); 

float4(fCenterZ,fCenterZ,fCenterZ,1.0f);

	if(fDot > 0.6f) // 小于0.6的不计算
    {
	     float fDepth = mDTexture[i2ScreenCoord].x;
		 fCenterZ = fDepth;

                 // 2.这里它用到了位移贴图的方法,这尼玛采样多了一倍,效果不好才怪
		 float fCenterNormalZ = mNTexture[i2ScreenCoord].z;//g_txNormals.SampleLevel( g_SamplePoint, f2TexCoord, 0 ).x;
		 fOffsetCenterZ = fCenterZ + fCenterNormalZ * g_fNormalScale;

		 // 这里的Ring有四个级别,相当于4圈,一共20个点,加上Normal40个点
		 for(int iGather=0; iGather<iNumRingGathers; iGather++)
		 {
		     // 这里都是分辨率纹理坐标
		     int2 i2MirrorRingPattern = (g_f2HDAORingPattern[iGather] + int2( 1, 1 )) * int2( -1, -1 );
             int2 i2OffsetScreenCoord = i2ScreenCoord + g_f2HDAORingPattern[iGather];
             int2 i2MirrorOffsetScreenCoord = i2ScreenCoord + i2MirrorRingPattern;

			// // 获取深度 
			 f4SampledZ[0] = GatherZSamples( mDTexture, i2OffsetScreenCoord);
             f4SampledZ[1] = GatherZSamples( mDTexture, i2MirrorOffsetScreenCoord);

			 // 检测深度 合格为1不合格为0
            f4Diff = fCenterZ.xxxx - f4SampledZ[0]; // 比较深度,如果在最大和最小半径区间内则为1否则为0
            f4Compare[0] = ( f4Diff < g_fHDAORejectRadius.xxxx ) ? ( 1.0f ) : ( 0.0f );
            f4Compare[0] *= ( f4Diff > g_fHDAOAcceptRadius.xxxx ) ? ( 1.0f ) : ( 0.0f );
            
            f4Diff = fCenterZ.xxxx - f4SampledZ[1]; // 对镜像也做一次同样的操作
            f4Compare[1] = ( f4Diff < g_fHDAORejectRadius.xxxx ) ? ( 1.0f ) : ( 0.0f );
            f4Compare[1] *= ( f4Diff > g_fHDAOAcceptRadius.xxxx ) ? ( 1.0f ) : ( 0.0f );

			// 这个就是咱要的结果 这个地方还有一个权重值哦,搞这么多名堂,都是效率啊
            f4Occlusion.xyzw += ( g_f4HDAORingWeight[iGather].xyzw * ( f4Compare[0].xyzw * f4Compare[1].zwxy ) * fDot ); 

			// Use Normal
			float4 f4SampledNormalZ[2];
			f4SampledNormalZ[0] = GatherSamples( mNTexture, i2OffsetScreenCoord);
            f4SampledNormalZ[1] = GatherSamples( mNTexture, i2MirrorOffsetScreenCoord);
			f4OffsetSampledZ[0] = f4SampledZ[0] + ( f4SampledNormalZ[0] * g_fNormalScale );
            f4OffsetSampledZ[1] = f4SampledZ[1] + ( f4SampledNormalZ[1] * g_fNormalScale );

			f4Diff = fOffsetCenterZ.xxxx - f4OffsetSampledZ[0]; // 位移后的差值,既然如此就不该两者都存在
            f4Compare[0] = ( f4Diff < g_fHDAORejectRadius.xxxx ) ? ( 1.0f ) : ( 0.0f );
            f4Compare[0] *= ( f4Diff > g_fHDAOAcceptRadius.xxxx ) ? ( 1.0f ) : ( 0.0f );
                
            f4Diff = fOffsetCenterZ.xxxx - f4OffsetSampledZ[1];
            f4Compare[1] = ( f4Diff < g_fHDAORejectRadius.xxxx ) ? ( 1.0f ) : ( 0.0f );
            f4Compare[1] *= ( f4Diff > g_fHDAOAcceptRadius.xxxx ) ? ( 1.0f ) : ( 0.0f );

			f4Occlusion.xyzw += ( g_f4HDAORingWeight[iGather].xyzw * ( f4Compare[0].xyzw * f4Compare[1].zwxy ) * fDot ); 
		 }
	}

	fOcclusion = (( f4Occlusion.x + f4Occlusion.y + f4Occlusion.z + f4Occlusion.w ) / ( 3.0f * g_fRingWeightsTotal[iNumRings - 1] ) );

	fOcclusion *= ( g_fHDAOIntensity );
    fOcclusion = 1.0f - saturate( fOcclusion );
    //return fOcclusion;
	Result[i2ScreenCoord] = float4(fOcclusion,fOcclusion,fOcclusion,1.0f);

 上面这个是SDK10里面的一个例子叫HDAO,其实本质上与SSAO是没有区别的,但是有几个小的区别:1.在test的时候采取的是要么对整个点忽略,要么就算整个点所有的采样。2.使用了位移贴图,位移贴图是tess里面比较常用的一种渲染,这里用来增加细节。3.这个ring把采样数组里面的点分成了几层,这种方式感觉比较好。

1.NormalRejectionTest:为当前点指定四个偏移,然后镜像,就是四对了。每一对求法线的夹角,累积判断当前点是否在峡谷中,如果就计算遮蔽,不在就忽略掉。

2.最多高达20对的采样来计算遮蔽,而且还添加了权重值,我觉得这完全是坑爹啊。

 

Alchemy AO:一个号称比HDAO更快效果更好的AO。

理论基础:

公式一:
1.Lx(C,w):一个辐射方程,里面的C表示点的位置,w是一个单位向量,r和无穷都表示的是C点的球半径。

2.V(C,P):可见性方程,如果V与场景无焦点,则表示可见,否则就是不可见。

这个公式把辐射分为近距离辐射和远距离辐射r是分界距离,把它分成两部分也方便计算。

公式二:
 AOr(C,n) :是我们要求的最终结果,就是光照到C的比率。Ar(C,n) :是遮蔽度,求出周围点的对该点的遮蔽。

公式三:
1.这里的数列之所以是π而不是2π,是因为只考虑法向量正面的这个半球的遮蔽。这里的1/π是求平均值用的。

2.g(t):t是碰到第一个遮蔽物的距离,而g(t)是距离对遮蔽的影响。

3.n*w:其实就是cosa,这个值相当于遮蔽物角度对C的影响,当然是角度越大影响越小。

4.V(r):V还是判断是否遮蔽的结果是0或1,只不过参数变成了一个。

理论上来说,我们按照公式三来计算就能求出遮蔽系数了,但是积分这种东西肯定是不适合实时渲染的,我们会选择采样估算的方式来计算遮蔽系数。但是这个公式很重要,是理论基础。

 

估算:

简化一:


首先,没有遮蔽的点,对我们来说是没意义的,因为它不会遮蔽。所以这里在(公式三)的基础上取了π的子集,这个子集射出的射线都被遮挡了。

化简二:


这一步主要是把g(t)这个公式:带入到A中,然后v其实就是一个有距离的w向量,最后结果如上面的8式。

 这里描述了u和r的关系。


 这个时候已经变成S个点的采样了,H是Heaviside方程,这个比较高端啊,不懂。

最终结果:这里考虑到了,漏光以及采样的时候屏幕上的圆到view空间实际上是一个椭圆等因素后的结果。


这里的常量值:

e = 0.0001f;   r = 0.5f;   k = 1.0f;  omga = 1.0f;   bta = 0.0001f; 

这里的z是用来缩放遮蔽的距离的,但是论文里面描述的不是很清楚。

 

采样方式:


 基本上所有的AO的采样方式都差不多,先在屏幕空间画个圈,随机采样,然后映射到view空间。然后根据公式进行计算。与其他的AO相比这个AO对距离和夹角做了衰减,这是它的亮点。至于效率上,这个肯不出来高低啊,还是得跟采样的点数挂钩。

 

细节: 这里计算的时候应该把空间转换到VS而不是WS,因为从屏幕映射回去的时候,如果涉及对VS的逆操作,就是对当前的视角做逆。

 

 总结:在效果上SSAO利用阴影使场景更加具有层次感,各种AO算法很多很多效率和效果也参差不齐。其实AO的本质很简单,就是在周围取点,然后计算点对像素的遮蔽影响。至于用何种算法,完全取决于场景的需要。AO有一个比较明显的缺点是,采样的规则无法实现随着场景的变化进行动态,这并不是因为参数的设置不能动态,而是后处理阶段你无法根据深度图和法线图来判断最好的方案。

 

效果:



 



 



 



 



 


 

  • 大小: 24 KB
  • 大小: 20 KB
  • 大小: 6.3 KB
  • 大小: 21.8 KB
  • 大小: 19.4 KB
  • 大小: 6.6 KB
  • 大小: 5 KB
  • 大小: 3 KB
  • 大小: 6.8 KB
  • 大小: 10.5 KB
  • 大小: 19.2 KB
  • 大小: 2.6 KB
  • 大小: 18.5 KB
  • 大小: 6.1 KB
  • 大小: 6.4 KB
  • 大小: 14.4 KB
  • 大小: 21.7 KB
  • 大小: 2.1 KB
  • 大小: 2.1 KB
  • 大小: 2.6 KB
  • 大小: 3.4 KB
  • 大小: 2.2 KB
  • 大小: 5.2 KB
  • 大小: 3.2 KB
  • 大小: 6.8 KB
  • 大小: 4.4 KB
  • 大小: 2 KB
  • 大小: 3.6 KB
  • 大小: 2.2 KB
  • 大小: 61.4 KB
  • 大小: 38.3 KB
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics