Revision control
Copy as Markdown
// Copyright (C) 2014 NVIDIA Corporation.
// Copyright (C) 2025 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
#ifndef SHADOW_MAPPING_GLSLLIB
#define SHADOW_MAPPING_GLSLLIB
#ifdef QQ3D_SHADER_META
/*{
"uniforms": [
{ "type": "sampler2DArray", "name": "qt_shadowmap_texture" },
{ "type": "sampler2D", "name": "qt_shadowmap_blue_noise_texture" }
]
}*/
#endif // QQ3D_SHADER_META
#if QSSG_SHADOW_SOFTNESS == 4
const vec2 POISSON_SAMPLES[4] = {
vec2( 0.23640461642415603f, -0.7455790794965167f ),
vec2( -0.524523693183717f, 0.8360051250157925f ),
vec2( -0.8303867012956179f, -0.22100142699203368f ),
vec2( 0.8316259303685443f, 0.3983087509528287f )
};
const vec3 CUBEMAP_SAMPLE_DIRECTIONS[4] = {
vec3(-0.4877236689784852, 0.44679483291345823, -0.75),
vec3(0.08464959396472493, -0.9645384628108966, -0.25),
vec3(0.5891183939391361, 0.7684006233226082, 0.25),
vec3(0.6614378277661477, 0.0, 0.75)
};
#elif QSSG_SHADOW_SOFTNESS == 8
const vec2 POISSON_SAMPLES[8] = {
vec2( -0.0874674259750227f, -0.7211290298533339f ),
vec2( 0.12554010991848777f, 0.969408902746625f ),
vec2( 0.9267513490906857f, -0.24897870637254021f ),
vec2( -0.9054894676343846f, 0.34600599115357455f ),
vec2( -0.8178505628144991f, -0.40138762250327864f ),
vec2( 0.10942236848429054f, 0.07217625956749806f ),
vec2( 0.861975194360118f, 0.4425266420410188f ),
vec2( -0.33944946656315295f, 0.5509253616181038f )
};
const vec3 CUBEMAP_SAMPLE_DIRECTIONS[8] = {
vec3(-0.3569771731011225, 0.32702033252495366, -0.875),
vec3(0.06824668448324298, -0.7776357695329124, -0.625),
vec3(0.5640379200352061, 0.7356875863859321, -0.375),
vec3(-0.9769901230486042, -0.17281579634244423, -0.125),
vec3(0.8371375040503463, -0.5325183558454643, 0.125),
vec3(-0.2406596316567426, 0.8952418341938904, 0.375),
vec3(-0.359795430847844, -0.6927642080398022, 0.625),
vec3(0.4841229182759271, 0.0, 0.875)
};
#elif QSSG_SHADOW_SOFTNESS == 16
const vec2 POISSON_SAMPLES[16] = {
vec2( -0.8706486263306604f, 0.29306121613135f ),
vec2( 0.9133375480173374f, 0.15086697583928435f ),
vec2( 0.02197503960070916f, -0.9340974876813268f ),
vec2( -0.026143912634373823f, 0.2642260889014032f ),
vec2( -0.5793670131538275f, -0.5167505662963623f ),
vec2( -0.23053981430283804f, 0.8077614627936293f ),
vec2( 0.39960043495074177f, -0.2045760409607943f ),
vec2( 0.5809502755699385f, 0.7696930884669377f ),
vec2( 0.6096170857392385f, -0.791637809856054f ),
vec2( -0.4788580330837991f, 0.3668196169738974f ),
vec2( -0.5400324102392389f, -0.1135665329722174f ),
vec2( -0.032606987036329485f, -0.30546143641964013f ),
vec2( 0.41507180983866865f, 0.23290979538051837f ),
vec2( -0.39037522026642774f, -0.8645330401408355f ),
vec2( 0.2285381977385323f, 0.7940269386679456f ),
vec2( 0.8696012264365776f, -0.33202273416645656f )
};
const vec3 CUBEMAP_SAMPLE_DIRECTIONS[16] = {
vec3(-0.2565935101015265, 0.2350606742391798, -0.9375),
vec3(0.05096580458913004, -0.5807290562410173, -0.8125),
vec3(0.44183879545435206, 0.5763004674919561, -0.6875),
vec3(-0.8141584358738368, -0.1440131636187035, -0.5625),
vec3(0.758720295154371, -0.4826357464184188, -0.4375),
vec3(-0.24660273969406132, 0.9173498998612161, -0.3125),
vec3(-0.45273265557802134, -0.8717091788969947, -0.1875),
vec3(0.9374848892962336, 0.3423679779728655, -0.0625),
vec3(-0.9225384272223549, 0.3808104519286515, 0.0625),
vec3(0.4163289179926954, -0.8896707155139094, 0.1875),
vec3(0.28429505799735505, 0.9063774434518329, 0.3125),
vec3(-0.7780138488858889, -0.4508749282692101, 0.4375),
vec3(0.8075128776755531, -0.17752944090529632, 0.5625),
vec3(-0.4176500064322479, 0.5940641565749807, 0.6875),
vec3(-0.07491674475807025, -0.5781273487345623, 0.8125),
vec3(0.34798527267687634, 0.0, 0.9375)
};
#endif
ShadowResult fullyVisible() {
ShadowResult result;
result.illumination = 1.0f;
result.isLit = true;
return result;
}
ShadowResult normalizedShadowResult(float illumination, float factor) {
ShadowResult result;
float normalizedFactor = clamp(factor, 0, 100) * 0.01;
result.illumination = min(1.0, 1.0 - normalizedFactor * (1.0 - illumination));
result.isLit = illumination > 0.0f;
return result;
}
// Directional
int findSplitIndex(in float depth, in vec4 splits, in vec4 splitActive, in uint numSplits) {
// Create comparison vector - 1.0 if depth < split, 0.0 otherwise
vec4 depthVec = vec4(depth);
vec4 lessThan = step(depthVec, splits);
// Mask by active splits
vec4 valid = lessThan * splitActive;
// Find first valid split using cascading mix operations
// Work with floats to avoid needing GL_EXT_shader_integer_mix
float result = -1.0;
// Check each split in order, only updating result if current split is valid
// and no previous split was valid
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);
}
#if QSSG_SHADOW_SOFTNESS > 0
#define BLUE_NOISE_TEXTURE_SIZE 64.0
mat2 randomRotation2D(in vec2 pixelPos)
{
vec2 blueNoiseUV = pixelPos / BLUE_NOISE_TEXTURE_SIZE;
// Sample precomputed cos/sin theta & phi from RGBA
vec4 noiseSample = texture(qt_shadowmap_blue_noise_texture, blueNoiseUV);
// Convert from [0,1] to [-1,1]
float cosTheta = noiseSample.r * 2.0 - 1.0;
float sinTheta = noiseSample.g * 2.0 - 1.0;
return mat2(cosTheta, -sinTheta, sinTheta, cosTheta);
}
mat3 randomRotation3D(in vec2 pixelPos) {
vec2 blueNoiseUV = pixelPos / BLUE_NOISE_TEXTURE_SIZE;
// Sample precomputed cos/sin theta & phi from RGBA
vec4 noiseSample = texture(qt_shadowmap_blue_noise_texture, blueNoiseUV);
// Convert from [0,1] to [-1,1]
float cosTheta = noiseSample.r * 2.0 - 1.0;
float sinTheta = noiseSample.g * 2.0 - 1.0;
float cosPhi = noiseSample.b * 2.0 - 1.0;
float sinPhi = noiseSample.a * 2.0 - 1.0;
// Build rotation matrix for Poisson disk sampling
return mat3(vec3(cosTheta * cosPhi, -sinTheta, cosTheta * sinPhi),
vec3(sinTheta * cosPhi, cosTheta , sinTheta * sinPhi),
vec3(-sinPhi , 0.0 , cosPhi));
}
// Core directional light sampling function
ShadowResult qt_sampleDirectionalLight_splitIndex(
in int splitIndex,
in float shadowBias,
in float shadowFactor,
in vec3 dimensionsInverted,
in float pcfFactor,
in uint numSamples,
in vec4 projCoord,
in vec3 worldPos,
in float layerIndex,
in vec2 uvAtlasOffset,
in float uvAtlasScale)
{
vec3 smpCoord = projCoord.xyz / projCoord.w;
// Handle Y-axis flip based on RHI properties
float flipY = step(qt_rhi_properties.x, -0.5); // 1.0 if negative, 0.0 otherwise
smpCoord.y = mix(smpCoord.y, 1.0 - smpCoord.y, flipY);
// Check bounds - create a validity mask instead of early return
// inBounds will be 1.0 if all coordinates are valid, 0.0 otherwise
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) * step(smpCoord.z, 1.0);
// Compute illumination regardless of bounds (will be masked later)
float illumination = 0.0;
vec2 radiusUV = pcfFactor * dimensionsInverted.xy;
float bias = shadowBias * dimensionsInverted.z;
mat2 randomRotation = randomRotation2D(gl_FragCoord.xy);
// Unified loop for both PCF and hard shadows
// For hard shadows, numSamples should be 1 and radiusUV should be 0
for (uint i = 0u; i < QSSG_SHADOW_SOFTNESS; ++i) {
vec2 sampleOffset = randomRotation * (POISSON_SAMPLES[i] * radiusUV);
vec3 atlasCoord = vec3((smpCoord.xy + sampleOffset) * uvAtlasScale + uvAtlasOffset, layerIndex);
float pcfDepth = textureLod(qt_shadowmap_texture, atlasCoord, 0.0).r + bias;
illumination += step(smpCoord.z, pcfDepth);
}
illumination /= float(QSSG_SHADOW_SOFTNESS);
// Blend with fully visible (1.0) based on bounds check
// If out of bounds, illumination becomes 1.0 (fully visible)
illumination = mix(1.0, illumination, inBounds);
return normalizedShadowResult(illumination, shadowFactor);
}
// Main PCF sampling function with cascade blending
ShadowResult qt_sampleDirectionalLight_pcf(in int lightIndex, in float zDepthViewSpace, in vec3 worldPos)
{
const uint numSamples = ubDirectionalLights.directionalLights[lightIndex].pcfSamples;
int splitIndex = findSplitIndex(zDepthViewSpace,
ubDirectionalLights.directionalLights[lightIndex].csmSplits,
ubDirectionalLights.directionalLights[lightIndex].csmActive,
ubDirectionalLights.directionalLights[lightIndex].csmNumSplits);
// If no valid split found, return fully visible
// Using a mask instead of early return
float validSplit = step(0.0, float(splitIndex)); // 1.0 if splitIndex >= 0, 0.0 otherwise
// Get current split shadow data
vec4 projCoord = ubDirectionalLights.directionalLights[lightIndex].matrices[splitIndex] * vec4(worldPos, 1.0);
vec3 dimensions = ubDirectionalLights.directionalLights[lightIndex].dimensionsInverted[splitIndex].xyz;
vec4 shadowAtlasInfo = ubDirectionalLights.directionalLights[lightIndex].atlasLocations[splitIndex];
float layerIndex = shadowAtlasInfo.w;
vec2 uvAtlasOffset = shadowAtlasInfo.xy;
float uvAtlasScale = shadowAtlasInfo.z;
ShadowResult shadow = qt_sampleDirectionalLight_splitIndex(splitIndex,
ubDirectionalLights.directionalLights[lightIndex].shadowBias,
ubDirectionalLights.directionalLights[lightIndex].shadowFactor,
dimensions,
ubDirectionalLights.directionalLights[lightIndex].pcfFactor,
numSamples,
projCoord,
worldPos,
layerIndex,
uvAtlasOffset,
uvAtlasScale);
// Cascade blending for smooth transitions
// Only blend if we're in split 1, 2, or 3 (not split 0)
float shouldBlend = step(1.0, float(splitIndex)); // 1.0 if splitIndex >= 1
float splitPrev = ubDirectionalLights.directionalLights[lightIndex].csmSplits[max(splitIndex - 1, 0)];
float bandLength = ubDirectionalLights.directionalLights[lightIndex].shadowMapFar *
ubDirectionalLights.directionalLights[lightIndex].csmBlendRatio;
// Check if we're in the blend region
float inBlendRegion = step(zDepthViewSpace, splitPrev + bandLength);
float prevSplitActive = ubDirectionalLights.directionalLights[lightIndex].csmActive[max(splitIndex - 1, 0)];
// Combined blend condition: should blend AND in blend region AND previous split is active
float doBlend = shouldBlend * inBlendRegion * prevSplitActive;
// Compute previous split shadow (will be ignored if doBlend is 0.0)
vec4 shadowAtlasInfoPrev = ubDirectionalLights.directionalLights[lightIndex].atlasLocations[max(splitIndex - 1, 0)];
float layerIndexPrev = shadowAtlasInfoPrev.w;
vec2 uvAtlasOffsetPrev = shadowAtlasInfoPrev.xy;
float uvAtlasScalePrev = shadowAtlasInfoPrev.z;
vec4 projCoordPrev = ubDirectionalLights.directionalLights[lightIndex].matrices[max(splitIndex - 1, 0)] * vec4(worldPos, 1.0);
vec3 dimensionsPrev = ubDirectionalLights.directionalLights[lightIndex].dimensionsInverted[max(splitIndex - 1, 0)].xyz;
ShadowResult shadowPrev = qt_sampleDirectionalLight_splitIndex(max(splitIndex - 1, 0),
ubDirectionalLights.directionalLights[lightIndex].shadowBias,
ubDirectionalLights.directionalLights[lightIndex].shadowFactor,
dimensionsPrev,
ubDirectionalLights.directionalLights[lightIndex].pcfFactor,
numSamples,
projCoordPrev,
worldPos,
layerIndexPrev,
uvAtlasOffsetPrev,
uvAtlasScalePrev);
// Compute blend factor
float t = clamp((splitPrev + bandLength - zDepthViewSpace) / max(bandLength, 1e-6), 0.0, 1.0);
// Apply blending only if doBlend is active
float blendedIllumination = mix(shadow.illumination, shadowPrev.illumination, t);
shadow.illumination = mix(shadow.illumination, blendedIllumination, doBlend);
// Blend final result with fully visible based on validSplit mask
shadow.illumination = mix(1.0, shadow.illumination, validSplit);
// Handle isLit separately (can't use mix with bool)
if (validSplit < 0.5)
shadow.isLit = true;
return shadow;
}
#else // aka QSSG_SHADOW_SOFTNESS == 0
// Hard shadow variant (no PCF)
ShadowResult qt_sampleDirectionalLight_hard(in int lightIndex, in float zDepthViewSpace, in vec3 worldPos)
{
// Check if beyond last split
float beyondLastSplit = step(ubDirectionalLights.directionalLights[lightIndex].csmSplits[3], zDepthViewSpace);
int splitIndex = findSplitIndex(zDepthViewSpace,
ubDirectionalLights.directionalLights[lightIndex].csmSplits,
ubDirectionalLights.directionalLights[lightIndex].csmActive,
ubDirectionalLights.directionalLights[lightIndex].csmNumSplits);
float validSplit = step(0.0, float(splitIndex)); // 1.0 if splitIndex >= 0
float shouldCompute = validSplit * (1.0 - beyondLastSplit);
vec4 projCoord = ubDirectionalLights.directionalLights[lightIndex].matrices[max(splitIndex, 0)] * vec4(worldPos, 1.0);
vec3 smpCoord = projCoord.xyz / projCoord.w;
float flipY = step(qt_rhi_properties.x, -0.5);
smpCoord.y = mix(smpCoord.y, 1.0 - smpCoord.y, flipY);
vec4 shadowAtlasInfo = ubDirectionalLights.directionalLights[lightIndex].atlasLocations[max(splitIndex, 0)];
float layerIndex = shadowAtlasInfo.w;
vec2 uvAtlasOffset = shadowAtlasInfo.xy;
float uvAtlasScale = shadowAtlasInfo.z;
float depth = texture(qt_shadowmap_texture, vec3(smpCoord.xy * uvAtlasScale + uvAtlasOffset, layerIndex)).x +
ubDirectionalLights.directionalLights[lightIndex].shadowBias *
ubDirectionalLights.directionalLights[lightIndex].dimensionsInverted[max(splitIndex, 0)].z;
float illumination = step(smpCoord.z, depth);
// If not valid split or beyond last split, return fully visible
illumination = mix(1.0, illumination, shouldCompute);
return normalizedShadowResult(illumination, ubDirectionalLights.directionalLights[lightIndex].shadowFactor);
}
#endif //#if QSSG_SHADOW_SOFTNESS > 0
// Main entry point for directional shadow sampling
ShadowResult qt_sampleDirectionalShadowMap(in int lightIndex, in float zDepthViewSpace, in vec3 worldPos)
{
#if QSSG_SHADOW_SOFTNESS == 0
return qt_sampleDirectionalLight_hard(lightIndex, zDepthViewSpace, worldPos);
#else
return qt_sampleDirectionalLight_pcf(lightIndex, zDepthViewSpace, worldPos);
#endif
}
// Point Lights
ShadowResult qt_samplePointLight(in int lightIndex, in vec3 worldPos) {
const vec3 lightPosition = ubLights.lights[lightIndex].position;
const vec3 L = worldPos - lightPosition;
const float dist = length(L);
const float nearZ = ubLights.lights[lightIndex].clipNear;
const float farZ = ubLights.lights[lightIndex].shadowMapFar;
const float mapSizeInv = 1.0 / (farZ - nearZ);
const float bias = ubLights.lights[lightIndex].shadowBias * mapSizeInv;
const uint numSamples = ubLights.lights[lightIndex].pcfSamples;
const float uvAtlasScale = ubLights.lights[lightIndex].shadowTextureSize;
// Center direction and hemisphere selection (needed for both modes)
vec3 d = (dist > 0.0) ? (L / dist) : vec3(0.0, 0.0, 1.0);
float forward = step(0.0, d.z);
float denom = mix(1.0 - d.z, 1.0 + d.z, forward);
denom = max(denom, 1e-8);
float u = 0.5 * (d.x / denom + 1.0);
float v = 0.5 * (d.y / denom + 1.0);
float flipY = step(qt_rhi_properties.y, 0.5);
v = mix(v, 1.0 - v, flipY);
float layer = mix(ubLights.lights[lightIndex].shadowAtlasLayer1,
ubLights.lights[lightIndex].shadowAtlasLayer0,
forward);
vec2 uvAtlasOffset = mix(ubLights.lights[lightIndex].shadowAtlasUV1,
ubLights.lights[lightIndex].shadowAtlasUV0,
forward);
float illumination;
#if QSSG_SHADOW_SOFTNESS > 0
// PCF mode: multiple samples with bias subtracted from current depth
float currentDepth = clamp((dist - nearZ) / (farZ - nearZ), 0.0, 1.0) - bias;
const float pcfRadius = ubLights.lights[lightIndex].pcfFactor;
float shadowAccumulator = 0.0;
mat3 randomRotation = randomRotation3D(gl_FragCoord.xy);
for (uint i = 0u; i < QSSG_SHADOW_SOFTNESS; ++i) {
vec3 offsetDir = randomRotation * CUBEMAP_SAMPLE_DIRECTIONS[i] * pcfRadius;
vec3 worldPosPCF = worldPos + offsetDir;
vec3 Lpcf = worldPosPCF - lightPosition;
float distPCF = length(Lpcf);
vec3 dPCF = (distPCF > 0.0) ? (Lpcf / distPCF) : vec3(0.0, 0.0, 1.0);
float forwardPCF = step(0.0, dPCF.z);
float layerPCF = mix(ubLights.lights[lightIndex].shadowAtlasLayer1,
ubLights.lights[lightIndex].shadowAtlasLayer0,
forwardPCF);
vec2 uvOffPCF = mix(ubLights.lights[lightIndex].shadowAtlasUV1,
ubLights.lights[lightIndex].shadowAtlasUV0,
forwardPCF);
float denomPCF = mix(1.0 - dPCF.z, 1.0 + dPCF.z, forwardPCF);
denomPCF = max(denomPCF, 1e-8);
float uPCF = 0.5 * (dPCF.x / denomPCF + 1.0);
float vPCF = 0.5 * (dPCF.y / denomPCF + 1.0);
vPCF = mix(vPCF, 1.0 - vPCF, flipY);
vec2 uvPCF = vec2(uPCF, vPCF) * uvAtlasScale + uvOffPCF;
float storedDepth = textureLod(qt_shadowmap_texture, vec3(uvPCF, layerPCF), 0.0).r;
shadowAccumulator += step(currentDepth, storedDepth);
}
illumination = shadowAccumulator / float(QSSG_SHADOW_SOFTNESS);
#else
// Hard shadow: single sample with bias added to stored depth
const float currentDepth = clamp((dist - nearZ) / (farZ - nearZ), 0.0, 1.0);
vec2 uvCoords = vec2(u, v) * uvAtlasScale + uvAtlasOffset;
float storedDepth = textureLod(qt_shadowmap_texture, vec3(uvCoords, layer), 0.0).r;
illumination = step(currentDepth, storedDepth + bias);
#endif
return normalizedShadowResult(illumination, ubLights.lights[lightIndex].shadowFactor);
}
// Spot Lights
ShadowResult qt_sampleSpotLight(in int lightIndex, in vec3 worldPos)
{
// Early cone test - this is often coherent so the branch is beneficial
vec3 dir = worldPos - ubLights.lights[lightIndex].position;
vec3 Ldir = normalize(dir);
float lightAngle = dot(Ldir, normalize(ubLights.lights[lightIndex].direction));
if (lightAngle < ubLights.lights[lightIndex].coneAngle)
return fullyVisible();
// Depth setup
float dist = length(dir);
float nearZ = ubLights.lights[lightIndex].clipNear;
float farZ = ubLights.lights[lightIndex].shadowMapFar;
float mapSizeInv = 1.0 / (farZ - nearZ);
float bias = ubLights.lights[lightIndex].shadowBias * mapSizeInv;
// Shadow atlas params
float layerIndex = ubLights.lights[lightIndex].shadowAtlasLayer0;
vec2 uvAtlasOffset = ubLights.lights[lightIndex].shadowAtlasUV0;
float uvAtlasScale = ubLights.lights[lightIndex].shadowTextureSize;
uint numSamples = ubLights.lights[lightIndex].pcfSamples;
// Center projection (needed for both modes)
vec4 projCoord = ubLights.lights[lightIndex].shadowMatrix * vec4(worldPos, 1.0);
vec3 smpCoord = projCoord.xyz / projCoord.w;
float flipY = step(qt_rhi_properties.x, -0.5);
smpCoord.y = mix(smpCoord.y, 1.0 - smpCoord.y, flipY);
float illumination;
#if QSSG_SHADOW_SOFTNESS > 0
// PCF mode: multiple samples
float currentDepth = clamp((dist - nearZ) / (farZ - nearZ), 0.0, 1.0) - bias;
float pcfRadius = ubLights.lights[lightIndex].pcfFactor;
float accum = 0.0;
mat3 randomRotation = randomRotation3D(gl_FragCoord.xy);
for (uint i = 0u; i < QSSG_SHADOW_SOFTNESS; ++i) {
vec3 offsetDir = randomRotation * CUBEMAP_SAMPLE_DIRECTIONS[i] * pcfRadius;
vec3 worldPosPCF = worldPos + offsetDir;
vec4 projPCF = ubLights.lights[lightIndex].shadowMatrix * vec4(worldPosPCF, 1.0);
vec3 smpPCF = projPCF.xyz / projPCF.w;
smpPCF.y = mix(smpPCF.y, 1.0 - smpPCF.y, flipY);
vec2 uvPCF = smpPCF.xy * uvAtlasScale + uvAtlasOffset;
float storedDepth = textureLod(qt_shadowmap_texture, vec3(uvPCF, layerIndex), 0.0).r;
accum += step(currentDepth, storedDepth);
}
illumination = accum / float(QSSG_SHADOW_SOFTNESS);
#else
// Hard shadow: single sample
float currentDepth = clamp((dist - nearZ) / (farZ - nearZ), 0.0, 1.0);
vec2 uv = smpCoord.xy * uvAtlasScale + uvAtlasOffset;
float storedDepth = textureLod(qt_shadowmap_texture, vec3(uv, layerIndex), 0.0).r;
illumination = step(currentDepth, storedDepth + bias);
#endif
return normalizedShadowResult(illumination, ubLights.lights[lightIndex].shadowFactor);
}
ShadowResult qt_sampleLightShadowMap(in int lightIndex, in vec3 worldPos)
{
if (ubLights.lights[lightIndex].coneAngle > 180.0) {
// Point
return qt_samplePointLight(lightIndex, worldPos);
} else {
return qt_sampleSpotLight(lightIndex, worldPos);
}
}
#endif