Revision control

++ // Copyright (C) 2026 The Qt Company Ltd.
++ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
++
++ #version 430 core
++ layout (local_size_x = 8, local_size_y = 8, local_size_z = 1) in;
++
++ layout(std140, binding = 0) uniform FroxelUniforms {
++ mat4 invViewProjection;
++ mat4 viewMatrix;
++ vec4 gridDimensions;
++ vec4 depthParams;
++ vec4 cameraPos;
++ vec4 qt_rhi_properties;
++ } ubuf;
++
++ layout(std140, binding = 1) uniform LightData {
++ mat4 matrices[4];
++ vec4 lightPos[8];
++ vec4 lightColor[8];
++ vec4 lightDir[8];
++ vec4 coneAngles[8];
++ vec4 dirLightColor;
++ vec4 csmSplits;
++ vec4 csmActive;
++ vec4 atlasLocations[4];
++ vec4 dimensionsInverted[4];
++ vec4 shadowBiasFactor;
++ uvec4 numLightsCsmNumSplits;
++ mat4 lightShadowMat[8];
++ vec4 lightShadowAtlas[8];
++ vec4 lightShadowAtlas2[8];
++ vec4 iesParams;
++ vec4 iesIndices[2];
++ } lights;
++
++ layout(std140, binding = 2) uniform VolumeData {
++ mat4 matrices[32];
++ vec4 extents[32];
++ vec4 color[32];
++ vec4 heightParams[32];
++ vec4 noiseOffsetScale[32];
++ uint numVolumes;
++ } volumes;
++
++ layout(rgba16f, binding = 3) uniform writeonly image3D froxelTexture;
++
++ layout (binding = 4) uniform sampler2DArray shadowAtlas;
++ layout (binding = 5) uniform sampler2D blueNoise;
++ layout (binding = 6) uniform sampler2D iesAtlas;
++
++ const float IES_OCT_U_SCALE = 254.0 / 256.0;
++ const float IES_OCT_U_OFFSET = 1.0 / 256.0;
++ const float IES_OCT_V_SCALE = 254.0 / 256.0;
++ const float IES_OCT_V_OFFSET = 1.0 / 256.0;
++
++ void iesOrthonormalBasis(in vec3 fwd, out vec3 right, out vec3 up) {
++ vec3 worldUp = (abs(fwd.y) < 0.99) ? vec3(0.0, 1.0, 0.0) : vec3(1.0, 0.0, 0.0);
++ right = normalize(cross(fwd, worldUp));
++ up = cross(right, fwd);
++ }
++
++ float sampleIESMask(in vec3 dir, in vec3 fwd, in float iesIndex, in float iesInvCount) {
++ vec3 right, up;
++ iesOrthonormalBasis(fwd, right, up);
++
++ vec3 local = vec3(dot(dir, right), dot(dir, up), dot(dir, fwd));
++ float absSum = abs(local.x) + abs(local.y) + abs(local.z) + 0.0001;
++ vec2 oct = local.xy / absSum;
++ if (local.z < 0.0) oct = (1.0 - abs(oct.yx)) * sign(oct);
++ oct = oct * 0.5 + 0.5;
++
++ float uvXBase = (IES_OCT_U_OFFSET + iesIndex) * iesInvCount;
++ float invScale = IES_OCT_U_SCALE * iesInvCount;
++ vec2 uv = vec2(oct.x * invScale + uvXBase,
++ oct.y * IES_OCT_V_SCALE + IES_OCT_V_OFFSET);
++
++ return textureLod(iesAtlas, uv, 0.0).r;
++ }
++
++ float getIESIndex(in int i) {
++ return (i < 4) ? lights.iesIndices[0][i] : lights.iesIndices[1][i - 4];
++ }
++
++ bool isInsideFroxel(ivec3 coord) {
++ return coord.x < int(ubuf.gridDimensions.x) &&
++ coord.y < int(ubuf.gridDimensions.y);
++ }
++
++ vec3 froxelToWorld(ivec3 froxelCoord, float zJitter) {
++ float u = (float(froxelCoord.x) + 0.5) / ubuf.gridDimensions.x;
++ float v = (float(froxelCoord.y) + 0.5) / ubuf.gridDimensions.y;
++ float t = (float(froxelCoord.z) + 0.5 + zJitter) / ubuf.gridDimensions.z;
++
++ float nearPlane = ubuf.depthParams.x;
++ float farPlane = ubuf.depthParams.y;
++
++ float viewDepth = mix(nearPlane, farPlane, t);
++ float flipy = step(0.0, ubuf.qt_rhi_properties.x);
++ vec2 ndc = vec2(u, mix(1.0 - v, v, flipy)) * 2.0 - 1.0;
++
++ vec4 farWorld = ubuf.invViewProjection * vec4(ndc, 1.0, 1.0);
++ farWorld /= farWorld.w;
++
++ vec3 camPos = ubuf.cameraPos.xyz;
++ vec3 rayDir = normalize(farWorld.xyz - camPos);
++ return camPos + rayDir * viewDepth;
++ }
++
++ // Directional
++ int findSplitIndex(in float depth, in vec4 splits, in vec4 splitActive) {
++ vec4 lessThan = step(depth, splits);
++
++ vec4 valid = lessThan * splitActive;
++ float result = -1.0;
++
++ float notFoundYet = 1.0;
++
++ result = mix(result, 0.0, valid.x * notFoundYet);
++ notFoundYet *= (1.0 - valid.x);
++
++ result = mix(result, 1.0, valid.y * notFoundYet);
++ notFoundYet *= (1.0 - valid.y);
++
++ result = mix(result, 2.0, valid.z * notFoundYet);
++ notFoundYet *= (1.0 - valid.z);
++
++ result = mix(result, 3.0, valid.w * notFoundYet);
++
++ return int(result);
++ }
++
++ float normalizedShadowResult(in float illumination, in float factor)
++ {
++ return clamp(1.0 - factor * (1.0 - illumination), 0.0, 1.0);
++ }
++
++ float sampleSpotShadow(in int i, in vec3 worldPos, in float dist, in float farZ)
++ {
++ vec4 projCoord = lights.lightShadowMat[i] * vec4(worldPos, 1.0);
++ vec3 smpCoord = projCoord.xyz / projCoord.w;
++
++ float flipY = step(ubuf.qt_rhi_properties.x, -0.5);
++ smpCoord.y = mix(smpCoord.y, 1.0 - smpCoord.y, flipY);
++
++ float inBounds = step(0.0, smpCoord.x) * step(smpCoord.x, 1.0)
++ * step(0.0, smpCoord.y) * step(smpCoord.y, 1.0)
++ * step(0.0, smpCoord.z);
++
++ vec2 sbf = lights.shadowBiasFactor.xy;
++ float invMapRange = 1.0 / max(farZ - 1.0, 1e-6);
++ float currentDepth = clamp((dist - 1.0) * invMapRange, 0.0, 1.0);
++ float bias = sbf.x * invMapRange;
++
++ vec4 atlasInfo = lights.lightShadowAtlas[i];
++ vec2 atlasUV = smpCoord.xy * atlasInfo.z + atlasInfo.xy;
++ float storedDepth = textureLod(shadowAtlas, vec3(atlasUV, atlasInfo.w), 0.0).x + bias;
++ float illumination = mix(1.0, step(currentDepth, storedDepth), inBounds);
++ return normalizedShadowResult(illumination, sbf.y);
++ }
++
++ float samplePointShadow(in int i, in vec3 worldPos, in float dist, in float shadowFar, in vec3 lightPosition)
++ {
++ vec3 toPoint = worldPos - lightPosition;
++ vec3 d = toPoint / max(dist, 1e-6);
++ float forward = step(0.0, d.z);
++ float invDenom = 1.0 / mix(1.0 - d.z, 1.0 + d.z, forward);
++ float u = 0.5 * (d.x * invDenom + 1.0);
++ float v = 0.5 * (d.y * invDenom + 1.0);
++
++ float flipY = step(ubuf.qt_rhi_properties.y, 0.5);
++ v = mix(v, 1.0 - v, flipY);
++
++ vec4 atlasInfo = mix(lights.lightShadowAtlas2[i], lights.lightShadowAtlas[i], forward);
++ vec2 atlasUV = vec2(u, v) * atlasInfo.z + atlasInfo.xy;
++
++ vec2 sbf = lights.shadowBiasFactor.xy;
++ float invMapRange = 1.0 / max(shadowFar - 1.0, 1e-6);
++ float shadowDepth = clamp((dist - 1.0) * invMapRange, 0.0, 1.0);
++ float bias = sbf.x * invMapRange;
++ float atlasDepth = textureLod(shadowAtlas, vec3(atlasUV, atlasInfo.w), 0.0).x + bias;
++ float illumination = step(shadowDepth, atlasDepth);
++ return normalizedShadowResult(illumination, sbf.y);
++ }
++
++ float sampleDirShadow(in vec3 worldPos)
++ {
++ float viewz = -(ubuf.viewMatrix * vec4(worldPos, 1.0)).z;
++
++ int splitIndex = findSplitIndex(viewz, lights.csmSplits, lights.csmActive);
++
++ float shouldCompute = step(0.0, float(splitIndex));
++ int splitIdx = max(splitIndex, 0);
++
++ vec4 projCoord = lights.matrices[splitIdx] * vec4(worldPos, 1.0);
++ vec3 smpCoord = projCoord.xyz / projCoord.w;
++
++ float flipY = step(ubuf.qt_rhi_properties.x, -0.5);
++ smpCoord.y = mix(smpCoord.y, 1.0 - smpCoord.y, flipY);
++
++ vec2 sbf = lights.shadowBiasFactor.xy;
++ vec4 shadowAtlasInfo = lights.atlasLocations[splitIdx];
++ float depth = textureLod(shadowAtlas, vec3(smpCoord.xy * shadowAtlasInfo.z + shadowAtlasInfo.xy, shadowAtlasInfo.w), 0.0).x +
++ sbf.x * lights.dimensionsInverted[splitIdx].z;
++
++ float illumination = step(smpCoord.z, depth);
++ illumination = mix(1.0, illumination, shouldCompute);
++
++ return normalizedShadowResult(illumination, sbf.y);
++ }
++
++ vec3 calculateLighting(vec3 worldPos) {
++ vec3 totalLight = vec3(0.0);
++
++ float iesInvCount = lights.iesParams.x;
++
++ int numLights = int(lights.numLightsCsmNumSplits.x);
++ for (int i = 0; i < numLights; i++) {
++ vec4 lc = lights.lightColor[i];
++ vec4 ld = lights.lightDir[i];
++ vec4 ca = lights.coneAngles[i];
++ vec3 lightPosition = lights.lightPos[i].xyz;
++ vec3 lightColor = lc.rgb;
++ float lightIntensity = lc.a;
++ float spotLight = ld.w;
++ float attenuation;
++ float dist;
++
++ if (spotLight > 0.0) {
++ vec3 sdir = ld.xyz;
++ vec3 toLight = lightPosition - worldPos;
++ dist = length(toLight);
++ vec3 ldir = -toLight / dist;
++ attenuation = lightIntensity / (1.0 + 0.01 * dist);
++ float cone = smoothstep(ca.x, ca.y, dot(ldir, sdir));
++ attenuation *= cone;
++ } else {
++ dist = length(lightPosition - worldPos);
++ attenuation = lightIntensity / (1.0 + 0.001 * dist * dist);
++ }
++
++ if (iesInvCount > 0.0) {
++ float iesIdx = getIESIndex(i);
++ if (iesIdx >= 0.0) {
++ vec3 dirFromLight = normalize(worldPos - lightPosition);
++ attenuation *= sampleIESMask(dirFromLight, ld.xyz, iesIdx, iesInvCount);
++ }
++ }
++
++ float shadowMult = 1.0;
++ float shadowType = ca.z;
++ if (shadowType > 1.5 && ca.w >= 0.001)
++ shadowMult = samplePointShadow(i, worldPos, dist, ca.w, lightPosition);
++ else if (shadowType > 0.5)
++ shadowMult = sampleSpotShadow(i, worldPos, dist, ca.w);
++
++ shadowMult = shadowMult * shadowMult;
++ totalLight += lightColor * attenuation * shadowMult;
++ }
++
++ vec4 dlc = lights.dirLightColor;
++ if (dlc.a > 0.0) {
++ float shadowMult = sampleDirShadow(worldPos);
++ shadowMult = shadowMult * shadowMult;
++ totalLight += dlc.rgb * dlc.a * 0.01 * shadowMult;
++ }
++
++ return totalLight;
++ }
++
++ float hash(ivec3 c) {
++ uint x = uint(c.x) * 1664525u ^ uint(c.y) * 1013904223u ^ uint(c.z) * 22695477u;
++ x = (x ^ (x >> 16u)) * 0x45d9f3bu;
++ x = x ^ (x >> 16u);
++ return float(x) / float(0xFFFFFFFFu);
++ }
++
++ float valueNoise(vec3 p) {
++ ivec3 i = ivec3(floor(p));
++ vec3 f = fract(p);
++ vec3 u = f * f * (3.0 - 2.0 * f);
++
++ float v000 = hash(i);
++ float v100 = hash(i + ivec3(1,0,0));
++ float v010 = hash(i + ivec3(0,1,0));
++ float v110 = hash(i + ivec3(1,1,0));
++ float v001 = hash(i + ivec3(0,0,1));
++ float v101 = hash(i + ivec3(1,0,1));
++ float v011 = hash(i + ivec3(0,1,1));
++ float v111 = hash(i + ivec3(1,1,1));
++
++ return mix(mix(mix(v000,v100,u.x), mix(v010,v110,u.x), u.y),
++ mix(mix(v001,v101,u.x), mix(v011,v111,u.x), u.y), u.z);
++ }
++
++ int findFogVolume(in vec3 worldPos)
++ {
++ for (uint i = 0; i < volumes.numVolumes; i++) {
++ vec4 volPos = volumes.matrices[i] * vec4(worldPos, 1.0);
++ vec4 ext = volumes.extents[i];
++ if (ext.w < 0.5) {
++ vec3 test = abs(volPos.xyz) - ext.xyz;
++ if (test.x < 0.0 && test.y < 0.0 && test.z < 0.0)
++ return int(i);
++ } else {
++ vec3 test = volPos.xyz / ext.xyz;
++ if (dot(test, test) < 1.0)
++ return int(i);
++ }
++ }
++ return -1;
++ }
++
++ float fbm(vec3 p) {
++ float v = 0.5 * valueNoise(p); p = p * 2.1 + vec3(1.7, 9.2, 3.4);
++ v += 0.25 * valueNoise(p); p = p * 2.1 + vec3(1.7, 9.2, 3.4);
++ v += 0.125 * valueNoise(p);
++ return v;
++ }
++
++ vec4 applyScattering(vec3 lighting, vec3 worldPos, int volIdx) {
++ float volDensity = 1.0;
++ vec3 volColor = vec3(1.0);
++ vec3 noiseOffset = vec3(0.0);
++ float noiseScale = 0.5;
++ float heightFactor = 1.0;
++
++ if (volIdx >= 0) {
++ vec4 col = volumes.color[volIdx];
++ vec4 noise = volumes.noiseOffsetScale[volIdx];
++ vec4 hp = volumes.heightParams[volIdx];
++
++ volDensity = col.a;
++ volColor = col.rgb;
++ noiseOffset = noise.xyz;
++ noiseScale = noise.w;
++
++ if (hp.w > 0.5) {
++ float range = hp.x - hp.y;
++ float t = (abs(range) > 0.001) ? clamp((worldPos.y - hp.y) / range, 0.0, 1.0) : 0.0;
++ heightFactor = 1.0 - pow(t, max(hp.z, 0.001));
++ }
++ }
++
++ vec3 p = (worldPos * noiseScale + noiseOffset) * 0.02;
++ float n = fbm(p);
++ float density = mix(0.001, 0.005, n) * volDensity * heightFactor;
++ return vec4(lighting * volColor, density);
++ }
++
++ void main() {
++ ivec3 froxelCoord = ivec3(gl_GlobalInvocationID.xyz);
++ if (!isInsideFroxel(froxelCoord))
++ return;
++
++ vec2 noiseSize = vec2(textureSize(blueNoise, 0));
++ vec2 noiseUV = vec2(froxelCoord.xy) / noiseSize * (vec2(ubuf.gridDimensions.xy) / noiseSize);
++ float jitter = texture(blueNoise, noiseUV).r - 0.5;
++
++ vec3 worldPos = froxelToWorld(froxelCoord, jitter);
++ int volIdx = findFogVolume(worldPos);
++ bool inFog = (volIdx >= 0) || (volumes.numVolumes == 0u);
++ vec4 result = vec4(0.0);
++ if (inFog)
++ result = applyScattering(calculateLighting(worldPos), worldPos, volIdx);
++ imageStore(froxelTexture, froxelCoord, result);
++ }