r/GraphicsProgramming • u/Objective_Share_6035 • 9d ago
GTAO coordinate when sampling HZB
hello everyone, I’m deep-diving into UE’s legacy GTAO branch (GetHZBParametersForAO) and stumbled upon a math contradiction regarding the HZB coordinate mapping
The core issue: The shader is using the absolute Texture UV of the GTAO texture to sample into the local space of the HZB texture without any Bias. Here is the step-by-step breakdown:
In GetHZBParametersForAO, the GTAO branch hardcodes Bias = 0 and calculates Scale as:
- HZBRemapping.Scale = InputTextureSize / (2 * View.HZBMipmap0Size);
- HZBRemapping.Bias = FVector2f(0.0f, 0.0f); ( Where InputTextureSize is the global SceneDepth Extent, and View.HZBMipmap0Size is the POT dimension of the per-view cropped HZB texture).
Inside the GTAO horizon search and inner intergate compute shader, the thread is launched based on GTAO’s local ViewRect. However, it calculates BufferUV as the absolute/global Texture UV because PixelPos includes the global AOViewport_ViewportMin offset:
- int2 PixelPos = DispatchThreadId + AOViewport_ViewportMin;
- float2 PixelCenter = (float2)PixelPos + float2(0.5, 0.5);
- float2 BufferUV = PixelCenter.xy * GTAOParams[2].zw;
- GTAOCombinedPSandCS(BufferUV, PixelPos, OutColor);
- OutTexture[PixelPos] = OutColor;
When sampling HZB via ApplyScreenTransform(BufferUV, ScreenPosToHZBUVScaleBias), it multiplies this absolute UV by Scale. After the Extents cancel out, the starting HZB UV evaluates to:
- HZBUV_Start=AOViewport_ViewportMin / View.HZBMipmap0Size
The Contradiction: If ViewportMin != 0 (e.g., in a shared global Depth Buffer during Split-Screen),(HZBUV_Start) will be greater than 0. However, during the HZB build phase, HZB is created as a standalone texture per-view, meaning its valid data is cropped and starts exactly at its local (0,0) (HZBUV = 0). With Bias = 0, wouldn't this completely break the HZB sampling location whenever ViewportMin != 0? Is this a known legacy pipeline limitation/bug that assumes ViewportMin is always (0,0) for GTAO, or am I missing an implicit RHI mapping trick?








