当前位置: 首页 > news >正文

CS001-7-hbao

HBAO

https://zhuanlan.zhihu.com/p/348467142

HBAO(屏幕空间的环境光遮蔽) - 知乎 (zhihu.com)

[摸着原神学图形]HBAO实现与优化 - 知乎 (zhihu.com)

https://zhuanlan.zhihu.com/p/367793439

Global Illumination_Horizon-Based Ambient Occlusion(HBAO)-CSDN博客 这个解释的比较好

Unity URP实现HBAO - 知乎 (zhihu.com)

https://zhuanlan.zhihu.com/p/545497019 这个解释的比较好

https://github.com/scanberg/hbao/blob/master/resources/shaders/hbao_frag.glsl 源码

https://developer.download.nvidia.cn/presentations/2008/SIGGRAPH/HBAO_SIG08b.pdf

SSAO对每个撒点都要采样,这对于手机硬件来说采样率太高。目前手机主流的是采用HBAO来做环境遮蔽,而HBAO不仅减少了采样数量而且效果也比SSAO更好。

HBAO全称Image-space horizon-based ambient occlusion。

Ambient Occlusion – approaches in screen space (SSAO) - mavaL - 博客园

每采样一个点就计算一下 a 角,然后找出最大的a 角即可。

这个算法可以理解为求周围的点,哪个点的最高,不过算法中是累加而不是求最大值,这样不用记录最小值,和最大值,再相减,而是每步只要大于前一个最大值就能作差,然后累加增量。

步进步数和步长

// Calculate the projected size of the hemisphere
vec2 rayRadiusUV = 0.5 * R * FocalLen / -P.z;
float rayRadiusPix = rayRadiusUV.x * AORes.x;

-P.z,因为z是view空间的坐标,所以p的z是负值。

R是球的半径,FocalLen是焦距。都是固定值,也就是越远的点,步进的uv越小。

// Compute the number of stepsComputeSteps(stepSizeUV, numSteps, rayRadiusPix, random.z);void ComputeSteps(inout vec2 stepSizeUv, inout float numSteps, float rayRadiusPix, float rand)
{// Avoid oversampling if numSteps is greater than the kernel radius in pixelsnumSteps = min(NumSamples, rayRadiusPix);// Divide by Ns+1 so that the farthest samples are not fully attenuatedfloat stepSizePix = rayRadiusPix / (numSteps + 1);// Clamp numSteps if it is greater than the max kernel footprintfloat maxNumSteps = MaxRadiusPixels / stepSizePix;if (maxNumSteps < numSteps){// Use dithering to avoid AO discontinuitiesnumSteps = floor(maxNumSteps + rand);numSteps = max(numSteps, 1);stepSizePix = MaxRadiusPixels / numSteps;}// Step size in uv spacestepSizeUv = stepSizePix * InvAORes;
}

水平角

hbao中的h是体现在哪里?在我的理解中是这样的。

所有的计算在摄像机空间,如下图:

对于每个点p,我们要找到周围一圈,有谁比自己高,比自己高,将可能对自己造成遮挡,而pq和相机垂直的xy屏幕的夹角认为是水平角,下图更清晰。

代码如下,解释在代码中阐述:

#version 430 coreconst 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);// 求第一次步进的切线,为啥这么计算呢?解释下:// dPdu为水平方向上走一个像素后(左、右各一个像素)求得的水平变化量的最小值// dPdv是垂直方向上走一个像素后(上、下各一个像素)求得的垂直变化量的最小值// 而deltaUV.x和deltaUV.y为每次步进的uv增量// 你用deltaUV.x * dPdu 得到向量a;而用deltaUV.y * dPdv 得到向量b,两个向量相加,得到向量c,c就是Tangent向量// 而P点的法线,可以直接用cross(dPdu, dPdv)得到。但是知道P点的法线其实不知道P点的切向量的。// 那有人提出了,为啥不直接用dPdu + dPdv得到Tangent向量呢?这个问题我暂时没想清楚。vec3 T = deltaUV.x * dPdu + deltaUV.y * dPdv;// 求得第一次得水平角float tanH = BiasedTangent(T);// 根据tanH求得sinH值float sinH = TanToSin(tanH);float tanS;float d2;vec3 S;// 步进从1开始,共numSamples次for(float s = 1; s <= numSamples; ++s){ // 每次步进deltaUVuv += deltaUV;// 获取新点在view下得坐标S = GetViewPos(uv);// 和初始点P连线,然后求得和xy平面得夹角,注意这里的xy屏幕是摄像机正对着的xy屏幕,摄像机的+z朝屏幕外tanS = Tangent(P, S);// 求得P和S点的距离,超过R2则不继续步进了,因为只在半径R内做遮挡检测// 判断的第二个条件是tanS值要大于之前的tanH值,否则我不认为这是更高点,不是更高的点,则不贡献遮挡增量d2 = Length2(S - P);if(d2 < R2 && tanS > tanH){float sinS = TanToSin(tanS);// 这个衰减是和距离屏幕成反比,然后本次高点的贡献度,用sinS-sinH计算,这是对原始公式中cos的积分的推导而得。// 因为cos得原函数为sin,所以直接用sinS-sinH计算得到贡献值。ao += Falloff(d2) * (sinS - sinH);// 更新更高得点为tanH和sinH,其实sinH可以不用更新,我们只需要tanH即可。// 此外要tan值在(-90,90)内是单调递增的,而水平角在(-90,90)内。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);
}// 这个步是根据传入半球的半径(它是像素为单位的),然后根据步进的次数numSteps,计算每步步进的stepSizeUv值是多少。
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;}// 每步步进的像素大小除以屏幕的尺寸,即得到每步步进的uv值     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的view空间下的坐标P     = GetViewPos(TexCoord);// 求周围上、下、左、右、四个点的view空间下的坐标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));// 求水平方向上变化最小的点,作为u向量vec3 dPdu = MinDiff(P, Pr, Pl);// 求垂直方向上变化最小的点,作为v向量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;// 计算每步步进的uv值ComputeSteps(stepSizeUV, numSteps, rayRadiusPix, random.z);// 原则上是一周都要寻找,但是这里只对numDirections个方向进行积分float alpha = 2.0 * PI / numDirections;// 共计numDirections个方向for(float d = 0; d < numDirections; ++d){float theta = alpha * d;// 得到旋转的方向vec2 dir = RotateDirections(vec2(cos(theta), sin(theta)), random.xy);// 每次步进的delataUV值vec2 deltaUV = dir * stepSizeUV;ao += HorizonOcclusion(deltaUV,P,dPdu,dPdv,random.z,numSteps);}ao = 1.0 - ao / numDirections * u_AOStrength;}Color_ = ao;
}

衰减

uniform float NegInvR2 = - 1.0 / (0.3*0.3);
float Falloff(float d2)
{return d2 * NegInvR2 + 1.0f;
}

这里的0.3是半球的半径。d2是距离的平方。

1-d2 / (0.3*0.3)

实现方式1

Global Illumination_Horizon-Based Ambient Occlusion(HBAO)-CSDN博客

6*6的滤波

_yyyy这里为上一步的hbao的rt,offset从-3,-2,-1,0,1,2,共计6次,第二层for也是6次,共计36个点的采样后模糊。

float4 frag(v2f i) : SV_Target
{float4 color;float2 TexelSize = float2(1.0f / 2400.0f, 1.0f / 1080.0f);for (int x = -3; x < 3; ++x){for (int y = -3; y < 3; ++y){float2 offset = float2(float(x), float(y)) * TexelSize;color += SAMPLE_TEXTURE2D(_yyyy, sampler_yyyy, i.uv + offset);}}color = color / 36;return color;
}

注意,源码中这里取round,这里去掉。

float2 SnapUVOffset(float2 uv)
{return /*round*/(uv * AORes) * InvAORes;
}

这里改下focalLen.x为height/width,可以对比下效果。

FocalLen.x = 1.0f / Mathf.Tan(fovRad * 0.5f) * ((float)AO_HEIGHT / (float)AO_WIDTH);

uv、深度转为view坐标,参考CS001-1-utils2这个章节。

实现方式2

参考unity中插件中实现的hbao实现算法:

是计算法线,然后dot,计算ao值。

inline float3 FetchViewNormals(float2 uv, float2 delta, float3 P) {#if NORMALS_RECONSTRUCTfloat3 Pr, Pl, Pt, Pb;Pr = FetchViewPos(uv + float2(delta.x, 0));Pl = FetchViewPos(uv + float2(-delta.x, 0));Pt = FetchViewPos(uv + float2(0, delta.y));Pb = FetchViewPos(uv + float2(0, -delta.y));float3 N = normalize(cross(MinDiff(P, Pr, Pl), MinDiff(P, Pt, Pb)));#else#if NORMALS_CAMERAfloat3 N = DecodeViewNormalStereo(UNITY_SAMPLE_SCREENSPACE_TEXTURE(_CameraDepthNormalsTexture, uv * _TargetScale.xy));#elsefloat3 N = UNITY_SAMPLE_SCREENSPACE_TEXTURE(_CameraGBufferTexture2, uv * _TargetScale.xy).rgb * 2.0 - 1.0;//N = mul((float3x3)_WorldToCameraMatrix, N);N = mul((float3x3)UNITY_MATRIX_V, N);#endif // NORMALS_CAMERAN = float3(N.x, -N.yz);#endif // NORMALS_RECONSTRUCTreturn N;
}

 

相关文章:

  • 海之淀攻略
  • 【视频时刻检索】Text-Video Retrieval via Multi-Modal Hypergraph Networks 论文阅读
  • 驱动开发硬核特训 · Day 21(上篇) 抽象理解 Linux 子系统:内核工程师的视角
  • Spring的xxxAware接口工作原理-笔记
  • 高等数学第三章---微分中值定理与导数的应用(3.1微分中值定理3.2洛必达法则)
  • 如何设置极狐GitLab 议题截止日?
  • 050_基于springboot的音乐网站
  • 图解YOLO(You Only Look Once)目标检测(v1-v5)
  • 零基础上手Python数据分析 (23):NumPy 数值计算基础 - 数据分析的加速“引擎”
  • 深度强化学习(DRL)实战:从AlphaGo到自动驾驶
  • React 文件链条
  • [论文阅读]ReAct: Synergizing Reasoning and Acting in Language Models
  • 设备接入与APP(应用程序)接入华为云iotDA平台的详细操作步骤及获取方式
  • 【动手学大模型开发】VSCode 连接远程服务器
  • Asp.Net Core 异常筛选器ExceptionFilter
  • 前端技术Ajax入门
  • 九、小白如何用Pygame制作一款跑酷类游戏(添加前进小动物作为动态障碍物)
  • WebUI可视化:第2章:技术基础准备
  • 阿里云基于本地知识库构建RAG应用 | 架构与场景
  • C++入侵检测与网络攻防之网络嗅探以及ARP攻击
  • 建投读书会·东西汇流|全球物品:跨文化交流视域下的明清外销瓷
  • 迟来的忍者与武士:从《刺客信条:影》论多元话语的争议
  • 俄总统助理:普京与美特使讨论了恢复俄乌直接谈判的可能性
  • 财政部:前3月国有企业利润总额10907.4亿元,同比增1.7%
  • 文庙印象:一周城市生活
  • 朱守科任西藏自治区政府副主席、公安厅厅长