Global Illumination_Horizon-Based Ambient Occlusion(HBAO)[通俗易懂]

Global Illumination_Horizon-Based Ambient Occlusion(HBAO)[通俗易懂]之前我们了解过AO与SSAO(可参照之前文章Vulkan_SSAO—屏幕空间环境光遮蔽和DirectX11进阶9_AO、SSAO、ParticleSystem(GPU))

之前我们了解过AO与SSAO(可参照之前文章Vulkan_SSAO—屏幕空间环境光遮蔽和DirectX11进阶9_AO、SSAO、Particle System(GPU))。

我们本次来看一下一种移动端友好的AO算法HBAO(Image-Space Horizon-Based Ambient Occlusion),相比SSAO对于每一个着色点需要进行64次(取决于随机点数量)采样,这对于移动端硬件来说采样率太高了(但可以用TSSAO来进行时空复用),因此目前手机主流的是采用HBAO来做环境遮蔽,而HBAO不仅减少了采样数量而且效果甚至也比SSAO更好(解决SSAO遮挡时不部分深度错误的情况)。
在这里插入图片描述

一、基本算法原理

首先我们来看一下本算法定义的AO公式:
在这里插入图片描述
其中 V(wi) 描述了以着色点 p 为坐标原点, wi 为方向发射一根射线,如果在半球空间 [公式] 的范围内有交点就为1,反之为0; W(wi) 为一个关于着色点 p 到随机采样点距离的线性衰减函数。如下侧视图所示:

在这里插入图片描述

本算法认为求AO不需要像公式中那样麻烦,根据上图AO的关键就在于在一定范围内找到一个最大的 a 角,大家可以想一想如果这个角度越大,那么就代表对于点P周围的遮挡概率就越大,如此一来公式就变成了(注意上图的坐标系z轴就是点p的法线):
在这里插入图片描述
接下来存在两种处理方式:

  • 将着色点周围360的角度进行均分,每个方向分别做RayMarching(可参照知乎大神HBAO文章介绍)
  • 不采用光线追踪,而是采取沿着一个方向进行多次采样的方式来模拟光线追踪

本文使用第二种方式,如上侧视图所示,采样了 S0、S1、S2、S3 四个点(那么上面公式中的 R 为限定的采样范围,r(Si) 为采样点 Si 与点 P 的距离),每采样一个点就计算一下 a 角,然后比较找出最大的 a 角即可。按理说我们需要对点 P 的每一个方向都进行如上方式求出 a 角,这当然是不可能的,因此本算法就提出只采几个方向来近似即可,比如可以按照如下的方向进行采样(注意下图这沿着z轴从上往下看的俯视图)

在这里插入图片描述

但是还有个问题就是 a 角相对难求(需要进行点积再反三角函数)。本方法的妙处就在:将全部都在视角空间中求解

在这里插入图片描述

注意上面的虚线就是视角空间的坐标系,其中T垂直于法线 n ,这样角 a 就变成了 H(Q)-t(Q) (因为 t(Q) 根据上图为负)。这样有什么好处呢?可能上图还不明显,我们将向量 S 单独提出来看看:
在这里插入图片描述

这样就非常明显了,只需要用 S 的三维坐标就可以求出 H(Q)
在这里插入图片描述
对于角 t(Q) 同理。

因此,我们可以将:
在这里插入图片描述
上式极坐标展开:
在这里插入图片描述
而上式是直接求出的 tan(H(Q)) ,根据正切求正弦:
在这里插入图片描述

二、基本求解步骤

2.1 代码思路流程

根据上述原理,我们整理一下整体算法实现步骤:

  • 1.对于点P,根据深度求出他的切线T。

  • 2.根据式3和式5带入切线T求出 sin(t(Q))

  • 3.随机初始一个方向Dir

  • 4.沿着这个方向Dir间隔一定距离进行采样得到位置 Si (注意这里的采样坐标可以通过一个随机数进行稍微的偏移以避免最后会出现带状的AO)

  • 5.通过位置 Si 求出向量S并和步骤2一样求出 sin(t(Q))

  • 6.循环n次步骤4,5求出最大的 sin(t(Q))

  • 7.根据下面公式求出AO,并循环步骤3,4,5,6(一般只需要找4个方向即可)在这里插入图片描述

2.2 根据深度求切线T

我们首先来看一下如何在相机空间中通过屏幕点求某一点空间坐标及切线;

2.2.1 线性深度

在光栅化结束后,从深度缓存中读取的值并不是物体的实际深度,而是一种非线性深度,那为什么会这样呢?这是因为从观察空间变换到NDC坐标系(Normalized Device Coordinate, 每个顶点x,y,z都在-1到1之间)过程中会乘以投影矩阵,并且经过透视除法。因此只需要进行一个逆变换就从非线性深度变换到线性深度。
具体步骤:

  • 从深度缓存中读取深度值depth,此时深度值范围为(0,1)

  • 将深度值depth 变化到NDC坐标系(-1,1):在这里插入图片描述

  • 接下来就进行反投影就变化到观察空间了,对于一个点乘以投影矩阵如下:在这里插入图片描述
    其中 Zc 就是我们要求的观察空间下的深度,而 ZeZn 的关系就在于透视除法即:在这里插入图片描述
    上述三式联立,可以求解出:在这里插入图片描述

注意观察空间下看向的方向为z轴负半轴,因此得出的 Zc 为负,所以为了方便经常取 Zc 的负值,一般所说的线性深度(为正)就为 -Zc

2.2.2 深度还原位置

接下来就用上面求出来的 Zc 去求位置(注意这里是用上一部分求出来的相机空间的深度而不是正的线性深度)。
在这里插入图片描述
整个摄像机范围如上图所示,注意其中的 Q 为1/2FOV角。我们把上图沿着y轴和x轴切开如下:
在这里插入图片描述
其中P点就要求的点,Q点就是映射到近平面的点。现在已知的信息是P点的深度,读取深度缓存的UV坐标,FOV角,近平面(n)和远平面(f)的值,以及屏幕宽高。接下来就用上面的值进行推导。

2.2.2.1 求y值

先看上面的左图,其中 BON 为1/2Fov角,根据几何关系得出:
在这里插入图片描述其中V为读取深度的v坐标。其中 PJ 就是我们要求的y值,根据上面推导得出y为:
在这里插入图片描述
注意这里的UV坐标是0-1的因此也需要像深度一样将其变换到-1到1才代入计算。

2.2.2.2 求x值

x的求法和y的求法类似,不过要注意 [公式] 跟Fov角没关系,因此还需要求 [公式] 的长度,而根据屏幕长宽可以得出;

在这里插入图片描述
如此一来就求出了 AN 的长度,因此得到x值为:
在这里插入图片描述
至此就利用深度信息还原出了位置信息,不过需要注意的是上面求出的 (Xc,Yc,Zc) 为观察空间下的不是世界空间。

2.2.3 深度还原法线

在了解如何用深度还原位置信息之后还原法线就非常容易了,其实对于一个着色点,只需要求出他的上下左右的位置信息,然后利用叉乘来近似计算该点的法线即可,伪代码如下

	vec3 P  = GetViewPos(v2f_TexCoords);
	vec3 Pl = GetViewPos(v2f_TexCoords + vec2(-xOffset,0));
	vec3 Pr = GetViewPos(v2f_TexCoords + vec2(xOffset,0));
	vec3 Pu = GetViewPos(v2f_TexCoords + vec2(0,yOffset));
	vec3 Pd = GetViewPos(v2f_TexCoords + vec2(0,-yOffset));
    vec3 leftDir = min(P - Pl, Pr - P) ? P - Pl : Pd - P//求出最小的变换量
    vec3 upDir   = min(P - Pd, Pu - P) ? P - Pd : Pu - P//求出最小的变换量
    vec3 normal = normalize(cross(leftDir,upDir))

其中GetViewPos()为上述计算相机空间位置算法;

求最小的变换向量是为了让法线的变换根据平滑一点,如果觉得采样太多也只需要采用x,y方向各一个点即可。

三、代码实现

HBAO就已经讲的很清楚了,相对来说比较简单,技术相对较老,用延迟渲染管线的话就很容易实现了,前向渲染的话可能需要在前一帧就把这些信息保存,下边来介绍一下整体实现shader部分:

3.1 G-Buffer生成

简单来看一下G-Buffer的提取,中规中矩,不再赘述。

顶点着色器:

#version 430 core
layout(location = 0) in vec3 _Position;
layout(location = 1) in vec3 _Normal;
layout(location = 2) in vec2 _TexCoord;

layout(std140, binding = 0) uniform u_Matrices4ProjectionWorld
{ 
   
	mat4 u_ProjectionMatrix;
	mat4 u_ViewMatrix;
};

uniform mat4 u_ModelMatrix;

out vec2 v2f_TexCoords;
out vec3 v2f_Normal;
out vec3 v2f_FragPosInViewSpace;

void main()
{ 
   
	vec4 FragPosInViewSpace = u_ViewMatrix * u_ModelMatrix * vec4(_Position, 1.0f);
	gl_Position = u_ProjectionMatrix * FragPosInViewSpace;
	v2f_TexCoords = _TexCoord;
	v2f_Normal = normalize(mat3(transpose(inverse(u_ViewMatrix * u_ModelMatrix))) * _Normal);	
	v2f_FragPosInViewSpace = FragPosInViewSpace.xyz;
}

片元着色器:

#version 430 core

in  vec3 v2f_FragPosInViewSpace;
in  vec2 v2f_TexCoords;
in  vec3 v2f_Normal;

layout (location = 0) out vec3 Albedo_;
layout (location = 1) out vec3 Normal_;
layout (location = 2) out vec4 Position_;

uniform sampler2D u_DiffuseTexture;
uniform vec4 u_DiffuseColor;
uniform float u_Near = 0.1;
uniform float u_Far = 1000.0f;

layout(std140, binding = 0) uniform u_Matrices4ProjectionWorld
{ 
   
	mat4 u_ProjectionMatrix;
	mat4 u_ViewMatrix;
};

float LinearizeDepth(float vDepth)
{ 
   
    float z = vDepth * 2.0 - 1.0; 
    return (2.0 * u_Near * u_Far) / (u_Far + u_Near - z * (u_Far - u_Near));    
}
void main()
{ 
   
	Normal_ = normalize(v2f_Normal);
	Albedo_ = texture(u_DiffuseTexture, v2f_TexCoords).xyz;
	Position_ = vec4(v2f_FragPosInViewSpace.xyz,LinearizeDepth(gl_FragCoord.z));
}

3.2 HBAO计算

你可以理解为在延迟渲染架构上计算HBAO。

顶点着色器:

#version 430 core

layout (location = 0) in vec2 _Position;
layout (location = 1) in vec2 _TexCoords;

out vec2 TexCoord;

void main()
{ 
   
	gl_Position = vec4(_Position, 0.0, 1.0);
	TexCoord = _TexCoords;
}

片元着色器:

#version 430 core

const float PI = 3.14159265;

uniform sampler2D u_DepthTexture;
uniform sampler2D u_NoiseTexture;
uniform float u_Near = 0.1;
uniform float u_Far = 100.0f;
uniform float u_Fov;
uniform float u_WindowWidth;
uniform float u_WindowHeight;
uniform vec2 u_FocalLen;
uniform float u_AOStrength = 1.9;
uniform float R = 0.3;
uniform float R2 = 0.3 * 0.3;
uniform float NegInvR2 = - 1.0 / (0.3 * 0.3);
uniform float TanBias = tan(30.0 * PI / 180.0);
uniform float MaxRadiusPixels = 50.0;

uniform int NumDirections = 6;
uniform int NumSamples = 3;

in vec2 TexCoord;

out float Color_;

float ViewSpaceZFromDepth(float d)
{ 
   
	d = d * 2.0 - 1.0;
	 //视线坐标系看向的z轴负方向,因此要求视觉空间的z值应该要把线性深度变成负值
	 return -(2.0 * u_Near * u_Far) / (u_Far + u_Near - d * (u_Far - u_Near)); 
}

vec3 UVToViewSpace(vec2 uv, float z)
{ 
   
	uv = uv * 2.0 - 1.0;
	uv.x = uv.x * tan(u_Fov / 2.0) * u_WindowWidth / u_WindowHeight  * z ;
	uv.y = uv.y * tan(u_Fov / 2.0)  * z ;
	return vec3(-uv, z);
}

vec3 GetViewPos(vec2 uv)
{ 
   
	float z = ViewSpaceZFromDepth(texture(u_DepthTexture, uv).r);
	return UVToViewSpace(uv, z);
}

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

float InvLength(vec2 V)
{ 
   
	return inversesqrt(dot(V,V));
}

float Tangent(vec3 V)
{ 
   
	return V.z * InvLength(V.xy);
}

float BiasedTangent(vec3 V)
{ 
   
	return V.z * InvLength(V.xy) + TanBias;
}

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

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

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

vec2 SnapUVOffset(vec2 uv)
{ 
   
    return round(uv * vec2(u_WindowWidth,u_WindowHeight)) * vec2(1.0 / u_WindowWidth,1.0 / u_WindowHeight);
}

float Falloff(float d2)
{ 
   
	return d2 * NegInvR2 + 1.0f;
}

float HorizonOcclusion(	vec2 deltaUV,
						vec3 P,
						vec3 dPdu,
						vec3 dPdv,
						float randstep,
						float numSamples)
{ 
   
	float ao = 0;
	vec2 uv = TexCoord + SnapUVOffset(randstep*deltaUV);
	deltaUV = SnapUVOffset(deltaUV);
	vec3 T = deltaUV.x * dPdu + deltaUV.y * dPdv;
	float tanH = BiasedTangent(T);
	float sinH = TanToSin(tanH);
	float tanS;
	float d2;
	vec3 S;
	for(float s = 1; s <= numSamples; ++s)
	{ 
   
		uv += deltaUV;
		S = GetViewPos(uv);
		tanS = Tangent(P, S);
		d2 = Length2(S - P);
		if(d2 < R2 && tanS > tanH)
		{ 
   
			float sinS = TanToSin(tanS);
			ao += Falloff(d2) * (sinS - sinH);
			tanH = tanS;
			sinH = sinS;
		}
	}
	
	return ao;
}

vec2 RotateDirections(vec2 Dir, vec2 CosSin)
{ 
   
    return vec2(Dir.x*CosSin.x - Dir.y*CosSin.y,
                  Dir.x*CosSin.y + Dir.y*CosSin.x);
}

void ComputeSteps(inout vec2 stepSizeUv, inout 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);
        stepSizePix = MaxRadiusPixels / numSteps;
    }

    stepSizeUv = stepSizePix * vec2(1.0 / u_WindowWidth,1.0 / u_WindowHeight);;
}

void main(void)
{ 
   
	vec2 NoiseScale = vec2(u_WindowWidth/4.0f, u_WindowHeight/4.0f);
	float numDirections = NumDirections;

	vec3 P, Pr, Pl, Pt, Pb;
	P 	= GetViewPos(TexCoord);
    Pr 	= GetViewPos(TexCoord + vec2( 1.0 / u_WindowWidth, 0));
    Pl 	= GetViewPos(TexCoord + vec2(-1.0 / u_WindowWidth, 0));
    Pt 	= GetViewPos(TexCoord + vec2( 0, 1.0 / u_WindowHeight));
    Pb 	= GetViewPos(TexCoord + vec2( 0,-1.0 / u_WindowHeight));
    vec3 dPdu = MinDiff(P, Pr, Pl);
    vec3 dPdv = MinDiff(P, Pt, Pb) * (u_WindowHeight * 1.0 / u_WindowWidth);
	vec3 random = texture(u_NoiseTexture, TexCoord.xy * NoiseScale).rgb;
    vec2 rayRadiusUV = 0.5 * R * u_FocalLen / -P.z;
    float rayRadiusPix = rayRadiusUV.x * u_WindowWidth;
    float ao = 1.0;
    float numSteps;
    vec2 stepSizeUV;
    if(rayRadiusPix > 1.0)
    { 
   
		ao = 0.0;
    	ComputeSteps(stepSizeUV, numSteps, rayRadiusPix, random.z);
		float alpha = 2.0 * PI / numDirections;
		for(float d = 0; d < numDirections; ++d)
		{ 
   
			float theta = alpha * d;
			vec2 dir = RotateDirections(vec2(cos(theta), sin(theta)), random.xy);
			vec2 deltaUV = dir * stepSizeUV;
			ao += HorizonOcclusion(deltaUV,P,dPdu,dPdv,random.z,numSteps);
		}
		ao = 1.0 - ao / numDirections * u_AOStrength;
	}
	Color_ = ao;
}

经过上述计算后得到AO图像:
在这里插入图片描述

3.3 滤波

针对计算好的AO纹理,最好是使用下滤波使得效果柔和(本文只是使用了简单的4*4平均模糊,如果有需要可以使用双边/联合双边滤波)。

顶点着色器:

#version 430 core

layout (location = 0) in vec2 _Position;
layout (location = 1) in vec2 _TexCoords;

out vec2 v2f_TexCoords;

void main()
{ 
   
	gl_Position = vec4(_Position, 0.0, 1.0);
	v2f_TexCoords = _TexCoords;
}

片元着色器:

#version 430 core

in  vec2 v2f_TexCoords;
layout (location = 0) out float FragColor_;

uniform sampler2D u_HBAOTexture;


void main()
{ 
   
    vec2 TexelSize = 1.0 / vec2(textureSize(u_HBAOTexture, 0));
    float Result = 0.0;
    for (int x = -2; x < 2; ++x) 
    { 
   
        for (int y = -2; y < 2; ++y) 
        { 
   
            vec2 Offset = vec2(float(x), float(y)) * TexelSize;
            Result += texture(u_HBAOTexture, v2f_TexCoords + Offset).r;
        }
    }
    FragColor_ = Result / (4.0 * 4.0);
}

在这里插入图片描述

3.4 显示

顶点着色器:

#version 430 core

layout (location = 0) in vec2 _Position;
layout (location = 1) in vec2 _TexCoords;

out vec2 v2f_TexCoords;

void main()
{ 
   
	gl_Position = vec4(_Position, 0.0, 1.0);
	v2f_TexCoords = _TexCoords;
}

片元着色器:

#version 430 core

in  vec2 v2f_TexCoords;
out vec4 Color_;

uniform sampler2D u_HBAOTexture;
uniform sampler2D u_Albedo;

void main()
{ 
   
	float Ambient = (texture(u_HBAOTexture, v2f_TexCoords, 0).r);
	vec3 Albedo = texture(u_Albedo, v2f_TexCoords).rgb;
	Color_ = vec4( Albedo * Ambient,1);
}

首先我们来看一下没有AO的渲染:
在这里插入图片描述
叠加HBAO之后的效果:
在这里插入图片描述

今天的文章Global Illumination_Horizon-Based Ambient Occlusion(HBAO)[通俗易懂]分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/86419.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注