当前位置:   article > 正文

图形学基础|环境光遮蔽(Ambient Occlusion)

ambient occlusion

图形学基础|环境光遮蔽(Ambient Occlusion)

一、前言

在现实世界中,仅有环境光(Ambient Light)的区域也不是所有像素的照明度都相同。

由于自身的遮挡(折痕、皱纹、角落)或被其他物体遮挡,一些区域会呈现出比较暗。

例如,墙角落中的像素比墙中间的像素具有更少的光(由于被更多地遮挡了)。

对于Self-Ambient Occlusion 自身的环境光遮蔽,通常可以通过离线烘焙一张AO贴图,它不会考虑场景其他物体的遮挡

  • AO通常为低频信息,没有必要使用分辨率非常高的纹理。

在这里插入图片描述

除了自身的遮挡,来自其他物体的遮挡也是非常重要!

环境光遮蔽(Ambient Occlusion)是计算机图形学中的一种着色和渲染技术,用来计算场景中每一点是如何接受环境光的。

环境光遮蔽是一种全局方法,意味着每个点的照明是场景中其他几何体的共同作用。

然而,这只是一个非常粗略的近似全局光照,仅通过环境光遮蔽得到的物体外观与阴天下的物体相似。

Wiki-环境光遮蔽 给出了实时环境光遮蔽算法分类:

  • SSAO(屏幕空间环境光,Screen space ambient occlusion)

  • SSDO(屏幕空间定向遮蔽,Screen space directional occlusion)

  • RTAO(光线追踪环境光遮蔽,Ray Traced Ambient Occlusion)

  • HDAO(高分辨率环境光遮蔽,High Definition Ambient Occlusion)

  • HBAO+(水平基准环境光遮蔽,Horizon Based Ambient Occlusion+)

  • AAO(Alchemy Ambient Occlusion)

  • ABAO(角度基准环境光遮蔽,Angle Based Ambient Occlusion)

  • PBAO(预烘焙环境光遮蔽,Pre Baked Ambient Occlusion)

  • VXAO(体素基准环境光遮蔽,Voxel Accelerated Ambient Occlusion)

  • GTAO(Ground Truth based Ambient Occlusion)

本篇博客将介绍并实现以下几种环境光遮蔽算法,以下是笔者的一些笔记。

二、SSAO(Screen space ambient occlusion)

如果一个像素被其他表面遮挡,那么到达它位置的环境光就变少了。

AO算法,主要通过构建从表面上一点朝其法线所在上半球的所有方向发出射线,然后检查它们是否与其他对象相交来计算环境光遮蔽因子。

到达背景或天空的光线会增加表面的亮度,而穿过其他对象的光线不会增加亮度。

因而被大量几何体围绕的点显示为较暗,而周围只有少量几何体的点则显示为较亮。这意味着彼此靠近的任何对象之间的阴影效果将更加明显。

基于顶点的AO技术在模型表面顶点足够密的情况下,能够得到很好的效果。但是基于物理精确的AO计算需要进行光线与场景的求交运算 , 十分耗时 ,所以这种方法只能用于离线渲染。

为了达到实时计算的目标,提出基于屏幕空间的环境遮挡技术,对每个像素的邻域进行随机采样快速计算AO的近似值。

屏幕空间环境光遮蔽(SSAO,Screen space ambient occlusion)是实际应用较多的一种AO算法。

顾名思义,SSAO所有的计算都发生在屏幕空间,利用深度缓存上的深度进行比较代替光线与场景物体的求交算法,大大加快了计算速度,该算法可以在实时运行的条件下较为逼真的模拟全局光照的渲染效果。

SSAO示例效果:

  • 可以明显看到右图墙角处比墙面处暗了不少,比较符合现实。

在这里插入图片描述

3.1 遮挡计算

在SSAO算法中,AO是这么定义的:

  • 在场景中的点上产生一个标量值,描述由这点向各个方向出射的光线被遮挡的概率

可通过在法线上半球对可见性函数进行积分得到,如公式(1)所示:

A p ( n → ) = 1 − 1 π ∫ Ω V P ( w → ) ( n → ⋅ w → ) d w (1) A_p(\overrightarrow{n} ) = 1 - \frac{1}{\pi} \int\limits_{\Omega} V_P(\overrightarrow{w})(\overrightarrow{n}\cdot \overrightarrow{w} )dw\tag{1} Ap(n )=1π1ΩVP(w )(n w )dw(1)

其中:

  • Ω \Omega Ω,是着色点p点法线方向所在上半球的方向集合。
  • V ( w ) V(w) V(w)是距离衰减函数,衰减函数从1开始衰减并在某个固定距离下衰减到0。

在这里插入图片描述

在计算中,我们不会对全部方向都进行计算,而是随机地上半球选取几个方向

SSAO的基本步骤如下:

对屏幕空间内每一个像素,计算其在三维空间里的位置p。

  1. 在以p点为中心其法线构成的上半球的空间内,随机地产生若干三维采样点(半径也随机)。

在这里插入图片描述

以下为产生64个采样点示例代码:

  • 由于在法线上半球(切线空间)采样, Z Z Z的取值区间为 [ 0 , 1 ] [0,1] [0,1]
  • Lerp函数采样点更加接近原点。
// 默认产生的[0,1]之间的浮点数
float RandomFloat(float LO = 0.0f, float HI = 1.0f)
{
	float random = LO + static_cast <float> (rand()) / (static_cast <float> (RAND_MAX / (HI - LO)));
	return random;
}

// make it closer to the origin point
float Lerp(float a, float b, float f)
{
	return a + f * (b - a);
}

// SSAO Samples
std::vector<Vector4f>	Samples;
Samples.resize(64);
for (int i = 0; i < 64; ++i)
{
    Vector3f Sample = Vector3f(
        RandomFloat() * 2 - 1.0f,
        RandomFloat() * 2 - 1.0f,
        RandomFloat()
    );
    Sample = Sample.Normalize();
    // Scale
    float Scale = RandomFloat();
    Scale *= i * 1.0f / 64.0f;
    Scale = Lerp(0.1f, 1.0f, Scale * Scale);
    Sample = Sample * Scale;
    Samples[i] = Vector4f(Sample, 0);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

使用上面的采样点,需构造 T B N TBN TBN矩阵,则需要一个世界空间的随机的方向。

在C++端生成一些随机的方向,存储成为纹理,在Shader中方便采样。

// SSAO Random rotation vector
std::vector<Vector4f> SSAONoises;
for (int i = 0; i < 16; ++i)
{
    // z is 0
    Vector3f Noise = Vector3f(
        RandomFloat() * 2 - 1.0f,
        RandomFloat() * 2 - 1.0f,
        0);

    Noise = Noise.Normalize();
    SSAONoises.push_back(Vector4f(Noise, 0));
}
g_SSAONoise.Create(4, 4, DXGI_FORMAT_R32G32B32A32_FLOAT, &SSAONoises[0]);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

效果如下:

在这里插入图片描述

  1. 估算每个采样点产生的 AO : 计算每个采样点在深度缓存上的投影点 , 用投影点产生的遮蔽近似代替采样点的遮蔽

如果采样点的深度大于深度缓存(离相机越近,深度值越小),那么就判断为被遮挡。

Occlusion += (SampleDepth < Offset.z ? 1.0 : 0.0);
  • 1

在这里插入图片描述

  1. 根据采样点的数量,对遮挡值进行平均。
Occlusion /= kernelSize;
  • 1

完整的代码如下:

static const int kernelSize = 64;
static const float2 NoiseScale = float2(1024.0f / 4.0f, 768.0f / 4.0f);

struct PixelOutput
{
	float4	Target0 : SV_Target0;
};

PixelOutput PS_SSAO(float2 Tex : TEXCOORD, float4 ScreenPos : SV_Position)
{
	PixelOutput Out;
	Out.Target0 = float4(0, 0, 0, 0);

	// World Pos
	float Depth = SceneDepthZ.SampleLevel(LinearSampler, Tex, 0).r;
	float2 ScreenCoord = ViewportUVToScreenPos(Tex);
	float4 NDCPos = float4(ScreenCoord, Depth, 1.0f);
	float4 WorldPos = mul(NDCPos, InvViewProjMatrix);
	WorldPos /= WorldPos.w;

	// World Normal
	float3 N = GBufferA.SampleLevel(LinearSampler, Tex, 0).rgb;
	N = N * 2 - 1.0;
	
	// Sample Random Vector Need Scale
	float3 RandomVec = NoiseMap.SampleLevel(WrapLinearSampler, Tex * NoiseScale, 0).rgb;
	// TBN
	float3 T = normalize(RandomVec - N * dot(RandomVec, N));
	float3 B = cross(N, T); 
	float3x3 TBN = float3x3(T, B, N);

	float Occlusion = 0.0f;
	for (int i = 0; i < kernelSize; ++i)
	{
		// Tangent Space to World Space
		float3 Sample = mul(Samples[i].xyz, (float3x3)TBN);
		Sample = WorldPos + Sample * Radius;

		float4 Offset = float4(Sample, 1.0);
		// View and Projection
		Offset = mul(Offset, ViewProjMatrix);
		Offset.xyz /= Offset.w;

		float2 SampleUV = ScreenPosToViewportUV(Offset.xy);
		float SampleDepth = SceneDepthZ.SampleLevel(LinearSampler, SampleUV, 0).r;
		
		Occlusion += (SampleDepth < Offset.z ? 1.0 : 0.0);
	}

	Occlusion /= kernelSize;
	// Unocclusion
	Occlusion = 1 - Occlusion;
	Out.Target0 = float4(Occlusion, Occlusion, Occlusion, 1);
	return Out;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55

结果如下:

在这里插入图片描述

3.2 模糊SSAO

观察上述的SSAO图,会发现明显的噪声,这是由于我们的噪声(随机转动向量)纹理是重复平铺的,所以我们可以通过对SSAO生成的帧缓冲进行模糊Blur。

  • 简单的对每个像素取周围64个像素(BlurRadius=4)进行取平均值,来获得最后的像素颜色值。

Shader代码如下:

cbuffer PSContant : register(b0)
{
	float2	texelSize;
	int		BlurRadius;
};

struct PixelOutput
{
	float4	Target0 : SV_Target0;
};

PixelOutput PS_SSAO_Blur(float2 Tex : TEXCOORD, float4 ScreenPos : SV_Position)
{
	PixelOutput Out;
	Out.Target0 = float4(0, 0, 0, 0);

	float result = 0;

	for (int x = -BlurRadius; x < BlurRadius; ++x)
	{
		for (int y = -BlurRadius; y < BlurRadius; ++y)
		{
			float2 offset = Tex + float2(x, y) * texelSize;
			result += AOBuffer.SampleLevel(LinearSampler, offset, 0).r;
		}
	}

	result /= (float)(BlurRadius * BlurRadius * 4);

	Out.Target0 = float4(result, result, result, 1);
	return Out;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

结果如下:

在这里插入图片描述

三、HBAO(Horizon Based Ambient Occlusion)

HBAO,全称为:Image-space horizon-based ambient occlusion,屏幕空间水平基准环境光遮蔽。

文章对AO的定义,如公式(2)所示:

A = 1 − 1 2 π ∫ θ = − π π ∫ α = t ( θ ) h ( θ ) W ( w → ) c o s ( α ) d α d θ (2) A = 1 - \frac{1}{2\pi} \int_{\theta = -\pi }^{\pi} \int_{\alpha=t(\theta )}^{h(\theta )} W(\overrightarrow{w})cos(\alpha)d\alpha d\theta\tag{2} A=12π1θ=ππα=t(θ)h(θ)W(w )cos(α)dαdθ(2)

其中, W ( w ) W(w) W(w)的定义如下:

  • 这是一个根据距离衰减的函数。

W ( θ ) = m a x ( 0 , 1 − r ( θ ) R ) W(\theta) = max(0,1-\frac{r(\theta)}{R}) W(θ)=max(0,1Rr(θ))

笔者注:

  • 公式2为什么是cosα,并不能找到解释。论文的作者就是这么规定的。

公式(2),可以改写成为如下形式,如公式(3)所示:

A = 1 − 1 2 π ∫ θ = − π π [ s i n ( h ( θ ) ) − s i n ( t ( θ ) ) ] W ( θ ) d θ (3) A = 1 - \frac{1}{2\pi} \int_{\theta = -\pi }^{\pi} [sin(h(\theta)) - sin(t(\theta))] W(\theta )d\theta\tag{3} A=12π1θ=ππ[sin(h(θ))sin(t(θ))]W(θ)dθ(3)

该公式的意义为:

  • 对于半球上的每个方位角,只要求出该方向上的**最大水平角(Horizon Angle)**即可。

3.1 水平角

接下来,对方位角(Horizon Angle)进行一个解释,即公式(3)中的 h ( θ ) h(\theta) h(θ)

在屏幕空间向任意一个方向步进,采样深度缓冲,都可以得到一个高度场。如下图所示,这个方向上最大的水平角就是最高的那个点。

在这里插入图片描述

而公式(3)中的 t ( θ ) t(\theta) t(θ)表示的是切面角(Tangent Angle),如下图所示。

在这里插入图片描述

通过这两个角度**(Horizon Angle、Tangent Angle)**,就可以求解出当前方向的环境光遮蔽值。

如下图所示:

在这里插入图片描述

那么如何如何求解角度值(公式中的Sin值)

可以根据三角函数,将sin值的求解转换为tan值的计算。

s i n ( h ( θ ) ) = t a n ( h ( θ ) ) 1 + t a n 2 ( h ( θ ) ) (4) sin(h(\theta)) = \frac{tan(h(\theta))}{\sqrt{ 1+ tan^2(h(\theta ))}}\tag{4} sin(h(θ))=1+tan2(h(θ)) tan(h(θ))(4)

tan值,可以在View空间的坐标系下轻松的求解得到。如下图所示。

在这里插入图片描述

笔者注:

  • 上图的Z轴正方向指向相机,这是OpenGL坐标系下的定义。
  • 对于D3D而言,需要对Z进行取反。

在View空间,只需要用向量的坐标就可以轻松地求得tan值,如下公式所示,水平角的tan值求解如下。

t a n ( h ( θ ) ) = S . z ( S . x ) 2 + ( S . y ) 2 (5) tan(h(\theta )) = \frac{S.z}{\sqrt{(S.x)^2+(S.y)^2} }\tag{5} tan(h(θ))=(S.x)2+(S.y)2 S.z(5)

对于切面角,其tan值也是如此。

当然,对于一个方向上的AO是如此计算,一般需要向多个方向进行Raymarching,对最后的结果求平均,如下所示。

在这里插入图片描述

3.2 实现

基于3.1所提及的理论,总结一下具体的计算步骤:

  1. 对于像素P点,求出其View空间的切线T;
  2. 根据公式(4)和公式(5)带入切线求解出 sin ⁡ ( t ( θ ) ) \sin(t(\theta)) sin(t(θ))
  3. 划分多个方向(如4个),每个方向Dir求解该方向上进行步进,求最大水平角 sin ⁡ ( h ( θ ) ) \sin(h(\theta)) sin(h(θ))
  4. 根据公式(3)求解出AO,对总的AO取平均。

笔者注:

  • 只采样4个方向,每个方向采样5次,一共的采样数量为20次。远小于SSAO的64次。

在实践中有以下几个问题需要进行处理。

Low-Tessellation问题

如下图左所示,圆拱门部分(曲面)出现了规则的条纹,这些其他的错误主要来自于:曲面是一个连续的面变化,切面总会将一部分区域计算入遮挡

在这里插入图片描述

HBAO希望这部分不要加入遮挡中,因此使用了一个对切线可以使用一个偏差值Angle Bias,从而将错误的遮挡给去除掉。

在这里插入图片描述

不连续问题

对于相邻的两个像素P0和P1,他们的AO值相差为: 0.7 − 0 = 0.7 0.7-0 = 0.7 0.70=0.7,幅值差距过大,会带来不连续的问题。

在这里插入图片描述

HBAO通过上面提到的衰减函数对这个问题进行解决,从而得到比较柔和的环境光遮蔽效果。

衰减函数的使用方式如下图所示:

  1. 首先初始化WAO为0,然后先计算S1点的AO,同时用衰减公式更新WAO;
  2. 计算S2时,只有当水平角大于S1时才会计算,用S2的距离来加权S2和S1的水平角差值,累加WAO;

在这里插入图片描述

这样的算法可以这么理解:

  • 距离P点近的AO权重比较大,而距离P点越远的AO衰减越大,权重越小。

噪声

同样地,对于HBAO的结果需要进行一下滤波降噪,从而得到比较平滑稳定的结果。

完整的Shader代码

#pragma pack_matrix(row_major)

Texture2D			GBufferA		: register(t0);		// Normal
Texture2D			GBufferC		: register(t1);		// DiffuseAO
Texture2D<float>	SceneDepthZ		: register(t2);		// Depth
Texture2D			NoiseMap		: register(t3);		// Noise

SamplerState LinearSampler		: register(s0);
SamplerState WrapLinearSampler	: register(s1);

cbuffer PSContant : register(b0)
{
	float4x4	InvProjMatrix;
	float4		Resolution;			// width height 1/width 1/height
	float		NumDirections;
	float		NumSamples;
	float		TraceRadius;
	float		MaxRadiusPixels;
	float2		ClipInfo;
};

struct PixelOutput
{
	float4	Target0 : SV_Target0;
};

float3 GetViewPos(float2 Tex)
{
	float Depth = SceneDepthZ.SampleLevel(LinearSampler, Tex, 0).r;
	float2 ScreenCoord = ViewportUVToScreenPos(Tex);
	float4 NDCPos = float4(ScreenCoord, Depth, 1.0f);
	float4 ViewdPos = mul(NDCPos, InvProjMatrix);
	ViewdPos /= ViewdPos.w;
	return ViewdPos.xyz;
}

float3 MinDiff(float3 P, float3 Pr, float3 Pl)
{
	float3 V1 = Pr - P;
	float3 V2 = P - Pl;
	return (length(V1) < length(V2)) ? V1 : V2;
}

void ComputeSteps(out float2 stepSizeUv, out float numSteps, float rayRadiusPix, float rand)
{
	numSteps = min(NumSamples, rayRadiusPix);

	float stepSizePix = rayRadiusPix / (numSteps + 1);

	float maxNumSteps = MaxRadiusPixels / stepSizePix;
	if (maxNumSteps < numSteps)
	{
		numSteps = floor(maxNumSteps + rand);
		numSteps = max(numSteps, 1);
	}

	stepSizeUv = stepSizePix * Resolution.zw;
}

float2 RotateDirections(float2 Dir, float2 CosSin)
{
	// https://zhuanlan.zhihu.com/p/58517426
	return float2(
		Dir.x * CosSin.x - Dir.y * CosSin.y,
		Dir.x * CosSin.y + Dir.y * CosSin.x);
}

float Length2(float3 V)
{
	return dot(V, V);
}

float InvLength(float2 V)
{
	return 1.0f / sqrt(dot(V, V));
}

float Tangent(float3 V)
{
	// in D3D, z is negative towards eye, so inverse it
	return -V.z * InvLength(V.xy);
}

float Tangent(float3 P, float3 S)
{
	return Tangent(S - P);
}

float BiasedTangent(float3 V)
{
    // Low-Tessellation问题
	const float TanBias = tan(30.0 * PI / 180.0);
	return Tangent(V) + TanBias;
}

float TanToSin(float x)
{
	return x / sqrt(x * x + 1.0);
}

float Falloff(float d2)
{
	// The farther the distance, the smaller the contribution
	return saturate(1.0f - d2 * 1.0 / (TraceRadius * TraceRadius));
}

float2 SnapUVOffset(float2 uv)
{
	// Rounds the specified value to the nearest integer.
	return round(uv * Resolution.xy) * Resolution.zw;
}

float HorizonOcclusion(
	float2	Tex,
	float2	deltaUV,
	float3	P,
	float3	dPdu,
	float3	dPdv,
	float	randstep,
	float	numSamples)
{
	float ao = 0;

	float2 uv = Tex + SnapUVOffset(randstep * deltaUV);

	deltaUV = SnapUVOffset(deltaUV);

	float3 T = deltaUV.x * dPdu + deltaUV.y * dPdv;

	float tanH = BiasedTangent(T);
	float sinH = TanToSin(tanH);

	float tanS;
	float d2;
	float3 S;

	for (float s = 1; s <= numSamples; ++s)
	{
		uv += deltaUV;
		S = GetViewPos(uv);
		
		// p as the origin of the space
		tanS = Tangent(P, S);
		d2 = Length2(S - P);

		// only above Tangent can make contribution
		if (d2 < TraceRadius * TraceRadius && tanS > tanH)
		{
			float sinS = TanToSin(tanS);
            // 不连续问题
			ao += Falloff(d2) * (sinS - sinH);
			tanH = tanS;
			sinH = sinS;
		}
	}
	return ao;
}


PixelOutput PS_HBAO(float2 Tex : TEXCOORD, float4 ScreenPos : SV_Position)
{
	PixelOutput Out;
	Out.Target0 = float4(0, 0, 0, 0);

	const float2 NoiseScale = float2(Resolution.x / 4.0f, Resolution.y / 4.0f);

	// view position 
	float3 P = GetViewPos(Tex);
	float3 Pl = GetViewPos(Tex + float2(-Resolution.z, 0));
	float3 Pr = GetViewPos(Tex + float2(Resolution.z, 0));
	float3 Pt = GetViewPos(Tex + float2(0, Resolution.w));
	float3 Pb = GetViewPos(Tex + float2(0, -Resolution.w));

	// used to calculate tangent
	float3 dPdu = MinDiff(P, Pr, Pl);
	float3 dPdv = MinDiff(P, Pt, Pb) * (Resolution.y * 1.0 / Resolution.x);

	// sample random vector need scale
	float3 RandomVec = NoiseMap.SampleLevel(WrapLinearSampler, Tex * NoiseScale, 0).rgb;

	// The 0.5 uv range corresponds to the camera fov angle
	// ClipInfo = 1 / tan(theta/2)
	float2 rayRadiusUV = 0.5 * ClipInfo * TraceRadius / P.z;
	// radius in pixels
	float rayRadiusPix = rayRadiusUV.x * Resolution.x;
	float ao = 1.0;
	if (rayRadiusPix > 1.0)
	{
		ao = 0.0;
		float numSteps;
		float2 stepSizeUV;
		ComputeSteps(stepSizeUV, numSteps, rayRadiusPix, RandomVec.z);

		float alpha = 2.0 * PI / NumDirections;
		for (float i = 0; i < NumDirections; ++i)
		{
			float theta = alpha * i;
			float2 dir = RotateDirections(float2(cos(theta), sin(theta)), RandomVec.xy);
			float2 deltaUV = dir * stepSizeUV;

			// accumulate occlusion
			ao += HorizonOcclusion(
				Tex,
				deltaUV,
				P,
				dPdu,
				dPdv,
				RandomVec.z,
				numSteps);
		}
		ao = 1.0f - ao / NumDirections * 1.9f;
	}
	Out.Target0 = ao;
	return Out;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215

笔者的实现效果

在这里插入图片描述

四、GTAO(Ground Truth-based Ambient Occlusion)

GTAO(Ground Truth-based Ambient Occlusion)是Jorge在2016年提出,其原理上借鉴了Nvidia的HBAO。

也是将DepthBuffer作为高度场,但是会同时计算两个方向上的AO

另外对公式进行了魔改,同时对AO的Visibility项计算上新增了一个CosWeight,也给出了模拟多次Raymarching的近似公式。

这样使得AO的遮蔽更加真实。

4.1 算法核心思想

GTAO给出其求解AO的算法,如公式(6)所示。

A ( x ) ≈ A ^ ( x ) = 1 π ∫ 0 π ∫ − π / 2 π / 2 V ( ϕ , θ ) ∣ s i n ( θ ) ∣ d θ d ϕ (6) A(x) \approx \hat{A} (x) = \frac{1}{\pi} \int_{0}^{\pi} \int_{-\pi/2}^{\pi/2} V(\phi,\theta )|sin(\theta )|d\theta d\phi\tag{6} A(x)A^(x)=π10ππ/2π/2V(ϕ,θ)sin(θ)dθdϕ(6)

其中:

  • 方位角 ϕ \phi ϕ,取值从 0 0 0 π \pi π,因为在求解AO时,GTAO会以一个Slice来进行考虑。

如俯视图所示,将方位角划分为一个个Slice进行考虑,一个Slice可以理解为一个球的侧切面

在这里插入图片描述

在一个Slice中是这样考虑的,每个Slice可以视为一个一维高度场。

  • 橙色的区域表示被遮挡的区域;
  • 为了直观的看到遮挡区域和可见区域(用绿色表示可见区域);

在这里插入图片描述

但是基于高度场,绿色的可见区域是无法被计算的,即这部分也会算成是遮挡区域,变成下图的情况。

在这里插入图片描述

那么,我们所要求解的是就是分别找到 θ \theta θ θ \theta θ方向上的最大水平角h1和h2

求解的方式如下:

  • 求解余弦值,再取反余弦。就可以得到对应的角度了。

在这里插入图片描述

再获得角度之后,还需要对角度进行Clamp,只有法线所在的上半球方向有效。

在这里插入图片描述

均匀权重下,一个Slice下的像素可见性(Visbility)计算公式如下:

  • 可见当看到h1和h2越大(像素周围的几何体越低),余弦值就越趋向于0,那么AO值会越大。

在这里插入图片描述

论文作者并没有使用均匀权重,而是采用了Cosine余弦权重(越接近法线方向权重越大),对应的求解公式如下:

在这里插入图片描述

上述的推导都假设物体表面法线总是垂于深度图所构成的高度场(也就是说平行于view空间下的z方向),这是不合理的。

作者对每个Slice还要乘以一个法线在该Slice上的投影值,作为Slice的权重。

如下图所示:

在这里插入图片描述

Multi Bounce(多次弹射)

上述值考虑了Single bounce of light,作者还给出了一个近似拟合的Multi Bounce的公式:

  • 给定了一个AO和albedo就可以返回一个MultiBounce的AO值。
float3 GTAOMultiBounce(float visibility,float3 albedo)
{
    float3 a = 2.0404  * albedo - 0.3324;
    float3 b = -4.7951 * albedo + 0.6417;
    float3 c = 2.7552 * albedo + 0.6903;
    float x = visibility;
    return max(x, ( (x * a + b) * x + c) * x);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

在这里插入图片描述

4.2 实现

UE4-GTAO给出了UE4中代码的分析。笔者将这些实现迁移到了自己的渲染器。

实现如下:

cbuffer PSContant : register(b0)
{
	float4x4	InvProjMatrix;
	float4x4	ViewMatrix;
	float4		Resolution;			// width height 1/width 1/height
	float4		ClipInfo;
	float4		GTAOParams;			// cos sin angle 
};

struct PixelOutput
{
	float4	Target0 : SV_Target0;
};

static const float	c_NumAngles = 8.0f;
static const uint	GTAO_NUMTAPS = 10;

float3 ReconstructViewPos(float2 Tex)
{
	float Depth = SceneDepthZ.SampleLevel(LinearSampler, Tex, 0).r;
	float2 ScreenCoord = ViewportUVToScreenPos(Tex);
	float Z = LinearEyeDepth(Depth, ClipInfo.x, ClipInfo.y);
	float2 XY = ScreenCoord * ClipInfo.zw * Z;
	return float3(XY, Z);
}

float InterleavedGradientNoise(float2 iPos)
{
	return frac(52.9829189f * frac((iPos.x * 0.06711056) + (iPos.y * 0.00583715)));
}

float3 GetRandomVector(uint2 iPos)
{
	iPos.y = 16384 - iPos.y;

	float3 RandomVec = float3(0, 0, 0);
	float3 RandomTexVec = float3(0, 0, 0);
	float ScaleOffset;

	float TemporalCos = GTAOParams.x;
	float TemporalSin = GTAOParams.y;
	float GradientNoise = InterleavedGradientNoise(float2(iPos));

	RandomTexVec.x = cos((GradientNoise * PI));
	RandomTexVec.y = sin((GradientNoise * PI));

	ScaleOffset = (1.0 / 4.0) * ((iPos.y - iPos.x) & 3);

	RandomVec.x = dot(RandomTexVec.xy, float2(TemporalCos, -TemporalSin));
	RandomVec.y = dot(RandomTexVec.xy, float2(TemporalSin, TemporalCos));
	RandomVec.z = frac(ScaleOffset + GTAOParams.z);

	return RandomVec;
}

float2 SearchForLargestAngleDual(uint NumSteps, float2 BaseUV, float2 ScreenDir, float3 ViewPos, float3 ViewDir)
{
	float LenDsSquare, LenDsInv, Ang, FallOff;
	float3 Ds;
	float2 BestAng = float2(-1, -1);

	const float WorldRadius = 30.0f;
	float AttenFactor = 2.0 / (WorldRadius * WorldRadius);

	float Thickness = 0.9f;

	for (uint i = 0; i < NumSteps; i++)
	{
		float fi = (float)i;

		float2 UVOffset = ScreenDir * (fi + 1.0) * Resolution.zw;
		// why?
		UVOffset.y *= -1;

		float4 UV2 = BaseUV.xyxy + float4(UVOffset.xy, -UVOffset.xy);

		// h1
		Ds = ReconstructViewPos(UV2.xy) - ViewPos;
		LenDsSquare = dot(Ds, Ds);
		LenDsInv = rsqrt(LenDsSquare + 0.0001);
		Ang = dot(Ds, ViewDir) * LenDsInv;

		FallOff = saturate(LenDsSquare * AttenFactor);
		Ang = lerp(Ang, BestAng.x, FallOff);

		BestAng.x = (Ang > BestAng.x) ? Ang : lerp(Ang, BestAng.x, Thickness);

		// h2
		Ds = ReconstructViewPos(UV2.zw) - ViewPos;
		LenDsSquare = dot(Ds, Ds);
		LenDsInv = rsqrt(LenDsSquare + 0.0001);
		Ang = dot(Ds, ViewDir) * LenDsInv;

		FallOff = saturate(LenDsSquare * AttenFactor);
		Ang = lerp(Ang, BestAng.x, FallOff);

		BestAng.y = (Ang > BestAng.y) ? Ang : lerp(Ang, BestAng.y, Thickness);
	}
	BestAng.x = acos(clamp(BestAng.x, -1.0, 1.0));
	BestAng.y = acos(clamp(BestAng.y, -1.0, 1.0));

	return BestAng;
}

float ComputeInnerIntegral(float2 Angles, float2 ScreenDir, float3 ViewDir, float3 ViewSpaceNormal)
{
	// Given the angles found in the search plane 
	// we need to project the View Space Normal onto the plane 
	// defined by the search axis and the View Direction and perform the inner integrate
	float3 PlaneNormal = normalize(cross(float3(ScreenDir, 0), ViewDir));
	float3 Perp = cross(ViewDir, PlaneNormal);
	float3 ProjNormal = ViewSpaceNormal - PlaneNormal * dot(ViewSpaceNormal, PlaneNormal);

	float LenProjNormal = length(ProjNormal) + 0.000001f;
	float RecipMag = 1.0f / (LenProjNormal);

	float CosAng = dot(ProjNormal, Perp) * RecipMag;
	float Gamma = acos(CosAng) - HALF_PI;
	float CosGamma = dot(ProjNormal, ViewDir) * RecipMag;
	float SinGamma = CosAng * -2.0f;

	// clamp to normal hemisphere 
	Angles.x = Gamma + max(-Angles.x - Gamma, -(HALF_PI));
	Angles.y = Gamma + min(Angles.y - Gamma, (HALF_PI));

	float AO = ((LenProjNormal) * 0.25 *
			((Angles.x * SinGamma + CosGamma - cos((2.0 * Angles.x) - Gamma)) +
			(Angles.y * SinGamma + CosGamma - cos((2.0 * Angles.y) - Gamma))));

	return AO;
}

PixelOutput PS_GTAO(float2 Tex : TEXCOORD, float4 ScreenPos : SV_Position)
{
	PixelOutput Out;
	Out.Target0 = float4(0, 0, 0, 0);
	
	// view normal
	float3 WorldNormal = GBufferA.SampleLevel(LinearSampler, Tex, 0).rgb;
	WorldNormal = WorldNormal * 2 - 1;

	float3 ViewSpaceNormal = normalize(mul(WorldNormal, (float3x3)ViewMatrix));
	
 	// view position
	float3 ViewSpacePos = ReconstructViewPos(Tex);
	float3 ViewDir = normalize(-ViewSpacePos);

	int2 iPos = int2(ScreenPos.xy);
	float3 RandomAndOffset = GetRandomVector(iPos);
	float2 RandomVec = RandomAndOffset.xy;
	float2 ScreenDir = float2(RandomVec.x, RandomVec.y);

	uint NumAngles = (uint)c_NumAngles;
	float SinDeltaAngle = sin(PI / c_NumAngles);
	float CosDeltaAngle = cos(PI / c_NumAngles);

	float Sum = 0.0;
	for (uint Angle = 0; Angle < NumAngles; Angle++)
	{
		float2 horizons = SearchForLargestAngleDual(GTAO_NUMTAPS, Tex, ScreenDir, ViewSpacePos, ViewDir);

		Sum += ComputeInnerIntegral(horizons, ScreenDir, ViewDir, ViewSpaceNormal);

		// Rotate for the next angle
		float2 TempScreenDir = ScreenDir.xy;
		ScreenDir.x = (TempScreenDir.x * CosDeltaAngle) + (TempScreenDir.y * -SinDeltaAngle);
		ScreenDir.y = (TempScreenDir.x * SinDeltaAngle) + (TempScreenDir.y * CosDeltaAngle);
	}

	float AO = Sum;
	AO = AO / ((float)NumAngles);

	Out.Target0 = float4(AO, AO, AO, 1);
	return Out;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175

参考博文

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/219859
推荐阅读
相关标签
  

闽ICP备14008679号