diff --git a/filament/CMakeLists.txt b/filament/CMakeLists.txt index 5e54c978a5..f33fa9a5a4 100644 --- a/filament/CMakeLists.txt +++ b/filament/CMakeLists.txt @@ -296,6 +296,12 @@ add_custom_command( APPEND ) +add_custom_command( + OUTPUT "${MATERIAL_DIR}/sao.filamat" + DEPENDS src/materials/ssao/dominantLightShadowing.fs + APPEND +) + add_custom_command( OUTPUT ${RESGEN_OUTPUTS} COMMAND resgen ${RESGEN_FLAGS} ${MATERIAL_BINS} diff --git a/filament/include/filament/View.h b/filament/include/filament/View.h index 6754d6964c..82cd5c860d 100644 --- a/filament/include/filament/View.h +++ b/filament/include/filament/View.h @@ -211,8 +211,8 @@ public: }; /** - * Options for screen space Ambient Occlusion (SSAO) - * @see setAmbientOcclusion() + * Options for screen space Ambient Occlusion (SSAO) and Screen Space Cone Tracing (SSCT) + * @see setAmbientOcclusionOptions() */ struct AmbientOcclusionOptions { float radius = 0.3f; //!< Ambient Occlusion radius in meters, between 0 and ~10. @@ -225,6 +225,22 @@ public: QualityLevel upsampling = QualityLevel::LOW; //!< affects AO buffer upsampling quality bool enabled = false; //!< enables or disables screen-space ambient occlusion float minHorizonAngleRad = 0.0f; //!< min angle in radian to consider + /** + * Screen Space Cone Tracing (SSCT) options + * Ambient shadows from dominant light + */ + struct { + float lightConeRad = 1.0f; //!< full cone angle in radian, between 0 and pi/2 + float startTraceDistance = 0.01f; //!< distance where tracing starts + float contactDistanceMax = 1.0f; //!< max distance shadows are cast + float intensity = 0.8f; //!< intensity + math::float3 lightDirection{ 0, -1, 0 }; //!< light direction + float depthBias = 0.1f; //!< depth bias in world units (mitigate self shadowing) + float depthSlopeBias = 0.1f; //!< depth slope bias (mitigate self shadowing) + float scale = 1.0f; //!< cast shadows scaling + uint8_t sampleCount = 4; //!< tracing sample count, between 1 and 255 + bool enabled = false; //!< enables or disables SSCT + } ssct; }; /** diff --git a/filament/src/PostProcessManager.cpp b/filament/src/PostProcessManager.cpp index 244a1137e0..3902fe6c0d 100644 --- a/filament/src/PostProcessManager.cpp +++ b/filament/src/PostProcessManager.cpp @@ -483,17 +483,29 @@ FrameGraphId PostProcessManager::screenSpaceAmbientOcclusion( const auto invProjection = inverse(cameraInfo.projection); const float inc = (1.0f / (sampleCount - 0.5f)) * spiralTurns * f::TAU; + constexpr mat4 screenFromClipMatrix { + 0.5, 0.0, 0.0, 0.0, + 0.0, 0.5, 0.0, 0.0, + 0.0, 0.0, 0.5, 0.0, + 0.5, 0.5, 0.5, 1.0 + }; + auto& material = getPostProcessMaterial("sao"); FMaterialInstance* const mi = material.getMaterialInstance(); mi->setParameter("depth", depth, { - .filterMin = SamplerMinFilter::NEAREST_MIPMAP_NEAREST - }); + .filterMin = SamplerMinFilter::NEAREST_MIPMAP_NEAREST }); + mi->setParameter("screenFromViewMatrix", + mat4f(screenFromClipMatrix * cameraInfo.projection)); mi->setParameter("resolution", float4{ desc.width, desc.height, 1.0f / desc.width, 1.0f / desc.height }); - mi->setParameter("invRadiusSquared", 1.0f / (options.radius * options.radius)); - mi->setParameter("minHorizonAngleSineSquared", std::pow(std::sin(options.minHorizonAngleRad), 2.0f)); - mi->setParameter("projectionScaleRadius", projectionScale * options.radius); - mi->setParameter("depthParams", cameraInfo.projection[3][2] * 0.5f); + mi->setParameter("invRadiusSquared", + 1.0f / (options.radius * options.radius)); + mi->setParameter("minHorizonAngleSineSquared", + std::pow(std::sin(options.minHorizonAngleRad), 2.0f)); + mi->setParameter("projectionScaleRadius", + projectionScale * options.radius); + mi->setParameter("depthParams", + cameraInfo.projection[3][2] * 0.5f); mi->setParameter("positionParams", float2{ invProjection[0][0], invProjection[1][1] } * 2.0f); @@ -506,6 +518,23 @@ FrameGraphId PostProcessManager::screenSpaceAmbientOcclusion( mi->setParameter("spiralTurns", spiralTurns); mi->setParameter("angleIncCosSin", float2{ std::cos(inc), std::sin(inc) }); mi->setParameter("invFarPlane", 1.0f / -cameraInfo.zf); + + mi->setParameter("ssctConeTraceParams", float4{ + options.ssct.enabled ? std::tan(options.ssct.lightConeRad * 0.5f) : 0.0f, + std::sin(options.ssct.lightConeRad * 0.5f), + options.ssct.startTraceDistance, + 1.0f / options.ssct.contactDistanceMax + }); + + mi->setParameter("ssctIntensity", + options.ssct.intensity); + mi->setParameter("ssctVsLightDirection", + -(cameraInfo.view * options.ssct.lightDirection).xyz); + mi->setParameter("ssctDepthBias", + float2{ options.ssct.depthBias, options.ssct.depthSlopeBias }); + mi->setParameter("ssctInvZoom", options.ssct.scale); + mi->setParameter("ssctSampleCount", uint32_t(options.ssct.sampleCount)); + mi->commit(driver); mi->use(driver); diff --git a/filament/src/details/View.h b/filament/src/details/View.h index af07f78997..9ee29624ac 100644 --- a/filament/src/details/View.h +++ b/filament/src/details/View.h @@ -288,13 +288,22 @@ public: void setAmbientOcclusionOptions(AmbientOcclusionOptions options) noexcept { options.radius = math::max(0.0f, options.radius); - options.bias = math::clamp(0.0f, 0.1f, options.bias); + options.bias = math::clamp(options.bias, 0.0f, 0.1f); options.power = std::max(0.0f, options.power); // snap to the closer of 0.5 or 1.0 options.resolution = std::floor( - math::clamp(1.0f, 2.0f, options.resolution * 2.0f) + 0.5f) * 0.5f; + math::clamp(options.resolution * 2.0f, 1.0f, 2.0f) + 0.5f) * 0.5f; options.intensity = std::max(0.0f, options.intensity); - options.minHorizonAngleRad = math::clamp(0.0f, math::f::PI_2, options.minHorizonAngleRad); + options.minHorizonAngleRad = math::clamp(options.minHorizonAngleRad, 0.0f, math::f::PI_2); + options.ssct.lightConeRad = math::clamp(options.ssct.lightConeRad, 0.0f, math::f::PI_2); + options.ssct.startTraceDistance = std::max(0.0f, options.ssct.startTraceDistance); + options.ssct.contactDistanceMax = std::max(0.0f, options.ssct.contactDistanceMax); + options.ssct.intensity = std::max(0.0f, options.ssct.intensity); + options.ssct.lightDirection = normalize(options.ssct.lightDirection); + options.ssct.depthBias = std::max(0.0f, options.ssct.depthBias); + options.ssct.depthSlopeBias = std::max(0.0f, options.ssct.depthSlopeBias); + options.ssct.scale = std::max(0.0f, options.ssct.scale); + options.ssct.sampleCount = math::clamp((unsigned)options.ssct.sampleCount, 1u, 255u); mAmbientOcclusionOptions = options; } diff --git a/filament/src/materials/ssao/dominantLightShadowing.fs b/filament/src/materials/ssao/dominantLightShadowing.fs new file mode 100644 index 0000000000..c437b48908 --- /dev/null +++ b/filament/src/materials/ssao/dominantLightShadowing.fs @@ -0,0 +1,110 @@ +/* + * Largely based on "Dominant Light Shadowing" + * "Lighting Technology of The Last of Us Part II" by Hawar Doghramachi, Naughty Dog, LLC + */ + +#include "ssaoUtils.fs" + +struct ConeTraceSetup { + // runtime parameters + highp vec2 ssStartPos; + highp vec3 vsStartPos; + vec3 vsNormal; + highp mat4 screenFromViewMatrix; + vec2 jitterOffset; // (x = direction offset, y = step offset) + highp float depthParams; + vec3 vsConeDirection; + + // artistic/quality parameters + vec4 coneTraceParams; // { tan(angle), sin(angle), start trace distance, inverse max contact distance } + float intensity; + float invZoom; + float depthBias; + float slopeScaledDepthBias; + uint sampleCount; +}; + +float coneTraceOcclusion(in ConeTraceSetup setup, const sampler2D depthTexture) { + // skip fragments that are back-facing trace direction + // (avoid overshadowing of translucent surfaces) + float NoL = dot(setup.vsNormal, setup.vsConeDirection); + if (NoL < 0.0) { + return 0.0; + } + + // start position of cone trace + highp vec2 ssStartPos = setup.ssStartPos; + highp vec3 vsStartPos = setup.vsStartPos; + highp float ssStartInvW = 1.0 / (setup.screenFromViewMatrix * vec4(vsStartPos, 1.0)).w; + + // end position of cone trace + highp vec3 vsEndPos = setup.vsConeDirection + vsStartPos; + highp vec4 ssEndPos = setup.screenFromViewMatrix * vec4(vsEndPos, 1.0); + highp float ssEndInvW = 1.0 / ssEndPos.w; + ssEndPos.xy *= ssEndInvW; + + // cone trace direction in screen-space + float ssConeLength = length(ssEndPos.xy - ssStartPos); + float ssInvConeLength = 1.0 / ssConeLength; + vec2 ssConeDirection = (ssEndPos.xy - ssStartPos) * ssInvConeLength; + + // direction perpendicular to cone trace direction + vec2 perpConeDir = vec2(ssConeDirection.y, -ssConeDirection.x); + + // avoid self-occlusion and reduce banding artifacts by normal variation + vec3 vsViewVector = normalize(vsStartPos); + float minTraceDistance = (1.0 - abs(dot(setup.vsNormal, vsViewVector))) * 0.005; + + // init trace distance and sample radius + highp float invLinearDepth = 1.0 / -setup.vsStartPos.z; + float ssTracedDistance = max(setup.coneTraceParams.z, minTraceDistance) * invLinearDepth; + + float ssSampleRadius = setup.coneTraceParams.y * ssTracedDistance; + float ssEndRadius = setup.coneTraceParams.y * ssConeLength; + float vsEndRadius = ssEndRadius * setup.invZoom * invLinearDepth * ssEndPos.w; + + // calculate depth bias + float vsDepthBias = saturate(1.0 - NoL) * setup.slopeScaledDepthBias + setup.depthBias; + + float occlusion = 0.0; + for (uint i = 0u; i < setup.sampleCount; i++) { + // step along cone in screen space + float ssNextSampleRadius = ssSampleRadius * (ssSampleRadius + ssTracedDistance) / (ssTracedDistance - ssSampleRadius); + float ssStepDistance = ssSampleRadius + ssNextSampleRadius; + ssSampleRadius = ssNextSampleRadius; + + // apply jitter offset + float ssJitterStepDistance = ssStepDistance * setup.jitterOffset.y; + float ssJitteredTracedDistance = ssTracedDistance + ssJitterStepDistance; + float ssJitteredSampleRadius = setup.jitterOffset.x * setup.coneTraceParams.x * ssJitteredTracedDistance; + ssTracedDistance += ssStepDistance; + + // sample depth buffer + highp vec2 ssSamplePos = perpConeDir * ssJitteredSampleRadius + ssConeDirection * ssJitteredTracedDistance + ssStartPos; + highp float vsSampleDepthLinear = -sampleDepthLinear(depthTexture, ssSamplePos, 0.0, setup.depthParams); + + // calculate depth of cone center + float ratio = ssJitteredTracedDistance * ssInvConeLength; + highp float vsConeAxisDepth = 1.0 / mix(ssStartInvW, ssEndInvW, ratio); + + // calculate depth range of cone slice + float vsConeRadius = (ratio * vsEndRadius) * vsConeAxisDepth; + float vsJitteredSampleRadius = vsConeRadius * setup.jitterOffset.x; + float vsSliceHalfRange = sqrt(vsConeRadius * vsConeRadius - vsJitteredSampleRadius * vsJitteredSampleRadius); + float vsSampleDepthMax = vsConeAxisDepth + vsSliceHalfRange; + + // calculate overlap of depth buffer height-field with trace cone + float vsDepthDifference = vsSampleDepthMax - vsSampleDepthLinear; + float overlap = saturate((vsDepthDifference - vsDepthBias) / (vsSliceHalfRange * 2.0)); + + // attenuate by distance to avoid false occlusion + float attenuation = saturate(1.0 - (vsDepthDifference * setup.coneTraceParams.w)); + occlusion = max(occlusion, overlap * attenuation); + + if (occlusion >= 1.0) { // note: this can't get > 1.0 by construction + // fully occluded, early exit + break; + } + } + return occlusion * setup.intensity; +} diff --git a/filament/src/materials/ssao/sao.mat b/filament/src/materials/ssao/sao.mat index 0d67d6438d..1cae424263 100644 --- a/filament/src/materials/ssao/sao.mat +++ b/filament/src/materials/ssao/sao.mat @@ -6,6 +6,10 @@ material { name : depth, precision: high }, + { + type : mat4, + name : screenFromViewMatrix + }, { type : float4, name : resolution, @@ -68,6 +72,30 @@ material { { type : int, name : maxLevel + }, + { + type : float4, + name : ssctConeTraceParams + }, + { + type : float3, + name : ssctVsLightDirection + }, + { + type : float, + name : ssctIntensity + }, + { + type : float2, + name : ssctDepthBias + }, + { + type : float, + name : ssctInvZoom + }, + { + type : uint, + name : ssctSampleCount } ], variables : [ @@ -86,6 +114,7 @@ vertex { fragment { #include "ssaoUtils.fs" + #include "dominantLightShadowing.fs" const float kLog2LodRate = 3.0; const float kEdgeDistance = 0.0625; // this shouldn't be hardcoded @@ -201,13 +230,7 @@ fragment { occlusion += w * max(0.0, vn + origin.z * materialParams.bias) / (vv + materialParams.peak2); } - void postProcess(inout PostProcessInputs postProcess) { - highp vec2 uv = variable_vertex.xy; // interpolated to pixel center - - highp float depth = sampleDepthLinear(materialParams_depth, uv, 0.0, materialParams.depthParams); - highp vec3 origin = computeViewSpacePositionFromDepth(uv, depth); - - vec3 normal = computeViewSpaceNormal(origin, uv); + float scalableAmbientObscurance(highp vec2 uv, highp vec3 origin, vec3 normal) { float noise = random(gl_FragCoord.xy); highp vec2 tapPosition = startPosition(noise); highp mat2 angleStep = tapAngleStep(); @@ -221,7 +244,45 @@ fragment { computeAmbientOcclusionSAO(occlusion, i, ssDiskRadius, uv, origin, normal, tapPosition, noise); tapPosition = angleStep * tapPosition; } - occlusion = sqrt(occlusion * materialParams.intensity); + return sqrt(occlusion * materialParams.intensity); + } + + float dominantLightShadowing(highp vec2 uv, highp vec3 origin, vec3 normal) { + ConeTraceSetup cone; + cone.ssStartPos = uv; + cone.vsStartPos = origin; + cone.vsNormal = normal; + cone.screenFromViewMatrix = materialParams.screenFromViewMatrix; + cone.depthParams = materialParams.depthParams; + cone.vsConeDirection = materialParams.ssctVsLightDirection; + cone.coneTraceParams = materialParams.ssctConeTraceParams; + cone.intensity = materialParams.ssctIntensity; + cone.invZoom = materialParams.ssctInvZoom; + cone.depthBias = materialParams.ssctDepthBias.x; + cone.slopeScaledDepthBias = materialParams.ssctDepthBias.y; + cone.sampleCount = materialParams.ssctSampleCount; + cone.jitterOffset.x = random(gl_FragCoord.xy) * 2.0 - 1.0; // direction + cone.jitterOffset.y = random(gl_FragCoord.xy * vec2(11, 3)); // step + float occlusion = coneTraceOcclusion(cone, materialParams_depth); + return occlusion; + } + + void postProcess(inout PostProcessInputs postProcess) { + highp vec2 uv = variable_vertex.xy; // interpolated to pixel center + + highp float depth = sampleDepthLinear(materialParams_depth, uv, 0.0, materialParams.depthParams); + highp vec3 origin = computeViewSpacePositionFromDepth(uv, depth); + vec3 normal = computeViewSpaceNormal(origin, uv); + + float occlusion = 0.0; + + if (materialParams.intensity > 0.0) { + occlusion = scalableAmbientObscurance(uv, origin, normal); + } + + if (materialParams.ssctConeTraceParams.x > 0.0) { + occlusion = max(occlusion, dominantLightShadowing(uv, origin, normal)); + } // occlusion to visibility float aoVisibility = pow(saturate(1.0 - occlusion), materialParams.power); diff --git a/libs/gltfio/include/gltfio/SimpleViewer.h b/libs/gltfio/include/gltfio/SimpleViewer.h index 978fd573a6..d4b9dcb3b8 100644 --- a/libs/gltfio/include/gltfio/SimpleViewer.h +++ b/libs/gltfio/include/gltfio/SimpleViewer.h @@ -484,6 +484,20 @@ void SimpleViewer::updateUserInterface() { mSSAOOptions.upsampling = upsampling ? View::QualityLevel::HIGH : View::QualityLevel::LOW; mSSAOOptions.quality = (View::QualityLevel) quality; mSSAOOptions.lowPassFilter = (View::QualityLevel) lowpass; + if (ImGui::CollapsingHeader("Dominant Light Shadows (experimental)")) { + int sampleCount = mSSAOOptions.ssct.sampleCount; + ImGui::Checkbox("Enabled##dls", &mSSAOOptions.ssct.enabled); + ImGui::SliderFloat("Cone angle", &mSSAOOptions.ssct.lightConeRad, 0.0f, (float)M_PI_2); + ImGui::SliderFloat("Start dist", &mSSAOOptions.ssct.startTraceDistance, 0.0f, 1.0f); + ImGui::SliderFloat("Contact dist max", &mSSAOOptions.ssct.contactDistanceMax, 0.0f, 100.0f); + ImGui::SliderFloat("Intensity##dls", &mSSAOOptions.ssct.intensity, 0.0f, 10.0f); + ImGui::SliderFloat("Depth bias", &mSSAOOptions.ssct.depthBias, 0.0f, 1.0f); + ImGui::SliderFloat("Depth slope bias", &mSSAOOptions.ssct.depthSlopeBias, 0.0f, 1.0f); + ImGui::SliderFloat("Scale", &mSSAOOptions.ssct.scale, 0.0f, 10.0f); + ImGui::SliderInt("Sample Count", &sampleCount, 1, 32); + ImGuiExt::DirectionWidget("Direction##dls", mSSAOOptions.ssct.lightDirection.v); + mSSAOOptions.ssct.sampleCount = sampleCount; + } } ImGui::Unindent(); } diff --git a/libs/math/include/math/scalar.h b/libs/math/include/math/scalar.h index 27174cf9cf..1de77d24c6 100644 --- a/libs/math/include/math/scalar.h +++ b/libs/math/include/math/scalar.h @@ -18,6 +18,7 @@ #define TNT_MATH_SCALAR_H #include +#include namespace filament { namespace math { @@ -87,6 +88,7 @@ inline constexpr T MATH_PURE max(T a, T b) noexcept { template inline constexpr T MATH_PURE clamp(T v, T min, T max) noexcept { + assert(min <= max); return T(math::min(max, math::max(min, v))); } diff --git a/samples/material_sandbox.cpp b/samples/material_sandbox.cpp index 5652a4c6a9..f8c98e5f4b 100644 --- a/samples/material_sandbox.cpp +++ b/samples/material_sandbox.cpp @@ -615,6 +615,20 @@ static void gui(filament::Engine* engine, filament::View*) { ImGui::SliderInt("Quality", &quality, 0, 3); ImGui::SliderInt("Low Pass", &lowpass, 0, 2); ImGui::Checkbox("High quality upsampling", &upsampling); + if (ImGui::CollapsingHeader("Dominant Light Shadows")) { + int sampleCount = params.ssaoOptions.ssct.sampleCount; + ImGui::Checkbox("Enabled##dls", ¶ms.ssaoOptions.ssct.enabled); + ImGui::SliderFloat("Cone angle", ¶ms.ssaoOptions.ssct.lightConeRad, 0.0f, (float)M_PI_2); + ImGui::SliderFloat("Start dist", ¶ms.ssaoOptions.ssct.startTraceDistance, 0.0f, 1.0f); + ImGui::SliderFloat("Contact dist max", ¶ms.ssaoOptions.ssct.contactDistanceMax, 0.0f, 100.0f); + ImGui::SliderFloat("Intensity##dls", ¶ms.ssaoOptions.ssct.intensity, 0.0f, 10.0f); + ImGui::SliderFloat("Depth bias", ¶ms.ssaoOptions.ssct.depthBias, 0.0f, 1.0f); + ImGui::SliderFloat("Depth slope bias", ¶ms.ssaoOptions.ssct.depthSlopeBias, 0.0f, 1.0f); + ImGui::SliderFloat("Scale", ¶ms.ssaoOptions.ssct.scale, 0.0f, 10.0f); + ImGui::SliderInt("Sample Count", &sampleCount, 1, 32); + ImGuiExt::DirectionWidget("Direction##dls", params.ssaoOptions.ssct.lightDirection.v); + params.ssaoOptions.ssct.sampleCount = sampleCount; + } params.ssaoOptions.upsampling = upsampling ? View::QualityLevel::HIGH : View::QualityLevel::LOW; params.ssaoOptions.quality = (View::QualityLevel)quality; params.ssaoOptions.lowPassFilter = (View::QualityLevel)lowpass;