Revision control
Copy as Markdown
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#if QSHADER_VIEW_COUNT >= 2
#define SAMPLE_INPUT(uv) textureLod(INPUT, vec3(uv, VIEW_INDEX), 0)
#define SAMPLE_DEPTH(uv) textureLod(DEPTH_TEXTURE, vec3(uv, VIEW_INDEX), 0).r
#define SAMPLE_NORMAL(uv) normalize(texture(NORMAL_ROUGHNESS_TEXTURE, uv).rgb)
#define SAMPLE_LAST_FRAME_INDIRECT_AND_AO(uv) textureLod(blurredIndirectAndAoSampler, vec3(uv, VIEW_INDEX), 0)
#define PROJECTION PROJECTION_MATRIX[VIEW_INDEX]
#else
#define SAMPLE_INPUT(uv) textureLod(INPUT, uv, 0)
#define SAMPLE_DEPTH(uv) textureLod(DEPTH_TEXTURE, uv, 0).r
#define SAMPLE_NORMAL(uv) normalize(texture(NORMAL_ROUGHNESS_TEXTURE, uv).rgb)
#define SAMPLE_LAST_FRAME_INDIRECT_AND_AO(uv) textureLod(blurredIndirectAndAoSampler, uv, 0)
#define PROJECTION PROJECTION_MATRIX
#endif
//#define ENABLE_DEBUG_MODE
vec3 getNdcPos(vec2 uv)
{
// not view space! rather, normalized device coordinates (-1..1)
// hence no depth linearization
float depth = SAMPLE_DEPTH(uv);
float aspect = INPUT_SIZE.x / INPUT_SIZE.y;
float fovRad = 2.0 * atan(1.0 / PROJECTION[1][1]);
if (NDC_Y_UP < 0.0) // invert with Vulkan, due to the -1 from clipSpaceCorrMatrix
fovRad *= -1.0;
float halfFovTan = tan(0.5 * fovRad);
vec2 ndc = uv * 2.0 - 1.0;
vec3 pos = vec3(ndc.x * aspect * halfFovTan * depth, ndc.y * halfFovTan * depth, -depth);
pos.y *= qt_normalAdjustViewportFactor; // isYUpInFramebuffer ? 1.0 : -1.0
return pos;
}
vec3 getViewNormal(vec2 uv)
{
return normalize(mat3(VIEW_MATRIX) * SAMPLE_NORMAL(uv));
}
uint countBits(uint value) // there is no bitCount() in GLSL < 400
{
value = value - ((value >> 1u) & 0x55555555u);
value = (value & 0x33333333u) + ((value >> 2u) & 0x33333333u);
return ((value + (value >> 4u) & 0xF0F0F0Fu) * 0x1010101u) >> 24u;
}
const uint sectorCount = 32;
const float pi = 3.14159265359;
// Screen Space Indirect Lighting with Visibility Bitmask
vec4 SSILVB()
{
uint indirect = 0;
float visibility = 0.0;
vec3 lighting = vec3(0.0);
const vec2 aspect = INPUT_SIZE.yx / INPUT_SIZE.x;
const vec3 position = getNdcPos(INPUT_UV);
const vec3 camera = normalize(-position);
const vec3 normal = getViewNormal(INPUT_UV);
const float sliceRotation = 2.0 * pi / (sliceCount - 1.0);
const float sampleScale = (-sampleRadius * PROJECTION[0][0]) / position.z;
const float sampleOffset = 0.01;
// Use TEXTURE_UV-based pixel coordinates: FRAGCOORD.y is bottom-up on OpenGL
// but top-down on Vulkan/D3D/Metal, which would produce different jitter for the
// same visual pixel. TEXTURE_UV.y is always bottom-up on all backends.
const vec2 jitterCoord = TEXTURE_UV * INPUT_SIZE;
const float jitter = mod(52.9829189 * mod(0.06711056 * jitterCoord.x + 0.00583715 * jitterCoord.y, 1.0), 1.0) - 0.5;
for (float slice = 0.0; slice < sliceCount + 0.5; slice += 1.0) {
const float phi = sliceRotation * (slice + jitter) + pi;
const vec2 omega = vec2(cos(phi), sin(phi));
const vec3 direction = vec3(omega.x, omega.y, 0.0);
const vec3 orthoDirection = direction - dot(direction, camera) * camera;
const vec3 axis = cross(direction, camera);
const vec3 projNormal = normal - axis * dot(normal, axis);
const float projLength = length(projNormal);
const float signN = sign(dot(orthoDirection, projNormal));
const float cosN = clamp(dot(projNormal, camera) / projLength, 0.0, 1.0);
const float n = signN * acos(cosN);
uint occlusion = 0;
for (float currentSample = 0.0; currentSample < sampleCount + 0.5; currentSample += 1.0) {
const float sampleStep = (currentSample + jitter) / sampleCount + sampleOffset;
// Convert omega (view-space direction, +y=up) to INPUT_UV offset.
// X is consistent across backends; Y must be flipped by qt_normalAdjustViewportFactor
// since INPUT_UV.y increases upward on OpenGL but downward on Vulkan/D3D/Metal.
const vec2 sampleUV = INPUT_UV + sampleStep * sampleScale * omega * vec2(aspect.x, qt_normalAdjustViewportFactor);
const vec3 sampleDistance = getNdcPos(sampleUV) - position;
const vec3 sampleHorizon = sampleDistance / length(sampleDistance);
vec2 minMaxHorizon = acos(vec2(dot(sampleHorizon, camera), dot(normalize(sampleDistance - camera * hitThickness), camera)));
minMaxHorizon = clamp((minMaxHorizon + n + 0.5 * pi) / pi, 0.0, 1.0);
uint startBit = uint(minMaxHorizon.x * float(sectorCount));
uint horizonAngle = uint(ceil((minMaxHorizon.y - minMaxHorizon.x) * float(sectorCount)));
uint angleBit = horizonAngle > 0 ? uint(0xFFFFFFFFu >> (sectorCount - horizonAngle)) : 0;
indirect = angleBit << startBit;
#ifdef ENABLE_DEBUG_MODE
if (indirectLightEnabled || debugMode != 0) {
#else
if (indirectLightEnabled) {
#endif
vec3 sampleLight = SAMPLE_INPUT(sampleUV).rgb;
if (simulatedBounceEnabled) {
// Fake an additional bounce by sampling the previous frame's indirect
// lighting result and pretending it is part of the current input.
vec3 indirectFromLastFrame = SAMPLE_LAST_FRAME_INDIRECT_AND_AO(sampleUV).rgb;
sampleLight = sampleLight * (1.0 + indirectFromLastFrame * indirectLightBoost * simulatedBounceFactor);
}
lighting += (1.0 - float(countBits(indirect & ~occlusion)) / float(sectorCount))
* sampleLight
* clamp(dot(normal, sampleHorizon), 0.0, 1.0)
* clamp(dot(getViewNormal(sampleUV), -sampleHorizon), 0.0, 1.0);
}
occlusion |= indirect;
}
visibility += 1.0 - float(countBits(occlusion)) / float(sectorCount);
}
visibility /= sliceCount;
lighting /= sliceCount;
return vec4(lighting, visibility);
}
void MAIN()
{
vec4 indirectAndAo = SSILVB();
#ifndef ENABLE_DEBUG_MODE
FRAGCOLOR = indirectAndAo;
#else
if (debugMode == 6)
FRAGCOLOR = vec4(getNdcPos(INPUT_UV) * 0.5 + 0.5, 1.0);
else if (debugMode == 7)
FRAGCOLOR = vec4(getViewNormal(INPUT_UV) * 0.5 + 0.5, 1.0);
else
FRAGCOLOR = indirectAndAo;
#endif
}