zoukankan      html  css  js  c++  java
  • 屏幕空间环境光屏蔽(SSAO)探秘

    屏幕空间环境光遮蔽(Screen Space Ambient Occlusion,SSAO),是一种在计算机图形学中实现近似环境光屏蔽效果的渲染技术。离线渲染中,在渲染一个物体A时,如果它的周围有一些别的物体B、C等,由于它们遮挡了光线,因此最终渲染出的物体A会显得有一些暗。这种现象在实时渲染就很难模仿。虽然在Phong模型中设置了Ambient一项来代表全局光照,但是这个值对于某个物体往往是统一的,就像下图这样:

    因此,我们需要一种方法可以模拟物体周围的遮挡情况,并反映在Ambient项中,这就是SSAO要解决的问题。下面展示一张利用SSAO技术的同样的模型的渲染效果,可以看到明显的凹凸层次:

    具体来说,SSAO的实现分为以下几个步骤:
    1.渲染当前的场景到一张RT中,该RT中的每个像素存储了当前渲染像素的法向和深度(均为相机坐标系下)。
    2.利用上一步所生成的法向深度图,采样生成一个初步的SSAO RT。
    3.对上一步生成的SSAO RT进行blur处理,使其更为平滑。
    4.将最终生成的SSAO图作为Ambient项的参数应用到渲染过程中。
    下面将对每一个步骤进行讲述:

    渲染法向深度图

    这一步很简单,主要的做法是最终写入像素的时候,给像素的前三位float存入normal,第四位存入depth就可以,在这里直接贴出shader代码:

    VertexOut VS(VertexIn vin)
    {
    	VertexOut vout;
    
    	vout.PosV = mul(float4(vin.PosL, 1.0f), gWorldView).xyz;
    	vout.NormalV = mul(float4(vin.NormalL, 0.0f), gWorldInvTransposeView).xyz;
    		
    	// Transform to homogeneous clip space.
    	vout.PosH = mul(float4(vin.PosL, 1.0f), gWorldViewProj);
    	return vout;
    }
     
    
    float4 PS(VertexOut pin) : SV_Target
    {
    	float3 n = normalize(pin.NormalV);
    	return float4(n, pin.PosV.z);
    }
    

    生成SSAO图

    这一步比较复杂。具体步骤就是,渲染一张和视锥体截面一样大小的长方形(我一般选取深度为zFar的截面),并记录下该长方形四个顶点相机坐标系的坐标位置,同样也是渲到一张RT中。在渲染时,输入上一步所生成的法向深度图。然后在这次渲染的PixelShader中,对每一个像素对应顶点的周边情况进行采样,确定该像素点的Ambient参数,最终写入到新的RT中,生成SSAO图。
    具体如下图所示:

    在该图中,当渲染到某个具体的像素v时,首先应当根据法向深度图复原出v点所实际对应的场景中的顶点p(p=pz/vz * v)。然后可以得到p的法向n。接着,对p点采样一个方向得到点q,再次利用r = rz/qz * q得到q实际对应的场景点r(qz在采样时就可以得到,rz则通过读取法向深度图获取)。最后比较r和p的距离与法向(r-p和n的角度),最终计算得到遮挡系数,存入SSAO图对应的像素中。
    shader代码如下:

    VertexOut SSAOVS(VertexIn vin)
    {
    	VertexOut vout;
    	vout.PosH = float4(vin.PosL.x, vin.PosL.y, 1.0, 1.0);
    	vout.PosV = float3(vin.PosL.x * gFarPlaneSize.x, vin.PosL.y * gFarPlaneSize.y, gFarPlaneDepth);
    	vout.Tex = vin.Tex;
    	return vout;
    }
    
    float4 SSAOPS(VertexOut pin) : SV_Target
    {
    	float4 normalDepth = gNormalDepthMap.SampleLevel(samNormalDepth, pin.Tex, 0.0f);
    	float3 n = normalDepth.xyz;
    	float pz = normalDepth.w;
    	
    	float3 p = pz / pin.PosV.z * pin.PosV.xyz;
    	
    	// Extract random vector and map from [0,1] --> [-1, +1].
    	float3 randVec = 2.0f * gRandomVecMap.SampleLevel(samRandomVec, 4.0f*pin.Tex, 0.0f).rgb - 1.0f;
    	float occlusionSum = 0.0f;
    
    	int sampleCount = 14;
    	[unroll]
    	for (int i = 0; i < sampleCount; ++i)
    	{
    		float3 offset = reflect(gOffsetVectors[i].xyz, randVec);
    		float flip = sign(dot(offset, n));
    		float3 q = p + flip * gOcclusionRadius * offset;
    		float4 projQ = mul(float4(q, 1.0f), gViewToTexSpace);
    		projQ /= projQ.w;
    		float rz = gNormalDepthMap.SampleLevel(samNormalDepth, projQ.xy, 0.0f).a;
    		float3 r = (rz / q.z) * q;
    		float dp = max(dot(n, normalize(r - p)), 0.0f);
    		float occlusion = dp * OcclusionFunction(p.z - r.z);
    
    		occlusionSum += occlusion;
    	}
    
    	occlusionSum /= sampleCount;
    
    	float access = 1.0f - occlusionSum;
    
    	// Sharpen the contrast of the SSAO map to make the SSAO affect more dramatic.
    	return saturate(pow(access, 4.0f));
    }
    

    关于如何对p进行采样生成q,因为一般shader中没有随机函数,因此常用的方法是通过一张随机生成的贴图信息输入shader中来引入随机性。gOffsetVectors是14个方向均匀分布的向量,通过对一个任意的向量执行reflect操作来引入随机性,最终生成了14个在p的normal半球面随机分布的q。

    平滑SSAO图

    上一步所生成的SSAO图因为采样数太少,因此会显得噪点过多,如下图所示:

    为了解决这个问题,我们可以blur一下SSAO图,使其更为平滑。Blur过程分为两步,第一步是先blur竖直方向,第二部再平滑水平方向,如此反复操作。
    下面是执行blur的shader代码。注意,在blur时,如果周围的像素点的normal和距离与中心点差距太大,那么则不参与该中心点的blur过程:

    float4 PS(VertexOut pin, uniform bool gHorizontalBlur) : SV_Target
    {
    	float2 texOffset;
    	{
    	if (gHorizontalBlur)
    	{
    		texOffset = float2(gTexelWidth, 0.0f);
    	}
    	else
    		texOffset = float2(0.0f, gTexelHeight);
    	}
    
    	// The center value always contributes to the sum.
    	float4 color = gWeights[5] * gInputImage.SampleLevel(samInputImage, pin.Tex, 0.0);
    	float totalWeight = gWeights[5];
    
    	float4 centerNormalDepth = gNormalDepthMap.SampleLevel(samNormalDepth, pin.Tex, 0.0f);
    
    	for (float i = -gBlurRadius; i <= gBlurRadius; ++i)
    	{
    		// We already added in the center weight.
    		if (i == 0)
    			continue;
    
    		float2 tex = pin.Tex + i * texOffset;
    
    		float4 neighborNormalDepth = gNormalDepthMap.SampleLevel(
    			samNormalDepth, tex, 0.0f);
    
    		//
    		// If the center value and neighbor values differ too much (either in 
    		// normal or depth), then we assume we are sampling across a discontinuity.
    		// We discard such samples from the blur.
    		//
    
    		if (dot(neighborNormalDepth.xyz, centerNormalDepth.xyz) >= 0.8f &&
    			abs(neighborNormalDepth.a - centerNormalDepth.a) <= 0.2f)
    		{
    			float weight = gWeights[i + gBlurRadius];
    
    			// Add neighbor pixel to blur.
    			color += weight * gInputImage.SampleLevel(
    				samInputImage, tex, 0.0);
    
    			totalWeight += weight;
    		}
    	}
    
    	// Compensate for discarded samples by making total weights sum to 1.
    	return color / totalWeight;
    }
    

    blur之后可以看到SSAO图比之前平滑了很多

    应用SSAO

    在生成了SSAO图后,接下来执行正常的渲染流程。此时输入SSAO图,并在PixelShader中对它进行采样,获取对应的Ambient值,最后在计算的时候将它乘到Ambient项中即可。

    float ambient_weight = 1.0; 
    if (gUseSSAO)
    {
          pin.SSAOPosH /= pin.SSAOPosH.w;
          ambient_weight = gSSAOMap.Sample(samLinear, pin.SSAOPosH.xy, 0.0f).r;
    }
    ...
    
    litColor = texColor * (ambient * ambient_weight + diffuse) + spec;
    

    最后展示一下我所实现的SSAO效果,可以看到加了SSAO后局部的阴影更明显了。

  • 相关阅读:
    idea配置tomcat运行按钮置灰,下拉没有自定义的tomcat选项
    配置多版本jdk,自由切换jdk版本
    五年经验程序员告诉你,如何确定自己是否适合做程序员
    你的编程能力从什么时候开始突飞猛进?
    10 个提升效率的Linux小技巧
    8 种经常被忽视的 SQL 错误用法,你有没有踩过坑?
    十大优秀编程项目,让你的简历金光闪闪
    一文掌握 Lambda 表达式
    一文详解微服务架构(一)
    Java的参数传递是「按值传递」还是「按引用传递」?
  • 原文地址:https://www.cnblogs.com/wickedpriest/p/13219981.html
Copyright © 2011-2022 走看看