diff --git a/engine/assets/render/deferred_shading.fs b/engine/assets/render/deferred_shading.fs index 54063d47b..311e0e8a9 100644 --- a/engine/assets/render/deferred_shading.fs +++ b/engine/assets/render/deferred_shading.fs @@ -8,6 +8,8 @@ uniform sampler2D albedoTexture; uniform sampler2D ssaoTexture; uniform sampler2D shadowAtlasTexture; +uniform sampler2DArray directionalShadowMap; // only one directional light with shadows is supported, for now + uniform vec2 viewportOffset; uniform vec2 viewportSize; @@ -15,7 +17,13 @@ struct DirectionalLight { vec4 direction; vec4 color; + mat4 matrices[10]; // hardcoded max splits float intensity; + float shadowBias; + float shadowBlurRadius; + float shadowMaxDistance; + vec4 shadowFarSplitDistances[3]; // hardcoded max splits + int numSplits; }; struct PointLight @@ -60,6 +68,8 @@ layout(std140) uniform PerScene uint numDirectionalLights; uint numPointLights; uint numSpotLights; + + int directionalLightWithShadowsId; // index of directional light that casts shadows, or -1 if none }; layout(location = 0) out vec3 color; @@ -120,9 +130,73 @@ vec3 spotLightCalc(vec3 fragPos, vec3 fragNormal, SpotLight light) return vec3(0); } -vec3 directionalLightCalc(vec3 fragNormal, DirectionalLight light) +vec3 directionalLightCalc(vec3 fragPos, vec3 fragNormal, DirectionalLight light, bool drawShadows) { - return max(dot(fragNormal, -light.direction.xyz), 0) * light.intensity * vec3(light.color); + // Shadows + float shadow = 0.0; + if (drawShadows) + { + // Select split + vec4 positionCameraSpace = inverse(inverseView) * vec4(fragPos, 1.0); + float depthCameraSpace = abs(positionCameraSpace.z); + int split = light.numSplits - 1; + for (int i = 0; i < light.numSplits; i++) + { + float far; + switch (i % 4) + { + case 0: + far = light.shadowFarSplitDistances[i / 4].x; + break; + case 1: + far = light.shadowFarSplitDistances[i / 4].y; + break; + case 2: + far = light.shadowFarSplitDistances[i / 4].z; + break; + case 3: + far = light.shadowFarSplitDistances[i / 4].w; + break; + } + if (depthCameraSpace < far) + { + split = i; + break; + } + } + + // Sample shadow map + vec4 positionLightSpace = light.matrices[split] * vec4(fragPos, 1.0); + vec3 projCoords = positionLightSpace.xyz / positionLightSpace.w; + projCoords = projCoords * 0.5 + 0.5; + if (projCoords.z < 1.0) + { + vec2 uv = projCoords.xy; + float currentDepth = projCoords.z; + float bias = light.shadowBias / positionLightSpace.w; // make the bias not depend on near/far planes + // PCF + if (light.shadowBlurRadius <= 0.001f) + { + float pcfDepth = texture(directionalShadowMap, vec3(uv.xy, split)).r; + shadow += currentDepth - bias > pcfDepth ? 1.0 : 0.0; + } + else + { + vec2 texelSize = vec2(1.0 / 1024.0); // largely arbitrary value, affects blur size + for(float x = -light.shadowBlurRadius; x <= light.shadowBlurRadius; x += light.shadowBlurRadius) + { + for(float y = -light.shadowBlurRadius; y <= light.shadowBlurRadius; y += light.shadowBlurRadius) + { + vec2 newUv = uv + vec2(x, y) * texelSize; + float pcfDepth = texture(directionalShadowMap, vec3(newUv.xy, split)).r; + shadow += currentDepth - bias > pcfDepth ? 1.0 : 0.0; + } + } + shadow /= 9.0; + } + } + } + return max(dot(fragNormal, -light.direction.xyz), 0) * (1.0 - shadow) * light.intensity * vec3(light.color); } vec3 pointLightCalc(vec3 fragPos, vec3 fragNormal, PointLight light) @@ -174,7 +248,8 @@ void main() } for (uint i = 0u; i < numDirectionalLights; i++) { - lighting += directionalLightCalc(normal, directionalLights[i]); + lighting += directionalLightCalc(position, normal, directionalLights[i], + int(i) == directionalLightWithShadowsId); } for (uint i = 0u; i < numPointLights; i++) { diff --git a/engine/src/render/deferred_shading/plugin.cpp b/engine/src/render/deferred_shading/plugin.cpp index 74b32d295..279a990e2 100644 --- a/engine/src/render/deferred_shading/plugin.cpp +++ b/engine/src/render/deferred_shading/plugin.cpp @@ -4,7 +4,10 @@ #include #include #include +#include +#include #include +#include #include #include #include @@ -19,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -26,12 +30,16 @@ #include #include +using cubos::core::gl::AddressMode; using cubos::core::gl::ConstantBuffer; using cubos::core::gl::FramebufferDesc; using cubos::core::gl::generateScreenQuad; using cubos::core::gl::RenderDevice; +using cubos::core::gl::Sampler; +using cubos::core::gl::SamplerDesc; using cubos::core::gl::ShaderBindingPoint; using cubos::core::gl::ShaderPipeline; +using cubos::core::gl::Texture2DArray; using cubos::core::gl::Usage; using cubos::core::gl::VertexArray; using cubos::core::io::Window; @@ -44,8 +52,16 @@ namespace { glm::vec4 direction; glm::vec4 color; + glm::mat4 matrices[cubos::engine::DirectionalShadowCaster::MaxSplits]; float intensity; - float padding[3]; + float shadowBias; + float shadowBlurRadius; + float shadowMaxDistance; + glm::vec4 shadowFarSplitDistances[(cubos::engine::DirectionalShadowCaster::MaxSplits + 3) / + 4]; // std140 layout aligns array elements to vec4 size, + // number of vec4s = ceiling(MaxSplits / 4 components) + int numSplits; + int padding[3]; }; struct PerPointLight @@ -89,6 +105,8 @@ namespace glm::uint numDirectionalLights{0}; glm::uint numPointLights{0}; glm::uint numSpotLights{0}; + + int directionalLightWithShadowsId; }; struct State @@ -101,9 +119,11 @@ namespace ShaderBindingPoint albedoBP; ShaderBindingPoint ssaoBP; ShaderBindingPoint shadowAtlasBP; + ShaderBindingPoint directionalShadowMapBP; ShaderBindingPoint perSceneBP; ShaderBindingPoint viewportOffsetBP; ShaderBindingPoint viewportSizeBP; + Sampler directionalShadowSampler; VertexArray screenQuad; @@ -117,15 +137,26 @@ namespace albedoBP = pipeline->getBindingPoint("albedoTexture"); ssaoBP = pipeline->getBindingPoint("ssaoTexture"); shadowAtlasBP = pipeline->getBindingPoint("shadowAtlasTexture"); + directionalShadowMapBP = pipeline->getBindingPoint("directionalShadowMap"); perSceneBP = pipeline->getBindingPoint("PerScene"); viewportOffsetBP = pipeline->getBindingPoint("viewportOffset"); viewportSizeBP = pipeline->getBindingPoint("viewportSize"); - CUBOS_ASSERT(positionBP && normalBP && albedoBP && ssaoBP && shadowAtlasBP && perSceneBP && - viewportOffsetBP && viewportSizeBP, - "positionTexture, normalTexture, albedoTexture, ssaoTexture, shadowAtlasTexture, PerScene, " + CUBOS_ASSERT(positionBP && normalBP && albedoBP && ssaoBP && shadowAtlasBP && directionalShadowMapBP && + perSceneBP && viewportOffsetBP && viewportSizeBP, + "positionTexture, normalTexture, albedoTexture, ssaoTexture, shadowAtlasTexture, " + "directionalShadowMap, PerScene, " "viewportOffset and " "viewportSize binding points must exist"); + SamplerDesc directionalShadowSamplerDesc{}; + directionalShadowSamplerDesc.addressU = AddressMode::Border; + directionalShadowSamplerDesc.addressV = AddressMode::Border; + for (int i = 0; i < 4; i++) + { + directionalShadowSamplerDesc.borderColor[i] = 1.0F; + } + directionalShadowSampler = renderDevice.createSampler(directionalShadowSamplerDesc); + generateScreenQuad(renderDevice, pipeline, screenQuad); perSceneCB = renderDevice.createConstantBuffer(sizeof(PerScene), nullptr, Usage::Dynamic); @@ -133,6 +164,24 @@ namespace }; } // namespace +/// @brief Gets a camera's frustum corners in world space. +/// @param viewProj Matrix that transforms world space to the camera's clip space. +/// @param corners Output vector where the corners will be stored. +static void getCameraFrustumCorners(const glm::mat4& viewProj, std::vector& corners) +{ + for (int x = -1; x <= 1; x += 2) + { + for (int y = -1; y <= 1; y += 2) + { + for (int z = -1; z <= 1; z += 2) + { + auto corner = glm::inverse(viewProj) * glm::vec4(x, y, z, 1.0F); + corners.push_back(corner / corner.w); + } + } + } +} + void cubos::engine::deferredShadingPlugin(Cubos& cubos) { static const Asset VertexShader = AnyAsset("9532bb85-e1c6-4087-b281-35c41b0aeb68"); @@ -149,12 +198,14 @@ void cubos::engine::deferredShadingPlugin(Cubos& cubos) cubos.depends(transformPlugin); cubos.depends(shadowsPlugin); cubos.depends(shadowAtlasPlugin); + cubos.depends(cascadedShadowMapsPlugin); cubos.tag(deferredShadingTag) .tagged(drawToHDRTag) .after(drawToGBufferTag) .after(drawToSSAOTag) - .after(drawToShadowAtlasTag); + .after(drawToShadowAtlasTag) + .after(drawToCascadedShadowMapsTag); cubos.uninitResource(); @@ -172,18 +223,23 @@ void cubos::engine::deferredShadingPlugin(Cubos& cubos) cubos.system("apply Deferred Shading to the GBuffer and output to the HDR texture") .tagged(deferredShadingTag) - .call([](const State& state, const Window& window, const RenderEnvironment& environment, - const ShadowAtlas& shadowAtlas, Query directionalLights, + .call([](State& state, const Window& window, const RenderEnvironment& environment, + const ShadowAtlas& shadowAtlas, + Query> + directionalLights, Query pointLights, Query> spotLights, Query targets, - Query cameras) { + Query, + Opt, const DrawsTo&> + cameras) { auto& rd = window->renderDevice(); for (auto [targetEnt, hdr, gBuffer, ssao, deferredShading] : targets) { // Find the cameras that draw to the GBuffer. - for (auto [localToWorld, camera, drawsTo] : cameras.pin(1, targetEnt)) + for (auto [cameraEntity, localToWorld, camera, perspectiveCamera, orthographicCamera, drawsTo] : + cameras.pin(1, targetEnt)) { if (!camera.active) { @@ -214,17 +270,136 @@ void cubos::engine::deferredShadingPlugin(Cubos& cubos) PerScene perScene{}; perScene.inverseView = localToWorld.mat; perScene.inverseProjection = glm::inverse(camera.projection); + perScene.directionalLightWithShadowsId = -1; perScene.ambientLight = glm::vec4(environment.ambient, 1.0F); perScene.skyGradient[0] = glm::vec4(environment.skyGradient[0], 1.0F); perScene.skyGradient[1] = glm::vec4(environment.skyGradient[1], 1.0F); - for (auto [lightLocalToWorld, light] : directionalLights) + int directionalLight_i = 0; + Texture2DArray directionalShadowMap = nullptr; + for (auto [lightLocalToWorld, light, caster] : directionalLights) { auto& perLight = perScene.directionalLights[perScene.numDirectionalLights++]; perLight.direction = glm::normalize(lightLocalToWorld.mat * glm::vec4(0.0F, 0.0F, 1.0F, 0.0F)); perLight.color = glm::vec4(light.color, 1.0F); perLight.intensity = light.intensity; + + if (caster.contains() && (perspectiveCamera.contains() || orthographicCamera.contains())) + { + float cameraZFar; + if (perspectiveCamera.contains()) + { + cameraZFar = perspectiveCamera.value().zFar; + } + else if (orthographicCamera.contains()) + { + cameraZFar = orthographicCamera.value().zFar; + } + + float maxFar = caster.value().maxDistance == 0 ? cameraZFar : caster.value().maxDistance; + for (int i = 0; i < caster.value().numSplits; i++) + { + float near = + std::max(caster.value().nearDistance, maxFar * caster.value().splitDistances.at(i)); + float far = maxFar * (i == caster.value().numSplits - 1 + ? 1.0F + : caster.value().splitDistances.at(i + 1)); + + auto cameraView = glm::inverse(localToWorld.mat); + + glm::mat4 cameraProj; + if (perspectiveCamera.contains()) + { + cameraProj = glm::perspective(glm::radians(perspectiveCamera.value().fovY), + (float(gBuffer.size.x) * drawsTo.viewportSize.x) / + (float(gBuffer.size.y) * drawsTo.viewportSize.y), + near, far); + } + else if (orthographicCamera.contains()) + { + auto& ortho = orthographicCamera.value(); + float aspect = (static_cast(gBuffer.size.x) * drawsTo.viewportSize.x) / + (static_cast(gBuffer.size.y) * drawsTo.viewportSize.y); + if (ortho.axis == OrthographicCamera::Axis::Vertical) + { + cameraProj = glm::ortho(-ortho.size * aspect, ortho.size * aspect, -ortho.size, + ortho.size, near, far); + } + else + { + cameraProj = glm::ortho(-ortho.size, ortho.size, -ortho.size / aspect, + ortho.size / aspect, near, far); + } + } + + std::vector cameraFrustumCorners; + getCameraFrustumCorners(cameraProj * cameraView, cameraFrustumCorners); + + glm::vec3 center = {0.0F, 0.0F, 0.0F}; + for (const auto& corner : cameraFrustumCorners) + { + center += glm::vec3(corner); + } + center /= cameraFrustumCorners.size(); + + auto lightDir = glm::vec3(lightLocalToWorld.mat[2]); + auto view = glm::lookAt(center - lightDir, center, glm::vec3(0.0F, 1.0F, 0.0F)); + + // Transform frustum corners to light view space + for (auto& corner : cameraFrustumCorners) + { + corner = view * corner; + } + // Find minimum/maximum coordinates along each axis + float minX = std::numeric_limits::max(); + float maxX = std::numeric_limits::lowest(); + float minY = std::numeric_limits::max(); + float maxY = std::numeric_limits::lowest(); + float minZ = std::numeric_limits::max(); + float maxZ = std::numeric_limits::lowest(); + for (const auto& corner : cameraFrustumCorners) + { + minX = std::min(minX, corner.x); + maxX = std::max(maxX, corner.x); + minY = std::min(minY, corner.y); + maxY = std::max(maxY, corner.y); + minZ = std::min(minZ, corner.z); + maxZ = std::max(maxZ, corner.z); + } + + // Expand space between Z planes, so that objects outside the frustum can cast shadows + minZ -= std::abs(minZ) * 0.5F; + maxZ += std::abs(maxZ) * 0.5F; + auto proj = glm::ortho(minX, maxX, minY, maxY, -maxZ, -minZ); + perLight.matrices[i] = proj * view; + switch (i % 4) + { + case 0: + perLight.shadowFarSplitDistances[i / 4].x = far; + break; + case 1: + perLight.shadowFarSplitDistances[i / 4].y = far; + break; + case 2: + perLight.shadowFarSplitDistances[i / 4].z = far; + break; + case 3: + perLight.shadowFarSplitDistances[i / 4].w = far; + break; + default: + break; + } + } + + perLight.shadowBias = caster.value().baseSettings.bias; + perLight.shadowBlurRadius = caster.value().baseSettings.blurRadius; + perLight.shadowMaxDistance = maxFar; + perLight.numSplits = caster.value().numSplits; + perScene.directionalLightWithShadowsId = directionalLight_i; + directionalShadowMap = caster.value().shadowMaps.at(cameraEntity).cascades; + } + directionalLight_i++; } for (auto [lightLocalToWorld, light] : pointLights) @@ -293,6 +468,9 @@ void cubos::engine::deferredShadingPlugin(Cubos& cubos) state.albedoBP->bind(gBuffer.albedo); state.ssaoBP->bind(ssao.blurTexture); state.shadowAtlasBP->bind(shadowAtlas.atlas); + // directionalShadowMap needs to be binded even if it's null, or else errors may occur on some GPUs + state.directionalShadowMapBP->bind(directionalShadowMap); + state.directionalShadowMapBP->bind(state.directionalShadowSampler); state.perSceneBP->bind(state.perSceneCB); state.viewportOffsetBP->setConstant(drawsTo.viewportOffset); state.viewportSizeBP->setConstant(drawsTo.viewportSize);