From 64af518b5802291a889fe0881dea49402660670e Mon Sep 17 00:00:00 2001 From: skallweit Date: Fri, 4 Dec 2020 11:30:15 +0100 Subject: [PATCH] Falcor 4.3 --- .gitignore | 1 + Build/deploycommon.bat | 53 +- Build/packman/bootstrap/configure.bat | 186 +- .../packman/bootstrap/fetch_file_from_s3.cmd | 16 +- .../packman/bootstrap/fetch_file_from_s3.ps1 | 24 +- .../packman/bootstrap/fetch_file_from_url.ps1 | 16 + .../bootstrap/generate_temp_file_name.ps1 | 16 + .../bootstrap/generate_temp_folder.ps1 | 16 + Build/packman/bootstrap/install_package.py | 28 +- Build/packman/packman | 118 +- Build/packman/packman.cmd | 58 +- Build/packman/python.bat | 21 + Build/packman/python.sh | 26 + Build/prebuild.bat | 2 +- Build/update_dependencies.bat | 6 +- Build/update_dependencies.sh | 4 +- Docs/Getting-Started.md | 6 +- Docs/Tutorials/01-Mogwai-Usage.md | 2 +- Docs/Tutorials/04-Writing-Shaders.md | 6 +- Docs/Usage/Custom-Primitives.md | 51 + Docs/Usage/Path-Tracer.md | 70 +- Docs/Usage/Scene-Formats.md | 206 +- Docs/Usage/Scenes.md | 4 +- Docs/Usage/Scripting.md | 437 +- Docs/Usage/images/ExampleScene.png | Bin 0 -> 707773 bytes Docs/Usage/index.md | 3 +- Falcor.sln | 7 + LICENSE.md | 16 +- README.md | 17 +- Source/Externals/args/args.h | 4283 ----------------- Source/Externals/hypothesis/LICENSE | 21 + Source/Externals/hypothesis/README.md | 7 + Source/Externals/hypothesis/cephes.h | 404 ++ Source/Externals/hypothesis/hypothesis.h | 355 ++ Source/Externals/mikktspace/README.md | 5 - Source/Externals/mikktspace/mikktspace.c | 1890 -------- Source/Externals/mikktspace/mikktspace.h | 145 - Source/Falcor/Core/API/Buffer.cpp | 19 +- Source/Falcor/Core/API/Buffer.h | 25 +- Source/Falcor/Core/API/D3D12/D3D12Buffer.cpp | 5 +- .../Core/API/D3D12/D3D12CopyContext.cpp | 7 +- .../Core/API/D3D12/D3D12DescriptorPool.cpp | 6 +- Source/Falcor/Core/API/D3D12/D3D12Device.cpp | 12 + Source/Falcor/Core/API/D3D12/D3D12Fbo.cpp | 52 +- .../Core/API/D3D12/D3D12RenderContext.cpp | 7 +- .../Falcor/Core/API/D3D12/D3D12Resource.cpp | 24 +- .../Core/API/D3D12/D3D12ResourceViews.cpp | 457 +- Source/Falcor/Core/API/D3D12/D3D12State.cpp | 2 + Source/Falcor/Core/API/D3D12/D3D12Texture.cpp | 45 +- Source/Falcor/Core/API/DescriptorPool.h | 1 + Source/Falcor/Core/API/DescriptorSet.cpp | 1 - Source/Falcor/Core/API/Device.h | 4 +- Source/Falcor/Core/API/Formats.h | 10 +- Source/Falcor/Core/API/Resource.h | 22 +- Source/Falcor/Core/API/ResourceViews.cpp | 89 +- Source/Falcor/Core/API/ResourceViews.h | 55 +- Source/Falcor/Core/API/RootSignature.cpp | 1 + Source/Falcor/Core/API/Texture.cpp | 45 +- Source/Falcor/Core/API/Texture.h | 19 +- Source/Falcor/Core/API/TextureLoader.cpp | 660 +-- .../Core/API/Vulkan/VKResourceViews.cpp | 6 - .../Core/BufferTypes/ParameterBlock.cpp | 84 +- .../Falcor/Core/BufferTypes/ParameterBlock.h | 92 +- .../Core/BufferTypes/VariablesBufferUI.cpp | 2 +- Source/Falcor/Core/FalcorConfig.h | 1 + Source/Falcor/Core/Platform/OS.cpp | 17 +- Source/Falcor/Core/Platform/OS.h | 15 +- .../Falcor/Core/Platform/Windows/Windows.cpp | 8 +- Source/Falcor/Core/Program/CUDAProgram.cpp | 1086 +++++ Source/Falcor/Core/Program/CUDAProgram.h | 84 + Source/Falcor/Core/Program/ComputeProgram.cpp | 10 + Source/Falcor/Core/Program/ComputeProgram.h | 9 +- Source/Falcor/Core/Program/Program.cpp | 246 +- Source/Falcor/Core/Program/Program.h | 27 +- .../Falcor/Core/Program/ProgramReflection.cpp | 133 +- .../Falcor/Core/Program/ProgramReflection.h | 9 +- Source/Falcor/Core/Program/ProgramVars.cpp | 8 + Source/Falcor/Core/Program/ProgramVars.h | 4 + Source/Falcor/Core/Renderer.h | 45 +- Source/Falcor/Core/Sample.cpp | 39 +- Source/Falcor/Core/Sample.h | 3 +- Source/Falcor/Core/Window.cpp | 6 +- Source/Falcor/Core/Window.h | 2 +- .../Scene/Lights/EmissiveLightSampler.cpp | 6 +- .../Scene/Lights/EmissiveLightSampler.h | 9 +- .../Scene/Lights/EmissiveLightSampler.slang | 40 +- .../Lights/EmissiveLightSamplerHelpers.slang | 4 +- .../EmissiveLightSamplerInterface.slang | 20 +- .../Lights/EmissiveLightSamplerType.slangh | 7 + .../Scene/Lights/EmissivePowerSampler.cpp | 182 + .../Scene/Lights/EmissivePowerSampler.h | 87 + .../Scene/Lights/EmissivePowerSampler.slang | 122 + .../Scene/Lights/EmissiveUniformSampler.slang | 28 +- .../Experimental/Scene/Lights/EnvMap.cpp | 13 +- .../Falcor/Experimental/Scene/Lights/EnvMap.h | 8 +- .../Experimental/Scene/Lights/EnvMap.slang | 18 +- .../Scene/Lights/EnvMapIntegration.ps.slang} | 0 .../Scene/Lights/EnvMapLighting.cpp | 137 + .../Scene/Lights/EnvMapLighting.h | 87 + .../Scene/Lights/EnvMapLighting.slang | 96 + .../Scene/Lights/EnvMapSampler.slang | 2 +- .../Experimental/Scene/Lights/LightBVH.cpp | 16 +- .../Experimental/Scene/Lights/LightBVH.h | 2 +- .../Scene/Lights/LightBVHBuilder.cpp | 51 +- .../Scene/Lights/LightBVHBuilder.h | 12 +- .../Scene/Lights/LightBVHSampler.cpp | 20 +- .../Scene/Lights/LightBVHSampler.h | 11 +- .../Scene/Lights/LightBVHSampler.slang | 276 +- .../Scene/Lights/LightCollection.cpp | 81 +- .../Scene/Lights/LightCollection.h | 23 +- .../Scene/Lights/LightCollection.slang | 9 + .../Scene/Lights/LightCollectionShared.slang | 9 +- .../Scene/Lights/LightHelpers.slang | 18 +- .../Experimental/Scene/Material/BCSDF.slang | 131 + .../Scene/Material/BCSDFConfig.slangh | 42 + .../Experimental/Scene/Material/BxDF.slang | 119 +- .../Experimental/Scene/Material/Fresnel.slang | 5 + .../Scene/Material/HairChiang16.slang | 499 ++ .../Scene/Material/MaterialShading.slang | 14 +- .../Scene/Material/TexLODHelpers.slang | 10 +- .../Scene/Volume/PhaseFunctions.slang | 71 + .../Scene/Volume/VolumeSampler.cpp | 90 + .../Experimental/Scene/Volume/VolumeSampler.h | 84 + .../Scene/Volume/VolumeSampler.slang | 256 + .../Scene/Volume/VolumeSamplerParams.slang} | 40 +- Source/Falcor/Falcor.h | 7 +- Source/Falcor/Falcor.props | 18 +- Source/Falcor/Falcor.vcxproj | 89 +- Source/Falcor/Falcor.vcxproj.filters | 189 +- Source/Falcor/FalcorExperimental.h | 1 + .../Falcor/Raytracing/RtProgram/RtProgram.cpp | 110 +- .../Falcor/Raytracing/RtProgram/RtProgram.h | 29 +- Source/Falcor/Raytracing/RtProgramVars.cpp | 71 +- Source/Falcor/Raytracing/RtProgramVars.h | 5 +- Source/Falcor/Raytracing/RtStateObject.cpp | 42 +- .../Falcor/Raytracing/RtStateObjectHelper.h | 44 +- Source/Falcor/Raytracing/ShaderTable.cpp | 2 +- .../BasePasses/RasterScenePass.cpp | 2 +- Source/Falcor/RenderGraph/RenderGraph.cpp | 2 - Source/Falcor/RenderGraph/RenderGraphExe.cpp | 2 +- Source/Falcor/RenderGraph/RenderGraphIR.cpp | 25 +- .../RenderGraph/RenderGraphImportExport.cpp | 6 +- Source/Falcor/RenderGraph/RenderGraphUI.cpp | 3 +- Source/Falcor/RenderGraph/RenderPass.h | 4 + .../Falcor/RenderGraph/RenderPassLibrary.cpp | 4 + .../RenderGraph/RenderPassReflection.cpp | 9 + .../Falcor/RenderGraph/RenderPassReflection.h | 1 + .../RenderGraph/RenderPassStandardFlags.h | 4 + .../Shared/PathTracer/InteriorList.slang | 34 +- .../Shared/PathTracer/LoadShadingData.slang | 36 +- .../Shared/PathTracer/PathTracer.cpp | 65 +- .../Shared/PathTracer/PathTracer.h | 4 +- .../Shared/PathTracer/PathTracerHelpers.slang | 17 +- .../Shared/PathTracer/PathTracerParams.slang | 5 +- .../Shared/PathTracer/PixelStats.cpp | 102 +- .../Shared/PathTracer/PixelStats.cs.slang | 2 +- .../Shared/PathTracer/PixelStats.h | 33 +- .../Shared/PathTracer/PixelStats.slang | 16 + .../Shared/PathTracer/RayFootprint.slang | 30 +- .../Shared/PathTracer/StaticParams.slang | 6 +- Source/Falcor/Scene/Animation/Animation.cpp | 346 +- Source/Falcor/Scene/Animation/Animation.h | 127 +- .../Scene/Animation/AnimationController.cpp | 136 +- .../Scene/Animation/AnimationController.h | 64 +- Source/Falcor/Scene/Animation/Skinning.slang | 20 +- Source/Falcor/Scene/Camera/Camera.cpp | 33 +- Source/Falcor/Scene/Camera/Camera.h | 13 +- Source/Falcor/Scene/Camera/Camera.slang | 5 + Source/Falcor/Scene/Camera/CameraController.h | 2 +- Source/Falcor/Scene/Camera/CameraData.slang | 1 + .../Falcor/Scene/Curves/CurveTessellation.cpp | 247 + .../Falcor/Scene/Curves/CurveTessellation.h | 91 + Source/Falcor/Scene/HitInfo.cpp | 92 + Source/Falcor/Scene/HitInfo.h | 53 +- Source/Falcor/Scene/HitInfo.slang | 97 +- Source/Falcor/Scene/HitInfoType.slang | 45 + .../Falcor/Scene/Importers/AssimpImporter.cpp | 130 +- .../Falcor/Scene/Importers/PythonImporter.cpp | 107 +- .../Falcor/Scene/Importers/SceneImporter.cpp | 120 +- Source/Falcor/Scene/Intersection.slang | 52 + Source/Falcor/Scene/Lights/Light.cpp | 231 +- Source/Falcor/Scene/Lights/Light.h | 218 +- Source/Falcor/Scene/Lights/LightData.slang | 4 +- Source/Falcor/Scene/Lights/LightProbe.cpp | 258 - Source/Falcor/Scene/Lights/LightProbe.h | 153 - Source/Falcor/Scene/Lights/Lights.slang | 110 - Source/Falcor/Scene/Material/Material.cpp | 179 +- Source/Falcor/Scene/Material/Material.h | 68 +- .../Falcor/Scene/Material/MaterialData.slang | 4 + .../Scene/Material/MaterialDefines.slangh | 85 +- .../Scene/Material/MaterialTextureLoader.cpp | 83 + .../Scene/Material/MaterialTextureLoader.h | 74 + Source/Falcor/Scene/NullTrace.cs.slang | 46 + Source/Falcor/Scene/Raster.slang | 34 +- Source/Falcor/Scene/Raytracing.slang | 3 +- Source/Falcor/Scene/RaytracingInline.slang | 124 + Source/Falcor/Scene/Scene.cpp | 1599 ++++-- Source/Falcor/Scene/Scene.h | 539 ++- Source/Falcor/Scene/Scene.slang | 295 +- Source/Falcor/Scene/SceneBuilder.cpp | 1989 ++++++-- Source/Falcor/Scene/SceneBuilder.h | 467 +- Source/Falcor/Scene/SceneTypes.slang | 128 +- Source/Falcor/Scene/Shading.slang | 97 +- Source/Falcor/Scene/Transform.cpp | 169 + Source/Falcor/Scene/Transform.h | 68 + Source/Falcor/Scene/TriangleMesh.cpp | 268 ++ Source/Falcor/Scene/TriangleMesh.h | 147 + Source/Falcor/Scene/VertexAttrib.slangh | 20 +- Source/Falcor/Scene/Volume/Grid.cpp | 255 + Source/Falcor/Scene/Volume/Grid.h | 128 + Source/Falcor/Scene/Volume/Grid.slang | 195 + Source/Falcor/Scene/Volume/Volume.cpp | 360 ++ Source/Falcor/Scene/Volume/Volume.h | 259 + Source/Falcor/Scene/Volume/Volume.slang | 66 + Source/Falcor/Scene/Volume/VolumeData.slang | 51 + Source/Falcor/Testing/UnitTest.cpp | 3 + Source/Falcor/Utils/AsyncTextureLoader.cpp | 129 + Source/Falcor/Utils/AsyncTextureLoader.h | 79 + Source/Falcor/Utils/Debug/PixelDebug.cpp | 6 +- Source/Falcor/Utils/Debug/PixelDebug.h | 3 +- Source/Falcor/Utils/Helpers.slang | 154 +- Source/Falcor/Utils/Image/Bitmap.cpp | 112 +- Source/Falcor/Utils/Image/Bitmap.h | 39 +- Source/Falcor/Utils/Image/DDSHeader.h | 120 - Source/Falcor/Utils/Image/DXHeader.cpp | 165 - Source/Falcor/Utils/Image/DXHeader.h | 167 - Source/Falcor/Utils/Image/ImageIO.cpp | 349 ++ Source/Falcor/Utils/Image/ImageIO.h | 116 + Source/Falcor/Utils/Logger.cpp | 43 +- Source/Falcor/Utils/Logger.h | 17 +- Source/Falcor/Utils/Math/AABB.cpp | 82 + Source/Falcor/Utils/Math/AABB.h | 196 +- Source/Falcor/Utils/Math/AABB.slang | 8 + Source/Falcor/Utils/Math/BBox.h | 97 - .../Falcor/Utils/Math/FormatConversion.slang | 58 +- Source/Falcor/Utils/Math/MathHelpers.h | 66 + Source/Falcor/Utils/Math/MathHelpers.slang | 22 +- Source/Falcor/Utils/Math/PackedFormats.slang | 18 +- Source/Falcor/Utils/NumericRange.h | 71 + Source/Falcor/Utils/Sampling/AliasTable.cpp | 127 + Source/Falcor/Utils/Sampling/AliasTable.h | 69 + Source/Falcor/Utils/Sampling/AliasTable.slang | 80 + .../Utils/Sampling/SampleGenerator.slang | 37 +- .../Sampling/SampleGeneratorInterface.slang | 35 + .../Sampling/TinyUniformSampleGenerator.slang | 17 +- .../Falcor/Utils/Scripting/ScriptBindings.cpp | 156 +- .../Falcor/Utils/Scripting/ScriptBindings.h | 42 +- Source/Falcor/Utils/Scripting/ScriptWriter.h | 113 + Source/Falcor/Utils/Scripting/Scripting.cpp | 59 +- Source/Falcor/Utils/Scripting/Scripting.h | 200 +- Source/Falcor/Utils/StringUtils.h | 45 +- Source/Falcor/Utils/Threading.h | 46 + Source/Falcor/Utils/Timing/Clock.cpp | 12 +- Source/Falcor/Utils/Timing/Clock.h | 33 - Source/Falcor/Utils/Timing/Profiler.cpp | 117 +- Source/Falcor/Utils/Timing/Profiler.h | 79 +- Source/Falcor/Utils/Timing/TimeReport.h | 2 +- Source/Falcor/Utils/UI/DebugDrawer.cpp | 6 +- Source/Falcor/Utils/UI/DebugDrawer.h | 2 +- Source/Falcor/Utils/UI/Gui.cpp | 25 +- Source/Falcor/Utils/UI/Gui.h | 5 + Source/Falcor/Utils/Video/VideoEncoderUI.cpp | 26 +- Source/Falcor/Utils/Video/VideoEncoderUI.h | 4 +- Source/Falcor/dependencies.xml | 51 +- Source/Mogwai/Data/Config.py | 2 +- Source/Mogwai/Data/PathTracer.py | 2 +- Source/Mogwai/Data/SceneDebugger.py | 13 + Source/Mogwai/Data/VBufferPathTracer.py | 2 +- .../Extensions/Capture/CaptureTrigger.cpp | 9 +- .../Extensions/Capture/CaptureTrigger.h | 4 +- .../Extensions/Capture/FrameCapture.cpp | 22 +- .../Mogwai/Extensions/Capture/FrameCapture.h | 5 +- .../Extensions/Capture/VideoCapture.cpp | 30 +- .../Mogwai/Extensions/Capture/VideoCapture.h | 5 +- .../Extensions/Profiler/TimingCapture.cpp | 13 +- .../Extensions/Profiler/TimingCapture.h | 3 +- Source/Mogwai/Mogwai.cpp | 54 +- Source/Mogwai/Mogwai.h | 28 +- Source/Mogwai/Mogwai.vcxproj | 2 +- Source/Mogwai/MogwaiScripting.cpp | 100 +- Source/Mogwai/MogwaiSettings.cpp | 2 +- .../AccumulatePass/AccumulatePass.cpp | 2 +- .../AccumulatePass/AccumulatePass.vcxproj | 2 +- .../Antialiasing/Antialiasing.vcxproj | 2 +- Source/RenderPasses/BSDFViewer/BSDFViewer.cpp | 9 +- .../BSDFViewer/BSDFViewer.cs.slang | 6 +- .../BSDFViewer/BSDFViewer.vcxproj | 2 +- Source/RenderPasses/BlitPass/BlitPass.cpp | 42 +- Source/RenderPasses/BlitPass/BlitPass.h | 12 +- Source/RenderPasses/BlitPass/BlitPass.vcxproj | 2 +- Source/RenderPasses/CSM/CSM.cpp | 4 +- Source/RenderPasses/CSM/CSM.vcxproj | 2 +- .../DebugPasses/ComparisonPass.cpp | 7 +- .../DebugPasses/DebugPasses.vcxproj | 2 +- .../InvalidPixelDetection.ps.slang | 16 +- .../InvalidPixelDetectionPass.cpp | 29 +- .../InvalidPixelDetectionPass.h | 3 + Source/RenderPasses/DepthPass/DepthPass.cpp | 2 +- .../RenderPasses/DepthPass/DepthPass.vcxproj | 2 +- .../ErrorMeasurePass/ErrorMeasurePass.cpp | 16 +- .../ErrorMeasurePass/ErrorMeasurePass.vcxproj | 2 +- .../ForwardLightingPass.cpp | 25 +- .../ForwardLightingPass/ForwardLightingPass.h | 1 + .../ForwardLightingPass.slang | 8 +- .../ForwardLightingPass.vcxproj | 2 +- Source/RenderPasses/GBuffer/GBuffer.vcxproj | 4 +- .../GBuffer/GBuffer.vcxproj.filters | 6 + .../GBuffer/GBuffer/GBufferHelpers.slang | 9 +- .../GBuffer/GBuffer/GBufferRT.cpp | 7 +- .../RenderPasses/GBuffer/GBuffer/GBufferRT.h | 7 +- .../GBuffer/GBuffer/GBufferRT.rt.slang | 161 +- .../GBuffer/GBuffer/GBufferRTCurves.cpp | 74 + .../GBuffer/GBuffer/GBufferRTCurves.h | 55 + .../GBuffer/GBuffer/GBufferRaster.3d.slang | 9 +- .../GBuffer/GBuffer/GBufferRaster.cpp | 14 +- Source/RenderPasses/GBuffer/GBufferBase.cpp | 23 + Source/RenderPasses/GBuffer/GBufferBase.h | 2 + .../GBuffer/VBuffer/VBufferRT.cpp | 10 +- .../GBuffer/VBuffer/VBufferRT.rt.slang | 10 +- .../GBuffer/VBuffer/VBufferRaster.3d.slang | 5 +- .../GBuffer/VBuffer/VBufferRaster.cpp | 17 +- .../GBuffer/VBuffer/VBufferRaster.h | 2 +- .../RenderPasses/ImageLoader/ImageLoader.cpp | 86 +- Source/RenderPasses/ImageLoader/ImageLoader.h | 8 +- .../ImageLoader/ImageLoader.vcxproj | 2 +- .../Data/MegakernelPathTracer.py | 32 + .../Data/PathTracerTexLOD_Megakernel.py | 5 +- .../MegakernelPathTracer.cpp | 7 +- .../MegakernelPathTracer.vcxproj | 2 +- .../MegakernelPathTracer/PathTracer.rt.slang | 3 +- .../MegakernelPathTracer/PathTracer.slang | 75 +- .../Data/MinimalPathTracer.py | 2 +- .../MinimalPathTracer.rt.slang | 9 +- .../MinimalPathTracer.vcxproj | 2 +- .../PassLibraryTemplate.cpp | 10 +- .../PassLibraryTemplate/PassLibraryTemplate.h | 2 +- .../PassLibraryTemplate.vcxproj | 2 +- .../PixelInspectorPass/PixelInspectorPass.cpp | 4 +- .../PixelInspectorPass.vcxproj | 2 +- Source/RenderPasses/SSAO/ApplyAO.ps.slang | 8 +- Source/RenderPasses/SSAO/SSAO.vcxproj | 2 +- Source/RenderPasses/SVGFPass/SVGFPass.vcxproj | 2 +- .../SceneDebugger/SceneDebugger.cpp | 430 ++ .../SceneDebugger/SceneDebugger.cs.slang | 343 ++ .../SceneDebugger/SceneDebugger.h | 71 + .../SceneDebugger/SceneDebugger.vcxproj | 106 + .../SceneDebugger.vcxproj.filters | 13 + .../SceneDebugger/SharedTypes.slang | 89 + Source/RenderPasses/SkyBox/SkyBox.cpp | 8 +- Source/RenderPasses/SkyBox/SkyBox.slang | 16 +- Source/RenderPasses/SkyBox/SkyBox.vcxproj | 2 +- .../TemporalDelayPass.vcxproj | 4 +- Source/RenderPasses/ToneMapper/ToneMapper.cpp | 49 +- Source/RenderPasses/ToneMapper/ToneMapper.h | 1 - .../ToneMapper/ToneMapper.vcxproj | 2 +- .../Utils/Composite/Composite.cpp | 85 +- .../Utils/Composite/Composite.cs.slang | 35 +- .../RenderPasses/Utils/Composite/Composite.h | 10 + .../Utils/Composite/CompositeMode.slangh | 4 + Source/RenderPasses/Utils/Utils.vcxproj | 2 +- .../Data/WhittedRayTracer_GB_rast.py | 2 +- .../Data/WhittedRayTracer_GB_ray.py | 2 +- .../WhittedRayTracer/WhittedRayTracer.cpp | 4 +- .../WhittedRayTracer.rt.slang | 24 +- .../WhittedRayTracer/WhittedRayTracer.vcxproj | 2 +- Source/Samples/CudaInterop/CopySurface.cu | 1 + Source/Samples/CudaInterop/CopySurface.h | 2 + Source/Samples/CudaInterop/CudaInterop.cpp | 7 +- Source/Samples/CudaInterop/CudaInterop.h | 3 +- .../Samples/CudaInterop/CudaInterop.vcxproj | 55 +- Source/Samples/CudaInterop/FalcorCUDA.cpp | 50 +- Source/Samples/CudaInterop/FalcorCUDA.h | 5 +- Source/Samples/CudaInterop/FalcorCUDA.props | 10 +- Source/Samples/CudaInterop/README.md | 17 +- Source/Samples/HelloDXR/HelloDXR.cpp | 4 +- Source/Samples/HelloDXR/HelloDXR.vcxproj | 4 +- Source/Samples/ModelViewer/ModelViewer.cpp | 8 +- Source/Samples/ModelViewer/ModelViewer.h | 2 +- .../Samples/ModelViewer/ModelViewer.vcxproj | 4 +- .../ProjectTemplate/ProjectTemplate.vcxproj | 4 +- Source/Samples/ShaderToy/ShaderToy.vcxproj | 4 +- .../Tools/FalcorTest/Data/pbrt_hair_bsdf.dat | Bin 0 -> 3400000 bytes Source/Tools/FalcorTest/FalcorTest.cpp | 4 +- Source/Tools/FalcorTest/FalcorTest.vcxproj | 18 +- .../FalcorTest/FalcorTest.vcxproj.filters | 51 + .../FalcorTest/Tests/Core/LargeBuffer.cpp | 363 ++ .../Tests/Core/LargeBuffer.cs.slang | 77 + .../FalcorTest/Tests/Core/ParamBlockCB.cpp | 50 + .../Tests/Core/ParamBlockCB.cs.slang | 41 + .../Tests/Core/RootBufferParamBlockTests.cpp | 2 +- .../Tests/Core/RootBufferStructTests.cpp | 2 +- .../FalcorTest/Tests/Core/RootBufferTests.cpp | 2 +- .../FalcorTest/Tests/Core/TextureTests.cpp | 65 + .../Tests/Core/TextureTests.cs.slang | 43 + .../Tests/Sampling/AliasTableTests.cpp | 143 + .../Tests/Sampling/AliasTableTests.cs.slang | 55 + .../FalcorTest/Tests/Scene/EnvMapTests.cpp | 4 +- .../Scene/Material/HairChiang16Tests.cpp | 239 + .../Scene/Material/HairChiang16Tests.cs.slang | 231 + .../Tests/ShadingUtils/RaytracingTests.cpp | 2 +- .../ShadingUtils/ShadingUtilsTests.cs.slang | 4 +- .../FalcorTest/Tests/Slang/CastFloat16.cpp | 62 + .../Tests/Slang/CastFloat16.cs.slang | 46 + .../FalcorTest/Tests/Slang/ShaderModel.cpp | 4 +- .../Tests/Slang/SlangMutatingTests.cpp | 2 +- .../FalcorTest/Tests/Slang/SlangShared.slang | 4 +- .../FalcorTest/Tests/Slang/SlangTests.cpp | 5 +- .../Tests/Slang/SlangTests.cs.slang | 20 +- .../FalcorTest/Tests/Slang/SlangToCUDA.cpp | 98 + .../FalcorTest/Tests/Slang/SlangToCUDA.slang | 44 + .../FalcorTest/Tests/Slang/TemplatedLoad.cpp | 76 + .../Tests/Slang/TemplatedLoad.cs.slang | 85 + .../FalcorTest/Tests/Slang/TraceRayFlags.cpp | 9 +- .../FalcorTest/Tests/Slang/TraceRayInline.cpp | 2 +- .../Tools/FalcorTest/Tests/Slang/WaveOps.cpp | 2 +- Source/Tools/ImageCompare/ImageCompare.cpp | 4 +- .../Tools/ImageCompare/ImageCompare.vcxproj | 6 +- .../RenderGraphEditor/RenderGraphEditor.cpp | 12 +- .../RenderGraphEditor.vcxproj | 2 +- Tests/environment/teamcity.json | 8 - Tests/image_tests/helpers.py | 14 + .../renderpasses/graphs/GBufferRT.py | 1 + .../image_tests/renderpasses/graphs/MVecRT.py | 15 + .../renderpasses/graphs/MVecRaster.py | 15 + .../renderpasses/graphs/WhittedRayTracer.py | 4 +- Tests/image_tests/renderpasses/helpers.py | 19 - .../renderpasses/test_BSDFViewer.py | 7 +- Tests/image_tests/renderpasses/test_CSM.py | 7 +- .../renderpasses/test_CameraAnimation.py | 13 - .../renderpasses/test_ColorMapPass.py | 13 +- .../renderpasses/test_CompositePass.py | 9 +- Tests/image_tests/renderpasses/test_FXAA.py | 15 +- .../renderpasses/test_ForwardRendering.py | 11 +- .../renderpasses/test_GBufferRT.py | 13 +- .../renderpasses/test_GBufferRaster.py | 11 +- .../renderpasses/test_GaussianBlur.py | 9 +- Tests/image_tests/renderpasses/test_MVecRT.py | 25 + .../renderpasses/test_MVecRaster.py | 25 + .../test_MegakernelPathTracerGBuffer.py | 11 +- .../test_MegakernelPathTracerVBuffer.py | 11 +- .../renderpasses/test_MinimalPathTracer.py | 11 +- Tests/image_tests/renderpasses/test_SSAO.py | 13 +- Tests/image_tests/renderpasses/test_SVGF.py | 7 +- .../renderpasses/test_SideBySide.py | 7 +- .../image_tests/renderpasses/test_Skinning.py | 7 +- .../renderpasses/test_SplitScreen.py | 7 +- Tests/image_tests/renderpasses/test_TAA.py | 11 +- .../renderpasses/test_TemporalDelay.py | 11 +- .../renderpasses/test_TextureLOD.py | 9 +- .../renderpasses/test_ToneMapping.py | 28 +- .../renderpasses/test_VBufferRT.py | 13 +- .../renderpasses/test_VBufferRaster.py | 7 +- .../renderscripts/test_BSDFViewer.py | 15 + .../renderscripts/test_ForwardRenderer.py | 19 + .../renderscripts/test_PathTracer.py | 19 + .../renderscripts/test_SceneDebugger.py | 15 + .../renderscripts/test_VBufferPathTracer.py | 19 + .../image_tests/scene/graphs/SceneDebugger.py | 13 + .../image_tests/scene/scenes/Volumes.pyscene | 24 + .../scene/test_AnimationBehavior.py | 23 + .../image_tests/scene/test_CameraAnimation.py | 13 + Tests/image_tests/scene/test_Volumes.py | 14 + Tests/testing/core/config.py | 4 +- Tests/testing/run_image_tests.py | 30 +- Tools/dependencies.xml | 4 +- Tools/update_dependencies.bat | 2 +- 466 files changed, 22295 insertions(+), 12712 deletions(-) create mode 100644 Build/packman/python.bat create mode 100644 Build/packman/python.sh create mode 100644 Docs/Usage/Custom-Primitives.md create mode 100644 Docs/Usage/images/ExampleScene.png delete mode 100644 Source/Externals/args/args.h create mode 100644 Source/Externals/hypothesis/LICENSE create mode 100644 Source/Externals/hypothesis/README.md create mode 100644 Source/Externals/hypothesis/cephes.h create mode 100644 Source/Externals/hypothesis/hypothesis.h delete mode 100644 Source/Externals/mikktspace/README.md delete mode 100644 Source/Externals/mikktspace/mikktspace.c delete mode 100644 Source/Externals/mikktspace/mikktspace.h create mode 100644 Source/Falcor/Core/Program/CUDAProgram.cpp create mode 100644 Source/Falcor/Core/Program/CUDAProgram.h create mode 100644 Source/Falcor/Experimental/Scene/Lights/EmissivePowerSampler.cpp create mode 100644 Source/Falcor/Experimental/Scene/Lights/EmissivePowerSampler.h create mode 100644 Source/Falcor/Experimental/Scene/Lights/EmissivePowerSampler.slang rename Source/Falcor/{Scene/Lights/LightProbeIntegration.ps.slang => Experimental/Scene/Lights/EnvMapIntegration.ps.slang} (100%) create mode 100644 Source/Falcor/Experimental/Scene/Lights/EnvMapLighting.cpp create mode 100644 Source/Falcor/Experimental/Scene/Lights/EnvMapLighting.h create mode 100644 Source/Falcor/Experimental/Scene/Lights/EnvMapLighting.slang create mode 100644 Source/Falcor/Experimental/Scene/Material/BCSDF.slang create mode 100644 Source/Falcor/Experimental/Scene/Material/BCSDFConfig.slangh create mode 100644 Source/Falcor/Experimental/Scene/Material/HairChiang16.slang create mode 100644 Source/Falcor/Experimental/Scene/Volume/PhaseFunctions.slang create mode 100644 Source/Falcor/Experimental/Scene/Volume/VolumeSampler.cpp create mode 100644 Source/Falcor/Experimental/Scene/Volume/VolumeSampler.h create mode 100644 Source/Falcor/Experimental/Scene/Volume/VolumeSampler.slang rename Source/Falcor/{Scene/Lights/LightProbeData.slang => Experimental/Scene/Volume/VolumeSamplerParams.slang} (62%) create mode 100644 Source/Falcor/Scene/Curves/CurveTessellation.cpp create mode 100644 Source/Falcor/Scene/Curves/CurveTessellation.h create mode 100644 Source/Falcor/Scene/HitInfo.cpp create mode 100644 Source/Falcor/Scene/HitInfoType.slang create mode 100644 Source/Falcor/Scene/Intersection.slang delete mode 100644 Source/Falcor/Scene/Lights/LightProbe.cpp delete mode 100644 Source/Falcor/Scene/Lights/LightProbe.h create mode 100644 Source/Falcor/Scene/Material/MaterialTextureLoader.cpp create mode 100644 Source/Falcor/Scene/Material/MaterialTextureLoader.h create mode 100644 Source/Falcor/Scene/NullTrace.cs.slang create mode 100644 Source/Falcor/Scene/RaytracingInline.slang create mode 100644 Source/Falcor/Scene/Transform.cpp create mode 100644 Source/Falcor/Scene/Transform.h create mode 100644 Source/Falcor/Scene/TriangleMesh.cpp create mode 100644 Source/Falcor/Scene/TriangleMesh.h create mode 100644 Source/Falcor/Scene/Volume/Grid.cpp create mode 100644 Source/Falcor/Scene/Volume/Grid.h create mode 100644 Source/Falcor/Scene/Volume/Grid.slang create mode 100644 Source/Falcor/Scene/Volume/Volume.cpp create mode 100644 Source/Falcor/Scene/Volume/Volume.h create mode 100644 Source/Falcor/Scene/Volume/Volume.slang create mode 100644 Source/Falcor/Scene/Volume/VolumeData.slang create mode 100644 Source/Falcor/Utils/AsyncTextureLoader.cpp create mode 100644 Source/Falcor/Utils/AsyncTextureLoader.h delete mode 100644 Source/Falcor/Utils/Image/DDSHeader.h delete mode 100644 Source/Falcor/Utils/Image/DXHeader.cpp delete mode 100644 Source/Falcor/Utils/Image/DXHeader.h create mode 100644 Source/Falcor/Utils/Image/ImageIO.cpp create mode 100644 Source/Falcor/Utils/Image/ImageIO.h create mode 100644 Source/Falcor/Utils/Math/AABB.cpp delete mode 100644 Source/Falcor/Utils/Math/BBox.h create mode 100644 Source/Falcor/Utils/Math/MathHelpers.h create mode 100644 Source/Falcor/Utils/NumericRange.h create mode 100644 Source/Falcor/Utils/Sampling/AliasTable.cpp create mode 100644 Source/Falcor/Utils/Sampling/AliasTable.h create mode 100644 Source/Falcor/Utils/Sampling/AliasTable.slang create mode 100644 Source/Falcor/Utils/Scripting/ScriptWriter.h create mode 100644 Source/Mogwai/Data/SceneDebugger.py create mode 100644 Source/RenderPasses/GBuffer/GBuffer/GBufferRTCurves.cpp create mode 100644 Source/RenderPasses/GBuffer/GBuffer/GBufferRTCurves.h create mode 100644 Source/RenderPasses/MegakernelPathTracer/Data/MegakernelPathTracer.py create mode 100644 Source/RenderPasses/SceneDebugger/SceneDebugger.cpp create mode 100644 Source/RenderPasses/SceneDebugger/SceneDebugger.cs.slang create mode 100644 Source/RenderPasses/SceneDebugger/SceneDebugger.h create mode 100644 Source/RenderPasses/SceneDebugger/SceneDebugger.vcxproj create mode 100644 Source/RenderPasses/SceneDebugger/SceneDebugger.vcxproj.filters create mode 100644 Source/RenderPasses/SceneDebugger/SharedTypes.slang create mode 100644 Source/Tools/FalcorTest/Data/pbrt_hair_bsdf.dat create mode 100644 Source/Tools/FalcorTest/Tests/Core/LargeBuffer.cpp create mode 100644 Source/Tools/FalcorTest/Tests/Core/LargeBuffer.cs.slang create mode 100644 Source/Tools/FalcorTest/Tests/Core/ParamBlockCB.cpp create mode 100644 Source/Tools/FalcorTest/Tests/Core/ParamBlockCB.cs.slang create mode 100644 Source/Tools/FalcorTest/Tests/Core/TextureTests.cpp create mode 100644 Source/Tools/FalcorTest/Tests/Core/TextureTests.cs.slang create mode 100644 Source/Tools/FalcorTest/Tests/Sampling/AliasTableTests.cpp create mode 100644 Source/Tools/FalcorTest/Tests/Sampling/AliasTableTests.cs.slang create mode 100644 Source/Tools/FalcorTest/Tests/Scene/Material/HairChiang16Tests.cpp create mode 100644 Source/Tools/FalcorTest/Tests/Scene/Material/HairChiang16Tests.cs.slang create mode 100644 Source/Tools/FalcorTest/Tests/Slang/CastFloat16.cpp create mode 100644 Source/Tools/FalcorTest/Tests/Slang/CastFloat16.cs.slang create mode 100644 Source/Tools/FalcorTest/Tests/Slang/SlangToCUDA.cpp create mode 100644 Source/Tools/FalcorTest/Tests/Slang/SlangToCUDA.slang create mode 100644 Source/Tools/FalcorTest/Tests/Slang/TemplatedLoad.cpp create mode 100644 Source/Tools/FalcorTest/Tests/Slang/TemplatedLoad.cs.slang delete mode 100644 Tests/environment/teamcity.json create mode 100644 Tests/image_tests/helpers.py create mode 100644 Tests/image_tests/renderpasses/graphs/MVecRT.py create mode 100644 Tests/image_tests/renderpasses/graphs/MVecRaster.py delete mode 100644 Tests/image_tests/renderpasses/helpers.py delete mode 100644 Tests/image_tests/renderpasses/test_CameraAnimation.py create mode 100644 Tests/image_tests/renderpasses/test_MVecRT.py create mode 100644 Tests/image_tests/renderpasses/test_MVecRaster.py create mode 100644 Tests/image_tests/renderscripts/test_BSDFViewer.py create mode 100644 Tests/image_tests/renderscripts/test_ForwardRenderer.py create mode 100644 Tests/image_tests/renderscripts/test_PathTracer.py create mode 100644 Tests/image_tests/renderscripts/test_SceneDebugger.py create mode 100644 Tests/image_tests/renderscripts/test_VBufferPathTracer.py create mode 100644 Tests/image_tests/scene/graphs/SceneDebugger.py create mode 100644 Tests/image_tests/scene/scenes/Volumes.pyscene create mode 100644 Tests/image_tests/scene/test_AnimationBehavior.py create mode 100644 Tests/image_tests/scene/test_CameraAnimation.py create mode 100644 Tests/image_tests/scene/test_Volumes.py diff --git a/.gitignore b/.gitignore index 0e3a4360fe..9aa64e443f 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ Source/Externals/.packman/* Media Tools/.packman Tests/data +*.tlog diff --git a/Build/deploycommon.bat b/Build/deploycommon.bat index 4fe442ee93..4526c3690e 100644 --- a/Build/deploycommon.bat +++ b/Build/deploycommon.bat @@ -8,35 +8,52 @@ rem %4 -> WINDSDK Directory setlocal -SET ExtDir=%1\Externals\.packman\ -SET OutDir=%3 -SET FalcorDir=%1\Falcor\ +set ExtDir=%1\Externals\.packman\ +set OutDir=%3 +set FalcorDir=%1\Falcor\ if not exist "%OutDir%" mkdir "%OutDir%" +set IsDebug=0 +if "%OutDir:~-6%" == "Debug\" set IsDebug=1 + rem Copy Falcor's files -IF not exist %OutDir%\Data\ mkdir %OutDir%\Data >nul +if not exist %OutDir%\Data\ mkdir %OutDir%\Data >nul call %~dp0\deployproject.bat %FalcorDir% %OutDir% rem Copy externals -robocopy %ExtDir%\Python\ %OutDir% Python36*.dll /r:0 >nul -robocopy %ExtDir%\Python %OutDir%\Python /E /r:0 >nul -robocopy %ExtDir%\AntTweakBar\lib %OutDir% AntTweakBar64.dll /r:0 >nul -robocopy %ExtDir%\FreeImage %OutDir% freeimage.dll /r:0 >nul -robocopy %ExtDir%\assimp\bin\%2 %OutDir% *.dll /r:0 >nul -robocopy %ExtDir%\FFMpeg\bin\%2 %OutDir% *.dll /r:0 >nul -robocopy %ExtDir%\Slang\bin\windows-x64\release %OutDir% *.dll /r:0 >nul -robocopy %ExtDir%\GLFW\lib %OutDir% *.dll /r:0 >nul +if %IsDebug% EQU 0 ( + robocopy %ExtDir%\deps\bin\ %OutDir% /E /r:0 >nul +) else ( + robocopy %ExtDir%\deps\debug\bin\ %OutDir% /E /r:0 >nul + robocopy %ExtDir%\deps\bin\ %OutDir% assimp-vc142-mt.* /r:0 >nul + rem Needed for OpenVDB (debug version links to release version of Half_2.5) + robocopy %ExtDir%\deps\bin\ %OutDir% Half-2_5.* /r:0 >nul +) +robocopy %ExtDir%\python\ %OutDir% Python36*.dll /r:0 >nul +robocopy %ExtDir%\python %OutDir%\Python /E /r:0 >nul +robocopy %ExtDir%\slang\bin\windows-x64\release %OutDir% *.dll /r:0 >nul robocopy %ExtDir%\WinPixEventRuntime\bin\x64 %OutDir% WinPixEventRuntime.dll /r:0 >nul robocopy "%~4\Redist\D3D\%2" %OutDir% dxil.dll /r:0 >nul robocopy "%~4\Redist\D3D\%2" %OutDir% dxcompiler.dll /r:0 >nul +robocopy %ExtDir%\Cuda\bin\ %OutDir% cudart*.dll /r:0 >nul +robocopy %ExtDir%\Cuda\bin\ %OutDir% nvrtc*.dll /r:0 >nul rem Copy NVAPI -set NvApiDir=%ExtDir%\NVAPI -IF exist %NvApiDir% ( - IF not exist %OutDir%\Shaders\NVAPI mkdir %OutDir%\Shaders\NVAPI >nul - copy /y %NvApiDir%\nvHLSLExtns.h %OutDir%\Shaders\NVAPI - copy /y %NvApiDir%\nvHLSLExtnsInternal.h %OutDir%\Shaders\NVAPI - copy /y %NvApiDir%\nvShaderExtnEnums.h %OutDir%\Shaders\NVAPI +set NvApiDir=%ExtDir%\nvapi +set NvApiTargetDir=%OutDir%\Shaders\NVAPI +if exist %NvApiDir% ( + if not exist %NvApiTargetDir% mkdir %NvApiTargetDir% >nul + copy /y %NvApiDir%\nvHLSLExtns.h %NvApiTargetDir% + copy /y %NvApiDir%\nvHLSLExtnsInternal.h %NvApiTargetDir% + copy /y %NvApiDir%\nvShaderExtnEnums.h %NvApiTargetDir% +) + +rem Copy NanoVDB +set NanoVDBApiDir=%ExtDir%\nanovdb +set NanoVDBTargetDir=%OutDir%\Shaders\NanoVDB +if exist %NanoVDBApiDir% ( + if not exist %NanoVDBTargetDir% mkdir %NanoVDBTargetDir% >nul + copy /y %NanoVDBApiDir%\include\nanovdb\PNanoVDB.h %NanoVDBTargetDir% ) rem robocopy sets the error level to something that is not zero even if the copy operation was successful. Set the error level to zero diff --git a/Build/packman/bootstrap/configure.bat b/Build/packman/bootstrap/configure.bat index 1bf84762d9..d606a50eda 100644 --- a/Build/packman/bootstrap/configure.bat +++ b/Build/packman/bootstrap/configure.bat @@ -1,146 +1,160 @@ -@set PM_PACKMAN_VERSION=5.14 +:: Copyright 2019 NVIDIA CORPORATION +:: +:: Licensed under the Apache License, Version 2.0 (the "License"); +:: you may not use this file except in compliance with the License. +:: You may obtain a copy of the License at +:: +:: http://www.apache.org/licenses/LICENSE-2.0 +:: +:: Unless required by applicable law or agreed to in writing, software +:: distributed under the License is distributed on an "AS IS" BASIS, +:: WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +:: See the License for the specific language governing permissions and +:: limitations under the License. + +set PM_PACKMAN_VERSION=6.15 :: Specify where packman command is rooted -@set PM_INSTALL_PATH=%~dp0.. +set PM_INSTALL_PATH=%~dp0.. :: The external root may already be configured and we should do minimal work in that case -@if defined PM_PACKAGES_ROOT goto ENSURE_DIR +if defined PM_PACKAGES_ROOT goto ENSURE_DIR :: If the folder isn't set we assume that the best place for it is on the drive that we are currently :: running from -@set PM_DRIVE=%CD:~0,2% +set PM_DRIVE=%CD:~0,2% -@set PM_PACKAGES_ROOT=%PM_DRIVE%\packman-repo +set PM_PACKAGES_ROOT=%PM_DRIVE%\packman-repo :: We use *setx* here so that the variable is persisted in the user environment -@echo Setting user environment variable PM_PACKAGES_ROOT to %PM_PACKAGES_ROOT% -@setx PM_PACKAGES_ROOT %PM_PACKAGES_ROOT% -@if errorlevel 1 goto ERROR +echo Setting user environment variable PM_PACKAGES_ROOT to %PM_PACKAGES_ROOT% +setx PM_PACKAGES_ROOT %PM_PACKAGES_ROOT% +if %errorlevel% neq 0 ( goto ERROR ) :: The above doesn't work properly from a build step in VisualStudio because a separate process is :: spawned for it so it will be lost for subsequent compilation steps - VisualStudio must :: be launched from a new process. We catch this odd-ball case here: -@if defined PM_DISABLE_VS_WARNING goto ENSURE_DIR -@if not defined VSLANG goto ENSURE_DIR -@echo The above is a once-per-computer operation. Unfortunately VisualStudio cannot pick up environment change -@echo unless *VisualStudio is RELAUNCHED*. -@echo If you are launching VisualStudio from command line or command line utility make sure -@echo you have a fresh launch environment (relaunch the command line or utility). -@echo If you are using 'linkPath' and referring to packages via local folder links you can safely ignore this warning. -@echo You can disable this warning by setting the environment variable PM_DISABLE_VS_WARNING. -@echo. +if defined PM_DISABLE_VS_WARNING goto ENSURE_DIR +if not defined VSLANG goto ENSURE_DIR +echo The above is a once-per-computer operation. Unfortunately VisualStudio cannot pick up environment change +echo unless *VisualStudio is RELAUNCHED*. +echo If you are launching VisualStudio from command line or command line utility make sure +echo you have a fresh launch environment (relaunch the command line or utility). +echo If you are using 'linkPath' and referring to packages via local folder links you can safely ignore this warning. +echo You can disable this warning by setting the environment variable PM_DISABLE_VS_WARNING. +echo. :: Check for the directory that we need. Note that mkdir will create any directories -:: that may be needed in the path +:: that may be needed in the path :ENSURE_DIR -@if not exist "%PM_PACKAGES_ROOT%" ( - @echo Creating directory %PM_PACKAGES_ROOT% - @mkdir "%PM_PACKAGES_ROOT%" - @if errorlevel 1 goto ERROR_MKDIR_PACKAGES_ROOT +if not exist "%PM_PACKAGES_ROOT%" ( + echo Creating directory %PM_PACKAGES_ROOT% + mkdir "%PM_PACKAGES_ROOT%" ) +if %errorlevel% neq 0 ( goto ERROR_MKDIR_PACKAGES_ROOT ) :: The Python interpreter may already be externally configured -@if defined PM_PYTHON_EXT ( - @set PM_PYTHON=%PM_PYTHON_EXT% - @goto PACKMAN +if defined PM_PYTHON_EXT ( + set PM_PYTHON=%PM_PYTHON_EXT% + goto PACKMAN ) -@set PM_PYTHON_VERSION=2.7.14-windows-x86_32 -@set PM_PYTHON_BASE_DIR=%PM_PACKAGES_ROOT%\python -@set PM_PYTHON_DIR=%PM_PYTHON_BASE_DIR%\%PM_PYTHON_VERSION% -@set PM_PYTHON=%PM_PYTHON_DIR%\python.exe +set PM_PYTHON_VERSION=3.7.4-windows-x86_64 +set PM_PYTHON_BASE_DIR=%PM_PACKAGES_ROOT%\python +set PM_PYTHON_DIR=%PM_PYTHON_BASE_DIR%\%PM_PYTHON_VERSION% +set PM_PYTHON=%PM_PYTHON_DIR%\python.exe -@if exist "%PM_PYTHON%" goto PACKMAN -@if not exist "%PM_PYTHON_BASE_DIR%" call :CREATE_PYTHON_BASE_DIR +if exist "%PM_PYTHON%" goto PACKMAN +if not exist "%PM_PYTHON_BASE_DIR%" call :CREATE_PYTHON_BASE_DIR -@set PM_PYTHON_PACKAGE=python@%PM_PYTHON_VERSION%.cab -@for /f "delims=" %%a in ('powershell -ExecutionPolicy ByPass -NoLogo -NoProfile -File "%~dp0\generate_temp_file_name.ps1"') do @set TEMP_FILE_NAME=%%a -@set TARGET=%TEMP_FILE_NAME%.zip -@call "%~dp0fetch_file_from_s3.cmd" %PM_PYTHON_PACKAGE% "%TARGET%" -@if errorlevel 1 goto ERROR +set PM_PYTHON_PACKAGE=python@%PM_PYTHON_VERSION%.cab +for /f "delims=" %%a in ('powershell -ExecutionPolicy ByPass -NoLogo -NoProfile -File "%~dp0\generate_temp_file_name.ps1"') do set TEMP_FILE_NAME=%%a +set TARGET=%TEMP_FILE_NAME%.zip +call "%~dp0fetch_file_from_s3.cmd" %PM_PYTHON_PACKAGE% "%TARGET%" +if %errorlevel% neq 0 ( goto ERROR ) -@for /f "delims=" %%a in ('powershell -ExecutionPolicy ByPass -NoLogo -NoProfile -File "%~dp0\generate_temp_folder.ps1" -parentPath "%PM_PYTHON_BASE_DIR%"') do @set TEMP_FOLDER_NAME=%%a -@echo Unpacking Python interpreter ... -@"%SystemRoot%\system32\expand.exe" -F:* "%TARGET%" "%TEMP_FOLDER_NAME%" 1> nul -@del "%TARGET%" +for /f "delims=" %%a in ('powershell -ExecutionPolicy ByPass -NoLogo -NoProfile -File "%~dp0\generate_temp_folder.ps1" -parentPath "%PM_PYTHON_BASE_DIR%"') do set TEMP_FOLDER_NAME=%%a +echo Unpacking Python interpreter ... +"%SystemRoot%\system32\expand.exe" -F:* "%TARGET%" "%TEMP_FOLDER_NAME%" 1> nul +del "%TARGET%" :: Failure during extraction to temp folder name, need to clean up and abort -@if errorlevel 1 ( - @call :CLEAN_UP_TEMP_FOLDER - @goto ERROR +if %errorlevel% neq 0 ( + call :CLEAN_UP_TEMP_FOLDER + goto ERROR ) :: If python has now been installed by a concurrent process we need to clean up and then continue -@if exist "%PM_PYTHON%" ( - @call :CLEAN_UP_TEMP_FOLDER - @goto PACKMAN +if exist "%PM_PYTHON%" ( + call :CLEAN_UP_TEMP_FOLDER + goto PACKMAN ) else ( - @if exist "%PM_PYTHON_DIR%" ( @rd /s /q "%PM_PYTHON_DIR%" > nul ) + if exist "%PM_PYTHON_DIR%" ( rd /s /q "%PM_PYTHON_DIR%" > nul ) ) :: Perform atomic rename -@rename "%TEMP_FOLDER_NAME%" "%PM_PYTHON_VERSION%" 1> nul +rename "%TEMP_FOLDER_NAME%" "%PM_PYTHON_VERSION%" 1> nul :: Failure during move, need to clean up and abort -@if errorlevel 1 ( - @call :CLEAN_UP_TEMP_FOLDER - @goto ERROR +if %errorlevel% neq 0 ( + call :CLEAN_UP_TEMP_FOLDER + goto ERROR ) :PACKMAN :: The packman module may already be externally configured -@if defined PM_MODULE_DIR_EXT ( - @set PM_MODULE_DIR=%PM_MODULE_DIR_EXT% +if defined PM_MODULE_DIR_EXT ( + set PM_MODULE_DIR=%PM_MODULE_DIR_EXT% ) else ( - @set PM_MODULE_DIR=%PM_PACKAGES_ROOT%\packman-common\%PM_PACKMAN_VERSION% + set PM_MODULE_DIR=%PM_PACKAGES_ROOT%\packman-common\%PM_PACKMAN_VERSION% ) -@set PM_MODULE=%PM_MODULE_DIR%\packman.py +set PM_MODULE=%PM_MODULE_DIR%\packman.py -@if exist "%PM_MODULE%" goto ENSURE_7ZA +if exist "%PM_MODULE%" goto ENSURE_7ZA -@set PM_MODULE_PACKAGE=packman-common@%PM_PACKMAN_VERSION%.zip -@for /f "delims=" %%a in ('powershell -ExecutionPolicy ByPass -NoLogo -NoProfile -File "%~dp0\generate_temp_file_name.ps1"') do @set TEMP_FILE_NAME=%%a -@set TARGET=%TEMP_FILE_NAME% -@call "%~dp0fetch_file_from_s3.cmd" %PM_MODULE_PACKAGE% "%TARGET%" -@if errorlevel 1 goto ERROR +set PM_MODULE_PACKAGE=packman-common@%PM_PACKMAN_VERSION%.zip +for /f "delims=" %%a in ('powershell -ExecutionPolicy ByPass -NoLogo -NoProfile -File "%~dp0\generate_temp_file_name.ps1"') do set TEMP_FILE_NAME=%%a +set TARGET=%TEMP_FILE_NAME% +call "%~dp0fetch_file_from_s3.cmd" %PM_MODULE_PACKAGE% "%TARGET%" +if %errorlevel% neq 0 ( goto ERROR ) -@echo Unpacking ... -@"%PM_PYTHON%" -S -s -u -E "%~dp0\install_package.py" "%TARGET%" "%PM_MODULE_DIR%" -@if errorlevel 1 goto ERROR +echo Unpacking ... +"%PM_PYTHON%" -S -s -u -E "%~dp0\install_package.py" "%TARGET%" "%PM_MODULE_DIR%" +if %errorlevel% neq 0 ( goto ERROR ) -@del "%TARGET%" +del "%TARGET%" :ENSURE_7ZA -@set PM_7Za_VERSION=16.02.4 -@set PM_7Za_PATH=%PM_PACKAGES_ROOT%\7za\%PM_7ZA_VERSION% -@if exist "%PM_7Za_PATH%" goto END -@set PM_7Za_PATH=%PM_PACKAGES_ROOT%\chk\7za\%PM_7ZA_VERSION% -@if exist "%PM_7Za_PATH%" goto END +set PM_7Za_VERSION=16.02.4 +set PM_7Za_PATH=%PM_PACKAGES_ROOT%\7za\%PM_7ZA_VERSION% +if exist "%PM_7Za_PATH%" goto END +set PM_7Za_PATH=%PM_PACKAGES_ROOT%\chk\7za\%PM_7ZA_VERSION% +if exist "%PM_7Za_PATH%" goto END -@"%PM_PYTHON%" -S -s -u -E "%PM_MODULE%" pull "%PM_MODULE_DIR%\deps.packman.xml" -@if errorlevel 1 goto ERROR +"%PM_PYTHON%" -S -s -u -E "%PM_MODULE%" pull "%PM_MODULE_DIR%\deps.packman.xml" +if %errorlevel% neq 0 ( goto ERROR ) -@goto END +goto END :ERROR_MKDIR_PACKAGES_ROOT -@echo Failed to automatically create packman packages repo at %PM_PACKAGES_ROOT%. -@echo Please set a location explicitly that packman has permission to write to, by issuing: -@echo. -@echo setx PM_PACKAGES_ROOT {path-you-choose-for-storing-packman-packages-locally} -@echo. -@echo Then launch a new command console for the changes to take effect and run packman command again. -@exit /B 1 +echo Failed to automatically create packman packages repo at %PM_PACKAGES_ROOT%. +echo Please set a location explicitly that packman has permission to write to, by issuing: +echo. +echo setx PM_PACKAGES_ROOT {path-you-choose-for-storing-packman-packages-locally} +echo. +echo Then launch a new command console for the changes to take effect and run packman command again. +exit /B %errorlevel% :ERROR -@echo !!! Failure while configuring local machine :( !!! -@exit /B 1 +echo !!! Failure while configuring local machine :( !!! +exit /B %errorlevel% :CLEAN_UP_TEMP_FOLDER -@rd /S /Q "%TEMP_FOLDER_NAME%" -@exit /B +rd /S /Q "%TEMP_FOLDER_NAME%" +exit /B :CREATE_PYTHON_BASE_DIR :: We ignore errors and clean error state - if two processes create the directory one will fail which is fine -@md "%PM_PYTHON_BASE_DIR%" > nul 2>&1 -@exit /B 0 +md "%PM_PYTHON_BASE_DIR%" > nul 2>&1 +exit /B 0 :END diff --git a/Build/packman/bootstrap/fetch_file_from_s3.cmd b/Build/packman/bootstrap/fetch_file_from_s3.cmd index 61a70fab48..78934c02b1 100644 --- a/Build/packman/bootstrap/fetch_file_from_s3.cmd +++ b/Build/packman/bootstrap/fetch_file_from_s3.cmd @@ -1,9 +1,23 @@ +:: Copyright 2019 NVIDIA CORPORATION +:: +:: Licensed under the Apache License, Version 2.0 (the "License"); +:: you may not use this file except in compliance with the License. +:: You may obtain a copy of the License at +:: +:: http://www.apache.org/licenses/LICENSE-2.0 +:: +:: Unless required by applicable law or agreed to in writing, software +:: distributed under the License is distributed on an "AS IS" BASIS, +:: WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +:: See the License for the specific language governing permissions and +:: limitations under the License. + :: You need to specify as input to this command @setlocal @set PACKAGE_NAME=%1 @set TARGET_PATH=%2 -@echo Fetching %PACKAGE_NAME% from packman-bootstrap over HTTP ... +@echo Fetching %PACKAGE_NAME% ... @powershell -ExecutionPolicy ByPass -NoLogo -NoProfile -File "%~dp0fetch_file_from_s3.ps1" -sourceName %PACKAGE_NAME% ^ -output %TARGET_PATH% diff --git a/Build/packman/bootstrap/fetch_file_from_s3.ps1 b/Build/packman/bootstrap/fetch_file_from_s3.ps1 index c679ec2b4f..3b681dc36e 100644 --- a/Build/packman/bootstrap/fetch_file_from_s3.ps1 +++ b/Build/packman/bootstrap/fetch_file_from_s3.ps1 @@ -1,8 +1,24 @@ +<# +Copyright 2019 NVIDIA CORPORATION + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +#> + param( [Parameter(Mandatory=$true)][string]$sourceName=$null, [string]$output="out.exe" ) -$source = "http://packman-bootstrap.s3.amazonaws.com/" + $sourceName +$source = "http://bootstrap.packman.nvidia.com/" + $sourceName $filename = $output $triesLeft = 3 @@ -15,7 +31,7 @@ do try { - Write-Host "Connecting to S3 ..." + Write-Host "Connecting to bootstrap.packman.nvidia.com ..." $res = $req.GetResponse() if($res.StatusCode -eq "OK") { Write-Host "Downloading ..." @@ -35,13 +51,13 @@ do Write-Progress "Downloading $url" "Saving $total bytes..." -id 0 } } while ($count -gt 0) - + $triesLeft = 0 } } catch { - Write-Host "Error connecting to S3!" + Write-Host "Error downloading $source!" Write-Host $_.Exception|format-list -force } finally diff --git a/Build/packman/bootstrap/fetch_file_from_url.ps1 b/Build/packman/bootstrap/fetch_file_from_url.ps1 index 5b566f148a..cf04df2d14 100644 --- a/Build/packman/bootstrap/fetch_file_from_url.ps1 +++ b/Build/packman/bootstrap/fetch_file_from_url.ps1 @@ -1,3 +1,19 @@ +<# +Copyright 2019 NVIDIA CORPORATION + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +#> + param( [Parameter(Mandatory=$true)][string]$sourceUrl=$null, [Parameter(Mandatory=$true)][string]$output=$null diff --git a/Build/packman/bootstrap/generate_temp_file_name.ps1 b/Build/packman/bootstrap/generate_temp_file_name.ps1 index cefbda4a51..b8d31c29de 100644 --- a/Build/packman/bootstrap/generate_temp_file_name.ps1 +++ b/Build/packman/bootstrap/generate_temp_file_name.ps1 @@ -1,2 +1,18 @@ +<# +Copyright 2019 NVIDIA CORPORATION + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +#> + $out = [System.IO.Path]::GetTempFileName() Write-Host $out \ No newline at end of file diff --git a/Build/packman/bootstrap/generate_temp_folder.ps1 b/Build/packman/bootstrap/generate_temp_folder.ps1 index b24a654771..b448c23fa6 100644 --- a/Build/packman/bootstrap/generate_temp_folder.ps1 +++ b/Build/packman/bootstrap/generate_temp_folder.ps1 @@ -1,3 +1,19 @@ +<# +Copyright 2019 NVIDIA CORPORATION + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +#> + param( [Parameter(Mandatory=$true)][string]$parentPath=$null ) diff --git a/Build/packman/bootstrap/install_package.py b/Build/packman/bootstrap/install_package.py index eceabb7b04..5b56a705f3 100644 --- a/Build/packman/bootstrap/install_package.py +++ b/Build/packman/bootstrap/install_package.py @@ -1,12 +1,26 @@ +# Copyright 2019 NVIDIA CORPORATION + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + import logging import zipfile import tempfile import sys import shutil -__author__ = 'hfannar' +__author__ = "hfannar" logging.basicConfig(level=logging.WARNING, format="%(message)s") -logger = logging.getLogger('install_package') +logger = logging.getLogger("install_package") class TemporaryDirectory: @@ -23,15 +37,19 @@ def __exit__(self, type, value, traceback): def install_package(package_src_path, package_dst_path): - with zipfile.ZipFile(package_src_path, allowZip64=True) as zip_file, TemporaryDirectory() as temp_dir: + with zipfile.ZipFile( + package_src_path, allowZip64=True + ) as zip_file, TemporaryDirectory() as temp_dir: zip_file.extractall(temp_dir) # Recursively copy (temp_dir will be automatically cleaned up on exit) try: # Recursive copy is needed because both package name and version folder could be missing in # target directory: shutil.copytree(temp_dir, package_dst_path) - except OSError, exc: - logger.warning("Directory %s already present, packaged installation aborted" % package_dst_path) + except OSError as exc: + logger.warning( + "Directory %s already present, packaged installation aborted" % package_dst_path + ) else: logger.info("Package successfully installed to %s" % package_dst_path) diff --git a/Build/packman/packman b/Build/packman/packman index e6127ec983..a1da690cfa 100644 --- a/Build/packman/packman +++ b/Build/packman/packman @@ -1,9 +1,35 @@ #!/bin/bash -PM_PACKMAN_VERSION=5.14 +# Copyright 2019 NVIDIA CORPORATION + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +set -e +if echo $PM_VERBOSITY | grep -i "debug" > /dev/null ; then + set -x +else + PM_CURL_SILENT="-s -S" + PM_WGET_QUIET="--quiet" +fi +PM_PACKMAN_VERSION=6.15 + +# This is necessary for newer macOS +if [ `uname` == 'Darwin' ]; then + export LC_ALL=en_US.UTF-8 + export LANG=en_US.UTF-8 +fi # Specify where packman command exists -export PM_INSTALL_PATH=$(dirname ${BASH_SOURCE}) +export PM_INSTALL_PATH="$(dirname "${BASH_SOURCE}")" add_packages_root_to_file() { @@ -21,7 +47,7 @@ if [ -z "$PM_PACKAGES_ROOT" ]; then # Set variable permanently using .profile for this user (if exists) add_packages_root_to_file ~/.profile add_packages_root_to_file ~/.bashrc - # Set variable temporarily in this process so that the following execution will work + # Set variable temporarily in this process so that the following execution will work export PM_PACKAGES_ROOT="${HOME}/packman-repo" fi @@ -31,36 +57,71 @@ if [ ! -d "$PM_PACKAGES_ROOT" ]; then mkdir -p "$PM_PACKAGES_ROOT" fi -# The packman module may be externally configured -if [ -z "$PM_MODULE_DIR_EXT" ]; then - PM_MODULE_DIR="$PM_PACKAGES_ROOT/packman-common/$PM_PACKMAN_VERSION" -else - PM_MODULE_DIR="$PM_MODULE_DIR_EXT" -fi -export PM_MODULE="$PM_MODULE_DIR/packman.py" - -fetch_file_from_s3() +fetch_file_from_s3() { SOURCE=$1 - SOURCE_URL=http://packman-bootstrap.s3.amazonaws.com/$SOURCE + SOURCE_URL=http://bootstrap.packman.nvidia.com/$SOURCE TARGET=$2 - echo "Fetching $SOURCE from S3 ..." + echo "Fetching $SOURCE from bootstrap.packman.nvidia.com ..." if command -v wget >/dev/null 2>&1; then - wget --quiet -O$TARGET $SOURCE_URL + wget $PM_WGET_QUIET -O$TARGET $SOURCE_URL else - curl -o $TARGET $SOURCE_URL -s -S - fi + curl -o $TARGET $SOURCE_URL $PM_CURL_SILENT + fi } -# For now assume python is installed on the box and we just need to find it -if command -v python2.7 >/dev/null 2>&1; then - export PM_PYTHON=python2.7 -elif command -v python2 >/dev/null 2>&1; then - export PM_PYTHON=python2 +install_python() +{ + PLATFORM=`uname` + PROCESSOR=`uname -m` + PYTHON_VERSION=3.7.2 + + if [ $PLATFORM == 'Darwin' ]; then + PYTHON_PACKAGE=$PYTHON_VERSION-71-macos-x86_64 + elif [ $PLATFORM == 'Linux' ] && [ $PROCESSOR == 'x86_64' ]; then + # This is temporary while we haven't built a static 3.7.2 that works on CentOS and Ubuntu + PYTHON_PACKAGE=3.6.8-linux-x86_64 + elif [ $PLATFORM == 'Linux' ] && [ $PROCESSOR == 'aarch64' ]; then + PYTHON_PACKAGE=$PYTHON_VERSION-linux-aarch64-static + else + echo "Operating system not supported" + exit 1 + fi + + PYTHON_INSTALL_FOLDER="$PM_PACKAGES_ROOT/python/$PYTHON_PACKAGE" + if [ ! -d "$PYTHON_INSTALL_FOLDER" ]; then + mkdir -p "$PYTHON_INSTALL_FOLDER" + fi + + export PM_PYTHON="$PYTHON_INSTALL_FOLDER/python" + + if [ ! -f "$PM_PYTHON" ]; then + fetch_file_from_s3 "python@$PYTHON_PACKAGE.tar.gz" "/tmp/python@$PYTHON_PACKAGE.tar.gz" + if [ "$?" -eq "0" ]; then + echo "Unpacking python" + tar -xf "/tmp/python@$PYTHON_PACKAGE.tar.gz" -C "$PYTHON_INSTALL_FOLDER" + else + echo "Failed downloading the Python interpreter" + exit $? + fi + fi +} + +# Ensure python is available: +if [ -z "$PM_PYTHON_EXT" ]; then + install_python else - export PM_PYTHON=python + PM_PYTHON="$PM_PYTHON_EXT" fi +# The packman module may be externally configured +if [ -z "$PM_MODULE_DIR_EXT" ]; then + PM_MODULE_DIR="$PM_PACKAGES_ROOT/packman-common/$PM_PACKMAN_VERSION" +else + PM_MODULE_DIR="$PM_MODULE_DIR_EXT" +fi +export PM_MODULE="$PM_MODULE_DIR/packman.py" + # Ensure the packman package exists: if [ ! -f "$PM_MODULE" ]; then PM_MODULE_PACKAGE="packman-common@$PM_PACKMAN_VERSION.zip" @@ -69,7 +130,7 @@ if [ ! -f "$PM_MODULE" ]; then fetch_file_from_s3 $PM_MODULE_PACKAGE $TARGET if [ "$?" -eq "0" ]; then echo "Unpacking ..." - $PM_PYTHON -S -s -u -E "$PM_INSTALL_PATH/bootstrap/install_package.py" "$TARGET" "$PM_MODULE_DIR" + "$PM_PYTHON" -S -s -u -E "$PM_INSTALL_PATH/bootstrap/install_package.py" "$TARGET" "$PM_MODULE_DIR" rm $TARGET else echo "Failure while fetching packman module from S3!" @@ -83,7 +144,7 @@ export PM_7za_PATH="$PM_PACKAGES_ROOT/7za/$PM_7za_VERSION" if [ ! -d "$PM_7za_PATH" ]; then export PM_7za_PATH="$PM_PACKAGES_ROOT/chk/7za/$PM_7za_VERSION" if [ ! -d "$PM_7za_PATH" ]; then - $PM_PYTHON -S -s -u -E "$PM_MODULE" pull "$PM_MODULE_DIR/deps.packman.xml" + "$PM_PYTHON" -S -s -u -E "$PM_MODULE" pull "$PM_MODULE_DIR/deps.packman.xml" if [ "$?" -ne 0 ]; then echo "Failure while installing required 7za package" exit 1 @@ -94,7 +155,12 @@ fi # Generate temporary file name for environment variables: PM_VAR_PATH=`mktemp -u -t tmp.$$.pmvars.XXXXXX` -$PM_PYTHON -S -s -u -E "$PM_MODULE" "$@" --var-path="$PM_VAR_PATH" +if [ $# -ne 0 ] + then + PM_VAR_PATH_ARG=--var-path="$PM_VAR_PATH" +fi + +"$PM_PYTHON" -S -s -u -E "$PM_MODULE" "$@" $PM_VAR_PATH_ARG exit_code=$? # Export the variables if the file was used and remove the file: if [ -f "$PM_VAR_PATH" ]; then diff --git a/Build/packman/packman.cmd b/Build/packman/packman.cmd index 6b0f84008e..c7bf3dcfa0 100644 --- a/Build/packman/packman.cmd +++ b/Build/packman/packman.cmd @@ -1,40 +1,56 @@ -:: Reset errorlevel status so we are not inheriting this state from the calling process: -@call :RESET_ERROR +:: Reset errorlevel status (don't inherit from caller) [xxxxxxxxxxx] +@call :ECHO_AND_RESET_ERROR :: You can remove the call below if you do your own manual configuration of the dev machines -@call "%~dp0\bootstrap\configure.bat" -@if errorlevel 1 exit /b 1 +call "%~dp0\bootstrap\configure.bat" + +if %errorlevel% neq 0 ( exit /b %errorlevel% ) :: Everything below is mandatory -@if not defined PM_PYTHON goto :PYTHON_ENV_ERROR -@if not defined PM_MODULE goto :MODULE_ENV_ERROR +if not defined PM_PYTHON goto :PYTHON_ENV_ERROR +if not defined PM_MODULE goto :MODULE_ENV_ERROR :: Generate temporary path for variable file -@for /f "delims=" %%a in ('powershell -ExecutionPolicy ByPass -NoLogo -NoProfile ^ --File "%~dp0bootstrap\generate_temp_file_name.ps1"') do @set PM_VAR_PATH=%%a +for /f "delims=" %%a in ('powershell -ExecutionPolicy ByPass -NoLogo -NoProfile ^ +-File "%~dp0bootstrap\generate_temp_file_name.ps1"') do set PM_VAR_PATH=%%a + +if %1.==. ( + set PM_VAR_PATH_ARG= +) else ( + set PM_VAR_PATH_ARG=--var-path="%PM_VAR_PATH%" +) -@"%PM_PYTHON%" -S -s -u -E "%PM_MODULE%" %* --var-path="%PM_VAR_PATH%" -@if errorlevel 1 exit /b %errorlevel% +"%PM_PYTHON%" -S -s -u -E "%PM_MODULE%" %* %PM_VAR_PATH_ARG% +if %errorlevel% neq 0 ( exit /b %errorlevel% ) :: Marshall environment variables into the current environment if they have been generated and remove temporary file -@if exist "%PM_VAR_PATH%" ( - @for /F "usebackq tokens=*" %%A in ("%PM_VAR_PATH%") do @set "%%A" - @if errorlevel 1 goto :VAR_ERROR - @del /F "%PM_VAR_PATH%" +if exist "%PM_VAR_PATH%" ( + for /F "usebackq tokens=*" %%A in ("%PM_VAR_PATH%") do set "%%A" ) -@set PM_VAR_PATH= -@goto :eof +if %errorlevel% neq 0 ( goto :VAR_ERROR ) + +if exist "%PM_VAR_PATH%" ( + del /F "%PM_VAR_PATH%" +) +if %errorlevel% neq 0 ( goto :VAR_ERROR ) + +set PM_VAR_PATH= +goto :eof :: Subroutines below :PYTHON_ENV_ERROR @echo User environment variable PM_PYTHON is not set! Please configure machine for packman or call configure.bat. -@exit /b 1 +exit /b 1 :MODULE_ENV_ERROR @echo User environment variable PM_MODULE is not set! Please configure machine for packman or call configure.bat. -@exit /b 1 +exit /b 1 :VAR_ERROR @echo Error while processing and setting environment variables! -@exit /b 1 +exit /b 1 -:RESET_ERROR -@exit /b 0 +:ECHO_AND_RESET_ERROR +@echo off +if /I "%PM_VERBOSITY%"=="debug" ( + @echo on +) +exit /b 0 diff --git a/Build/packman/python.bat b/Build/packman/python.bat new file mode 100644 index 0000000000..ef37691b2f --- /dev/null +++ b/Build/packman/python.bat @@ -0,0 +1,21 @@ +:: Copyright 2019-2020 NVIDIA CORPORATION +:: +:: Licensed under the Apache License, Version 2.0 (the "License"); +:: you may not use this file except in compliance with the License. +:: You may obtain a copy of the License at +:: +:: http://www.apache.org/licenses/LICENSE-2.0 +:: +:: Unless required by applicable law or agreed to in writing, software +:: distributed under the License is distributed on an "AS IS" BASIS, +:: WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +:: See the License for the specific language governing permissions and +:: limitations under the License. + +@echo off +setlocal + +call "%~dp0\packman" init +set "PYTHONPATH=%PM_MODULE_DIR%;%PYTHONPATH%" +set PYTHONNOUSERSITE=1 +"%PM_PYTHON%" -u %* diff --git a/Build/packman/python.sh b/Build/packman/python.sh new file mode 100644 index 0000000000..4013ab404c --- /dev/null +++ b/Build/packman/python.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +# Copyright 2019-2020 NVIDIA CORPORATION + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +set -e + +PACKMAN_CMD="$(dirname "${BASH_SOURCE}")/packman" +if [ ! -f "$PACKMAN_CMD" ]; then + PACKMAN_CMD="${PACKMAN_CMD}.sh" +fi +source "$PACKMAN_CMD" init +export PYTHONPATH="${PM_MODULE_DIR}:${PYTHONPATH}" +export PYTHONNOUSERSITE=1 +"${PM_PYTHON}" -u "$@" diff --git a/Build/prebuild.bat b/Build/prebuild.bat index b4c60c8a9f..5022ff3274 100644 --- a/Build/prebuild.bat +++ b/Build/prebuild.bat @@ -45,7 +45,7 @@ rem Call Update Dependencies - Runs packman. call %~dp0\update_dependencies.bat %1\Falcor\dependencies.xml if errorlevel 1 exit /b 1 -%1\Externals\.packman\Python\python.exe %~dp0\patchpropssheet.py %1 %2 %falcor_backend% +%1\Externals\.packman\python\python.exe %~dp0\patchpropssheet.py %1 %2 %falcor_backend% if errorlevel 1 exit /b 1 if exist %1\Internal\internal_dependencies.xml (call %~dp0\update_dependencies.bat %1\Internal\internal_dependencies.xml) if errorlevel 1 exit /b 1 diff --git a/Build/update_dependencies.bat b/Build/update_dependencies.bat index a26157ca71..a88ff5a819 100644 --- a/Build/update_dependencies.bat +++ b/Build/update_dependencies.bat @@ -1,10 +1,10 @@ @echo off -IF [%1] == [] GOTO helpMsg +if [%1] == [] goto helpMsg set PM_DISABLE_VS_WARNING=true -call "%~dp0packman\packman.cmd " pull "%1" --platform win +call "%~dp0packman\packman.cmd " pull "%1" --platform windows-x86_64 if errorlevel 1 exit /b 1 exit /b 0 :helpMsg echo Please specify a dependency file -exit /b 1 \ No newline at end of file +exit /b 1 diff --git a/Build/update_dependencies.sh b/Build/update_dependencies.sh index cfff8e7796..8a718f22e5 100644 --- a/Build/update_dependencies.sh +++ b/Build/update_dependencies.sh @@ -1,7 +1,7 @@ #!/bin/bash +x export FALCOR_ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -$FALCOR_ROOT_DIR/packman/packman pull "$FALCOR_ROOT_DIR/dependencies.xml" --platform linux -status=$? +$FALCOR_ROOT_DIR/packman/packman pull "$FALCOR_ROOT_DIR/dependencies.xml" --platform linux-x86_64 +status=$? if [ "$status" -ne "0" ]; then echo "Error $status" exit 1 diff --git a/Docs/Getting-Started.md b/Docs/Getting-Started.md index 7c08d985b9..aca1bf2e93 100644 --- a/Docs/Getting-Started.md +++ b/Docs/Getting-Started.md @@ -7,7 +7,7 @@ ## Solution and Project Layout ### Falcor -All core framework features and code are contained within the `Falcor` project in the solution. This is a DLL project and is not executable on its own. +All core framework features and code are contained within the `Falcor` project in the solution. This is a DLL project and is not executable on its own. ### Samples Projects in the `Samples` folder (`HelloDXR`, `ModelViewer`, and `ShaderToy`) are applications using Falcor directly that demonstrate how to use some of the fundamental features and abstractions Falcor provides. @@ -16,7 +16,7 @@ Projects in the `Samples` folder (`HelloDXR`, `ModelViewer`, and `ShaderToy`) ar The `Mogwai` project is an application created using Falcor that simplifies using render graphs and provides some useful utilities. Some sample render graphs are located under its project folder: `Source/Mogwai/Data/`. ### RenderPasses -The `RenderPasses` folder contains a number of components ("Render Passes") that can be assembled to create render graphs. These components are required for some of the included sample render graphs, but will not build unless you select them manually (Right-Click -> `Build`), or use `Build Solution`. +The `RenderPasses` folder contains a number of components ("Render Passes") that can be assembled to create render graphs. These components are required for some of the included sample render graphs, but will not build unless you select them manually (Right-Click -> `Build`), or use `Build Solution`. ----------------------- ## Workflows @@ -32,7 +32,7 @@ The recommended workflow when prototyping or implementing rendering techniques i 3. Press `Ctrl+O`, or from the top menu bar, select `File` -> `Load Script` 4. Select a Render Graph (.py file) in `Source/Mogwai/Data/`. Such as `ForwardRenderer.py`. 5. Press `Ctrl+Shift+O`, or from the top menu bar, select `File` -> `Load Scene`. -6. Select a scene or model, such as `Media/Arcade/Arcade.fscene` +6. Select a scene or model, such as `Media/Arcade/Arcade.pyscene` Scenes and Render Graphs can also be loaded through drag and drop. diff --git a/Docs/Tutorials/01-Mogwai-Usage.md b/Docs/Tutorials/01-Mogwai-Usage.md index 874ede886f..10e9495749 100644 --- a/Docs/Tutorials/01-Mogwai-Usage.md +++ b/Docs/Tutorials/01-Mogwai-Usage.md @@ -49,7 +49,7 @@ Here, we'll load the Forward Renderer, located at `Source/Mogwai/Data/ForwardRen ### Loading a Scene Mogwai loads the scene specified by the script, if any. If the script did not load a scene or you want to load a different scene, open the load scene dialog by either going to `File` -> `Load Scene` or hitting `Ctrl + Shift + O`. Navigate to the location of the scene file you wish to load and select it. Alternatively, you can also drag-and-drop scene files into Mogwai. -A sample Arcade scene is included, which can be found at `Media/Arcade/Arcade.fscene`. +A sample Arcade scene is included, which can be found at `Media/Arcade/Arcade.pyscene`. ## Mogwai UI Once you have a script (and optionally, a scene) loaded, you should see something similar to diff --git a/Docs/Tutorials/04-Writing-Shaders.md b/Docs/Tutorials/04-Writing-Shaders.md index ee04cb6025..651fb169c2 100644 --- a/Docs/Tutorials/04-Writing-Shaders.md +++ b/Docs/Tutorials/04-Writing-Shaders.md @@ -96,7 +96,7 @@ mpVars["perFrameCB"]["gColor"] = float4(0, 1, 0, 1); #### Rendering a Scene Using the Shader With our scene, shader, and both the `GraphicsState` and `RasterizerState` set up, we can finally render our scene at the end of `execute()`. This is done through the `render()` method of `mpScene`, like so: ```c++ -mpScene->render(pRenderContext, mpGraphicsState.get(), mpGraphicsVars.get(), renderFlags); +mpScene->rasterize(pRenderContext, mpGraphicsState.get(), mpGraphicsVars.get(), renderFlags); ``` Your `execute()` function should now look like this, with a check for `mpScene` so we avoid accessing the scene when it isn't set: ```c++ @@ -113,11 +113,11 @@ void WireframePass::execute(RenderContext* pRenderContext, const RenderData& ren Scene::RenderFlags renderFlags = Scene::RenderFlags::UserRasterizerState; mpVars["PerFrameCB"]["gColor"] = float4(0, 1, 0, 1); - mpScene->render(pRenderContext, mpGraphicsState.get(), mpVars.get(), renderFlags); + mpScene->rasterize(pRenderContext, mpGraphicsState.get(), mpVars.get(), renderFlags); } } ``` -Using the Render Graph Editor, create a graph solely containing this pass then launch it in Mogwai. You should see a black screen as there is no scene currently loaded. Load a scene by going to `File -> Load Scene`, and you should now see the wireframe for the scene you selected. We used Arcade.fscene (located in the `Media` folder), which looks like this: +Using the Render Graph Editor, create a graph solely containing this pass then launch it in Mogwai. You should see a black screen as there is no scene currently loaded. Load a scene by going to `File -> Load Scene`, and you should now see the wireframe for the scene you selected. We used `Media/Arcade/Arcade.pyscene`, which looks like this: ![WireframePass](./images/WireframePass.png) diff --git a/Docs/Usage/Custom-Primitives.md b/Docs/Usage/Custom-Primitives.md new file mode 100644 index 0000000000..6dc3189320 --- /dev/null +++ b/Docs/Usage/Custom-Primitives.md @@ -0,0 +1,51 @@ +### [Index](../index.md) | [Usage](./index.md) | Custom Intersection Primitives + +-------- + +# Custom Primitives +Custom intersection primitives are supported when using DXR. To render them, you must: +1. Define bounding boxes containing the objects. +2. Implement intersection shaders for each type of primitive you want to render. +3. Implement additional hit shaders to handle custom primitives. + +## Adding Custom Primitives to the Scene +Currently, custom Primitives must be manually added through the `SceneBuilder`: +```c++ +auto pSceneBuilder = SceneBuilder::create(filename); +pSceneBuilder->addCustomPrimitive(0, AABB(float3(-0.5f), float3(0.5f))); +``` + +The interface contains two arguments: the first is an index to which intersection shader should handle this primitive (more details below), and the second is an AABB object describing the bounds of the primitive. + +## Shader Setup +In Falcor, hit groups for custom intersection primitives are declared separately from those handling triangle meshes. Intersection shaders themselves are separated as well, even though DXR considered them part of the hit group. + +```c++ +RtProgram::Desc desc; +// Shaders for triangle meshes +desc.addShaderLibrary("Shaders.rt.slang").setRayGen("rayGen"); +desc.addHitGroup(0, "scatterClosestHit", "").addMiss(0, "scatterMiss"); +// Shaders for custom primitives +desc.addIntersection(0, "myIntersection"); +desc.addAABBHitGroup(0, "isectScatterClosestHit", ""); +``` + +`addIntersection()` accepts an index identifier argument similar to `hitIndex` and `missIndex` in `addHitGroup()` and `addMiss()` respectively. When adding custom primitives in the `SceneBuilder`, the `typeID` selects which intersection shader will be run, corresponding to how it's declared in `addIntersection()`. For example, every custom primitive declared with `addCustomPrimitive(0, ...)`, will be handled by the intersection shader declared as `addIntersection(0, ...")`, and so on. + +**Users must also write additional hit groups (Closest Hit and/or Any Hit shaders) to handle intersection shaders, and declare them using `addAABBHitGroup()`.** This is because intersection shaders can output user-defined hit attributes to hit shaders, while hit shaders for triangle meshes always use `BuiltInTriangleIntersectionAttributes`. Using `BuiltInTriangleIntersectionAttributes` in custom intersection shaders and hit groups is valid, but you must still implement separate hit shaders at this time. **All intersection shaders and hit shaders handling custom primitives must share the same attributes struct.** + +Typically, each "AABB Hit Group" should match the behavior of its corresponding "Triangle Hit Group', as the ray index (`RayContributionToHitGroupIndex`) specified in the `TraceRay()` argument in the shader affects both hit group types the same. For example, both hit groups at ray index 0 would be scatter rays, both hit groups at index 1 would be shadow rays, etc. There are no separate miss shaders for custom intersections. + +## Writing Shaders +For details on the HLSL syntax for intersection shaders, see the DXR documentation [here](https://microsoft.github.io/DirectX-Specs/d3d/Raytracing.html#intersection-shader). + +On the shader side, user-defined Custom Primitives are represented by their world-space min and max points, their typeID, and their instance index. This is all stored in the scene data structure as `StructuredBuffer customPrimitives;`. + +The instance index differentiates between primitives of the same type. It is zero-based and counted separately for each type. With `typeID` and `instanceIdx`, users can uniquely identify custom primitives and look up additional data needed to render them. + +To access the scene data for the current custom primitive being processed, use +```c++ +gScene.customPrimitives[GeometryIndex()]; +``` + + diff --git a/Docs/Usage/Path-Tracer.md b/Docs/Usage/Path-Tracer.md index 1117eddd38..c86d804c4c 100644 --- a/Docs/Usage/Path-Tracer.md +++ b/Docs/Usage/Path-Tracer.md @@ -14,7 +14,7 @@ - Press `Ctrl + O`, then navigate to `PathTracer.py`. - Drag and drop `PathTracer.py` into the application window. - Load at startup using the Mogwai `--script` command line option. -5. Load a model or scene using one of the following methods. A sample scene is included, located at `Media/Arcade/Arcade.fscene`. Falcor can also load any format supported by Assimp. +5. Load a model or scene using one of the following methods. A sample scene is included, located at `Media/Arcade/Arcade.pyscene`. Falcor can also load any format supported by Assimp. - From the top menu bar, click `Load Scene`, then select a file. - Press `Ctrl + Shift + O` then select a file. - Drag and drop a file into the application window. @@ -26,7 +26,7 @@ The `MegakernelPathTracer` render pass implements an unbiased path tracer in DXR 1.0. Paths are created in a raygen shader and hit/miss points are reported back from the respective shader stages. The raygen shader loops over path segments up to the maximum configured path length. For each pixel on screen, `samplesPerPixel` paths are traced. -At each path vertex (black dots), a configurable number `lightSamplesPerVertex` of shadow rays (dashed lines) is traced to sampled light sources. +At each path vertex (black dots), a configurable number `lightSamplesPerVertex` of shadow rays (dashed lines) is traced to sampled light sources. The sampling of a light is done by first randomly selecting one of up to three light sampling strategies (I: analytic lights, II: env map, III: mesh lights), followed by importance sampling using the chosen strategy. For (II) and (III), multiple importance sampling (MIS) is used. @@ -36,7 +36,68 @@ Note that at the last vertex, if the scene has emissive lights and/or MIS is ena ## Configuration -### Inputs +The render script in `Source/Mogwai/Data/PathTracer.py` provides a base configuration for the path tracer. + +### Nested Dielectric Materials + +Materials can be configured in a .pyscene file to be transmissive (glass, liquids etc). +To render such dielectric materials that are overlapping (e.g., liquid in a glass), +the meshes should be overlapping to avoid numerical issues and air gaps at the boundaries. +It is also important to make transmissive materials double-sided. + +Falcor uses a stack-based method to resolve the priorities between such *nested dielectric* materials. +A material with a higher `nestedPriority` property takes precedence. + +**Note:** The default value `nestedPriority = 0` is reserved to mean the highest possible priority. + +Example of material configuration in a .pyscene file (see [Scripting](Scripting.md) for details on the Python API): + +```python +# Absorption coefficients (or extinction coefficient in absence of scattering) +# From https://cseweb.ucsd.edu/~ravir/dilution.pdf and rescaled for Falcor scene units (meters) +volume_absorption = { + 'white_wine': float3(12.28758, 16.51818, 20.30273), + 'red_wine': float3(117.13133, 251.91133, 294.33867), + 'beer': float3(11.78552, 25.45862, 58.37241), + 'bottle_wine': float3(102.68063, 168.015, 246.80438) +} + +# Configure the scene's existing materials for nested dieletrics. +glass = sceneBuilder.getMaterial("TransparentGlass") +glass.roughness = 0 +glass.metallic = 0 +glass.indexOfRefraction = 1.55 +glass.specularTransmission = 1 +glass.doubleSided = True +glass.nestedPriority = 5 + +bottle_wine = sceneBuilder.getMaterial("TransparentGlassWine") +bottle_wine.roughness = 0 +bottle_wine.metallic = 0 +bottle_wine.indexOfRefraction = 1.55 +bottle_wine.specularTransmission = 1 +bottle_wine.doubleSided = True +bottle_wine.nestedPriority = 5 +bottle_wine.volumeAbsorption = volume_absorption['bottle_wine'] + +water = sceneBuilder.getMaterial("Water") +water.roughness = 0 +water.metallic = 0 +water.indexOfRefraction = 1.33 +water.specularTransmission = 1 +water.doubleSided = True +water.nestedPriority = 1 + +ice = sceneBuilder.getMaterial("Ice") +ice.roughness = 0.1 +ice.metallic = 0 +ice.indexOfRefraction = 1.31 +ice.specularTransmission = 1 +ice.doubleSided = True +ice.nestedPriority = 4 +``` + +### Render Pass Inputs The path tracer can take either a G-buffer as input, where all geometric/material parameters are stored per pixel, or it can take a lightweight V-buffer as input. The V-buffer encodes just the hit mesh/primitive index and barycentrics. Based on those attributes, the path tracer fetches vertex and material data. @@ -49,7 +110,7 @@ When configured to use G-buffer input: - `vbuffer` is optional but needed for correct shading with dielectrics (glass), as the renderer fetches the material ID from this input. - All other inputs are required. -### Outputs +### Render Pass Outputs - All outputs are optional. - Only outputs that are connected are computed. @@ -76,6 +137,7 @@ Note: Multiple importance sampling is applied to the strategies marked MIS. - Disney isotropic diffuse. - Trowbridge-Reitz GGX specular reflection/transmission with VNDF sampling. - Diffuse/specular reflection or transmission is chosen stochastically. +- Ideal specular reflection/transmission when material roughness is near zero. ### Environment map sampling (MIS) diff --git a/Docs/Usage/Scene-Formats.md b/Docs/Usage/Scene-Formats.md index d55700e1bf..7d1e40e545 100644 --- a/Docs/Usage/Scene-Formats.md +++ b/Docs/Usage/Scene-Formats.md @@ -35,32 +35,206 @@ From assets, Falcor will import: ## Python Scene Files -You can also leverage Falcor's scripting system to set values in the scene on load that are not supported by standard file formats. These are also written in Python (using `.pyscene` file extension), but are formatted differently than normal Falcor scripts. +You can also leverage Falcor's scripting system to build scenes. This can be useful for building simple scenes from scratch as well as modifying existing assets (e.g. change material properties, add lights etc.) at load time. Python scene files are using the `.pyscene` file extension. -### Usage +### Basic Usage -The first line must be a Python comment containing only a path to the base asset to be loaded. File paths in Python scene files may be relative to the file itself, in addition to standard Falcor data directories. +When a Python scene file is executed, the script has access to a global variable `sceneBuilder` of type `SceneBuilder`. As the name suggests, the `SceneBuilder` class is used to build the scene. For a full reference see the [scripting documentation](./Scripting.md). + +A very basic Python scene file might just load an asset: + +```python +# Load asset +sceneBuilder.importScene('BistroInterior.fbx') +``` + +If nothing else is added to the script, this will behave the same as loading `BistroInterior.fbx` directly. However, we can do more. For example, let's add an environment map: + +```python +# Create environment map +envMap = EnvMap('envmap.hdr') +envMap.intensity = 2.0 +sceneBuilder.envMap = envMap +``` + +We can also change properties of existing materials in `BistroInterior.fbx`: + +```python +# Change glass material +m = sceneBuilder.getMaterial('Glass') +m.specularTransmission = 1.0 +m.indexOfRefraction = 1.52 +``` + +We can add a new light source: + +```python +# Create directional light +light = DirectionalLight('DirLight0') +light.intensity = float3(3.0, 2.0, 3.0) +light.direction = float3(0.0, -1.0, 0.0) +sceneBuilder.addLight(light) +``` + +We can add a new camera and select it: + +```python +# Create camera +camera = Camera('Camera0') +camera.position = float3(0.0, 1.0, 10.0) +camera.target = float3(0.0, 1.0, 0.0) +camera.up = float3(0.0, 1.0, 0.0) +camera.focalLength = 50.0 +sceneBuilder.addCamera(camera) +sceneBuilder.selectedCamera = camera +``` + +### Advanced Usage + +Besides modifying and extending existing scenes, Python scene files can also be used to create scenes procedurally. This can be useful for debugging, but is not meant as a replacement for the importers loading larger assets. Load performance and functionality is limited. + +#### Create Geometry + +We start by creating some geometry: + +```python +# Create triangle meshes +quadMesh = TriangleMesh.createQuad() +cubeMesh = TriangleMesh.createCube() +sphereMesh = TriangleMesh.createSphere() +``` + +This creates simple triangle meshes of standard primitives. You can also create a triangle mesh from loading a file, for example from a Wavefront OBJ: + +```python +# Create triangle mesh from a file +bunnyMesh = TriangleMesh.createFromFile('Bunny.obj') +``` + +Note that `TriangleMesh` does not know about materials and only geometry is loaded from the file. We will define materials later. + +As a last option, you can also create triangle meshes completely from scratch: ```python -# BistroInterior.fbx +# Create triangle mesh from scratch +customMesh = TriangleMesh() +normal = float3(0, 1, 0) +customMesh.addVertex(float3(-10, 0, -10), normal, float2(0, 0)) +customMesh.addVertex(float3(10, 0, -10), normal, float2(5, 0)) +customMesh.addVertex(float3(-10, 0, 10), normal, float2(0, 5)) +customMesh.addVertex(float3(10, 0, 10), normal, float2(5, 5)) +customMesh.addTriangle(2, 1, 0) +customMesh.addTriangle(1, 2, 3) ``` -The asset will be loaded and will be bound to an object called `scene`. Through this object, you have access to any script bindings accessible through Scenes. See the [scripting documentation](./Scripting.md) for a full list of functions and properties. +Each vertex has a _position_, _normal_ and _texCoord_ attribute. Triangles are defined by indexing the vertices. -Example: +#### Create Materials + +Next we need to define at least one material to use for our meshes: ```python -# BistroInterior.fbx -# Line above loads BistroInterior.fbx from the same folder as the script +# Create materials +red = Material('Red') +red.baseColor = float4(0.8, 0.1, 0.1, 0) +red.roughness = 0.3 + +green = Material('Green') +green.baseColor = float4(0.1, 0.8, 0.1, 0) +green.roughness = 0.2 +green.emissiveColor = float3(1, 1, 1) +green.emissiveFactor = 0.1 + +blue = Material('Blue') +blue.baseColor = float4(0.1, 0.1, 0.8, 0) +blue.roughness = 0.3 +blue.metallic = 1 + +emissive = Material('Emissive') +emissive.baseColor = float4(1, 1, 1, 0) +emissive.roughness = 0.2 +emissive.emissiveColor = float3(1, 1, 1) +emissive.emissiveFactor = 20 +``` -scene.setEnvMap("BistroInterior.hdr") # Set the environment map to "BistroInterior.hdr" located in the same folder as the script +We can also create materials using textures: -bottle_wine = scene.getMaterial("TransparentGlassWine") # Get a material from the scene by name +```python +# Create material with textures +floor = Material('Floor') +floor.loadTexture(MaterialTextureSlot.BaseColor, 'Arcade/Textures/CheckerTile_BaseColor.png') +floor.loadTexture(MaterialTextureSlot.Specular, 'Arcade/Textures/CheckerTile_Specular.png') +floor.loadTexture(MaterialTextureSlot.Normal, 'Arcade/Textures/CheckerTile_Normal.png') +``` -# Set material properties -bottle_wine.indexOfRefraction = 1.55 -bottle_wine.specularTransmission = 1 -bottle_wine.doubleSided = True -bottle_wine.nestedPriority = 2 -bottle_wine.volumeAbsorption = float3(0.7143, 1.1688, 1.7169) +#### Building Scene + +With some geometry and a materials at hand, we can now register meshes to the scene builder: + +```python +# Add meshes to scene builder +quadMeshID = sceneBuilder.addTriangleMesh(quadMesh, red) +cubeMeshID = sceneBuilder.addTriangleMesh(cubeMesh, emissive) +sphereMeshID = sceneBuilder.addTriangleMesh(sphereMesh, blue) +bunnyMeshID = sceneBuilder.addTriangleMesh(bunnyMesh, green) +customMeshID = sceneBuilder.addTriangleMesh(customMesh, floor) ``` + +Each call to `addTriangleMesh()` returns a new ID that uniquely identifies the mesh and assigned material. + +Next, we need to create some scene graph nodes: + +```python +# Add scene graph to scene builder +quadNodeID = sceneBuilder.addNode('Quad', Transform(scaling=2, translation=float3(-3, 1, 0), rotationEulerDeg=float3(90, 0, 0))) +cubeNodeID = sceneBuilder.addNode('Cube', Transform(scaling=float3(15, 0.2, 0.2), translation=float3(0, 1, -2))) +sphereNodeID = sceneBuilder.addNode('Sphere', Transform(scaling=1, translation=float3(3, 1, 0))) +customNodeID = sceneBuilder.addNode('Custom', Transform(scaling=1, rotationEulerDeg=float3(0, 45, 0))) +bunnyNodeID = sceneBuilder.addNode('Bunny', Transform(scaling=12, translation=float3(0.3, -0.4, 0)), customNodeID) +``` + +Each call to `addNode()` returns a new ID that uniquely identifies the node. By using the (optional) last argument in `addNode()`, we can attach nodes to a parent node and build a scene hierarchy. + +The `Transform` class is used to describe the relative transformation of the scene node. When constructing a `Transform`, we can directly pass in `translation`, `scaling`, `rotationEuler` or `rotationEulerDeg`. Alternatively we can create _look-at_ transformations using `position`, `target` and `up`. + +With a basic scene graph in place, we can attach mesh instances to it: + +```python +# Add mesh instances to scene graph +sceneBuilder.addMeshInstance(quadNodeID, quadMeshID) +sceneBuilder.addMeshInstance(cubeNodeID, cubeMeshID) +sceneBuilder.addMeshInstance(sphereNodeID, sphereMeshID) +sceneBuilder.addMeshInstance(bunnyNodeID, bunnyMeshID) +sceneBuilder.addMeshInstance(customNodeID, customMeshID) +``` + +As in the basic example, we obviously should also add a camera and at least one light source to make the scene renderable. + +```python +# Add camera +camera = Camera('Camera') +camera.position = float3(0, 0.3, 10) +camera.target = float3(0, 0.3, 0) +camera.up = float3(0, 1, 0) +camera.focalLength = 35 +sceneBuilder.addCamera(camera) + +# Add lights +light0 = DistantLight('Light0') +light0.direction = float3(1, -1, -1) +light0.intensity = float3(5, 5, 1) +light0.angle = 0.1 +sceneBuilder.addLight(light0) + +light1 = DistantLight('Light1') +light1.direction = float3(-1, -1, -1) +light1.intensity = float3(1, 5, 5) +light1.angle = 0.1 +sceneBuilder.addLight(light1) +``` + +With all of this in place, we can render our scene and get the following image: + +![Example Scene](images/ExampleScene.png) + +Additional examples of Python scene can be found in the `Media/TestScenes` folder. diff --git a/Docs/Usage/Scenes.md b/Docs/Usage/Scenes.md index db6f27e092..249e81990a 100644 --- a/Docs/Usage/Scenes.md +++ b/Docs/Usage/Scenes.md @@ -88,12 +88,12 @@ The scene can be rendered using functions from the `Scene` class. There is no lo To rasterize, use: ```c++ -void Scene::render(RenderContext* pContext, GraphicsState* pState, GraphicsVars* pVars, RenderFlags flags = RenderFlags::None); +void Scene::rasterize(RenderContext* pContext, GraphicsState* pState, GraphicsVars* pVars, RenderFlags flags = RenderFlags::None); ``` To raytrace, use: ```c++ -void Scene::raytrace(RenderContext* pContext, const std::shared_ptr& pState, const std::shared_ptr& pVars, uvec3 dispatchDims); +void Scene::raytrace(RenderContext* pContext, RtProgram* pProgram, const std::shared_ptr& pVars, uint3 dispatchDims); ``` ## Shaders diff --git a/Docs/Usage/Scripting.md b/Docs/Usage/Scripting.md index 7ba6f80bac..408cdcfd67 100644 --- a/Docs/Usage/Scripting.md +++ b/Docs/Usage/Scripting.md @@ -18,12 +18,6 @@ | `exit(errorCode=0)` | Terminate the application with the given error code. | | `cls()` | Clear the console. | -#### Global variables - -| Variable | Description | -|----------|-------------------------| -| `m` | Instance of `Renderer`. | - ## Mogwai API #### Global functions @@ -35,48 +29,39 @@ #### Global variables -| Variable | Description | -|----------|------------------------------| -| `t` | Instance of `Clock`. | -| `fc` | Instance of `FrameCapture`. | -| `vc` | Instance of `VideoCapture`. | -| `tc` | Instance of `TimingCapture`. | +| Variable | Description | +|----------|-----------------------------------------------------------------------------| +| `m` | Instance of `Renderer`. | +| `t` | Instance of `Clock`. **DEPRECATED**: Use `m.clock` instead. | +| `fc` | Instance of `FrameCapture`. **DEPRECATED**: Use `m.frameCapture` instead. | +| `vc` | Instance of `VideoCapture`. **DEPRECATED**: Use `m.videoCapture` instead. | +| `tc` | Instance of `TimingCapture`. **DEPRECATED**: Use `m.timingCapture` instead. | #### Renderer class falcor.**Renderer** -| Property | Type | Description | -|---------------|---------------|---------------------------------| -| `scene` | `Scene` | Active scene (readonly). | -| `activeGraph` | `RenderGraph` | Active render graph (readonly). | -| `ui` | `bool` | Show/hide the UI. | +| Property | Type | Description | +|-----------------|-----------------|---------------------------------| +| `scene` | `Scene` | Active scene (readonly). | +| `activeGraph` | `RenderGraph` | Active render graph (readonly). | +| `ui` | `bool` | Show/hide the UI. | +| `clock` | `Clock` | Clock. | +| `profiler` | `Profiler` | Profiler. | +| `frameCapture` | `FrameCapture` | Frame capture. | +| `videoCapture` | `VideoCapture` | Video capture. | +| `timingCapture` | `TimingCapture` | Timing capture. | | Method | Description | |-------------------------------------------------------------|-----------------------------------------------------------------| | `script(filename)` | Run a script. | | `loadScene(filename, buildFlags=SceneBuilderFlags.Default)` | Load a scene. See available build flags below. | +| `unloadScene()` | Explicitly unload the scene to free memory. | | `saveConfig(filename)` | Save the current state to a config file. | | `addGraph(graph)` | Add a render graph. | | `removeGraph(graph)` | Remove a render graph. `graph` can be a render graph or a name. | | `getGraph(name)` | Get a render graph by name. | -| `graph(name)` | **DEPRECATED:** Use `getGraph` instead. | -| `resizeSwapChain(width, height)` | **DEPRECATED:** Use global `resizeSwapChain` instead. | - -enum falcor.**SceneBuilderFlags** - -| Enum | Description | -|-------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `Default` | Use the default flags (0). | -| `RemoveDuplicateMaterials` | Deduplicate materials that have the same properties. The material name is ignored during the search. | -| `UseOriginalTangentSpace` | Use the original bitangents that were loaded with the mesh. By default, we will ignore them and use MikkTSpace to generate the tangent space. We will always generate bitangents if they are missing. | -| `AssumeLinearSpaceTextures` | By default, textures representing colors (diffuse/specular) are interpreted as sRGB data. Use this flag to force linear space for color textures. | -| `DontMergeMeshes` | Preserve the original list of meshes in the scene, don't merge meshes with the same material. | -| `BuffersAsShaderResource` | Generate the VBs and IB with the shader-resource-view bind flag. | -| `UseSpecGlossMaterials` | Set materials to use Spec-Gloss shading model. Otherwise default is Spec-Gloss for OBJ, Metal-Rough for everything else. | -| `UseMetalRoughMaterials` | Set materials to use Metal-Rough shading model. Otherwise default is Spec-Gloss for OBJ, Metal-Rough for everything else. | -| `NonIndexedVertices` | Convert meshes to use non-indexed vertices. This requires more memory but may increase performance. | - +| `resizeSwapChain(width, height)` | Resize the window/swapchain. | #### Clock @@ -95,26 +80,39 @@ class falcor.**Clock** |------------------|-----------------------------------| | `pause()` | Pause the clock. | | `play()` | Resume the clock. | -| `stop()` | Stop the clock (pause and reset) | +| `stop()` | Stop the clock (pause and reset). | | `step(frames=1)` | Step forward or backward in time. | +#### Profiler + +class falcor.**Profiler** + +| Property | Type | Description | +|-----------|--------|-----------------------------| +| `enabled` | `bool` | Enable/disable profiler. | +| `events` | `dict` | Profiler events (readonly). | + +| Method | Description | +|-----------------|----------------------------| +| `clearEvents()` | Clear the profiler events. | + #### FrameCapture The frame capture will always dump the marked graph output. You can use `graph.markOutput()` and `graph.unmarkOutput()` to control which outputs to dump. The frame counter starts at zero in Falcor (it starts by default at one in Maya, so the frame numbers may be offset by one frame). -By default, the captures frames are stored to the executable directory. This can be changed by setting `fc.outputDir`. +By default, the captures frames are stored to the executable directory. This can be changed by setting `outputDir`. **Note:** The frame counter is not advanced when time is paused. If you capture with time paused, the captured frame will be overwritten for every rendered frame. The workaround is to change the base filename between captures with `fc.capture()`, see example below. class falcor.**FrameCapture** -| Property | Type | Description | -|----------------|----------|------------------------------------------------------------------------------| -| `outputDir` | `string` | Capture output directory. | -| `baseFilename` | `string` | Capture base filename. The frameID and output name will be appended to this. | -| `ui` | `bool` | Show/hide the UI. | +| Property | Type | Description | +|----------------|--------|------------------------------------------------------------------------------| +| `outputDir` | `str` | Capture output directory. | +| `baseFilename` | `str` | Capture base filename. The frameID and output name will be appended to this. | +| `ui` | `bool` | Show/hide the UI. | | Method | Description | |----------------------------|-----------------------------------------------------------------------------| @@ -123,27 +121,26 @@ class falcor.**FrameCapture** | `addFrames(graph, frames)` | Add a list of frames to capture for the given graph. | | `print()` | Print the requested frames to capture for all available graphs. | | `print(graph)` | Print the requested frames to capture for the specified graph. | -| `frames(graph, frames)` | **DEPRECATED:** Use `addFrames` instead. | **Example:** *Capture list of frames with clock running and then exit* ```python -t.exitFrame(101) -fc.outputDir = "../../../Output" -fc.baseFilename = "Mogwai" -fc.addFrames(m.activeGraph, [20, 50, 100]) +m.clock.exitFrame = 101 +m.frameCapture.outputDir = "../../../Output" +m.frameCapture.baseFilename = "Mogwai" +m.frameCapture.addFrames(m.activeGraph, [20, 50, 100]) ``` **Example:** *Capture frames with clock paused and then exit* ```python -t.pause() -fc.outputDir = "../../../Output" +m.clock.pause() +m.frameCapture.outputDir = "../../../Output" frames = [20, 50, 100] for i in range(101): renderFrame() if i in frames: - fc.baseFilename = f"Mogwai-{i:04d}" - fc.capture() + m.frameCapture.baseFilename = f"Mogwai-{i:04d}" + m.frameCapture.capture() exit() ``` @@ -157,15 +154,15 @@ enum falcor.**Codec** class falcor.**VideoCapture** -| Property | Type | Description | -|----------------|----------|------------------------------------------------------------------| -| `outputDir` | `string` | Capture output directory. | -| `baseFilename` | `string` | Capture base filename. The output name will be appended to this. | -| `ui` | `bool` | Show/hide the UI. | -| `codec` | `Codec` | Video codec (`Raw`, `H264`, `HVEC`, `MPEG2`, `MPEG4`). | -| `fps` | `int` | Video frame rate. | -| `bitrate` | `float` | Video bitrate in Mpbs. | -| `gopSize` | `int` | Video GOP size. | +| Property | Type | Description | +|----------------|---------|------------------------------------------------------------------| +| `outputDir` | `str` | Capture output directory. | +| `baseFilename` | `str` | Capture base filename. The output name will be appended to this. | +| `ui` | `bool` | Show/hide the UI. | +| `codec` | `Codec` | Video codec (`Raw`, `H264`, `HVEC`, `MPEG2`, `MPEG4`). | +| `fps` | `int` | Video frame rate. | +| `bitrate` | `float` | Video bitrate in Mpbs. | +| `gopSize` | `int` | Video GOP size. | | Method | Description | |----------------------------|-------------------------------------------------------------------------------------------------------| @@ -173,18 +170,17 @@ class falcor.**VideoCapture** | `addRanges(graph, ranges)` | Add a list of frame ranges to capture for a given graph. `ranges` is a list of `(start, end)` tuples. | | `print()` | Print the requested ranges to capture for all available graphs. | | `print(graph)` | Print the requested ranges to capture for the specified graph. | -| `ranges(graph, ranges)` | **DEPRECATED:** Use `addRanges` instead. | Example: ```python # Video Capture -vc.outputDir = "." -vc.baseFilename = "Mogwai" -vc.codec = Codec.H264 -vc.fps = 60 -vc.bitrate = 4.0 -vc.gopSize = 10 -vc.addRanges(m.activeGraph, [[30, 300]]) +m.videoCapture.outputDir = "." +m.videoCapture.baseFilename = "Mogwai" +m.videoCapture.codec = Codec.H264 +m.videoCapture.fps = 60 +m.videoCapture.bitrate = 4.0 +m.videoCapture.gopSize = 10 +m.videoCapture.addRanges(m.activeGraph, [[30, 300]]) ``` #### TimingCapture @@ -198,7 +194,7 @@ class falcor.**TimingCapture** Example: ```python # Timing Capture -tc.captureFrameTime("timecapture.csv") +m.timingCapture.captureFrameTime("timecapture.csv") ``` ### Core API @@ -222,9 +218,9 @@ enum falcor.**ResourceFormat** class falcor.**RenderGraph** -| Property | Type | Description | -|----------|----------|---------------------------| -| `name` | `string` | Name of the render graph. | +| Property | Type | Description | +|----------|-------|---------------------------| +| `name` | `str` | Name of the render graph. | | Method | Description | |--------------------------------|------------------------------------------------------------------------------------| @@ -243,12 +239,6 @@ class falcor.**RenderGraph** #### RenderPass -class falcor.**RenderPass** - -| Method | Description | -|--------------------------|-----------------------------------------------------------| -| `RenderPass(name, dict)` | **DEPRECATED:** Use global `createPass` function instead. | - #### Texture class falcor.**Texture** @@ -267,51 +257,74 @@ class falcor.**Texture** |---------------|-----------------------------------------------| | `data(index)` | Returns raw data of given sub resource index. | +#### AABB + +class falcor.**AABB** + +| Property | Type | Description | +|------------|----------|-------------------------------------------| +| `minPoint` | `float3` | Minimum point. | +| `maxPoint` | `float3` | Maximum point. | +| `valid` | `bool` | True if AABB is valid (readonly). | +| `center` | `float3` | Center point (readonly). | +| `extent` | `float3` | Extents (readonly). | +| `area` | `float` | Total area (readonly). | +| `volume` | `float` | Total volume (readonly). | +| `radius` | `float` | Radius of an enclosing sphere (readonly). | + +| Method | Description | +|-------------------|-----------------------------------| +| `invalidate()` | Invalidate the AABB. | +| `include(p)` | Include a point in the AABB. | +| `include(b)` | Include another AABB in the AABB. | +| `intersection(b)` | Intersect with another AABB. | + ### Scene API #### SceneRenderSettings class falcor.**SceneRenderSettings** -| Property | Type | Description | -|---------------------|--------|-------------------------------------------------------| -| `useEnvLight` | `bool` | Enable/disable distant lighting from environment map. | -| `useAnalyticLights` | `bool` | Enable/disable lighting from analytic lights. | -| `useEmissiveLights` | `bool` | Enable/disable lighting from emissive lights. | +| Property | Type | Description | +|---------------------|--------|----------------------------------------------------| +| `useEnvLight` | `bool` | Enable/disable lighting from environment map. | +| `useAnalyticLights` | `bool` | Enable/disable lighting from analytic lights. | +| `useEmissiveLights` | `bool` | Enable/disable lighting from emissive lights. | +| `useVolumes` | `bool` | Enable/disable rendering of heterogeneous volumes. | #### Scene class falcor.**Scene** -| Property | Type | Description | -|------------------|-----------------------|--------------------------------------------------| -| `animated` | `bool` | Enable/disable scene animations. | -| `renderSettings` | `SceneRenderSettings` | Settings to determine how the scene is rendered. | -| `camera` | `Camera` | Camera. | -| `cameraSpeed` | `float` | Speed of the interactive camera. | -| `envMap` | `EnvMap` | Environment map. | -| `materials` | `list(Material)` | List of materials | +| Property | Type | Description | +|------------------|-----------------------|---------------------------------------------------| +| `stats` | `dict` | Dictionary containing scene stats. | +| `bounds` | `AABB` | World space scene bounds (readonly). | +| `animated` | `bool` | Enable/disable scene animations. | +| `loopAnimations` | `bool` | Enable/disable globally looping scene animations. | +| `renderSettings` | `SceneRenderSettings` | Settings to determine how the scene is rendered. | +| `camera` | `Camera` | Camera. | +| `cameraSpeed` | `float` | Speed of the interactive camera. | +| `envMap` | `EnvMap` | Environment map. | +| `animations` | `list(Animation)` | List of animations. | +| `cameras` | `list(Camera)` | List of cameras. | +| `lights` | `list(Light)` | List of lights. | +| `materials` | `list(Material)` | List of materials. | +| `volumes` | `list(Volume)` | List of volumes. | | Method | Description | |--------------------------------------|--------------------------------------------------------| -| `animate(enable)` | **DEPRECATED:** Use `animated` instead. | -| `animateCamera(enabled)` | **DEPRECATED:** Use `animated` on `Camera` instead. | -| `animateLight(index, enabled)` | **DEPRECATED:** Use `animated` on `Light` instead. | | `setEnvMap(filename)` | Load an environment map from an image. | | `getLight(index)` | Return a light by index. | | `getLight(name)` | Return a light by name. | -| `light(index)` | **DEPRECATED:** Use `getLight` instead. | -| `light(name)` | **DEPRECATED:** Use `getLight` instead. | | `getMaterial(index)` | Return a material by index. | | `getMaterial(name)` | Return a material by name. | -| `material(index)` | **DEPRECATED:** Use `getMaterial` instead. | -| `material(name)` | **DEPRECATED:** Use `getMaterial` instead. | +| `getVolume(index)` | Return a volume by index. | +| `getVolume(name)` | Return a volume by name. | | `addViewpoint()` | Add current camera's viewpoint to the viewpoint list. | | `addViewpoint(position, target, up)` | Add a viewpoint to the viewpoint list. | | `removeViewpoint()` | Remove selected viewpoint. | | `selectViewpoint(index)` | Select a specific viewpoint and move the camera to it. | -| `viewpoint()` | **DEPRECATED:** Use `addViewpoint` instead. | -| `viewpoint(index)` | **DEPRECATED:** Use `selectViewpoint` instead. | #### Camera @@ -319,7 +332,7 @@ class falcor.**Camera** | Property | Type | Description | |------------------|----------|----------------------------------------------| -| `name` | `str` | Name of the camera (readonly). | +| `name` | `str` | Name of the camera. | | `animated` | `bool` | Enable/disable camera animation. | | `aspectRatio` | `float` | Image aspect ratio. | | `focalLength` | `float` | Focal length in millimeters. | @@ -348,13 +361,13 @@ class falcor.**EnvMap** enum falcor.**MaterialTextureSlot** -`BaseColor`, `Specular`, `Emissive`, `Normal`, `Occlusion`, `SpecularTransmission` +`BaseColor`, `Specular`, `Emissive`, `Normal`, `Occlusion`, `SpecularTransmission`, `Displacement` class falcor.**Material** | Property | Type | Description | |------------------------|----------|-------------------------------------------------------| -| `name` | `str` | Name of the material (readonly). | +| `name` | `str` | Name of the material. | | `baseColor` | `float4` | Base color (linear RGB) and opacity. | | `specularParams` | `float4` | Specular parameters (occlusion, roughness, metallic). | | `roughness` | `float` | Roughness (0 = smooth, 1 = rough). | @@ -373,56 +386,222 @@ class falcor.**Material** | `clearTexture(slot)` | Clears one of the texture slots. | | `loadTexture(slot, filename, useSrgb=True)` | Load one of the texture slots from a file. | +#### Grid + +class falcor.**Grid** + +| Property | Type | Description | +|--------------|---------|-------------------------------------------------------| +| `voxelCount` | `int` | Total number of active voxels in the grid (readonly). | +| `minIndex` | `int3` | Minimum index stored in the grid (readonly). | +| `maxIndex` | `int3` | Maximum index stored in the grid (readonly). | +| `minValue` | `float` | Minimum value stored in the grid (readonly). | +| `maxValue` | `float` | Maximum value stored in the grid (readonly). | + +| Method | Description | +|-----------------|--------------------------------------------------------| +| `getValue(ijk)` | Access the value of a voxel in the grid (index space). | + +| Static method | Description | +|--------------------------------------------------------------|---------------------------------------------| +| `createSphere(radius, voxelSize, blendRange=2.0)` | Create a sphere grid. | +| `createBox(width, height, depth, voxelSize, blendRange=2.0)` | Create a box grid. | +| `createFromFile(filename, gridname)` | Create a grid from an OpenVDB/NanoVDB file. | + +#### Volume + +enum falcor.Volume.**GridSlot** + +`Density`, `Emission` + +enum falcor.Volume.**EmissionMode** + +`Direct`, `Blackbody` + +class falcor.**Volume** + +| Property | Type | Description | +|-----------------------|----------------|---------------------------------------------------------| +| `name` | `str` | Name of the volume. | +| `gridFrame` | `int` | Current frame in the grid sequence. | +| `gridFrameCount` | `int` | Total number of frames in the grid sequence (readonly). | +| `densityGrid` | `Grid` | Density grid. | +| `densityScale` | `float` | Density scale factor. | +| `emissionGrid` | `Grid` | Emission grid. | +| `emissionScale` | `float` | Emission scale factor. | +| `albedo` | `float3` | Scattering albedo. | +| `anisotropy` | `float` | Phase function anisotropy (g). | +| `emissionMode` | `EmissionMode` | Emission mode (Direct, Blackbody). | +| `emissionTemperature` | `float` | Emission base temperature (K). | + +| Method | Description | +|-----------------------------------------------|-------------------------------------------------------------------------------------| +| `loadGrid(slot, filename, gridname)` | Load a grid slot from an OpenVDB/NanoVDB file. | +| `loadGridSequence(slot, filenames, gridname)` | Load a grid slot from a sequence of OpenVDB/NanoVDB files. | +| `loadGridSequence(slot, path, gridname)` | Load a grid slot from a sequence of OpenVDB/NanoVDB files contained in a directory. | + #### Light class falcor.**Light** | Property | Type | Description | |-------------|----------|---------------------------------| -| `name` | `str` | Name of the light (readonly). | +| `name` | `str` | Name of the light. | | `active` | `bool` | Enable/disable light. | | `animated` | `bool` | Enable/disable light animation. | -| `color` | `float3` | Color of the light. | -| `intensity` | `float` | Intensity of the light. | +| `intensity` | `float3` | Intensity of the light. | + +class falcor.**PointLight** : falcor.**Light** + +| Property | Type | Description | +|-----------------|----------|----------------------------------------| +| `position` | `float3` | Position of the light in world space. | +| `direction` | `float3` | Direction of the light in world space. | +| `openingAngle` | `float` | Opening half-angle in radians. | +| `penumbraAngle` | `float` | Penumbra half-angle in radians. | -class falcor.**DirectionalLight** +class falcor.**DirectionalLight** : falcor.**Light** | Property | Type | Description | |-------------|----------|----------------------------------------| -| `name` | `str` | Name of the light (readonly). | -| `color` | `float3` | Color of the light. | -| `intensity` | `float` | Intensity of the light. | | `direction` | `float3` | Direction of the light in world space. | -class falcor.**DistantLight** +class falcor.**DistantLight** : falcor.**Light** | Property | Type | Description | |-------------|----------|-----------------------------------------------| -| `name` | `str` | Name of the light (readonly). | -| `color` | `float3` | Color of the light. | -| `intensity` | `float` | Intensity of the light. | | `direction` | `float3` | Direction of the light in world space. | | `angle` | `float` | Half-angle subtended by the light in radians. | -class falcor.**PointLight** +class falcor.**AnalyticAreaLight** : falcor.**Light** -| Property | Type | Description | -|-----------------|----------|----------------------------------------| -| `name` | `str` | Name of the light (readonly). | -| `color` | `float3` | Color of the light. | -| `intensity` | `float` | Intensity of the light. | -| `position` | `float3` | Position of the light in world space. | -| `direction` | `float3` | Direction of the light in world space. | -| `openingAngle` | `float` | Opening half-angle in radians. | -| `penumbraAngle` | `float` | Penumbra half-angle in radians. | +class falcor.**RectLight** : falcor.**AnalyticAreaLight** + +class falcor.**DiscLight** : falcor.**AnalyticAreaLight** + +class falcor.**SphereLight** : falcor.**AnalyticAreaLight** + +#### Transform + +class falcor.**Transform** + +| Property | Type | Description | +|--------------------|------------|---------------------------------------------| +| `translation` | `float3` | Translation. | +| `rotationEuler` | `float3` | Euler rotation angles around XYZ (radians). | +| `rotationEulerDeg` | `float3` | Euler rotation angles around XYZ (degrees). | +| `scaling` | `float3` | Scaling. | +| `matrix` | `float4x4` | Transformation matrix (readonly). | + +| Method | Description | +|--------------------------------|----------------------------------| +| `lookAt(position, target, up)` | Set up a look-at transformation. | + +#### Animation + +enum falcor.Animation.**InterpolationMode** + +`Linear`, `Hermite` + +enum falcor.Animation.**Behavior** + +`Constant`, `Linear`, `Cycle`, `Oscillate` + +class falcor.**Animation** + +| Property | Type | Description | +|------------------------|---------------------|--------------------------------------------------------------------------| +| `name` | `str` | Name of the animation (readonly). | +| `nodeID` | `int` | Animated scene graph node (readonly). | +| `duration` | `float` | Duration in seconds (readonly). | +| `interpolationMode` | `InterpolationMode` | Interpolation mode (linear, hermite). | +| `preInfinityBehavior` | `Behavior` | Behavior before the first keyframe (constant, linear, cycle, oscillate). | +| `postInfinityBehavior` | `Behavior` | Behavior after the last keyframe (constant, linear, cycle, oscillate). | +| `enableWarping` | `bool` | Enable/disable warping, i.e. interpolating from last to first keyframe. | + +| Method | Description | +|--------------------------------|----------------------------------------------| +| `addKeyframe(time, transform)` | Add a transformation keyframe at given time. | + +#### TriangleMesh + +class falcor.TriangleMesh.**Vertex** + +| Field | Type | Description | +|------------|----------|----------------------------| +| `position` | `float3` | Vertex position. | +| `normal` | `float3` | Vertex normal. | +| `texCoord` | `float2` | Vertex texture coordinate. | + +class falcor.**TriangleMesh** + +| Property | Type | Description | +|------------|----------------|------------------------------| +| `name` | `str` | Name of the triangle mesh. | +| `vertices` | `list(Vertex)` | List of vertices (readonly). | +| `indices` | `list(int)` | List of indices (readonly). | + +| Method | Description | +|-----------------------------------------|-----------------------------------------------------| +| `addVertex(position, normal, texCoord)` | Add a vertex to the mesh. Returns the vertex index. | +| `addTriangle(i0, i1, i2)` | Add a triangle to the mesh. | + +| Class Method | Description | +|------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------| +| `createQuad(size=1)` | Creates a quad mesh, centered at the origin with normal pointing in positive Y direction. | +| `createCube(size=1)` | Creates a cube mesh, centered at the origin. | +| `createSphere(radius=1, segmentsU=32, segmentsV=16)` | Creates a UV sphere mesh, centered at the origin with poles in positive/negative Y direction. | +| `createFromFile(filename,smoothNormals=False)` | Creates a triangle mesh from a file. If no normals are defined in the file, `smoothNormals` can be used generate smooth instead of facet normals. | + +#### SceneBuiler + +enum falcor.**SceneBuilderFlags** + +| Enum | Description | +|-----------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `Default` | Use the default flags (0). | +| `DontMergeMaterials` | Don't merge materials that have the same properties. Use this option to preserve the original material names. | +| `UseOriginalTangentSpace` | Use the original bitangents that were loaded with the mesh. By default, we will ignore them and use MikkTSpace to generate the tangent space. We will always generate bitangents if they are missing. | +| `AssumeLinearSpaceTextures` | By default, textures representing colors (diffuse/specular) are interpreted as sRGB data. Use this flag to force linear space for color textures. | +| `DontMergeMeshes` | Preserve the original list of meshes in the scene, don't merge meshes with the same material. | +| `UseSpecGlossMaterials` | Set materials to use Spec-Gloss shading model. Otherwise default is Spec-Gloss for OBJ, Metal-Rough for everything else. | +| `UseMetalRoughMaterials` | Set materials to use Metal-Rough shading model. Otherwise default is Spec-Gloss for OBJ, Metal-Rough for everything else. | +| `NonIndexedVertices` | Convert meshes to use non-indexed vertices. This requires more memory but may increase performance. | +| `Force32BitIndices` | Force 32-bit indices for all meshes. By default, 16-bit indices are used for small meshes. | +| `RTDontMergeStatic` | For raytracing, don't merge all static meshes into single pre-transformed BLAS. | +| `RTDontMergeDynamic` | For raytracing, don't merge all dynamic meshes with identical transforms into single BLAS. | + +class falcor.**SceneBuilder** + +| Property | Type | Description | +|------------------|-----------------------|--------------------------------------------------| +| `flags` | `SceneBuilderFlags` | Scene builder flags (readonly). | +| `renderSettings` | `SceneRenderSettings` | Settings to determine how the scene is rendered. | +| `materials` | `list(Material)` | List of materials (readonly). | +| `volumes` | `list(Volume)` | List of volumes (readonly). | +| `lights` | `list(Light)` | List of lights (readonly). | +| `cameras` | `list(Camera)` | List of cameras (readonly). | +| `animations` | `list(Animation)` | List of animations (readonly). | +| `envMap` | `EnvMap` | Environment map. | +| `selectedCamera` | `Camera` | Default selected camera. | +| `cameraSpeed` | `float` | Speed of the interactive camera. | -class falcor.**AnalyticAreaLight** +| Method | Description | +|-------------------------------------------------|-----------------------------------------------------------------------------------------------------------------| +| `importScene(filename, dict, instances)` | Load a scene from an asset file. `dict` contains optional data. `instances` is an optional list of `Transform`. | +| `addTriangleMesh(triangleMesh, material)` | Add a triangle mesh to the scene and return its ID. | +| `addMaterial(material)` | Add a material and return its ID. | +| `getMaterial(name)` | Return a material by name. The first material with matching name is returned or `None` if none was found. | +| `loadMaterialTexture(material, slot, filename)` | Request loading a material texture asynchronously. Use `Material.loadTexture` for synchronous loading. | +| `addVolume(volume)` | Add a volume and return its ID. | +| `getVolume(name)` | Return a volume by name. The first volume with matching name is returned or `None` if none was found. | +| `addLight(light)` | Add a light and return its ID. | +| `addCamera(camera)` | Add a camera and return its ID. | +| `addAnimation(animation)` | Add an animation. | +| `createAnimation(animatable, name, duration)` | Create an animation for an animatable object. Returns the new animation or `None` if one already exists. | +| `addNode(name, transform, parent)` | Add a node and return its ID. | +| `addMeshInstance(nodeID, meshID)` | Add a mesh instance. | -| Property | Type | Description | -|-------------|----------|-------------------------------| -| `name` | `str` | Name of the light (readonly). | -| `color` | `float3` | Color of the light. | -| `intensity` | `float` | Intensity of the light. | ### Render Passes diff --git a/Docs/Usage/images/ExampleScene.png b/Docs/Usage/images/ExampleScene.png new file mode 100644 index 0000000000000000000000000000000000000000..1332f5a4ec19cb99747fdd52e8d2a335a0e756f9 GIT binary patch literal 707773 zcmeFYXIE3-^FEAS5ot=3CLo|x5$Vm3bP$medQqw-^cEl#3rep_N01Hy5+Q^hP+E`x zp@jgUBP~Ee4=r%>eei!3_wTHA*4|IfTIbBnx#pUgYe&C+rG1C?J}nIm%^h7G4PzRb zYZu$ggEucN@y1FPYT63YJ9St|-6Bfr4e7*Ia-|3J4`E_vka=~-!B4aDSJ(m4$UVsSTwGk2^ z`rk)>Bia{bHhP+TgInHse_clqk?-&d%Oo}sZ>G4C`q zBC>)s!b-oE>19B5qE<=OnL1GwS)C+@68-{ao&shURwjNT6LWqE4{!m}^Ji4E%hqkg zi(%rWfQ<&gnev(?`S_&}6SR3<2qq$NWZ5Yo8C31=!pwIx&(GhyawDP~I#5xV_4Qqt z79jp6Lfiq{HimVI$ao*29o19Pq+OzZRPZ71a0snU-?L}DHn=E8+|A@B89;Q@6U4Nm zG^QB^aZ?Q2-a|ZO3R=;y={&wfNqQ|M1IDgMc|MsHgfit*16L@~5!#{BQP8ht$2`!Z zjKK3&rHqY&`DE}zwK=)7e%>W*1{%AUk{=EBnQ+!n?V-=B`7qc#KS!^Y9p0(2A+IfL zEDSDYv>gJEiOyR6&mmD>H%H?Y6Q0>HQcc&CE+n5T@aC4yZmRw6^;_nE#9uLqE*7kc z1-T(0+e`;7uOaXFgJOLR!FE%{kZ<&d-J3D{*I%wt{TZ{_BAuC49}3zXxL0~F zI?Oj>vVp+jrS;{2qfA%F={H=6nJSy=WmJW!%4)#sHw1=0=57OOFN)k>Jahf7NPaVu zY;k_SRrxoSu^jjnS|DYAs|+VF!OI>h@jzq&mJPT%E7e#Ab=k_x(_Fb7t*rkfRq>V= zgApR+T0zj|e10ZX-rC^3-NpmPTjqp^JIf~tE`G3GXFaX;UBJs3xyx^80{@^}g3E)g zH_|4>lMNQEIt5=Wh%69K6?s;+H+@pE(m|<9wu^um*tXZ`?dw&TdL9jlzL$Hc{5P1= z8)(Cy9vEsUcOkAY(kyCmT$ylBrrBR1aZ{M(`O-> zYj{{Vfn8s?$AN(wIxat?U8;0`eS=F|R1M0J(<3LNK8n`DwB3|{2@q51|7t!>;SK+m zG)UJYn#HBfK&Acpiq|i?2F;6mIVDZoFIF2-W;OFUZdja`26>7$yTPYu@B9z{P^3E2 ztz;p^-a$o8WWlo5<^F!s$`3?q>Hd8F_ja1!w`G=*1Zcgfq^9IW911F*l5}<@-g+vp zYLV{V(zbVY>8lJv+9$ks&c($>Zv-8n?wMv!>ia=5qrR$fpG6#&@{_h|FJ1i|IAmL; zh|0S3UAtIHrL*ZDpf=VMxsY>=i1GgpwQE%M$1+c-`1Z_m4D~aLzfhDDHIqT{6)Zr? zoR_==L(pHINY|18?i;x*U766$;AQ%jXC@^>f9*28bIkgz_*^xYrD_IQ;#WGJ@V`c! zX&c3BFaBT#n*A4`W}Io3o9S0g`$amK#gf=3T@$zUGS7;Q6U}U4HLZYA-r7-o?(bAk zC_wh6!WY&upP5&gHkii2?*Dj;r=Lc%-A9;NL-kY<&8eWVtsa6G8@B@SR5;D`8a2i# zH^1zLnm^6fi(qbV>rFvmIy5rMgjH61syqsOHgLXldVt@}`8D;8KW&0N$H>n%5$m|v zq-RD=>9o_H@@7V{N+tK#rDNt}H6EV6I{Y@zn=ZfYb^&gIOg8n3n)jKwZzSHvu=lnK) zcP9`b)@XOr4AjFiZTA+luI9d}dgvDop|q;S6?Xr@pvYYt^`|%P&;-t)QT2Y1zKwjw z^oF#CgNAJLMeP0UZmP?|>XBL_Q~!+h18&!IAUD*AjNA=x4Iz$8D-TF<^9u#nc8F@W zX`I5yc{k64qJONgJ)r5nJr|c=Z`fZ?YWM za}3Nhf=f*Qu~o}4FB#&@IghUlrML7ca)8ccV$bfeG{1NLQPJA~Oy2z#WNKq6h-Xsx zrze`mB{(1eB%ea9W_MTR&C&yGl7BJ14e+aSn3k=qTjKbyl8QaDoGL7han`NRG~*7^ zYF{7~l+1gSkZ^JCAh4h7&a`cnhXNF_!4P2s8K3|wAoGb(~L`QW#J}vJ#GL|w%=1Gr01b#p~O+&~Q zjHucE?vy)r-6>OQNp_z%whfHDw~GfhkRKy?+~U)rwnCRVG?B;9x+YRFcZ z9}L)4>3fmhir-uMS>L_fJ^wd3eOJbgW&5A7HMhaWxIiC6$ z7OIf%0%*zk7!aX98Xu8GG=6J~Tji5W9NtaHH0^-z^*yQLapRco^?y3Qk4RHcBLc6}N5{1mL~0I9n2JDK z@ib*pS44wb)5?Pz`lIbqX<@HlgJ=Wn=a9YGfQQ{VRZ`$4Qf&XtUpx^pE|C9{8qAw) zwTJ5lt^vL94d+JL#ZBD}#)C{U$4|!s7NaapRhFNabLH8hxs&mxtL%jASrnnvt{q?B01V78q=e$qmDliLVkZ(%?@uMyTjzzsm2ck-X@M~zPxyB=ArlIiEu}Fg3#Ol zndhn$8zBPM^Y*XzZ{J5_Hzm>)%`m}i20%a&P>|_N$Nvi%Fy;ujYz=bD|H2#xB{I zncAlW&{=j(@+0=aouOIHpMVl(C_|Yke}WHXaOx7yU@Y{qR6*d#)>+f;e?(5~EjyIo zqf!M|8&d8x9*FwaxG;8XEW$-TAZGYuaDie4X%*3~C-u+t}^5kTMzP>8zyXq(=R}tLy^SIlc zc*Vi9vGn>$>)*HOTVhfbLh}vl`16OS-$Hz8E&w|>^0&%$c@dnowU=}YBJ+@|v`A4~ z#`#p9zn7NYeQl@zyT=N*X7J2#%dnrP2RIrUwPg0NJhVk-Zpini&*s4`vf^8Yp-JOx z8FUj}2uum*ne>Y~6T!apVba)Wl$;>GN7Jus9%Nu~`k9q_nwk!15CPGo~G(d2&4C+oXIo-N!9D+N}y^S)j= z{W(jdSQAegO^}qU)u5jhaxEuw)z$rZ`%689c?@hWVYuh4T zO`VMbC1oqNd*Sk>bJt zx0>T2p!i*t`XDsj4sBpA<)!7PN`&E~Ulk65!`xX8^sDaGyi~2wY}gOAFJX6O^VZC# z7ULd)%HwnRnB^(plT2`ht&KTVP~KQgOMgCd)%-E0zGG2)AsqH;@H;+Vd9 zF@2j^8m(F(l3za%_ z6TVI}*VyR>8Qn-uQaLRz6D!+u;8RFsP;;ddqjc6X(8}YPI6;72)ckt98hoA<2&dLK zZZKI{YaRkA8-U>;AQzsgxyOagGo@i?J)HSWKV;;8G1O|i?sGmw_ZBqgdtDz@DJOX& zZ9ctuh1dDjhA!w2%F*<#T7&k zNNCMw^0=SrH-E1m+bw^&o~zIz5S-1P@9l~N3)iiBou1a+)y2DL^Sn2$hb>dT$CU0m z&G!45evAayR};%3+o}T`U{Ns&ES2^m@4|6*k`H!rUWhN3UZ+uZU3E!Qia&^I=4X;K zY0X1TDyXhMsMxK?pZ4CkT6R3Kh=W|QYPNR^@%*hU`J6sCNl%_G2eP#LtDa=p~~lIdQ%fDu;9 z-f}q&0BeHvOI`mjwk#TgZVB}neCykzgUP-qMBfsv^b2-6)jQM$Un(ulzlaFo6a+qIy$T%Ld?6IFS`Jy*j7a#ZbQLdFibn48~4s0jxD!pKF>Zm*w9p56^n z`XB8M)A-HIr&_88ex38j9CkA+vo-F|sNWbHJl_gi7<03NU5B_JSIYaP&vZK~a#=)Z zExO0nKslWS?;(W6*i_xlLbb9m<6W#bi}e>xR+3hoJCgR_xu#1oTCbF?#SpZMAhNuo zl7ZAe9(YN*w>wRI9?+!16()STmP8Hd#(h2@%}`Q1?;Fhu^Oz~9ZIDdXw(H6qH;mScz0WmB zDz}qH2OQs6LJQG!$qm8{IIJQED8wr#d>(US)X_g?TC-oGP4DzCmH3(2JjC&TMjO!n z&A~c0%*(>W8|#MCD)&z3Uw@RSO88=J&P|E3vuLh7*)9uDpZBwWwa`))kV8-DdU8eB zKBcIJ4ssy%$Wn=|93xgG_Gf^@-0{6}X`#QFiS_0sSo^$NLKUqi(?r@)uYibIVpg}z zbvNDglxBU3Jbn?xjWPRgM<;*ae8dCEy$c(7(aTelTD~Qf+S~P#pKO;{GaOc39g^es zA|Th>v|E;P6D-^8GmJ@XRs39dQdk)rqba>=#*$bZuPiV%6*v{i*d+Fqo&g!3r$}?y z{uI)eAj#0idE%n`Oy59q81 z@zsVUs9{fGKf3A*7j-vs8_btqoFJYwTo33zE1K*ISm#_g&O2UH5BdB*qe&>ixB7|J zv;K%y#<&@Lqi=v|H8kOTq>+L&i9)H!Arg}bA+^vT?2Fm^%BVDIzz4SGJaN2bH+|yJ zumFp~*yDKPxyk?@CjVs-^uOg^Xm^T_Y^t8UKEOoV+yl!$=WJ6qeniY;UyptIIjh<~ zR8hrKlGIstphL{foR-c(?<4b~d9nm?#_Rbd0xGjXGv2>dL=ym2T2b%s%*GtsytQqy z>nIj=%lk`_x@ry7sRNSv(rzMh9;M9+>l2j?#k2~261JZfUcFD#nq(e|8mEUd^8Yu>(zOKq0 zaQ3V;bu09iO$em3OGttONb4CM&`iZTSc0l3X)}2U>4dKtk(Ycj5483`#{XK4|5Lad z*gdz9l^xN1ARk-0vQqw~-t`JhoNV!R)^W17S8QxxZ#Cmps=bw&M`nU<55Rz%iBxFW zB=lcAX8DP}+vRy8rkF|FT~#V7FVnF*K)jI%krxBG&N&*AA;&lOkv>5CUwt&jgU zK-@<=XP1c@&(Srd^eF$w_6bHpv(#CE@j6ve4(W!m_co_3mwvN7Ih>}4n?fF)!a8-G zB3knxaB}?W)}NypEXy9Te~D?u2$X~8n{D3i>8^<8^5t+v4$5724D~XpR-HSW9VZC8 zKDI|@-pIZ3^yJh^(;=}zplPD2zH5!`^td#l>&63`Wxn6OTR!9Fbx}Mz`NNHqOwTP1 zzC-E9jPxrI&h>hZkWVA*9?3)S7%l%ktr7|K7J&^t4l~Y*uGR57;A(OFz5VB+WLq@> z=?wuwrAn8GQiL=uVFd+Qwl@3DSj7Y|=6=kHFrt5CGOH zd=V86XebrkCo8v8oM11!E zT^;_XQiNQ`qw`R=ClsA8lKt~44-xz`z!zra??*yqpT6RCP~{njy+ji@mZ<`aOxL#g zq9(q;-`W?{YPbhE$8Az3A}Mo&^;*X9`6)G{GKu?l@5_B2h9DY(^E9=s_J^r|#g@@I z+HHz1hBKVk3DmE);@bhdtrYYI|3uf~w9>t&^)v&zL)GLalpA4Y$z1JE+&e~^D_GRc zhSF!4993l9V#F$62wVll{W`d8vW(2VnB)ISZvfO0#pOEtzLPm1DC)fjL+@{l>q{s{ zdr-Qiwdp0;Xjz;^0MQfx1NYW~zW6%A;uls10LN2;%4s87U@5 z5R(FylHD!uOIM4QTY;_Bmvtl(d!G=5eypVSh^}UUrwt|$(t$il*rtV8~BELnqvrjTBN}73z$N&7J-uUishai(falpdd;J*HaNP3#w ze$46o*8L&z)Az}?or}J7%~qeSR1$$|Xdpj|$!S0)7Ap7I$yT*%$~{opP)WM`!1A-x z$UX|_|4)V7jhb%vsSA9(tk~1~O(K}_){s5ZKVhw}(~ z>fsT;^Tls`_yX!Ulw~6LW7{NEjGcaHTuIaNl1bm%pqY$mbsFE2OsBKQxBi%EuK6Ms z9rJ5@?AHL%-L1jzkaK3%9<8DRuQvEvItlV1`;tACn?=*|9Q! z_AksLQ|YVoa-F~3R`8DBwOSQ!P$tdv-L9JShHg_d9d{ui%Yp6-PvcZeLX!kb-{eSX z`T>!L)S?>2^3P1p|IYQ>Lv%5YV{poXK=RFz?at2e8Ji(DUQvxc1X;?wQjRA`hBh%^ zb|CA71cml8aMLJ*{*X5}w}?^A3t&x~_C=S~iWR}Z;}75AQ}r%^Zl40@4yGZ5PylKA zO%dh%-hB#vKr8uOh2^<&)2Ch8D*DFt0)Nl?Ut*DX&<{lJmrBHmoCeDL!E$Pt{Z99a z@NI8eFcMv7ZBGWPF>c?RO?p6cJ@+mfdbOt{t?i;2Y?D7WKEL{*A9_-I-4Um=WO0BQ z6I-HvuJk5NOrI393YAWi;7vJuvL>U&<4|qJXwF*|<0ncU4}DgDt82J)l*!UC8anD> z@DwIuk6>awXv-qFqs1zn4@~3gRuR!Ae2wGp5vl=WWxN2mSjREPc}g6oJF#&qrudNZ z7Tqc)o3Y`WRm6@@lfy+$AGYI;7iN!!>rX`!XG3?6OV6;0AsdM`k#{rPAjR&97d)*- zKd?$ACm${rPTx4R3NeHGVpl5!7jRhFAw5+*aF)}vjj3>;aj^P&lmAc+gf{mCyNf!) zPblQ9i<{C*EfOH)pqYWXds*=`7dUMmDy6J|Fxm>m%np01YzbG<}IWz36rutwfS3~XvUj-yu)RA22$u7%MB_diA z`*fcxT7pyv_^>kd73lo-&jW7whsow&jj&@W&|gg-GfQ;eeP4Beny%vqv)5O@x(+UJ zGMrD6sdZ=%HId-JT>>@yEH=Ddt#$VoO#xwkOOT)WtVl;^gAeA)M5g=Tsr5_lGT}@= z4qA2cDvWfT+Yqi3(niyeoe^9VWu{=>sxlz-Vx4)myN}|*Ee)E@nwDfvlX*4PR8L^P z^aJr+y+uvze2z*i7<(R5)~V~aE?P(FQeFIq!Y=K`LqfZ~qSps670oC{!U(^4bo%L5j*ZkJm z&n$G!O(;EDbH_C*k?YVqF#t^D^+{32-?@;KVt255g(pOwC zVQ5fU_6`W!(`Q-@kdWg~-ybOb4e`oM0GQohyy_UI0B` zTuyJV=dTP)OPV@in4v`rBhs`FICES1)w{Ys{i5K2C$rZgg^Mp>;}QPkV;}XlaI*Tj z%!;?0EBcN%H&1Y2>V!bpzCi8O&ljNl=@#3XgB^GJbEM_Gv)@BJHV0skgs zx+Hd6zedF;_X&|uljfqAa?hQ};(sSzDOKd}e$jBwb-a22;y?gUzXpzp3R+4IjWTz@ zQr7J&7Gn>zb?B4ne;~();A+6lnyerDJQM@`r*h|N1T?BSV(j1A5MHUHiu6xvHq z>_*)njqVJQvrH4s%SX!vh4SlX+>Z$jrRV0@*@6{s@)0NcRkU}?@$X7co$;H>CrN&{ zv;p`%&(XYnH`>~ZAtpzE0Mj^K@OrN2ieq)r=iy>mCA3WySaSJk2QX%s-dU#9Ghk4Z zuLc}*_~f(Ggr;1yiJHdZwbV$P^643g*)?-DE&V4qKYWyoCS6}!=Ge&xoMZ+H@>r4LhPs$0@$Fe&!_4)?G)4M}}0cF-J5JSnk(ZBkG zf0`K{XMNdnT&Rt9TB?K1-eJ*fj`fhqxE&}I(m`nrqBKwkI|?jw-_+BGK;|_=b=A1+ z#T^<~AMyXIT$D@u0Kkr&Pf5=QRR%OLEd@vpoq{Uo$sx^C*{pVB-2Qt6nHMrpk&fwX z^{QHiAQ{rgf|7&F-Ra=0+V>usQ1(EBYp!!ZeGP7XqJ^hZiKcQmYH$6U_s<87dQg#K z(e-9ZRORPRRncO{?*Lc25b>IcD)8ZS9_=~%Eb@ODlS&hK)MbY+%Y9pHwp4A`FNcAR z=iZyhoK8N*V7~O;{O7lEzc&IXY`fwyP20L&vK+J(rRR8cUEl?YuTN1(0mtaDiE%Cw z4dp0pu{qx@L3h$_ercj2LFB9VIo1@VQnawXv%iy70jyWP5LFpk3Ap zBd|gYCbRzY5;lUAr4HP(_8;2sT^q3R=2fovJO11n@>S#zW@IdojhfO<`SzEZ$HuUo6{Rv|^-UCkYf>`c{yJ$^GOT0*=Q)oYVgo0cHioL=KS*;QBleANx{ z%bCxwVUG(2H74PoBiluqD&)Jp)%Tv3HhNdTl2hbXD+!Jdnx^(X-tDmb=^>)Zb2T|v zM%;B`+d`xqP5YRmIeF`74;el$K6_>`8!$z0c)Y##?R*>@IyXIg94h%M0YN^Mobg{j z=pfA3bpoP}4lFhfMyE@I4z?ZW!u$#J_F^^KIU=iB-egNLQH1006yf%RWGJT%SJ;TuK%?9ETo!CSZ1Ufp zr{v8i#k|W2KtkQ^V#>9gRM0y6)4kJLF8uk{4U(9^U|r8wC1v^>H%V#l8VgRV+sxf2 z@U^4LV8APTY#=#oSakU@Ep1dCZ)?XyVCFJN3gr_Pt6C7_>xboP;xHT7Rl3 zgHiuh);JiHJ8YQcqN1!8kO%fWBFZU;C^w83XN_0~|M*W+uEd`b((#MS^meJc zth0zpa?6*@0oz%BL-xwL%F9o<)mhg^2=Qj~gAfYOlC{qlJ1iP{?7H3zDI>o75i}R` z-t+ozS^jzY6m+QUkX3NeqVs>LG4ENvfugt`79EGlJbc9uZPE|K%dYe{t>zajDTb@z z2rIWDbT;Pm37D6&By%#Gmd5}1YMn$*VJ^N)cJs*8p#*u;4hpS*r3jk)Wkt_Bpg;b! zK4_zrsd_?$v+#{ofADk_+_%*AcO?$5=!1qDDSezqPDZB-{!VST)%)q|R!FG^C%D0a zr$wyNs%x~DV+xx{o;Nb@U}cgkzE#SuR}0p5+sAhq?#@Hmt7HHAXZ}~<;v3}Q;w2Zq zbcdzCdsxQy8c2vq_P(2X_p`N^TM6+ksgHPed)t>YPvrlU%`96bSMERZf1>K5hLw4x zxYK03m;>0agea%s4TgA`at$3=O)))@i@PgB){;`&1|cSFsL-YDZppAkPwIU53H-de z?R57Xrxw~8?#~8SI=y|qgZGS#-P}s*ArUqQ3pF!3?;3`Db>)?;i9PHJ2n_1fT^RK} zAoo)xlDC5LeT4lC287-BA`_aHv#Uu~Sw+&JBKlVeceR{e0&|K73PN%t?86sKfP*WMX=9^=g?v6f`je_x;;Mn29!9M_lS5Czad(dh-#c)V!lv|CO zL#b$U%fVqmq}>r`IK>lk4n8mBJ(QW{HB?SC`7^tXp55P4msFVvjxKBFvXL}^kvq5V z9ghmD{Y50Q4J{EQ>Nks-z$Xj!T5XfDXKr?)o^DUFJoiZ1wgGpcxz3+`?{*${H@2~HT7*@3dQ@EioS4ICr9VPk=rLd{s z+aAoSjQwb>PzPnz`e!xwa%RlxN@|k4u7;CSc6h3ZaFU^j5RnN&xso^%X}j<+cmp=q z@+`61LKtZsa75pBk{I>zf0fIOX8+3=DW1pS_K=3PDLeCgQ`wI(Uzh*5zl#rz5gG2c zGbV2~6^P9I=S-j6J2>PQ@e<0%+KQ)}r_C*NRl|?X^)0}ZqR{*Dx8ua%29B|$32#c} zJ}tOPrNeh}`%xSZ+uRl-vL)UBUX=(FPkvqhQiF8aX4fm<>ltFPUg)`Bs@D?sW%LaD ziQO(Zi65l>*N*+hD^0$Ij*0eWEESFRGKUk6FU3a??=6hf<+SCk?Cl4dNr(1GXtN{l zVnLyt5}c44oNV%A87#)NTh>{lAmU>7G4^C-*8Zpt9KLj8=5TyTUqfT@xJ)QOc`|_G zINtO07(Gilq?}8JZwkzwNMq5Gn|EeUian2d;jJwpMfgyAo@cPrU$N1Lchz=wl}`eb zpESD@1O$%b5pT_XSG$<0xwfb4Phq|lNi!QKDi!VnDa&V!RfCg2z%bX2Jik-s=bEBY zGp(MZ$$2D;Ws}E-+b|$N?2DCQ{p5?Hiu9^)Sk|Mo7&ky|p}j(s2ePnixLqWe%ie$G zBa*`Bp1Q-ri@q_RIqiRoO+|0RYPyA|TGz%jKu$Iq6m6^NSQO>Vrr^I5#5S?=FzM*{ zkHCxl6n6FtaU|m2Wpv7=jrBBFjC<++u{m-rrI2g05!tCDdX-k%7k*7OXgZ})<`E{x zr)GOx^C5zxLHH3qme6P^XIi|_PBcGY+`T!XCo}Uds0IQr9Y&6R^#>h8-$V}O3$L$7ExxF%}`Ae zRqw$vz^`Se>_(4i#|d>`?29C|9o3AecK8+1mD%w!Q&CY5s-XC^kPzG0|{A}=3SaV$38wrrH!L4 z5`rE#$bA~#Wzhx^u>ETA@C=SOxGKWef!q0_$2jl3Y)LkL9~}ARBh1vVC+&+89|z1? z3z)~u-+dckh=hKPu@H{gU8)7Vu#hWPoj35iZIo=V_Xx-`rRV$=0wBvi?ND@FtQM5l zsJ|a`A)Uz3x6@cObsbO6-+*zjtP~HzfmF|&+TfCdu(u1wGBZJ|NpF8iLZtZ}%=?wM zPX1k!t!2FMs&Ah%GM;CUH}&a8<@~$%xZH{s(y*mZmI_YlrM6!N@{cdb4bTVe*I0P; z6$&%YuJG*ed|9$VKVil4PG~~Qw5xuaS2^G&rY3|>ItMXPUh^Zx8xr=u`c*fAWHAoel_$e3KQ2EN|6YVn85$|GJ=z=RFqRRg&gec=ss1L&SndBYuP#W3Ffh= z$}!XXnJ0L@1wKC{%$%`!1};|kH79Mp3092RNKWTNA-QLdmOMk(!*@#ZsJ3d9v+%7m z-V+pgH*piYZx2XZ^*ruqBaO}1MU4koYpBasML*jal>oyj<{>NTp~vv^%UD&r)giJt zfhVCM%Y;J}GVlu?j9_-HvdF#iM2jczdWB=+v`xTwCO9@uZIee(p`7uuZP7m9Y zlaw1m*Sl|v4b$XG&z$p~>Pt7oR7aR~Yx?_9JDGbrhkpw!K#4ph_g!Zzvq1DpFw6>d>=3rrFU2jFRyxg)FRAI{)FanpV4cU5Y5(z< zj=akr;hd%X?Kgiv5K0@qiz>Wpu`6zQ6v9~D16<+Q<`KOPJCvObWSlnDW^RZr7U)a} z{q1FA7e6`{y=D8b)3A|M{wb+{&ohTVAiDC?jKHe}b7njXCmI=n_o{42>j|3!mDdL% zX zERQM+E^?_&G0fX|O+~?nRa7$BDQ_#lzS4i*Tf?tG=5iP92odFb%^o~C%G#czE$&-~ z?|W4d1*#uSn7{;~Ouo$^2dzITo=gmF1hc|@gMP-5%FEiln51sQnuOy0M!b?5Gs+|t5ZzMPK=M8W2A*^qtezS@58x#s7 zI5dZ>+Ij}A;Hmlb;fIv)e<-!Bj`Krs=#y92Dxq5VQ3o8d4w|iXuQyFR>X_TN#*iAg zO-VXYofzKGCH?UA^pG7xlbi8u@AfLt{XCWhvlcL!0*Ya6-;S828R_7X*HT#eym^h8 zJebDKb^F)EX=Ib%?L4wnie*`mV|$cK|Jb9Cr0XL6%vF^Z33dkGOZ{bvdNsuO)fQQ0 zZh$zy3}!(RYj!LX`OVIeL)TlPd5+;iHRbUi8uiLSLSsGGDH;aT58Jy8_tRn`R6^jN zR^&CgIt}mR`B}Xg+n{sGFd4B0D-9n2K(Gu{QtKyek{}uz^^NG=Of@(e3C~mhUq1|c zn<4v_-FEJCL6uafNIpoVs{f4`!C91}xv%l&t<8L0rXTO=qI0DipNaV&c-iy_3}Bob z2ar&Z=jQjs8E0Oe)T$rS4?!^iuwd7O$lOeRh00e@MA^haAxnC&?LZ;9&e;d4j!^&> zp+nY;zjEzHG*d+Jj+P%wqn71Y^@aj!q3c5VVP$R``?hsY5>MSUSRBojr)0*~6^kuA z+?V}}8Sp29FZ+snkQ3u+KH$Ia3F~ilRBoy+5Us$10`+-|9U7y}OdQy((xqE%%|GgH zhpsyLXgkhsyjwQ+;qw&xA%QM>T3kI1!~rnN<~5jIiIf z<~cqfp9(2W&D1SQ6cjY`>@;uQ3uacz$e4Wm3P>=g)`6*1HEMbwar;GFSpDYKJMFe= zjW#)lBA&?>2?55&(lzkfq3x$}PRrSSxU_yw>Yy2Nu!NQW$X+GS8?dCm7*!AXu*=v0 zmU{@g|A}slGn(vB_n_f^eA4138jmTO>K;e}mOi7Ma*jDo)d@KtkHbC9$uhw??~A zq3OU$DL5_(N?W*mbHUO<1<${4SH$Gg4|p!Uk*4rYzx$?e@_<=|o-?a@At3`$BWvXv z)?`w`kGLhLRlIl85)|TdsDnJPIl-B2XjFRbT*Hoi?s4%(i-em`>t56JX8xAeUi8Pi)5P={W1;}{;# zsm}?I9Z;WKy<(Yq{|f+V##)ed4LfwVs=c#DFpx7l$uCWDg|Sk6?`Pqq@uyEz-?@C) zd{6*H+vOHf?QwKpHJu61;6NUJn2&yYhv9s4_m@LtOlO#QU2I;bVU6wvc+{{U^0kSa z5ks43frCg`RRMqL47B0i6waRFA+{6qakE{bbrK%-ogDtZh$uq*dy^&r(umW2C3Rg7 z_X?lj*r8Odz~3hl-_JTi2di{=Ss}8JJYH!wT&X+8{uk{;X~h9nwy#}4Rj7v{|>x=jZ}U= zojS@u?n%zlTG4gXi1i)xNxYP;4h0##jk}@chXFZ7BKAZ4L~G(wb{f$Q9{mE(Y7&G0 z3gyW?!|GV?X|dbk<~UAc<3h-XR|PVwY~R4sE}3;6`Ot^VgzmWJ#ntD~*Ds>Owzb2k zpfgn70WbB>p(@~Z8-xSjZ0ASFcG*UiXKYBMVH(<24@6s++-FbY`Dt7_*xkh4C9 z(<$D=EyTHC$hv(XT9h~JsLEP${XZ#YnLQa&=Q6UxG_Oguu`GjI(i3$=q3){fWS_BK z5IdbA_{9vhp_6A9UnS-mv(jb`AVr6{AEiow_N>`T;~e{4=o=8N0;@8Jgs)YxdHrs5 z{k_V+2CnJ&Yr@Y1u9={J`(KLo{zQ~8g6uyPb}L}^UR}5-{gbK?t=Xe_a>B11xCEmq@a|$VPJnIxL0?TSkt< zyA0>?P1E$KlyQK#Mi(5Xv2YLOplI^PTRh8I9?vi2E!d<#3TTy}FwNrw4*9q7$7>y@ z_by}vfy)vVj#o{tKPOec{FTJvVn%Sgp=T$ELe{uGy9N@UFTY*2@?q(f05R?zWN(WZ z5!09Qu0q+d)28E|ZC9N1kP^lfb_2QG% zAhtv}>})M5X%UAuR6A)q-^imHz)$LD$~>w1=HVL&A9df!F^8V zQFht}C80aDCuQesp{EtC$5+s8+;}eJ+%=#4x=MUhC`>u&hWTja5PV?aVX{A+U!`;& zK-<7~>A6yu5~7uIYTjR$d}jdiSn9=kkbJ9{=xSZ8-a){hahuTBCc-{cxNBtwCzl4G zu8aWRHost#T?MX66+GR)Di}>vVz|2`G#P-FOrfKjHSKdP=fEasIyYcs?)UeL56#j4 zbkxer$in`KUPu!+^v0z$>VvS921d2Ntrw?h-WA>0p4xo44@JLR&{GGArA76m_87Hm z7>542ldPic)Hqv>k&94cI@1kfeojNDj3~uqeS;}3+kE;?|NE)p(^U3Y-_gp;kKm2H zF7wGlRZCWHD9%7XxtwmEP)#O;h1UJVTHiQgDdLW<%Fg=@0JH%_SNvVd5BS{*kX?l@ zmM&Q6x}OI)30tmQrDsB}JhXNa8i7RB_7NDiEumtI_a4lSDt!XU3`OC0#b)p>~AEW(>H}fR> ztSd8B&q!DDx!9VibUT2{t=TSb-KA_?`^_6>tYHhsNvI0)o>%Z5-EKS35ASFTJ5f7@ zfZNEW)JOmW9-mk380AsI2ECtug9$GVqLT%%_3M2`OGvgIVA^&rIyFrUX9;;BSl+|7 z+*3?m7%S;qj+*WnyqcWu88AODA`-w+vm`fI&N*SoXK24^`J1=&8+4{Mh4H6L`rcuYQvWfC+r;#DNJ9`=QFKh#Y9KqVTL;nGDtbX+1H$5eq;hbI1~Qpm~`V z2yyMV{(24QcjfW=kz?1KH8v10#T+rKt9TU<)(7=d!y-MQJR-(@QaxW&#lCkQK zIdan6k?v1TVQ&e+xu+!Hp2}(fAJC^koFAw?L7%zPziQsEVbEs^aH~LXdod=DgDV6U zG4h*)`A1A)O=%rcA&Dc89oUqj$qT|0@Fp#fSnAJUIgL*EXbqkF-L~GJ^$7=ydV4Sa zU7TX9gZcS#$ZUG=PzM6mH%m)r1-#SqZu~C?MU>fYG`0u0iNqkbr?dIra1Zrv<=%B$|x|Wve*6r^7;38U;`je=KGrX;;#u z`t>IOEIzK4Q~hnJ@T$i~GLB8gc~Ud%)txiU6OKy~sQxL;m^%ckB=CfGM<)lZ*)% z0sPF`pXt00{i|5P=__30mt=>gGh#a&BK$D?F>UjVqq7ZLV1IaG_z&UbO|TE6$S`fo zsh)<;9!!#t0rjl}@}VJ_yic{-gn87rlOx<+&~X9+Fikmpf9EsN0!>+5y6wzaw_NMK z=-D8r+7?80i{Wg9x8eAYQeK;qXX1sZ^`P6DtzNJ~bpJ({&C4#p{=YP}Yk3OsZ@ro) zSxL-iEVHLbUgpbhl*!zQlJQ$N?fGY*_n@?u&NaL7JwXPSB!|fFn3@dL$<(tqE}0tu z9MkpeiL^y{_k(v#)#8M^J2e)%%gzj5s8ug)MbXO5zao;i{jn*0(SuPotEGJR^OJ3B zWXggtp*sALNmVOqyy}vs5WZhLof4XI(-Iml>kHl3fH$f?duR`ZVBTu9HMu*K)B)(G zIPW6sP0KE#^-E_fhE&7-Q2ABqFOx$a0#7N(`!1ogZtELK2wJTVe&a48;yrDBLsIm5 zU18wYW+4%N?TaBlMxuIUX3k4x>g?-?NqG8Qr~k!K=Y$VKc!~lMFXBZEtO9}1U$a9G znEqz$u=^JHezCOM%@LKj(|ZX9DKb~ET-Smw1>cbmV*M0P$X*^>2IJ^io(BY_zNwn zZ)J88iZcsswXDieM*K%BT|sQ1t?_r`hGng0Y02=M#yxr|0swKRCYBV&;X*T&pSG_F z&L?sIE)k;oWhM0Pmn{&hFJZK@>tjw(RiML==95H2{7Kkbg1=4Z#^GJbeY{(6AVvFp zOs?(NI&6Q_Uea(qkvKaAC%Y-I3muGuhYDRBy*9Ep_=&+c6*4knBZ&Xio2%|^$)WZ+ zXZPA#kFM~BoHI}lc(?2iTf~TmO=^eT>8QOW8Sb$JeuBi<=RO&=CGNy^m2g@gu3#zw z&2zGVK3~O$ug)Ivwcgk|MRN1RZ$+s{b~Vv|Z))5z$6lGuNG4kt_(@Yh>%T;$O{9!0 z9c)cxOfalYCKWw$vmVxA|LVA(j`fE>WgZYAl2ET+s93*kvJvB-64-I%=kJ8PF8zsZKv)WByD>V}qwTGP3bxUspon^!VP%;{y|}1n;JV5oFo_N7Fe**BLHt zIE|eqjT+mw)5d6Qqp^)0+qP{sHk!u9j%}lRCp&18FK2z{toPshd)7U3&s;N4)lDzE zitTjr4wAF;yuwesq*xaPNin~^j)jy+W?QP>jS>%}EeF3X|A-yy4}Re-%e&GX-aQp1 zCcL;8kQ zl^qGAm8sQWwpEy-AK8n=P?JQywWt^DS$8jBHtboawHMO!BiSX!+p*KhyaLsO6ZXZdctJ^Y(Qo z+fVB1;RaF$9yjb0UjirUSqTFqKiY?2j0kBw#|v#DVVHY>&2enKTnk7I!ZbRtqLnck z(M0>M77_5Ab%kf-47kD&zPnx#eLeOR{&%HBEON(LK^ncN2DnQW+HLHJe=YU+Jgrh` z;^IfX!h$d#^W01BV)Aewok9@`H3{{Nra1`sfA$%SPB( zITi~pMb6mPbKH@Vp(iBBCxbH75f{_wm9hV+py$o+WmX>~9C$+6EWcPX>Yg#ovVrX| z(%Vb{i1VMVE_8QTU7QEyU^*TVgR1=nDB|6$Anc+NBxzTTnFAV%-9@3IH5eP9>l&FjV0IWyCkPXs?9w}=UR zR{()O*8rhE_u~x>L&9z-8~ZK8ABJn3{wDd%Kh0p8Tq!2vKl&{f_aIa`Oa%IxGsg0< zn(Pfvu}{I$6cIyA(i+qL2u!$J{PZ#FFSwGlXaEt{c=)b$aiS-RQ%$1VPR}+CR>B+p z={d|#`y6q|J#B&Hu#Ay_(p3e9{W+bbeGmw!ou9vq&`H0s*?G2mi@WSMF#4K{FbLqe z?pT4pKi_qLO)tyvsuj#vW0#)x>JzJ4*(6>DmIZ`1mPGNmrDvR!+E>ZWUpgnGfH-o+3+UClEkKK)l$D{80qeA^R8 z1trD_1Cw`t>%v$|n3ne3@C*60Tg;pt1npx;MnF+slrupCh|Gc&i6-WN*^KLxUW@M{ z20JZX(QTA(48}`u{b-wxN7(w2kVD+=D`|qKH*%h?a##K;wianX&UeykVU zAt5HVS>7{;SFG2-yaMp#bdAVC<}9<=|8bmAig77O4$56@QS|!@0H(c zDAXhLkHIuUB*PHz-QyLsW&zJNqu`<4EADm!2LWJo{4L3gO@B zb+73<*K|iS5LP|w3?btQK|}YzCiqFF3hnG@NSgkOX;`+DPcD%|+XY)%QQ54$ zjE+2>`Ueqti7^7_x&_-D*VdeyxEhzRhRQ+jj^~Wwg&*j^!1n4@gR`xy8v3ei1L*k} z4$5nW=v;UgoMoSBNrca9(P3k%;FMKHN0xv8S8ahQOYd36B^$@eP<-HL0n3I?b&3CM z*?d^G6!^!2#T1xZY~cV;xw>ZT*~)_#zav99ql!eGBA$!sk@j5@#aGc7I&Av2=7-MT zPYlT%YNo0*Y4j>-I#i4rR7nij9QQ-dJAci#5l+DvAPDqU{YG})`3Z!(yyaFV?& zF5zmqNks$hx@bWKNF90PUk&L}a)xkVeXY48n9&&FU{Es!?YtEejFhi^-#_MBE9ilc z)`oy0epeWCp1=fN|IZ57o4Ep?YQ(xWA;GvYOI)q^3f+XpOOn8n=k^oqp=mM-1EGz} zify%cW+tvWzd{5{_uCb-Rbe#2kWwyWkhsZGmfHYIkA7egEG1Kb-R0?av?v&hp%|NW%9 ztKa=w#TnwBTlv!U=29eml-7Hi$6H1`_-;DDJGX%_=HzT)c1C}an;_toFEHSiMgAUl zy~%Nd^`=W84?l<_(R%@83e|U<@96h$Ujbfl#TCo%)z2{(yz@BcD*F2C`RxR~!09XT za-|1+qS^LRme*g?sgWwxVFMW)?^CZ?(tw+lo~x5>kl*_>r021{;0+I;CvZERAX=YH zj=;5va?wxEvMeBGf67~FQ>sHAewN4Zg!2BI^nG;asqd9zb>*h03^1uO^c>?((>b1C zb?O=8-5xb}q5eX^I;8*$Mh3L*Y0BPNCRfWvRwgekIXTMd&}F;K^;grEl@_07>S*jm zHW**ars&8cR04ZQIjeQ*xZgOcn9H=e@lthVGbNV-7uS}wW^7bK$&SUS0t0_jzVq`V z(Qtf%9nPZbP6?FnB9db{yKvFMsZg|nu`c1Wt5Q!`C~`naZ!T<)BaO~ zSdhc`I_z4Ywd1jVv}Nydrar();B;y()#>_xI;nA{fZI<=F&_oznMzu?d4#XjdHBIih}yB;2J zsql2FCgu2Vy~X92e%)lt>b+`nlvoJrb(P2A)!^3}a!Lr`aeP)K4I2NH$&b|txJVvG z;Dx`}^|h!-KS&_Qy{K=PK}G%dwX2yWo5xlyHVx!3^=*=f69XNEor}(8ca%CsQBJP9Kufg ztWo|4riQViSD{pj0X=^w+3Xrb%ip*oj;t=2H2J&JDH;;{gKA=f#CK8r9a!+x$NI9J z;D2%AFXHnO@N_zTz>Gi_;qU0WIzNjB*D{XxiPKeqtkCAfwVN;xJz#em!RVzx+nycurRw9?kIV03`r!P!=PjDJYfLmNV-0yD5U?uY=?9bF*FS6nBdE~(Bb zsjdZ;>$>gKKjjUdsY*eQikJ(S42%rD!Lzu&)vZyssRK?Cmti;Q+QpogX+~<_tJzhW0ge_A{BiG@}DQTWxIg#on$3dHwLS=%L zC)uQ2m4(&h@UfJgeFk5nScKDH=?Zdb0&wJ&>1#4D%MKVv2f8ibhL@sU|A?R|v+bn6 zX3G@kK>Y^KShQV{;AEYrbRFnEoCW1@Uq6$e`;@r>dU>a}}{ zKvZ-1qT}w%*Jz&D&g0t_hxMnL#n2mw{sRGKlesm7Zv;NMUeoACM_WWJMVd|H1^?po zRu_C!&A(mQqW2kO?5QQ{6md}UhF8*A6i}g3U<)s|@h3d!S+`5ALVWyAc_VVLJGQgJ z?Edi>=(fqz^~pDa#XIa`%T9g=SBsx5xTBMy7HJa@HUTQyEg0k3^I8Vh&HvQ2lrms+-7={2g%? z9DyFu@Os|xHlWmV^`SEX@BfrUGxAG41D*)eq=dtX2Xo0#&x?!^?_|`z4b<`kgnyyp z^49sBLVz`}(RVHH0oBv6L%DIc{ESlgsS<6~oHYuL2hO!1E}HO~SSlm!MbZ*bmVqy; z)V0sECC43QqnIK?>8~bN&3_^*Q6a<~*TO*0Z)sgNC~~;`OA=@knB(1geq5%>DNmoK zDo;<*m0Ao%FGUO7l#gn$3M`7GA%`ZA)So&ar2du*%f-Z#iYk@Y09`7DrW$T>oK=mT zmP<$BnqHfm{G&Q$tW=y#6-rcBTcna%(b4lLQTu$CM za5Vm>PFL+{D`N!uLIVnc#h(HRVhvos`Udjdc&+kcJ86x!1&|I0zE0LgDd5ki2{YQ* zo&O%A-IfsHEsWu|^DaS7cc@Pr0i%y?ZO({i$8d)O{)wo1w5_d_aQ)gy>(1OLscb}G z_Te3DlSX3;o$HmgK6LZIzKTm|{cb)wX+o*f6|jijhuZe?o2( zQ$U>yyd<5xuwbG)jg`Z4gK`lq)3|&YE>%QgN7@la&-HY}%Z%s?r6?Fi^!?_wyxy3^ z%L}qMqUW&byMH`A%EIEX!?*3qx6LO4-u>x$!DQ*#FPGdzg*l!Y`JEO-zBRO7=L4e>|1ChNx*qpwLkuQZYH9xLH|OnM+1RTXNzGTbZ%E zWNF|1?r7p~7?WpRu0oTFPRi0G*43aB@4|04GnT;N`7ty&%2ybutHWdy^ae8z>xmqP z#_C>q>sLNZ7k$wTbkIOK9}Y& zyz9eWOyA`_0T4^$^>QqsSoKlOfQ)XM7&%_p()K4ff){M}8kZ+wm8AO)r@NY3)SB6+i(z(1%a)G93p_7QCRWbehdupzzFX`2Ivum} z&(`QE9;4^wRrGl_;7;@riUDf#vFSjs|0#;M@5&uRvk)uEAVY(dXPSuqd>-soz z7sgbE%Lz1_hdbRz=QQE7&>J@;Gx_Gig$J!Dn}~|cO-Wo?^}f(}HjzqYD?pVcL8E{$ zA95Tr_UEk9x>(A@DY$$Yw`{%5&NFr2{O;(```E=rWrA9sA}zTrVeMt+E2Sl;>(6Sa zP2|!6MmJLs-dAmMC?XFB15{KmQb3IW-KT+Fbj=!1cmK`F6bIHz$V}gV(tzJjwR&_vWnFblIl&t{_$@tKxti@_oCsL3GcOHCW+CdeH_B~C2on;ydqvh z5EVa%NDuiK&`pR$z0wt_Vs6T3^qP+4?{pSLpn=daL4xyiKXiy%9D8%Oq_M^g8ApYL zxdTi}l76e~^nT+ddT{r5H?YHCA4c()UUBnn5wwe0QS@)b&z_fkvOlla4C7oYyx4{) zn~@C`rndPuNsWk#pTCuQ_epcKbMbhG?S2XwJ;L0kHHn<|i(K~k3_gxnU72jU^zL`< z5+Y)O;w_Qu_Nop!RAPDGwag%WtR~!X3Glxy$sqMj5)DXc$eJ)=84V#J743PFzjJ&W zJMv_HS{R->_x9ea=4LfjD8{W`46n6#7g^std2^1LnG_ZVXI*Ov6@ zDT37h_Mz^j;;hg*x%~GI%{|whxVtWje}+s=915?CW%bH_Yle~da6)5Mb|+JD%&pyc zN^z#1vzU$F<--BQf01&w_vXvXePe^xg<@^C!;;JB-bVO~0(!cyCRw^&=Z*$}u+?sm z|NPp{{d>uU7Jm7TGG|ORje@LXiiHAyS?Whb&RA!c^DDW|+>Hv&Mx?r`mW`!0SGUbs z#S7DbX-L4VyC)!3IfDsRHI_0fxk**^dtbvbXcqPoSIfF|)ne}|zjvo!ncLZ%=kbNh zW;$z_#iw(kD2Ou9Va3?gj}}?^Mp&S+wPUQrGpr zb7NIPNmm)7oup*!pD>CVsB&g7M!S#)q9)5CLoH;-Av#F$c1*gNBrWe!sh4z9B98A7)bLXDBhC_(!HUH#=abjf?ms-ee~GG#^%k?zz^#@ z<0oi48X5BV;(`+MfuR%1T-#oyNb*quu+3Nxh|eAS!=qvD1)*9V8y=y#kMVOb$|Jko zBV2YWHZUi1DfI38a{wpkC)9vgV)DAS$bJtVuuolh2U2@pH}vz7(QNPS{wsv&lhrJ^ zZCeL^H?(`l?#mBmEKU<(1KjeG$J~q2S{?wNj8r^@vx;0wF*L|?FU|dH# zf%mqX(xLdJu2ny5xH>oIh@rjwjBR%=s=0Yc4LwIajRO72$H%+ROB_FHo$WdS+AFLE z`DO;=D_T#i5!2I4Cfkr*+Q3J2>Riw_=epKu%JJuAfSziz)iV~)u(NcqgEh2eg<0k6 z92~?vgT$$pv~zxk=)K?cXz)wu>@3x05$^W8T~OhtU>ZA0GJbZVz2BVg9rk1DJC#C9Abqs_9Y#S{PTKcp; zb^QstyomVA=Ox!zy7I{3k6&%4S-U>rqrE3%D=Y-pCd)0PYDlOlddp?IzMezyc*>Md zu`&{b+Ft4NAFZkK>m60+cHlNH9SQ*voiDfAw)6XtGjA$W{Bp)d?aYEHURW&IT`6Dj zJ0R7NBIdbA{dw_iPWAyKP=<9Tj%n3BC7)ru9;BvkwKBhuf|P@$~=!KP|81Amx87K$~>D(7Ex7 zpW5T0Et%Xb$A*~^DJzJ4V+YJucYW+R_e>k*MSPMiZ^lLozy%`9 z5!Z|jK2H8%_JGj_W>Qtb(!FcIpp7C`)mJ;`9}a= z@dznkq(A@Wd7Rk0q<4H3Uep4i@V>Y71-xVj0x1i-Px(;?>cEv+Bvmb<;h0+LR7d<~ za|&0v=_(DK*&KI-)Ck}2s3o{IV^ROK=gB_ z>Jou0E^?McQ>o74RBBZgC@F%nB5D*S;mgTXEuUF(CajLm^yt%J({5rGP$}l5)bl#K zdIw1Iomro;vL|ZdH~dvuAuj0LV(zO$qJ+{16`hsWs@B(XsETlm|RJ!s`7q7c{QAm8kzqNoiBcvAqHwhZ(6ll3F%Q-ZSSIu(xHdL^2=UkR!zC~*LMhlVignTq*;E9v1kC|>^m&(JW&hE9!l*-UbR|GkPRcA>< zSzoqV{=dQ3KQs>tH@|PCZ2RI5{Xn-jZ3*SZt12DnjPP?|Cv{`A%|&}*YlPiXf*(WU zFZ`Kg|IG7Nb}-<|$n=GVIVU^kmdVYHg%YWzWT&-U2G(M^bVhUDt_JOrn@BPIhYmEJ zUJC^;6fu(t{rODxO%-x?lF)VRS9&nE`nFOS7Li$Z-VhzBtum`q!^_ z>caAEPkQI}X4?bM>h)Fjk^HPmy&|URjEbx%hz?k-*L{7;di%vQzMzY{e{Te(-H1u) zLWAWDb=B-lr4O|DHwH%w(?;YDu@rBXG@{ZAKAI2;b6gm!Zn$rFnqeM!A9_F^@OO*v z#T9hjC>w;p*$Ku>QA;zC;f1r+P9&6MDq9YC`XKT-cOHM8yhD_(w0`#)nhV~hf-R~% zi5GGl8><8fPqeFL2&d%%#Pb0&3PMPKO6}9}-$S3qMY}CUvb(FRJ*M`-3vt_A(E`bwz zg_B&@Z){AuX0(S@9_wVQKM13mNQ6rS<;eNX4V~Qk(qT{G99%&49dY0s)%q7$Evo3 zPz5)|`NK2k%|7hHW(~*GDrxkK47FWmUEMbF<-#UV)99AGF7@Q3@~HsOr; z^p-3NUYu1t5Ze4V&sMxR3MRBsI(vq-<&#`gN%lUb*=a5t7B9lj*Y#BJtMmTSxO=#q zKHV@*f|Vu2P1kVQkXRx7ye$X_&FR}AP+rRR>7@1Ir{jtjI?k5|+FEaX{k@!cLbFS< zM6>*(v52%S6WWV5m3)0879rS(F@Dp>+F^971Y=y<9!{KvWPV#y_J?N(pfmal}U zyzGrJf1gNcdLBIuJnsua`BN_|z2fyhmKj0X5FED7$+uO2SLjqpHru@r9MrCoID;vD zvSHN5llu}6SCRbq6OinMSr=1}YL4T^iEte$y^9vR@NUBU0`R3sgFB1)8lJaqopakC?{+^vU?=-4{@FIQPA7qx<{ z1l5~f4cs{o2&B4O37k>WnURzP_jLSp*r0|o-0o#4%k~Zx&FQSlFjc6mtCiBV6U0_Q zuvVx69-3}V!v-fqv6(B2>!JnAX2MsI5X&w_Q79kz7BZ+ivGZO-DDK4A>CvI7%Xv^7 zhoXiPqkhEp%$Yi{93Rs-G@iH02vp*Dy6R2U$ZG0gakHB*58D=-HOR|fxdbB9GzNa% z2cK7NZPut5uD;3G-^;Kl78=U>SLV|`lH9ldSN=kdYUFU@9IPvymmbi07FK)@zaO|i zcvUUrG|SaWQI*KY9sO~(cn=6J^0&zdEpp9-wnSQ!d2UCYiC$|7#KQ2H z?zZlA5P5DO#nh;iEKph_-ZT5>3d5jtqIivO#ELS9!m>}T{w-vcsL=wnkyCK-Gw>te? zI52kVx4Y7I@n-9eRkKq3W6hoOp3wR4vwD{6$Br`5QDb=+n9%@n=(=ndxA)`R4-TH| zh+YZwAFcav5g&o|AH_S^^UA&HJfWb@J2~^pby-eTb?T=_YR94*m^$HKu{rse_g6rKO|_rIrSqmAmJI%LNlAG z;dA#vX;h|CC9hNBl+i?z(PMCGILV9!i~p|eP@W2=`rPu%SRH33xF(wEbMPV6jKeNl zv#NGYYV%5zKolvk+K0P=M%Hh_5Ld34rU5D07=>1@W_~_VqgWq1=-MGTEy#WWl^xqurtI{kp@l?M&~P6< z+Md9%SL@MZ#|C`T5@>EDy&azhdsW0onP~laKIxqZT+jQn89YOCBfEqi3D=h3boR-t z#Bxw`_G0X4&Ee$ITgxxz{x0D)=x3U@XDLh5{?)_$JRvsAXgn&rvWKHeyt8Wz@7fRi z%^^E8yRH)vtVk`!fm0g$39Tp}R|W{49t8#}noCPixL(|pGGCh;#Hxj0SSM~MhSuLJ z_C>#Ft)lyP-6Wa^?7xZHf;SXWpG84ZzuBC3>P*8sBD|^xW2=W)2M+^3FiMf!u^k-> zOWEjjwC2hLEcwm4<&1|*CL!x#(8+|FQ)J3F-5!Rd#&ny6FZ{dL=aYWMMIuT%A8Zs- zNH^KjH^kUn{$IA#+fYJ*E}y1M0tK6Q87}by5vze`UcZcBUH}?()&P5Y*4nKgTUhWw z`vFG+ns!?v-X5Z^!CZQ!g};gdfh0SUK^93~Lke>ar|$feL+?j}SOa+rq! zGRbFUK_8778opzj{z$ImC#lUduBo4-KRJ;pJiHPCdvIMLsbDI3xpn8tKQ9ScE9Z;q zL(D?6%`j!UGgSoYb(*vWf2`3lb(g*=k3FyHQp_hIAR%+#(=rYG9h-uNVvwlnL_<+0 z|C3r79P>xVN!e_WYAo1#aYyTu>yJMM7aO&pk;531D!!@OH1j<-a(8D&hhcXty`>qA zax-}ox!NcVdgN&b%WQ!!jrxq=Z0wxQowRlr1QWfkRPToCZ2Ig^N?wlqEw5}5242=z z91#M2@>HN$l2ia($qD#l5yEct;k5>1>tSQ3-xYpegD><#F#atrh9Fh&#BATrNfkDN zH&?!;W6Eip0;sHhXMEtSEmY zHqK=o_gqg0!HW=c92?gYn5?JR60%N^pl$(&$5$dU5 zREUA!Ig(_1@S$W>4L_XX3*3h_omK5UqKl8l%_40d=1Fy;D{e=&?1cA^)co86ZyuMY zxV%vE0XtC5ts@#^;sBM|E9jB^UGvc+e$G5O@sVIe9Nc*wj0_7Y@j4K(#)F23effSY zF6}qCpcC-R90AcJiZz=r&8sLVeuFx-I|w$Xb&>ogM;W-wQdQHM4|C<{t0}n8gNFY| z3Uy}W=d2h^bHr-=yxinbNLlxzZUM=PghS0WV`sy4|7;mCQg4s;I7 zc7un;%Yj)$BhB*#m_u$gtP}t00rx*&rd$7;Ca2Hq=M0riLllX!H`689OSLd{g62^+ z7L(R2y0!v0_DbE{&=?}x-4ka^eB1K+d8sZ(_}5LG-hIx*ENUwb=2yhYH=n(`4@?~k zS${O;nWWdhUwvL`II^!p(yM6^G%Y*bg589+ll-0Boo4?MdHHvqeN(@Q@L^TU?rUcA znorNo{`3YT!@)(1_%~l_hGMBKY|NT81`&avx|rhbk}02HF5PsT`e76#-XqRF(*%X{ z2f6ss3oP+3hA4~YYnukx=;)kx{yV+Fy#Lxo&?qp9n&o8}noi?rmXL3gKB0D&0INTkyNI78Q<7sQ#&{CcGr+7!mfgk4?U z+zAc2AL>!qj~%f@a2`=EW+EM;mq$Om(_N%6QfY@{RC?1A8a&e_S`6_IoR?(W^=F+J(C2J!~NHzn9s|pj(jbXp10J8?deq5IgZ8>W| zvF6c8(~PAlwqLm=yr8_i{x8&k>&DXxlyoEB9eXUCCx~G-l;>l*ACA4|&##O-9<$BX z2IzM5dNX%)Pu|Oc9LD7O{kkZ)Zpb!#P}6|8 zmxno_m{5pL8!3H&O}wyen=Dh*oxIkZ-Ais89r%!~_PugCLt8+%N2da{?&tO#2pNk5 zW0X(5_Dw9Z%`!!hjRvt=xP(I;dvF6x=z~g8dHnjWmEkb})pq@_6oj|KpLxRPhx)3q zdw@h?mrlF<#ys zpth2HUGj+&6>?>&1m=VRYgRHDlx2Iwp4M*j;ewjwlC6o&jkIu4y&j9%=GQ?O+ZQH9 z1}5hWAB}+NyX{xt&Pt70)i%QKUc-W~37+#LsE}la9-MbOh?5-@NLOOOM2Z4`!3q_X z2>NshIardexVU(|bBDfyXnE)0&Pix0Vu)+@qYHOTucVfJ!ZoXtLX|crI=TL9iDq~% zJZSnyX}YP!e*R`J%_M)CenoQpXlH76N_r`+%S6?(ZnR`XQY0)qt~B=v!P9BrJY&}L z_@B8&Mw4SXUGwa@w#!T&!PYeN2q>R83_{DVV7n9aaPfC0?TV4;d?O_y*lRvI#Sz(8 z(|zkfRWAW{^K@A^jy#FqK17a1)jy&v1(0O1!>I5iuWBWJ{{5o-%^q!_D9Pi~d8-8z zm~nLaqHM~sPD`|J$MnBYieBCAAiCbidoM-iTW+%e83#YVA!?K9Gy;L#{D9%XUmmLy z%UM*tFSLJyv>LapBv2nODDTH}0<%{NyXbZu!g0`FJPz$C7kH9u2%l|#shYqv;Zjd4 ze6O?1RCVjO`@-lNQlo{797@-f*_nU}k4fyqaHeQPyNK!V{X3p(*UHHCgW*oS`?+*3 z$s)7E2)RHwe`rm7_eS!D|BS{_uaw-&?)d1Ljhclo^8>25o31UR{^spC$(Oeg-4-`y zC&b;MKw|oR_^z-3+nZ1RIJHOP&ytQ$3-6~J@2`$eo_*A1zAZj88An-`MwJ;Az9md} z8Krso5y@xYsFIZhHq#j9&NZmBax^jopC9k)JYVN6A7yc>`h)^U6+5cYL2^y%?KaM2 z4^2>WLft-a)hlW_^^ep?eP@ObuA)&0!Ug2BzxRvB4RMc`A37&W!-imX^02d9&WG}3 z7lBQouS(`S!H|0!9guH#s-PVX>Lm`8w%}K??QOOf;FfYGnkIw*Y;Q8UvdN-wD^#7J z`zagsqCq*CW}b?I{wVb?*ECs$cmB!KXT(P~B{dPE@|YO$fnD#J#Ztx=U+Ly-M>S`QzAPVwIM@(ys>u;p=0~VCk@BLOtpVQx4__q^|ey{fRswxsqeN)4C>3-G^%$N9$?p2JIt&0J0*r3+nradaX^ z4!;eXDBG2UJ_eG{{TW-0my6h@Dmx$2>Pf?eL>naAnya1IQ!)~U2-2ExYhQb%Zd_gk z)kla-4O#RM$V%z5^mo>k4oN^oR{-r@!==oJCfY+wlP<%20HG~>7 zq*HnwTg31A@e8B}MXSTl>q>f7)JN^QyAq=B+*zBUS+gb}|H;_>ll}sEeVI&0)$zbl z^B=z_LochGz5RwGCO**%oIM(d3o_sK!7ggwa!_0ln&~k$y3+4d)hc*}0{>&J2RuKc zhHz?^@8gCnekdk$F~2qx&2q>qUFnK_gd{LrR=E)Ebav3-x4}!T%1qHRte@cZKD%-L zRXz4r{LlExb%OsS6@An@-*qOtf=j_VTXjZ2p*ru)o_z6Vv9K5?QR&iowG%|w0H~I% zt|4*!!Q8nndvRH`SzDVbR;4bJvzT%vMuM4Ywilc0&Q_kSgA*^J@^NR?pbR|Q&owt| zI%9J5+-qRSe&)&q(sZZSVcAjgQx<)@lftGI3#^q9)4-#!%>5!!#d$H{-{#o%L$L?ES55Ryg6YdOvIN00#Zz3HuUY^6!~YNO+o1;R^-%eM zlsTo}sgZ)=>ZaUb$;WYV1l!5Ar`J~c?R;F@NVGWr`JB4$w8jzfnm8 z>VgDTBo3Bc*^+a%=ugLou1vT;drix4#?N1>W4&m>jOPn-Sn0Xt?)bKCOU(Cvi-v9p zXU{MS$Zq=@6X`A?PtZ?+rmQKQxW{N~^0<7-HQ z=A&PrM4LYg_lxoOGH9U-u4(x$s-v*Qt=dKM zNu~bBp_|T|n9#G!ij*DxG20HRF9nra#}~re=znU<(p~8JN7JzJi1*}s0q`AZj+n@W zT~)WOnK%^{LSw9y3`NT@z-;73qeMyxVa%9zeFJL!4ydOQ=YY-+HPZ&|%{NIvSijr% zpGkUl)Vpp20K8TB>rGATdjB$4+q`I#18b`yra^?hjghBbv`Whc}&@H+t!CD+0sxCfxc9mg-5>t zyBvoB_Zxf;uA3yBIyGobG6MG7MuIzSCr?gIkgmxYfqKi^q)JW3x%{vx(>3rb?#}*L zJOS#hWnK<676KGaB3uTJ4m-UobY$vpdo*OKC^PG?h{d0aWNK`dFOQ;=d$0@#QFA|i z!-CEeUq4oayDYQuW-G2LnHW4b$RgSJfVcjiTS~XBIVUz{sEK5+hWJww(qRrg@~?;c z+D}~Jf4%%V{0W4A-200(C&8oSk+OIl>Y4#DibJC(+r5aRwoS<7A$JNlI>gq4e&D8u?~=dYE%+DiolDmPq2jAZ{oX0Rudt$ZZldXVpE2X(|N$iqo}RfkipB< z)0Hbtdea!@Eq*MV$dr1hAa1x~nTXt;j!Qh{TT;JPou9{u)Pq)xwO1a5O!&1eCx&ZH zNBJ*B3o=HW&f+ng{HE~gfqV>lpYPDBfuO+U1+Yy7bhxPh;EVakohc_a)VF{RLVJ>$B< zDcq(-o-o(AoANiwfCRRqb2Du6H;L;`_W-~~k?*NZKoR5~nI(zvch$27jEOBM0~`$V z)r_h=N_q%+>7dXjv&4AuIp+8%YOs=LktzGpd*Fa3bb0AfGdj9FReUbjRWVz2;lNo@ zcC;b)fOlc5bD@7uK6dnK;Im~Ydf-Y*!IzF2-kfkuiLV;_qq3%fwWTRkX%p5zk=O{J~LV#zv zqVB^DGpB^gby}r3Gn`; z{Q=QWS`Mg9>;Z?ZvU$3Tnyjc2nN{7vPsStUqFu+ZBuox!FD_mNf~?kk=T()uIPVLa(q9|dE4amyIOgR z;uU%udAaVz`vdX4>=yNfSoR1gby=c_lAGxybkopqQQUip zTPQ_KcR2MtSV035GD9vfDQDm*>TcDJ8g~kA&b7`}mYEkZ!SM_d0UP<6emYZ0-{HRXxRLPX`+2Ij zh)@-}H+G7bMTEv)%}D1`uN0R%BYyc5_SuoHlT?iEg4eE2M$dhqQer*46d$R%TcEd0 z{}uG=vGMEHEus%lX^?Pl%I0VU`Fa*n^1Qd6!c?f{#G zw&S8Jj97h^@MfU#dwoqsDfA9D|kIf+`fO`eQp*%O`dvK)KHmTmn7kHY3_e3&WK zcY)-3anA1pII$3D2_sTytLfE^(j%C{_NT#fVG_$`!*6GYt;)EL49^}4{N1?mZ2BwB zR~L{?z>ZPC&db~L%Rc1&_5I(D{}HL*%@-}l04M*$ORbEfnTp8<)z>fgArV7&AV=RP zwTF=am-_%gG69LN>cqnfT%NgjrE=^^Yy(J+0<|g7qT|FU-j7Z*mPU!U)$5%@maP~qMkSqnP^NnAS?7q7f~M`9M^wi z=eO|!d*Q#ukza;%s_u-w(-Z_vbBlT|aIs^i<8Vy8J~ew`A^HgC?774^-R?Z>*S_`N zxUmy0hn%mFpQ7p56?R&15eJRe__bkF5^^BFM+MqCxz-@+HZ$j?#wYsNO*ifVT!-ra z)_FmA^$r?$ZO4{nT+fesSK#Kr)8UIr+tClBACE_*mL;VxcUmQ2k?Ptu#rdwUW^J&r zI(J-S*SGARZ}+aA2S3jfeNHe5_?fWB_yPY*-OcI0n)Q8M@qKL&1>d>F^y>#;IbqHU_-+4tdOtkBiQ((6rF1aC#1^MQ zjqGTIZV4Q2t(GwDd4GO-a_qPpZQA5w4J3^A?giuRc-{|&7wC0oYn&b@V?CfkmuY3p z>nkQ9ka(K~?0wjZdT)ON`?Zrk*^ocsGxrLw4d6%Ahw2>rO(0@y;>BaLrH-;@<>tnj z4Kx4j-~)WvsisD0g${k-|Izf0?RCB1*LSSOw%ypao!z9d+n}*+?ASINqp{7#+DV%1 z*tT)!cYW{wv7W3KFxNT9`pj|8;jk51TE|K2x8)v$6fr^tB@Bh1=tfUEV!P2?=gduaf%im*HgPfV!%t zMY+Y&*!ar$is~`{O=D|(gDJbdW^uQ_Kzzi%uR&(m`ovK?rwC&%j!bay6NP`Qc1AeJ zK4?CNkAmm!*%XL(NXlKB5YXSspv6MRFU8TGJqJvq$BS;J+~D-*h^k(%{pSA<*GC2k z9@|XV76oPT@a=|%8NU6apE76ZLM@gZw70*q)u23_v_4L*ae=a#^dNm-NpOnBO2zHp8cYh_Z;N#k<0w#Nc)d8nFv~1S zHko&0$ne6l)6C@p(+Gq2P*I#WW&|!`SzHaL*?kEyRI|DNaLewxIgEe+l3imG2JPS5 zB1$I9=SFbPB7Rl*?K*mDef1W?KFYP*7!(wg1|OG=AMXS_*0XowH(QWM|9xS8c(Hn$ z?0If^-#2>G`#6y8*}>YwW4Q_n4x1P4diG!E{76_jaDNB9mD76JwSOtlgUVL30>0OM zg!-~%=*;<*K2r!UoTJWx?YCb)qXRzjeIj?j{W)cDBxm`KN-;z&F4gdxl%1{{&_^-O zPkZz46=x?nI`cjoPp=Zda+$J$8s32+?A8N)zL$$8s{k{}e)`~{nZqVTh7hDPqJN0t)-PP zDVCd?<1V`|dm#pFq&V!t-7A zo_ZRx7^e#YmDL%2#9Otv6m-~93w`x@H+x4n=N6PWg z&F5HuAUeV=X!^`6K2!Cz+2D)uN)TGDw8@{bWPzEXVWW^V)15xFk+TwI`)a}>3unbl zslf`^YghXwh_{CCt>bA6r+l_H9iUGF^+) zsHmE0ir|;w4!tqC+8RkBmGnw=0Ks?NEt6`lhF|nJiX}5s5de>V86oMJ^&NR zz53^ykF}nCw#3+d+W_0@HR<Ki;SJrAP|eR@XT;zZ6-Wo^0^Y<_EXc>Bipjm^%5_16A?AGJd$bgAfG=Og`(P~eO`;3tOZmu}`3Q`aG)ZDOw zndCgv^V5v#s-sX1=TYveld4<20Qd{A=H|5?{1aop;zS4zg0%B8=2ZjZhD`jOJju|g z^I~2&Z8Bx3*;Z_#W*QDnos3NWN>vbH1rTmyG;F>7b&2aRbW9)FU`FY*(TJja>7(h$ zqmP?iVZ>sHqIjjN7L`_#v;tRqzNp^S#CVJ6IN5glPa)%9qR85f%nfR8_as6XQZIWh zFf}R@r^T zQHW5lIp95gta!I*uQs0wl--F8>sw#=P6?*w%z|&Tnv=h$@VT)S+k;AU5 z*%y50T&MVWHJtXemC9dBA}@dL^_{EvM=ZNq;MXLX3C?yruvG`MR1M4?0>#x(9}-`S z9D_7u$fWp~f##d0mnH1kO~K71&gQe_s#BACr0EkGEou11Dez8hNMrq za{p#3qWd(o%fFGZSp6eTpaf_X^MR$Mw|RY9;gPKcpe`OZJiA`6JW4US>;gK!f37pQ zfAZhGP(GJIP9;H4Mr1oa-I&#E!bEvbQ1##T>6&`}DBN}ObnWtWz0^t&k-;Y+^6K{9 zX^zY6pDHrAlp0#`CLz|PI5b&LIDrM~%x``uYNM1wgT#!`>2gRZ`XJUw#ME8gd83HB zvr6*{$sFyW<{Z39JIlISL`Hh3p>OBpEebd6s#n{2+%q%U9)5^*lq$lC>%MYX1xuVQ z^dWznlOqwC3=2oSmMKS=M?u6nQ65!#bXI;^%fr9%t|*cK7ZmkanX?zo!+Ay~5#cQLK0D6P+x zZa2SH3t9_Y1o~O>mbMZ4&hMdQ2gNn0K=^=kKBBn^mf!r)L?(8rG2xrOyF{PMu$+m> zPUPi`+i43EPn5wSqb`(sr9%xIaa6}k0wYPp}+7d8u$|N4^5%+R$IbeL4gI z=Z+Hc?d$Lp>67;ZNk~l718C-XzOjbT%s2Tv3igbrxr;a0<;c08F+=CMqMi0G*FFrx ze*2 zLB;0bn1%{bqzTo7{I73ppcDV+_Y}G%M^`8@^;3$@{8I63$;dJ$fI>HopzU}C4mnDpD$iTnHO&h*uxMEq)B&JqSZW~2fMV^pcCQDt7& z?Z)yH-g%Y1IrI4H4?Y-Gk!gurlExppwPYl|e^`UB6kY3n?>J>8_39fkVA3kb=ZuED z)l6nkwer?Wo8GTZ)&%@2n4b_!24wSx*iJ$JvP%f!&bqTiO?j&d1-*FPhcE8! zy1cEMZQq)CPGla|y)Qs!T&L^a?l-opa-Z+nf(WxP$?M?Ih4@#_#6TLGQh|IaNFsgm z&_c_vHjrxyarOTcja0j5DGT)NExMqawhAvWq* zw(~f)pJVph8^wqzfBmP7WmHW%FHZSe97Hw-F>j1aUT>jJ+h*%D8s`}{u*+2C3CwAZ z=5*T}&ABtFxOj5jD6B8q@Gy&T08n%IlxS2xd|PDA-=PF_Za{=r5UJ_MU?R)V!}RP; z^M3p-=?r8uBne~J@_lR=r~CN+Qd)F}qui!YW@>_Jm9MQim;^jaF4Pr$C-Xb7|DgML z^x2wvHV3^8(0TPlEFj3?x4d&~zx982fWpUM__4uEY(Xd{=l^qB7>b$mWqeN_#@qt1Y^G_Z_{Z}Aw zet4qYAqwSs2STwHUYhBc%2z1yZ&)N})>=^e=PcD`oQ4(pkqiBlZ6WYCF;bjW!=S6y zKdyE}dOOYayzA>N-LpJ%V=5O@=g~%A6o2uyQ;Bu2>eq~{LQoyy4SY<9lcz(y)JUV! zr@>ou!Rwz0uKQnfLQMZxJ{n18*$;CG+KG@nqiV&0Za-)*PhHjmogIS@w`sKyQ3)3mDx(tQ`eJTbW!QmT01%kyE$4bS079!l?4 zPQnw;9lQOxJ|9H6hD%&7Zq`klX>on{KEaoPP+P;-(HqE;TUDP8b<(%MiIx95fPnK+ zelOs9uP!A*StYU5{h@Z@-W;hO+1p-Kp&`0)UAWisUD~vTWjuuX;pwX6hRic=UR(DE zQaBpcjgp*5Z3o&n-}4R;7Fj%QF%8YiGL9I}AP*e|MgS$V(5kIhg2Y7@`I_ZW#5Rka zu7Znmcjx=R5z_b2HH<{Yp=HRYg==Q}`;8Y&=3*}hJmVzqb7JJX&TBK?T&5Y-*GuT00=WSmkQ0#-HhFbIrFkxgCwiAL-gR+je1BH#Ii5SSn5~${1ev z6XJP78QV|lp6H!auQWHU?(FZk=yeF&wXA{)y-vVh7k2wsWV|f|z+F$91Xop2h(~e} z#DYsGS1%6@ocsZv@P6#Vcqohw9-L*QAYE>y0_ku47+5-`tyUMUhO;P3uzEjXC@S+A z5klfpu=FfAZDLM_?>?Wi;qFe6dKayKKAP%aEuC|MfQar?0v*>&5}n@t~lybELR{0AGjxYR?gynYxv%1Gio zg`m2zKvFD|44pwUE&Cn)2$yVdpvZG@7Tw$$GgBEOy;p_smALcA3Qq(l!y4PZ1 z#h&5q@WHuB^DHk2Bmu4JQJ2MLz8=TVBzM&o_S6fLwq+H#vx>WDF*3@K+bRIFc~)I8 ziE+eCkrkEJ;LIXeL7?g5>_GGe@7!OlSU_(Sk){OoTKM_&;*ND_v9dSEhKNut97=3+ zi@q}B77QVFm(wCzZjbj7l*78Avyt7dFDmTXU^SB}klqi$UytyNFFP#ULA-6a16Lt_ zg##;zQ6W&pC}cnA6dd4SveJ?9XLN9H7d1vrz>&8O%GBJ2_oT~JZvw1mz0~n_%oSX- zq{zG4Y-r6;5*0pL!mSt@Uz9BOEm2a7MBE$os5YYoq}Ol<0yPP*H``jr#aXZZ17&91 z*_nfMFS;IH6&lw2&_9ONGR;+j>^|ppk?R3p;F*cl6!h;IzmB(Z4pnLrhOiFGD2U+{HDiJb{fXXNcoYVfw4VxaW&?A4!5JF71}bp)7|aU1ZSj#T!+JB z=~Dv;@=XxL1mgl|PX5#fqgtp_o;G%FVT4j#r$KH;=9aOABjk^Am8pHoWakON%_$8h z!4d9>neKRBPruoMd+;1(6Bb>3u%ZoN`FyK9M=d>cZYqD){h5A_Ul4u3%EJv$)kujd zi96g2B4Inkke-vlMgP~_}7O!*zIr4Q?C6HHY}zCWm?_e16c)#T}D213Q2d7jPS z=yf*XxDq6|lK+CRk0}#&)ao2|zs8ivA31zOw=-2frgTmC3E5Yo!Wol|lNBg+VMC;6 zw(kC1i^rI0Bs zL{oEf$3zgNZA6P4ta9tY2G3I}uoN#$`bYT4_*C z&@gwHX?MrDO^5pmkLJ5n{=u0xa7sGY)BT3cJOuk$8u@M zLd6$xci`s3lF_TuTbX0j_%7P2{aedPt;?MIG=x4Z#Fw=)DJ%+gNAyK*Dobn}=Dhdr zi)ZC*h{q~>fB{Rb-KsbHG%;>Bq)O$cgiXBk&AlID(ANfMuIu^+XKjj!UTuov0O5?e z=#5*34-?%cYlAX(^j< z7x$;eLm_@?x}OF1wKC*Z|>;}*j%`08^ca8JdR%-%H zSX7mQY~QyV!9kH|iRbD**sGx#v}^|}z}|oQ6fz#r98y-VTQ2=mJRCAi;^ZpbKLFg! zIX4SRH>JMuj0C>KS7i;df)-~4gh>*{$RWaxd=e?l4b13+JN%2O1&%`aUhowp-oJm) zp5VL7eo(#)vS-O$S28<&8_**B)*dr!gWo-~X5yfd_w7UI5!#VMCVBq-*s}TZf2S5v zZKvTJG^;H%W5l`|y)JPfAXbkjMH(VO;r_7fG5128I$3{)_pE|IO=x|Mt5o z!tVaX$J!a5-`G0M3>r|q7YU;35{?2aMC~4xk5Jqu$jsvM>uL0aOwaGI;O$f%Md*a6 z=cbaZ{OT97U!NbIHHykTLqp=`b1AHKhr?OQ4s}vu{WLEGOW-%gTKA}Hem}PT} zbFi5=+Q@J?M_hZ`c2lro7uCrM{azQ<%&s~unwl-TX}V38%+gW6X6t0#3@3yVV}3Dz zE-!J1P?8bJYi;0;x;krbvOQY`k=dWLH{-E_R&9Mdt54e3?OI&H%^=X~S+m_aZt=kk zMAk?t*U33X|L*xVDj@iLa8%fwF4CU#e zWdK%Jeh%x7z%3sSPx#x+vyDs!16KoW6ep_+pw}lR*N#1NKo3LCH2V(gdBzUQU+FP->_LtQ!VfY z#byT=3ET3?ZlcqY-Xlf_hTo7#e&;JdQg8p6Uh&Hmob$jOOu|Ana3nU0ip=Vq9FsT7TpV71X$H9&CXU!Qr zJdjB5ZEFHhAj2j$`rk615@Us^^Y~#=HLgu8#c9 zfX9qy#kh4x&zO7mI4W;$L?X3uD6_g|XP345YkeZ?#k`xldB1wZCmXaZSY6`Z)N&cL zPcE8-GM~rfc}-*-j2)AfF`fQ)ztWr*ZEBQua&qv~F#e~e%~kXQP! zra}XD-pI?{!%3_RGd0nfDwoX#!A+5x8HMxUwChl>cW+flVry-McK5CdAAyd-P!IDf@Chc%1n4*wf2w zIV)l~{b?TM;?vq7ZpF2nzZL{k_E2i{TCo>m0`jo+lO ztA~Eiu((V^77f&mHh;}81@+sY72U_+0^yCQ#fSx{J%OqO-G(I6?q8B6PNDF8n_X0! z&rRfB*@{`|cPuFKpvd>hVm2<@JN(D1N@O6m4;>=0C7?qSaMPkJX$I=V0eEcbih$Hp z%&{Ps-}*2R$l?1_bx!#b!(d*y1waO*#8xg2?xcawtOSG|SP{0XNm)~o!OqS+E-sF; zKIQt+BkEPxJ@e(~A3L`$rlwT%ch;u(1&lae;<|~6md{qX+MMbrtJeVrjf~t~UAc8s zx45DZ-Zs~mH_H$LPRuFD&yPM%&qd50<&KrOX}MtdiZRFqLwZR}jDhej`V`V~sloE2 z#u{H)W5F%`IqQ)u+uv;+4H}tDW7?4r3}VR%DNSSSuzNF;ET?0p-YvOW7GFWtt`{=S zncgQ{Uy7=UU%h}2qtfs%;Znb#H=r5=BiV!M;cmf80FsN4_*|SH9NFRdELzO9ZN7J% z`2V2?<>W#PmLQBoPWQ@sXAjpkANI#y2_-8BCH(n$ZI`qc<+MFEpIC8A<9~>joZNgt z0>b4}&_3|jLcPrqJ?u9mP%Fo|7bROb*CR3 zC6sQ)OVo;OlH8PT(53ssGy{o3DJ}S%wUhDapHDC6Q^7O@RA(Q;RTW1HPg7# z+3h>lyrW}Samesh$W{M}vShzfv5oeAys+;3cus{`_)Gp?)y!o-O$B>mGdiqHw?8U@ z1WqK?^WF;LrVUe7WnB;;s3;r=U=9z3#St>Q0t_`-RG=q3!--bUT<&=z#r#gk@-TJC z=O{w4>rC_!#X?1S{Wtn4NfVI-Qsmm}H-9eUL8I?V64foyX!dTH@cM=<%=h$w6@Xb_fr3{U6(t?S(i>rwe25;zh?LClhGal`R=Q!PD`v^ zdd#0Y=%VZ>XI0~1HrHbYK?2~G>W-HvypvBRVcSt{y}7k|2GZRC8H=^J8g$%&GDntM z9@u7(Bw5HHmJK*@A_#SN%GC(V#W3V%*)rEd#A;1>k4Yc`IsDD-uPR0Wuou76{+^#I z0Bs7RbaARH6u0Ho(dxemwg=zlzR~o z9kNPA0~Qr=x*NGPndNj_-PBns-8RV=R~J=E?%C(>$65b}C0w<$(Qdj%Uvf<6AR{A+ z`KZi@ssEtSHt{6d2L-0g=$J7bCEZuiEaoK~Cf!J6>;ppNFnG22oT@#%dx zbI3a!)%XiRVJh1}Y8B5gFqLgWwr&FN{>RdJrvzf3pJ55I;6TpJLD4;cM2yCdMi#_5 zgeXYmo7h0093=KPBWah|Dp`CzHo;d{Ix00eLQsl?N-K(zYs4hq5d_>^zsj(GOBX{F zE!L1-g+{t6aQ&RbK|!z?No$0WobU1%ALuU+{e-G`6?_o&(OUOvWc>`qmcr@gN1XPh zT)9V`2$v?&ip8=ns_&qW>d)CO45i$-zwrNHeVzQ+@}Zf2hO>J~Z8@R@XbLa1O4pv} zxC9nz6i}OEmFXWE+$p%pY9W+JBEeLADT!bdpB)8ov~nVRWO$k7$5BI*n4F5=!9td% zWW%9B@c*p>!pKY6J<~HX{?m|dT!=pS^ekj@AE0i+&)h=hpt~;cg?nXC*bzbtCY4Zch1Z<;9g2pho_jK z0AhW7F+G)-9JWm|tPOS`((EQiD!S+RfMOfqUJAxtESoK@WbSD4ud8Fx_dDg{vhSf{ zcqm)VxTP1>o&qaJDm?CV9WTG^&a*sExE`uKPa3DzJ9+Qncz zZT?ByWPh_f<)Zh7NK|T38xxFoe zmySe8mr6;nh70h~kI@Lkwl+vR;-D+cdz@(bfck%2wT_fjMAwO;K`)@@Q7AXOhIMaF=Mx_E_D~_7*HmasOv2xh z5L+l^A?PS&o5Mn5A?Uh6CU7!6K2+hMjVNXE*DI(;(Gx}9b$a+y#tJd zSg_{a9BQ)x1MO%y>`*bZE=Yw7b>Fg$-S`}TR(L~=A_(DQ@2%_;-|n*cZ3YxMO9-Tc z;301sqg7_SAXbADedWIWYihb0EW@(Am@_&}-dKU{O+NY!n>HN6{98NWX^O%bS2fG*amCMuQvFDdR!_|uu10ooU<30Ar5+CT8- zyGy7lrp4G9owtpC_sgD^>h2pyvmDxMbkNU|>KiXbdGa#kAuc&jX7#*sLBDGYcK{_L zjX_+Itz(LKqRC;S))*C_3ub$t{d0H-_yrSoc&lVQm|}V%{HgATgGj+8S7`6$B3_pB zf=p)&=Kb$!9*VyV3_U&W&U#*Rj2_44$?mG`HH3Zl%x3ErPdGPn)4W`H7YPdvrSSwJ zSlR=i)TP}oee$sAGn^vfsRsy=-ekw`@Sof{J{qA_M zgYj+vd0Pwr?ykQ3B(J`!rfaKS)_T@g6yeid2X!lB3Gzxe@h8J;yiXbhyyh&YSGQ(V zfCvnd5U34$?5-eT#32P9g~%ET7Y4i$WrYIGc54GQ0q?u-QqsS!l=J!FQ!9XPu$EKl zV1X_ogo|O42f~4y6#;MLKrP*aDbI6aXJZHG7*>8E;ND_1tc*uowEuD4kifo*(yg9+ zaBtVr-uwS4v@Hl}ni8rp2j#!ViW#z$n1ns@=X-J^1d{)58?sq?7>a0e^Ni`P60my^c8H5 zw&{hfXKtEF4)pW7!o(e8p4DcI7i`r@1J~U>Qrb;NsS1wl@@?d!3DWDMctOOKo$Pbn z)2no|$0|WZuf{LC=*?WJ;NGG7q)i|L7p9HbE)k8e*CEiXaI#2T>z9rdfAqb2%<)PxzW1an1U@E@QGz8T?O{2SB`+jlB@hll%MG=GVo zV|q~ffvBEaTPvtQMw`wXx!#D{?&%?;Kkv(iofa(z0F4bVB9mZv`YcZD0MALkm%OLI z&)@UA7(K&0k*C8bJfm5syY59Nw6eqP#$w4)3CDU!i4Zs6S->cqI+8~6T!BPra!{cz z%V5P_-<$82@UyZ;Af*PN4~no!W`S)P4kNRI(9ijZ62sIu(hE7XtBF(~A0#nTDW1;3 zYghx*+}u>XSkhdbqMQ0t=NCf>CR0fO_98x3hoDrmt8;tCNL`CX|=gY zF%{2NqA=wV1A2|dCRrui5C&k2P?ASTxp?4ATxYeD?F zD+RcirBH>H-1zT#yGEHDuX>8E4-v|T$#)K+Q%Jfo{ExZlzJKT77MT;5qPyO%Ei9D8 z?gSFM(GIO>XOu03v(-9`Jql%Tbb`)MhHrAss0d0Pyw`?545`-+ ze-$`5%E@tV3}wB@CGOk7krll zs*hl2g-pJ>!?ZL^o$n?}Y#qpBh^l35TROv$?} z=ouE!?zZ)O4pz8?W}YvNp0hQv~~!Hst6{e|(%h9qX0oH|oPydC6X! z*XU}VgMGC%H5+8}BXQ@1ssf}TSe;~#;-ixLDFI?Q8aNCA*q!@tuyyfpIJ)qx#?WIL ze>;+CK$hWr2GWSqzb?4-k0!G4m9Q-<+MBp*#UI9piY#o+746f;7bkvI>aEncFRec- zmsmy&2IxleOAmSh!?Q9vTsa?H4ZyL&XgcWWTY+9ArixLJIjnNQ9P z9u<~{iq{A&QfzQWS_m->G*wkHP1YtH4$7XVlZJotI%<{lH2hbFj@uVSpuFm`u3mLNnf{!U~{^@nMYr ziErc__GL0md;%Up&SjpiKtNTRqh}ZftgI2*?;D9qnV9;6Rtkl*QPr+Nj-vpW*Bnyg zoj5U&4=Yg|tz*8-)=15xD#9}`%rqB6ile_N}%MHtuUKZ4-D&jKbuY$1OgN(cX@Y9)TZd7b&VA zDW&!w%{B^r3VwoupehTEFqJN_2B89*xmo2 zR#ZSWnNT?-DB3f7{fRQFau@JsZer5?3u^wUv*xLjs-3IA(5Dp(mzn|DXs2g={%Hn! zV1GE%Tb)?*;=KW7Ww1@xbtK(z$=HY+h`hR1uul5^INI#d{Oi`V%K)+Xq9W*szCZL@68RoN)I?^zQ<#rO})sO*o-aK)}>z&B=sHhOY zFPDm|7w>=XJ%@(S7{fiNj>2>Ox}T%;j*oL#!l%nAwQs%rFDgi6AdO+8ZZZ_apXxnd zKxD7EMnfOW5Qg1**H>g8yMm&Zvn`@`3A{$WJN2L34G{TjR&6yR(sK6G>*%t^Ic%7t z(#xRLQmSg`|Dyc1)-v->{m)3Z=lQiV?*sg~JGAF2%Ky^H=iNuvmyb0G*g_7qpnD;O z3w6%5nmW$e9)&#zR=zv-!YJRbO#K3BK}9(8O|BSHAc8Ua)Vt#VgHM~Y^}ZjG(l&PlRHMXkx(>&?h`hT z)K60!FEx(4)}q|k-gJy8%fxy%t3hJ3eEt^#4<){lDUO)T-zHa%qk@-L`AFkaHOLP9 z)v;9bT03kmS{N3D*wU`cYBcV=9}X4%KCcmC$rX|eLOU`1&}r8z@*I(9eZG_pWaxHn z_`eqeA@AiDpBjga_1!;MNc*Ge=(K;DndMuPqp(B5yEYSIwt8VMemnmvZH&mou^ut7 znK`qWLs`-z!ZghmvHuiJmy>PW?&GLRCvpO9plaj~_*u{arMeqayAAgAPk%W@zC+3K zsBoqa7535TG+lS?e?kd3*+bb_k6Dc!fb6_DTlyE3BGs0!?5xwJ^3+W9x;)Nj zTF~C_cv8#Ix5QAPy5zHzDJzaXvq#06e(CC4e6iP_u;Pm7zt)0eUL>Gk(b(F%-aQ*1 z(5v;m_3n9rH+mhp@P8lA`*`{|F70{D%aMDO)SS-sJ@)c#Z!&BhL~gKTzFRj4&ieM( z!1^NPS*(7=yF@?7Kn)vD_FMYZpH#t$j!f)OLdDtkRyWjM8hC7th ziVWtdPf{w*U*M!vsn@*R=k!+BR&0UYSDfJX&y-nEv}K`%9rTSOd;b9qXC8eBwD2-` z_N#>#2L7fCBryQJkY3XZ&m^)$T&hKdqy`{vPp|cD=%vYGk`ZWnRSi4}o+r^27^WSY5rRH%uEwQ_g4rdTPNKOEK?hPoVV_79s3H5-#Z z;}VX$QY$J-Mw7JG`B}&QbM~eqaQ$J~4{*^sgR}R9d4ux4R*|{ugy{_fIZu!q^q*z0 zW(YYK&ZFn=3%}9ecdHsN{(Obz&q&3o3B zNA;pb3Je*$1laW^np8*A|P+%v#7pQqOTD~X)T}p$=Wg>2Y= z45PU0kooBLoM5H35>|i!Q7W;#UjT02}|M_5pS*N8jDaAGeVP=us&W7|5|)+7pGrKE*{TBAF@6!YhS_c zANOg~yvx9%)bX$8Z}mb)UCeh95_j8hmRsqz>)@^`v+xbNELIZ_uf zA$sm;TE_@jaPzPHG?)r4)e-h$TIYmQ`%+Oo*xWx)^H-^||x6j7d7-FFTW5(MvCu{KDQ^WAh4sNyX8IdQZoWpnl|M`mPDX)4x=)gRvLo4oCt!L~=uhld$>egWQ6x ze=lGNA|c=HAcsWwY{`?L1>m!oK1oo&>C-m3q!H}U(}2dDVhfyJDF~P*OtiZyo51?A z#TWU>6{LJ+LQcg4H#!garW^!QSON#0WLp5IboKFU4>ZMw29d+N#SPDoZ8QT?qw?B7 z{oAR_{u^crFg0sIG8+C28v+TOSc5#QTA>-Hz4XBk%+@FSe>b#Rr&b&Jwn%EM1@x`9 zr!t|f>QeODdRoVtKRgzmTy-`!Qag`BBp3`s{f*bf-oL-R($!wP5{W(?^gKS&7$E(k zHSSGebR@Uo&5z-tbX&6v#spxmuu zoY>P7eQr~IKiGJ#^uPLegcp5xKmAfF@d#~H+j%|Q?7C<;jzb@>c!9Fb7!|d+XF^>< zlwaryUZ*)+N2F705(7WVF|#o3^%oZJ5te@W6Bs&odySL>kXj?~Qpa_qV_^(pV>S0t zMBx1%T8>R8u0FGdJv5UtBn4H=fJlcPpU9~ghkdPl6x4PZ} z_oL@lc0$ed=Qm-eck>3Wyb~*>WpM48keyTHLhVGRdUNvy%Mhn6fe!Ge|EXPD}P}Rk|>9RIaV!oU>Oz%?%Gf*Gp&p-WDZX`Aj zwD1i$8SlAT^cLzETRPu0Nf;M4^2cM9k}|+!8O9U(4LmEiSkcBvG=;*z!*Qf$mkQXA z*>P$*F3?7fyDPXbQn&Q#vJKYmbrAc>OE+L$Q^6Oa$`aFy@yzK?nF#bVm*`y^PN zb+0dH{2B(J!P3RciDv*ISG^AI1cSS*nh83d;IvXI&9K_=J4T~4Ml3_;hDr=eu+|!x z&ROcq4cC;r973$3Ty(V-TZ?2}>WUb73)OSO5(T+|UGdsk+eBGj$A%{nPgnp_{E{@5 z@kz9ru|k(6jjqX1L8m6I<7XR4S9<*IE$OD>zY2D3O+XDV@6%SnX3V~lF0}H`){#os zOL{F1kmwL^L^$e08fL3XV9+ZWBW)B6sS|=Javvxx7fi zlHuk9o?%MK)$axqxag-z(h*p(!C+{0sc6Uu8V|yK6-3#at$%KYoEAu0xNZZ*XaWt7RkxM*pzAtHZqA#}VXomBeoPk%hJ zzK`X7Wca^aJTATZ^!TEm#PIg`>(rp3)j}>#NM3yf=(>wD?=iN2W-O(->Wv3Qer5P{ zkQr&LWf&Pu_?*2GaB4I^Gs)x#wf|rJVNu-rKG@jKdah!#nwvHBx_qdNsQk zn@&vduII{UI;f8;y;xp%9?!8Wf3B%X*R83Jrtz;M`ime)0d;2>qW4?{0gV5~z+2i8 z&KoGfNXmQPm5C8{B(1uK6CL6z>2-x~s4CRWR zkL(3&AZJO_|6?@-6(h{Lk-m7>E7QOvu*^9z9*2C5JU=uoB``M~6oa4V5zGq_{JQ3B zy=!af8~lk#p$gB?88JRYmFrqJvZxIqXPp>+s%xgyFO1i(ZEOgak30E4T<(qycxwJP z68-0hNUP%t&&2V^YMVap=-`$*HoLcAl;qMiotH!@VPqhYa!a%?A{dU-Eq}s5qBF{- z2!|N7FpH@2{}FXg;hBZYwvMe1JL%Z!*tTuk_8+5T+qP}nwr$%wS!b_(&h@;VPgQ-R z-WtR1r!-@hOQG!L+zK$!=dRaSA`};)ULGW@l$M1qJx2j458|9=F)yjjHu#T`CF-ar zF__4=4W1}TL>M|2<1J0v`JaWrBIMfOs!45~CJ@9(`5`jMyM@FfLQDBubO0dll&9Idy5e9~g0yLmVgADYmfR+g~^4 zL}YXGzVF>cWZhj+nNsKMD8Su^t$}keQ3mywD!fRTLfC~8{NTgDK3zA4AG)!r)?&L~ zlVGXxAoTmy^_w5y>x=9=`TL>!eNOj%^ZxI%UwQM^+f;BcBqKpN=A>LQXN^{=kbI@D zjb^-9F^%MTWWht8u|1nR@E_#`aVzEQmb4uNN@S5qtfYWi1f`G=wg-N#j_R^2nnImO znmwdj9%cGgh`5~e@R?k~*a9Gd-GXv*4yEw-B8x&LWoO4osT|)f!ZIwl(wG~tJRDD| zw$}N+ubGXLU6{C5k*s{(Bh-{zf-35(UU+0p-GpL4{yu5m633#{+}_xH7sw z=JYg4nLFsH_TY46(VYk&QKB3(>v)fO6heh7bOaxgzrDSJ2^>W+B<~DD;*0s|3d~x;9 zrmk2vyOaj^=Dp(oKG#BhQ7p=$D$fw4gXAFAadew0$&lXZ+=-?haVtk?PqbEBCRyf^%9=Z%Sxo>|sBtoIYB73YL z!7-j9^k+{+x_Ukc3zJYMn;InNAFQ`-%Kcny$44S#g#W>=v-bA*+8|G-UT(on0~Gh4 zZh`kX(63F~cHf=91?u~ek+-2$cJkDY=X1L=D*FwDa6~{BQF-Tv*lY3c?`2qY?8dI%z2n)-{Nt5-jBVzr z)VR9NZV4A7vFzONFuhhdo6|r^7=Nr{?)GtAn?%oVwbY!bTqi9={%rDsS8mJy0pV3C zB;?Q#_$!?Rqzb|oY9bXeDEOJ$P)aUI8-RU@!e{3( z!-E>QexI-&be-BCKKu>nWq*hNNrGMuLQFcwAOQITr3=CA?z!>D0zsi6B*em=6$;;3 z{VM73?cwws002l#Q&Fa#c_MS7gXd_wxoYaR#+6VWgcefp&p^q0H5Z8a@RcQ`@!<+A5fyv(H#1OI<>a%5m(${GIK)5CuE=<)05St zPyc-zQq-a19Dj)((egSL2%gO^n;-&2JBvA_MhvdBuKP#!{sO>|Kk4l`+2P7VK&G3K z^1I)p)W826VML+iGc1i;S=dECWR{K?+A@GGzCw9uT;B}ivX~O-xXP{WqfEcLaS_H> zkZ^)~*5sSd*T1ucCB=h$?v_M%-;HV{`+O{Be{RfQwrJJFlWY(r7Q6V&Bew z@7}a2wq7MwF+z^T?>Pbvzd>@Xq5g*yCk(dd7ZNNc#_zi=MhLtYD&`9oi%+aqkOKw{ zO(5wPbAv>}hrF(bIvka`BHp}t#ryp3D6_a_IoaEvVR`w+(eeIPmSo%B{dL9ee5UlD z5A4G)@PJgm5WBi7!7-qxN=vVKJ0bO+3j%fyd++-t%>JZCL${NS8pzjK|R4C+AIE*+H7m$TrrjUX*bN<^=!TUW;KOS0K=y zEkFO3)`>I!HH*g_ojw&HEhtdwH8%Z36+z;|dH_72d1HRyMNFhGHt-Gux7iVqIp$rg zfc9#$!j%uZerRI1Qg*cd=^v}B-TSvvL^i?WN%~vLCqj@hOE`dVDdit2H4KYs2Z1ma zMx(zMYWW|zekS_0Z*Gw;J75Tx9A9Ck72vKp^j8ua*jrX8m(7n~_rLv7Esl}cqUEfS zD&KzJgoRMl<}hnKNg>{BV0#UGh}_VME>6xqWHas_Tn=_=o!gfJ#vNKXx&H%BUR_;X zUtRwMVAUTNzX#7l;(33oet%U0byaEtduleLF&B~&NvQU6%&+8OYKM>J_O`pIT0}5A zszqqKmQy%v6}POMZ^@5cGF8Ywi;Rl)*$1XX#W~It(;?psosb*HP2B8ZB3u9`uPFxA zHyEpY#g8-JC7W2t5mT;yNXp_aNZ~F?QHEWMeK;?@F!nF^=X-)H;M($E9eG&l%W^r0 zQcjBkOAOKG${zGrTDDWZ2qB+V$^jA39@aalb5d^)w!MOZI~aT2_hzP738bemk=PIvOIxzI-07kCOaQr z1|H|Y?bt^ooQ!EcXA@`p<3N)eH;kF+fl)_b-Jb5(9k82m$YoX>VsKm=yrVhyo61T> zP1v6$iM&&1NwXub#2c7MG4 z+#!Aqe&6|gjCK1E8GbIQ{p`uN>P-PXAZQw^EL_y0bs5v2`s^dS|2REVEx#^yzfL=n zG_+c*%Ez;5qHPd@KYG_i%AKxDaAb*8y#&?y@D%VA&;}WRTQbzN);eqpIAsSk{Y=PS zNSvOdBI#U<|6buh5Rz~scL35t$xSxf+a*GI%t2FHMTx*T6*{?ch6PDb-A(c1ux94f z0}cc|iUQxo0>Z|5(+X7kOe1_o`MAlDsAw}7$P+V~)B>f55t$=Ph_38GC6dPEyMofv zgotnd)sV=!@d*Polr8l}JGbye;gCmR8iBnt%zY4?M=>wTu~$T@*+sg3!sdMsEzxU7 z-Ii{En$17qoxaef+=B=viBO?oY`A;_2OQWkWu~*azfbJAxILcsT(z>gU0rYYuCA)G zqF51@tT$nH*4rvqMJCxV8}_1(&Y+&c`Sg5DsVyAs$f4^p9HV~g!{N_CnMOlx6DX(I z_5U|R1Q|_xgtcKR0R{5?(<{BJk_eJP&YWL@vE7e};IG=v4jmtU zi~ZIett~G3+E^ z_{*G!e#k9JZZrHeOlupaYI+137o#4^G50(OQ!flJrc@T$6`1y?!+)YBa=<6^l zHt&1;<6J&jXtD_@z3p_dBzVTW8Yf{)qN`tr+*Lo>7=s6$d%Mq{y*{U=-Y?Z3jd<;& zxjwKJ&%CbJ`O#UGmiEVW)P)O66$xi`O`%MHhpHJ4Et*d|Esf{mag}Cw9aW{WlaAU( z?-E)TuEr+Jbl)q}dJ>R$IT)=fn?exbDUdb};cF49I^j0HbfuBAPIZ zMF;!}wN6fMpBA5&lm6&zZV%U|4{!C0!}0vou7`E+ciQi{>~8J!W@jp2+w-QOmTMgC znjn1iQNgHR!&yj1P+WH@#S_vN2GuE-i_{S=1CA;^r-|uwW$I(_!dDeK!>FuM0*>wz zpS&Hd8s7-L92QN39*lk6km1h_H~f_&>&yIVJ{w#ujALU}(3G?5Y*rc=dlT_A;%Q z!8-08()pqK?2=iNFl1JAy~9A>teB=vLjl`IFWrE-J>yPom!HA!=rSGSnbzm)XAjZ0 zulwzhtgH6O$2s3b{sr_<)n#9X>XFRK-D$I)4va_E)YaK@A3hbnc)!#>Z|R4opy{ZM z1zl@xW!sfi#MAOHS-)lTw2NaYe(z~H3KkNCQ2s0_W$7M1FaxgwU1B{1swP?axeH!% z$edE2Kyez)yOzI1gdl3y=!&ndes`pJ*6{71P-hkwZv6arUdu_IziAz@`c)v2od>JF zLX6^ZK^aijGqcsLiH}6`tv|lW$)*cK>z2wK)8!=vw|*Wf>0`o^1pcMJL*bRI+#e z*RZGE!>=Z&yhFDpzGOzlA!sPuFmfF$RbT3FKt8JK;#VpxLxmt*f8`~Yqu=fr^ zvir{?F4n}MdYB0Osm~GCWUq>p#@m-}h?I@xnV1*L#naHG+cyijGD6^DC(ePzX~bYV zf}TPyCx|QCYMwXaUX7FQul_8Q(E~~g9Cz>}0i(#Iw=e+#mshuXkqFQHj%d{G-=JlH z)m;}z)u z_iQ{ld}iC{8@3ttzR3ps#(Gm_tKYADMc2WU1|QVh-`81L0>Sv2lcJAe@1Dlt3yq0v z{Jq1e%xfE(xDWS<6Q+04h3>)Gt8~jrnUTNnkpNAWp41cSZ0s zU#PA{naRWOu}GR-G1Ty}A~uXvD4ALm-Cw21^HN+fZpHmyaU#O#%S7^VL?^#auJH&NIi*K%4KX+joP0)7LYeT50?9HiN9_ zW!Cgd$hgxh8a^844bMEKXrF_f!6eb>BiNT+5O6)5A$56Z`ueLY!TsAcj9~RdNx|EG zO$H8TC|PKLOuuB=++)3C`GDFs&x0iTxr_*-fRz{|k7ru1yG`CZvadnjZ$Y24pI%R0 z9nItUwe>z-s38e##ZHavQ9Q6jsFn7^^)^(Emn}K1o$WScEFT5@RdKgo+SbvbEmpME zd}buxQ{roDYEz1lzalFQR&-#^1 zTjv-0Ptk-lP(s{8pjU|$6o|VE7I))F>|u$AAPU_X8S@Z7Y{rjJYRXZmjChkSQ3?AZ z&LL5XzHLU8`D|+uF_k4kQJ|uRdZ5>IJZzaYhHrdcP$J3TK$FjSmlAQ^nwT-tz?H*U zP+dhtavT!G-P?l*%DOM)3Esml3VH~SRLs#9^Zn$d+5RJLCQ`h|fT9prvJ`Pz$MdE) z_~*U;e$DH1yXWMi8b#K$#q)Z=X@ha}-=V<^iQYiF7PMvv%E%K(bam*IZ_v3@kib9I zjvFV_ytcg(z+`AIt?XTV%1IuRn~rwu%LdDZC|efcKn8j5rV$;|%Z6KYT%jSb+=wZiX5Now9tLBi(>dBKnl}Lj%EpKINEJj6Y zGq{Z6P|pK-PRPKZtw>w3^~zrh%`_folv#XMwYwi*-`F;91=l69pgi>;4l?6G?${%? zzvUN&J(?@W)>J4YWL0r*v*G)(DR*Y}SI_f@=JzV?mryNF2E3$Y;q&DqYt~y4e}v?s zw^nVQ*vv?nZI}D~1$)%t&4>Tpo8^^*Q||5UrExb>|G9|*u^Ma*G7N&(`QE0znqETc zUr4DS>`LBB4UaJb482XhyN4R;!k-`!YSh{in^$@n4lkEx21knUz$vkGil04-Y?}!P zSs&6!J6?11OC2Jd#>Np+& z>(k8jAQ{%^r2chK1-q-mJ-Z*EE5vh|{KDilg=hPeejvqOXAqjq zDfB(-Rz_5LA2agz9uy)orE%B3DT2NeZYRpI)UXXr6d4}*>4yt~^D&*knhxF7)UhiK zyIB@HjNd+P$5F$J=1MZuif!r(+!32^N4=dh?M` zZ2E#JqD9;xlklT|`A6h(^-n<7esy+_;PP6BnQx;e_A z0#a?ZN#A-6i>4gVlbOhYOgGcIV0!KI@O28?-PQ4ki3xGsuxC1K>WWeUM~qK=P0TFx zl`4ol7t%297_lNGDQf8YCYn!fOmxc-@jiZ5-#8@DMtUf7Wins6li4r@vXA&RgZTZ4 z_dWPy>0Z6X6OcXmSV+s=-OU@vyyqa^Bm#Jfy1GFCI1RUzvTCXDVvo1I?XRk-BMhKK zpV!^Gvg^WyzmB?_VklSOw%eZCnL^paW;B3%Lr~5b5;<+pai%eVDYEA@XI@VKTP-P^ zB+n1w5fH%^CaRA(B8fC^7^85m3+jbXfb%Hqp+=mY*OQKr-V{;eQvv8}^OYqq+L--z z-~YLHnJ8`)g_^Gx=4bndV8t|+PlI@2MBt0bVIz#(5F6e=->AeGtiP-Y&KqA*&oWh< zP}Vp!z<;eZc9C8$VohzG)7xBojNr!vy=`!;bnxjQ)&rb#=@)W#UjU(RR=|EVVc&Fe zRjCFpmZb30`DuAyUpk+++rLlp+cBn-HPm6X9^<2Rw`eBT+t7NNoi%^^2l1!xE+#Mn zh@?Tc={}}*k8=b99jD;7dKs)J`(49EjTaWL3c=_c_NphOuIj8bj)7EPnH#jHxR7I= z&@}&j?wU?ilk|=L>_s3dyi7Pt)x=TmTzHIJuS8Dog7Gcwj0XLuR`2hy64I{ zMR~dL`k%s_&9Y2Su7TWJ&2DBnM}XZXvhQQ_)pn$?y0ih=>5WzA=d9J=i`Sq1--Fv9 zL|d(`y<9SG!-8%Sg6><~9UfYg+I^!fk>mO24(jc1_U!1`2$IT|KR#%O1OZ&gi$W`Y z9D>rR$*Tnd=>4~1X;V}>oyv$~*kXMgu+G0!eY*db1zD!Gf9i#H>Jy?04#xWecwhK@m40{lbZ+tlXZJq#LKe5TgBlg4@B!NLS*_r&PHN`5Z=t|1_iX z07T^1cMwhpueG%!g0H%}I1QMVdPM(dnWp1}6PTYpwa5Q({AGnn0xpXwyi$deQkrWr zN&M5^WIo^(nM}zcbcH0(6i4wyE`lHTiv-%3e8EaiAWpP$)MlXKOz`PK%+~Q@Jd=EP z9Ou!;Q#Dx#7kOd#k68~0MPA|vlDXvujEJ8EC`O)N(m@$;HW|hX$?M`Vr9NGg~9bt1dm?XQ)M55*Q<&RT7buA+T zB}wK)Bq!vlIXLe8dg%==>1l*^NVWa37$@$|#wtD#2%Tl%>wWzL>aW%w!y>K(#SPuz z8y3x&5T*v@dkj1RaY5Qlv^v;^IH5Yvr(A|f9J~yVYP$YxMuXL25(iS6%cYxJE>UT( zbReIqrRa}&-aFN=JKf$lr459Z?uPq+`M#uNEU10)6EXrUb0SzmxxQ%bAihiOt~At; z*b`&=wTHq#rZ92iUMEa$V6vD-lyu&3c-ejPWFW?fGZ3&*4f_U-xYxQHKf=tZ(+g_( zacW#ZYZKQUVmUA=BCps*N@)@Fzd!mWEtJXZH0e$U7y@Vi*zq;wA;aS4RkgiLGEw-^ z&i2<%?xRQ)_ z^>fFwxH&ieU$I>^k1&f)wP1{FA&9|&?qU)&+K`R7yBXIO1~&yy@vulzW4GQ^?jmY8 zb-&C|y*rj?T~$uAwsNZ_6HZ3s^oVtO#!fNek2*0=?#um()Qy};H1d!kD^0EDd|_%o zb&29CzF1~-W;Nkr`Mt*Azm7x|Sz7m07Q=3b?80dkr4wHTcVPK@rKVy~n}Te5vnn8# zL2`zjda8|=Y+aRJ$!_H!)||3Uk+id)iCNgt-mIq}AfnaUHM3eUE&o(*Eb$pI*L%un z!e(Lh+umQ#Yinuxb7H#F%k#b;@B7~8wA?4DC_i(_n|`g(nyCzmY#;oJXssd&5u1Nz z-An*@O-oqXtG0jhRBYJc1hTjAsH^dcrSqDoq`WSNvp4M@N)kvg4!Y*gX!`f?;bWBy z&*$aSr`^k#Q7}4TDkao;f-11dwB~{Z4lloem@7JKXW+M#)qB;-2xD{N&8@G( z***jG0wWoLi&VadnR@`7qsDhEiUDw@$@ZsL_deD4F74-C_dBaicK`IkTh{ew?(uXI zA2+0c?$pvD+IrC5g?49uL}3o9b-9?6x+9aSx;o{#ficC}hyi$FUgBO?D%x>g7Ytw~ zzd|97WeC3odu9r%v5Rxp!PPb((9~D|4LmX&0&E~}&tT0_pSfk2C;@kfgK|3sMYS)u zjkErD!3boQKIE)@P^#d`nMva_4+Gka2cAHFiD7)ypc2I}HzB?nYQ}7B(r;db710)c zSA;E~7R@3(w~3J8v|?cV6-VEZelw3wM=;E~h;v))O?oOuGGHVt%&1V|H7GyW4hoaj z5Xo)mo~WC8HtFuyIdCBAu^lUOvOCHvc5dq)56JD|N+6TKAq9)daeek5Fk*8P2lpyD zIQY<&8UQF%p-BYE?q=Jpn*8s0K)*Xemrf-yHxdWWVwW`+wB(u)H9w7M+Aoh6#<&l# zA_rCmsWP;$me`~=e9|VnKW4nmk#sPX-P-g%vT_LK!HaAJI(#Y@&0_a!>d!6kR+WAz@RYV^oeO= z9@Dau^NbTmSDHw$CgZwC_kVEHxyL{+Jy!(F*}){8*7)!DDEzI(E?Nh0R5C=Q*8Z*Htlap)RC zRbjDfQZjtq{=L=R>Q%YyZ)-~yPkWffLXycnR6GB`89#Avv4=ljWXe#qu4-@Ae?g9{ zb!U>elZ7fH#I5!BUb~1Fi+!Qa_lb^-%lhrlop>KGD#PA{+6CbIg!g{&XE!wcy?nlY zd{bYKyWPc+Sq20cs+z-LdZIzD%>AvIvom+$e?$)6Xs50QQwb)Np`!kg^95D1V0fPH zk&n+fza%bl(yq*oG`G4_+sFbWgOAt)k&hZfq5{<;xKXg97ioUa#;Ar`a~7Ar_)A&=*hCe?llBz0%WJjmB7-;Nf8y%nPJ6ENO9W070QXaJ4L_DMDBsDL=%f3$z z;Rb%NB#Q&pTD&}A9AP1?|Lk{7JK64PJu5IX0vpAGF|(jQJ+*oRaVo{dD*Eh9aXUuv z;mh6O&ykh`$J(x(MWcp#vJ`*Ek0kq;t_Y-yC~kA!!~=W&{l)6z6cSrxY7+LYfPN_$ zKLI^~g*sfaJR~w$nkW;iyyog={fTW{Y_4(WYssuH6d{I7wD3#k9J|8&@^Q;`Emksv zY(z=IfR@4w3UxSos){919O1S_o48K(7tYSc3NoS>dJEtw>S`~d(itsBsjG_FP)|c)Q8xxuw5GcG= zXwcD4Tj;P!=ZRh=fQ10b*f)`d`~G^djxI02jo^XtxY(pHR;N^q{YPjlPGnwGwJN4T z!@|H4sL}fdyi-S~dr;f!wpY|=x$BmKcHv(@ABWjp&GOps;sc5gtf}VhCNU!J+!?G` zvJnq#$Vy0H;Jk53t(A;n7iT1iIbeBDHpbCajEO&#=4aMgWxyzB0u4xV<;={G71YBh zK8Lg)b3eh89bQnpisj6$`9!UzEoIJegV#H(8Y$5KgKMk*fVI z4N3&7q)SC@^%)#)PCz!UOaza;|C279L+Hx{ZHTnotAAPha&z5OOm|LMhbItTi{xcQ zp6Zh4JQ5e~z!@ps4YwZv9dnnAbNSxJT7QvZt2veh&t!DpZ1QM{hJ1Lp@EY!R$X8JB=ihAfLK;IA_JI&Y=#X z@`3{ixN(c6o+C$?*5a=SIsVQ+t^=9Bnf(P!Fs)d28S)Dz`j3GW(_0~9S4@Wf5vvho z7#bk6Q|+6Z$(PPBU6`bKQwVay)KHF&M?w=Fz!`Drm;8djyjVT^3|cZbk+~kJ(@O%kA_9$(d(Jw5;@eRQG^_jBZFX8!tpH$%}sTP@=>QK zmNOA2H|YDTp3SWOn$gLyT+8#Ph%;+o=MM5DmJZCrWrcb;cK)n-_PL(+e#0mORKrr? zMNMzNG)E0yU4;P({?Kb$vTeo&X^@Lvx3$v-=eI7TFD=qyJW89%U9T@SJ2O*Ji%?<1 z7$uv+xil*zv5Afs)(3nQWrLq=-SloQU*o)?r_H8yL{C@BqCi%Xdrrgy?0l-lDKut0tQyZ7~8I#qqzz`d= zH2q_7d`>KXW>=3dQQPdE=>-NuUk)`<+dJRcEN!w|ZeU#R5#^$D{j4`43Ik2ZR`(a%IB zR#UeCe1J|(D&KZtrSO@jt;nXJpWr7TaD@DqN6iM8SFZ!{x%8<+T`gLh99z?XkWTb+ ziV{ebLo)-RhA`TDA$o>s)Rnc?@UT3w=FP4jL(u?4`Tks$o^*4iaxRd~A>CIZ$=d(| zdzLaVzH^cyxx_)9^4Km2h8gTqidt@D6Q7LMtRTu2THk58=#(#MKKAGWEQV9GfX>;t zgW8z`7j>~BH#W5s7%?^dL38-s|L9grYMi_hf-_tPZDO}`Axv0?{Z7{i3Boz}K~*cl zf@~Wii^Q)Zz^VGp_1A6VPS#v52$c84baQBY*EZ;ZK{cLbMtDlSaI)PHQG zD{I@n=VOc}lL@ey4fM!S$z0X~6xS14dz&s?#21dPSL~Xyk7<+3l#^n8SsoHse@@A7 zhIQ9cT1`O-wX=t)XRXQgY|kB=acQHw6~2=?hxbnjD~R9YUp2~7+bdEe2g&mk#tni& zmz71z0@nF}S8D&wYZu;L8gh0&j;mWF9b-}c9%CjRsvq-dP15IO#o^hsG&$q#Z63C-u`!TmZg6VA*PD=+GuoRH z%9^5@j?DQH76kEZaP(dfYnT)*se_`7;M{Ov2`DiW5$Zg>W~4I$Sk_56yRO8N1^HSr z5~cflc|SRne~~-E`Z#M^)3&NV*23PcYzWkDQ?$39_?UwSZtp> z1(~gin>4JE8vy%~fKR`EkG~WnCn&G^ZRw%WCr5E)kT3=dGGxkR!(b+s!IzAvrqq>R zK%)U;l=AcUW5t=FmynFRd#dFff?M_f!1ZJpC7SGT;#%gHb88|7>so0^^&c0j{||g| zRtB1`*~kbHfpjTWQ$GnOM2}|-)7i5@#NFmGlUH?F`rU6Kq;nLVv1K4_dLf5ONG`aUlHL z--8}#jk69mgbFoW;Ws4}}Xx+g7i>qrXsKxaof!z|<`>a6G__EULDNBBG z<#vH6g;huOH0&e;1+XpXKTP0>4iDu!Qx)%`r`Jj!cuk~56#Nw7Ir1B7d+FHrGqn4A zSZBFNTE_0OF_(-@l3aF2G|2M;!+CJwBUPyU=9YO*d=19(QQaYCkQdYxpCe z9dN{K#D4+ny{P;cE}_GG&;S`XfX1c=vkwwGuTSEih0HP|>c0AuL`1R-7Tsk42hfVN z;&}^nV0%g-E&6W3YVNpR!}Ge|{!*p&-mBRlug)&Sa=p;r+X9Ik8wgzbIgPEcZmVdk z@K$$jeKEejU&^MblstB}J|iH6@q>rYdz|T;Hz!TbWmEZKn1TqI%3vX?eKoH-y_hV- zP-~+zx^{Sighn9{Q~Bug3*m>&&~rsiNLcKtNE+tBDigFtbRS|X|hFQ#T7K-h^?1jb{|4EN9_J96y zHnGS$#)HGBt|BeQ`OR`v=Zn^KkUq{)GZ%W?!_`!T+HuG3KLmgZ(DstGwsH@Gj1w64 z^@{WSa#H19Y(J}_?fd};la?y!4m)FGxtrO7F(`Paqg>;?NuEUj=B6i3%B{M3PWkgIc+@CK^v*voVW%Uy3TmpJ%+6pXm8WBPj9~!eQkMh zTXh251GjWoVsX698S=*w2fN~*xAmuadrq%!6an1WVUkxIV@ZQuP1-tHV@r+9z*?+- zVJ9UhDjG9&^}Q;e4$f-E6ed{B<0s^j9VW(Cmz_N}^lWrpRl0AoaIPz&(qcr4SMnVe zy*us6hBRV#Hs}QUOuoVb5#_W;b477w6X`lC98Ikh&U6Rl(uj}QnN$6}_(IE@J9eiC zW|~Q+NvhBbTH9c0$RwFrf(376*1GQiUnjEO-+23%8Sj`>K8Car@O?N}7Sf&invKlb zG#t84T`%+T-ChsP$KE>c>BZKYYx{?UeH4K^x~QIxj!7D zkkZmENZ@B~`^j)gT}cO&;rttYVuBNhcD3@q|4@kmQs}$pDoi+%j{vPe^<^>s6x70- zQof^{67turR58A(XsW>=e)d2a93frtXGZvm`U3iU+6si|-TngW5j%2&7*ci|=6tM6 zz{Ln%Lkx7P>bw18uKn00ImWZ&;7tLWEYwTt1oec5G4H>Hk|%uOzUmii`@>>7Kof%k zE(wk|sE3)%%Mml{f&4}1Tlpn;Fkz?#mwwFBF$Lr$*jt}9!qnB58&M@3R2wvgj&EH| zTmAsB> zMDnRq6BF7%U{{f|wn+nzoCNOqBoL8H4|}Y<>t4KZb<3q#tOGSIk@}v^7Nf50clgX0K*cXokxf{ZVokskp&VsF_w-9W+5GMmosCCrTLMIi%+{ zA`!$2(qHIX_A+`*kfN&yC~^~F>>C^Sqe=VO@9n&gw4SfoonH97oR72+B3ixnXKO6h z=}ReZI!>)8rx(ZJxY-|qPx~1)rpw$Xbu>KbPh8>SZcpKr{hf0+bOFMQ0tqAJq5G~bp zL4roD*M&xJtOBC}8f2g`1%=hCF+RY{+=KQFqP@ndAax9oV=h3p^vVZ$q+?UO#nKI4|{CA5I2p zxJP%&?=Naqa|yyQ9)qTu2Sbe_LBW`Yb=#Aju-jNS-oC;m?#3?g*5TaB=+c1`2V&)o z^6crhhB6;)yjMS$bIJ*htD(M5dWrA_=HiYKypM_-z)?4BjXGp5fjE6jh| zzG(NIZ9#?J0~-SaBV+0A3?vtJ%YZJ@hl1O+SNq52lX-5S`Y!OHViT!ypLU_qyLoAY zhoQ214X%9j8g6Z8631#zel04E5-{S!v9X(r?KJM$=Y9WfXCas7Yf-YJ=C;U;{04DC z@LKGYv3LMFg;kJXZ|y#pjn3>M*1QhgiTXqs6I(40sTXH~XdLMp_Z5aXu|skiC|ew| zN`LQc9Xu)P64>8@c&3upEm2j=f?j)yCBm8CAP>iDx?i7fUd{Pere+k-(pP8Nn)7XE zp10&))27^6UfPdG);=8X>%(8Jr^%I>w3UtCTQr@`SDo)?mF=zL{@+|r8z4;2HUUtg z#eBFEDx!W^UprQh$+io1+H7wK%UCI6UK8Ta&uh75I=%qtrS+&T7u+u_2<|)qq7umS4m_ z$2R^U($xS)AM#~(@YNTeHUNjM^aUrMO^`)M`kNyRkNsu~6oT&0S1uQ4x&AMvVtcI!yIjhyF8SKL-06rvf%#6=9u}2}G=j8GT1+R}0`0sO zsvb((HHT-=R}_@)Vpmigz@EK7gE_^)qPgO+4LI68iU(Y#31kP4eSmfzLo3Ig&J(=$ zSVZJql?`m*bc6EzMZ~k33NWk}*Kk~mJTWg>fXI&C}+c;450)zd*zd9?$2f3Wuvb5``zsJR@ZD{zk-aW+vTL+ z5n-(}DuMd8v-{-gf&)eUky06t0hnk0WrjKgl+~#B+ z*Bf1j#+RRU%fzMiVv&a-EAZ;VL>PTCPq*sb^ny(|l+^IO?wP<=^f8y1NDw_^7oXa< zuS0O*KpX*80zjSt=smqMLd9l??>J`?bd5_e<)Rz70(P~(Roz8bq`+^EyT0yuUuF?E zc#^VH%R#Shsg(vTboJK5aPe2e({N>XYejOUuDc^3n5Da+ z$=O!3J;tJ|SG$X!v>QL}g8{Eg?9ZXRzHkfj*ssb+CPC0Qa^v(s3)U+67r4hrEp<{# z!m9)DgOUJ>WHco71pR$ND${Eja^lTb~WioXa9Zne_!7^zbzg8vQ8*n^;`HY zT`=WhWP~7ZDVCqOi&Gl-kICQ7K`7%&6h26N9VnMYxfoAC{x*6rZ{uf0jF)9WleR}8)jO{F;M7KHEv<| zyhKlvLzd{(BM8|nQ<45GQ*8;s2(%>1G3HTO{*Str{&;q)nBUq4CpN5Z;xaU{5D3z- zGrGy&G5fl_wqAXHCm2l2z3GUymP0@W{oc%>MBbL;fWAnc(K>6cA=eTo^WGe>^kv%nGyr-(D z44>Ch+xm|V^mI4Zo%`F-@1KO!q6r?2g4=!kC58Oc>V7D@;~86C*l%u5$Lm7WJi;EI zlgj-bP6g?5(ZoDlMrU{#<^z*K#xmx}$6n!w%c3-l$?(|NxEGy*CRz=*pr zC=lS`vR3Mj5Ihg0C0M7gayMuM{1>w0m#-15I>S&T$so4AcNNJWQ>Hl*2^<2(QrNWO z#ui)h#4RJp2gffh&<6R0#1?ZP@Gb!ybal~N(xXr@3nX)ARiXKC<>evjBYDZo%FJom zdocCwA7|40Yx?xuxnt3Zf<>kv<8OWxUF)QBgUc2(5t5EkXf z{wUrVm*U~B*5yk`8#@nOVjXS=sSMKO8egRH$I_2SOr+qZ1CMEEkJ27xnSsGm(CnMy zv%Igs8emL4^vaQ27FPr*Eay$Ytt3Wf=g{-xUeHdBXkCsBdLC%IbHmJ}#&Vwj+~YU) zqU_&L1w%L-g*(uh?Q6EXxX3ybdw%_+0NI{ZZ;3LK>7PdmB!h~W-{o9GSJ%G=yH2Uq z)W@C_otu9FQZu0l43tz@soAG6>CCFBT7TQ9_k6 zCpp-ZcKj1?n*Fl)_>H<#8NG!r~Uf=G%s~UWOzz>i@l0S1+xJHC(R}f&j)B}xV;6Yy|U}xJ*^lw6MEXQ1@G@H zM>8Oxtjt5Rps>>3B+y8Xe`;Lrz8$@fw#}ZH76PcBO*Ts>hm)P2S^IcC%y4B-e?2VF zmg7!;c05U*+h)^XG!A>KdvHFFmpPwfBm=CYL{dEGAJXQ0Apn44kDet4a?X6LAI8rb zPG0b6rvZHHjtYe}73R1FNvlK!U%i%ElfC~Dq*2OuaIf1^fxSQh2wjyn@amt09f==| zlWeOuM@T24xTDoW6!6}IK9&TY@WrlSaK&!WB$o3L@uk;Afjg2?P5zEaFx80RM5naH zK^)B|9M;Cj{tX*#4Ty?b;a@%A5oDwa0ieR3%tB-^%M9b343>F|gaIahfExdL+-0xC zwSX}Fo3IN0C~Zc@nf`rn(=As%BA)IHr1X6FA{%Oh3-E%6$K%;z`LgYH*N-RgnH}58 zW@c-W=JM-*%QJ6{IF)hR7`x@bEMoHCdQVU!*P~EC-xauvpUDwc`}LJHj>!Be%VhO* z(^_fZ(`tv^mVE~)+{uiQvwxTz4doLWq+I= z`y4#&encqsoS&T+-M4nT&2M+ys#dQTXFnnFc6(h!%U*igAar?aEpIh%O|QEdc0V{8 zo_jfNA5^{%HacHVA@qGBqwBgm8AaL!{Oh7Q`D3J?^lObzV-@KL)fEyyVb>f_df6*G zw>{mM!?qSVUAHtbAhAzRivV14P{5?(%EmV_?oa3et^fq+rWG&;wv9(PTW_jd%B5%% z$h}eALM-xd-0W~Q=5cv23dtvpqf~j91QkZW-cykGM9uw*qIkwt;u&PpJ7@g7Cy) z{lD+;Y}PS1T!-@d^O786Lusrd{VuKz##~_Tb(`@qB7+W zU9j@K=c8>;z_l-d5O-&MOqO-`?M!t%RMt^~jP)1~Wvi<8Nvbhj&dlN4rt=54X(W$p zC60TC$L*uQD&1}ce16v+>t?^#5U1;w08u}F&OB#sqxIC=9Qvnk#|c3C^z(g4arzpa zHct?nvc0cOTm=^RDE2XRfEc!TAO+M=sL@^PS=!v{;n5W#_<=6o_gzCyxd1z*XD(FN zAwI)D=dJZ7AC5NVds6*dzvt(^qVPve0vw|rTw&!gJk{U^5dFyUvM`s&Y%uWjX208c zAE^2xL6mzlf{D^R_d1QzI1D&ZqvjP##Yu5Dx7a^Fp61D?jF<8QC*KqPY_cV&F02;Z z-MG9Ku@gsdp?;u9Xm*h~@Tz#B$v(_UT+$Ed?aRBl1<37Qxo26wcXRlX$cH}y-b(^r ztpE4D&0L#bVZk-j3=nMDbk=?~w^dwQ-OhCU@KziYUeeh{ehch7SUiBo zSS)VnSGxaHD7W+ofm3y4PFKH_)u1)96B<;ro`bLRw3FKzo<8EcIZcPr{~2Mlq^ul& zD8fte4m0A@c0)hFqBBH-!!{Q40CvxmT-ZGo>_UJpjdN(3f=P{4@)bi>DP$l`)S?hv zCR><;!%#p>^PR#I^HRA{$2#}|UBK9qgv=y$RuBF2p}zQIWcGdkz>&64j=0uD!_L`p z?SbyOH21I1Wmkk|4K7FL0~@D@{GIKd(8x+GDgHuuEtozc@+6{7f2=7r zAL?I>!#{pr=EfC3-D6E1K7NsAYIgSb08ZhgSnJXyReSl3=)yu|A!z#w5mohUfmnJr zZF-2WWWWD`S72ZzwYyJnSo6SIf0u}KIx!WIzO{LRhG?*~UQvjmoI=XGY`BnicEhsq z7|=7OWEt)|IVu0W!2_Xjy$-SrA71^|@RK1@V3a3%+e@u=Uxxl#9f9L z;NPs`TJxY{Q-0Rf56z2)FU%am$1fVTS|Ni%Uv{QE0wh7?*S={XWa|*cH->^R-xLQY z(u)Ml5il&=wxR*Tihk2o8mK11Fsps>P8CAX^$%hO11CO;iM3J$Dbr_hM$l)N7{%u~ zT`8b+;;tsXD}fbYg53xG+B!rFteoSou+pQiHc@PzAkqjS?!%V!Y9uc>Hr9~7tt;+~76D=?^u(r+^tlY_2cV1#$`!GfH?Yibf`2+00xhBo9YL7Ks+aHVOa z4_?a(i8{FPQ@|kGSF~Q`o@wq|v_iXz87h~T8&otAOPg&^y6l18Qgnm)MWP6fV{=U6 zq?fv<$Ab)qMsWENAIy%tDM`hEzDt)J$URXc!2kCn|4sU~B~CF(&#rGusru*$r2H=Y z!B71>t-yAS?t)T7gWOFSLPmS?$$lL^6{Xq0~P!iV?BOaCy-HhLCld;aQAI@EnVhuiJ@ntA!UCN8dImDsuJ3*Y^nQo_Ah zN~3NSJ^(6o&8uMz^AS}-YM`$GT5UySZSuMS$$|GSJwXa421BUPm_fd@2Rcn7y6jjZ zP-m6CPQ=QkLFzH^OQ5^PIz$qDU)czS&;tWAs6vWV2GgdM?%}-FWTo z@cCu)b#LYN`tI24!EQbG{%*ww$T4;5q&YHmdPtJ_%B@i<(2V?O+#0(UNQ6!hxSxOb zHm-gwauzvf^U6>Wdbx)~ zRal%zjlv$DKAGXhxdvs5Md=UXi!jJUDA@}Tx2X?^GkT*DBA zITsyOcX#*uRfs34aJvoh_*@R4OE`C!n&7;i8T!gxAM{J$4nXq_4)YIh)}as%z&Z9- z=&+T8>WB=-G|I;kB@Tsj{f!4}U6$<(=y-BGasAvNTa*jVyN|3f>fS^0nMten$vCy>%;T8;EOF zhCIlW3RHpNGK+9DG`NNLfb5i|0=4+D>C=eyRsv|(3uIQPrV+yI(W7Ow>DpHkM<6S936+f7thbN zTATr>L8t9}SdBYJla0p2%i%Cc)m%vVRTe+tVz}^vMaY9$V~x*x<1f6?=44V4!OeFS zg#2PGl4bMapIaC-eRt~i=Mb|t(2!H4%-?fM zSu8zHu`wZw2C=G~?UT(!F&xdCvHGbj>mH=P@$6wP!&TrdA&$we)7(_Vi^Jtj<77>9 zZY)?WGks5`q-8D7^$6>ilGl>E6Z!33_00-Vq*=oFo}us7)m|SxFRu*WMKTZlUsMB) zCPp3hcJ{Bv{4A_Og9?~NOF;h1Kl<0&toIVmRST^UaPqn)z-I~Gp1=MQD&WoN$)W^R z*!O;=;dOk{OFI?fsh;^1mB@HhgKj}qbYIlci#6(-Lm>&$)pzBly+pYSyCGSfP1FeZ zP^J(k^-MNJr`N_-g;_R6Q+YghP(RFImWt)_cC~M{>_Z6KKcc#QoZ^kOENF)Dy6DkA z_8s$0KI8L)9M(%I^su8n1?qFE-oP)o?pZN++*D~1rUafhyzbJvMO%Vqd5;Oleq=ybu+j(H~1z`uzE^F z|AadBLi%$LUc-7IwySk-X`ecDQQGg6m-7i_Khvxs)_0oWo=OPR@w6FjD1c4Rq$@En zSoh{YE-T#f7foTZuE^jnD_k6bO{QG&&&4=rmhC?d)%>*!Ag@_eY)K`2Zu@u=3V1pG z_^Eie^w;BayxDZ|y7q~RbA)-2C`=u|nyy|=a@q(^7|w_kUBBZVTrX(M(j#LGS8_(F zPA>*W<2K{W37Lr{f0zo<7-qp#;4rER#-Uw8wX#Yjki=)aK8r|3NKF&Y8ZPm0FzNLP z`|@bOJR92zMkD;R4fyl~yxt!91CJgboq@nj?#e%orZQJEkxo{>>}UMoPm$S*l%8e? zZf0O)cf^$9EYpvidUOW38}=b#(()l~S3?LVwmnBijYa9QfJ%nvSTU=iG<+1V!=ljk zLE#YJo~J+){8bpS;iqmo36fbn8i(*))?SSu$_06Vplcdb+P|D7R`roTIirZol#;gS zO{M5GP~Oz`M!7o79L;GE2zg-oiNniUkf+8J4f z-|5hf`X|du*ADFmH4O<}zv3ZGl)hcZpyJ9u{?gD7#F|AxU& zns{v9epMITu>FR?Aks(Kf@_Fa=@0~T6C}85nvk~rXoQ{5=9dZUq53ECz3v~8)_DU+ z+w*XA$aDOTTq`kVVBBs zsrT8n{yQ7Fer7E8{5*R0UN0Gb57Yqf=>XWC5^}E!%+2dp#q1m=Q1u=s+{{H7G95S# zGTMqD;|}g_uc(g53>niOLz#<5qmWJq{hmly-*@JZGr9AFIKWCZe5cJgWzN(x646KJ zBy%)Ma=u#Wrzp1(IA|X!z66g>CC+g0c&yrCm#?%0b9rQn%sMCtmq0PJ-~#`tNKHV^5+LsZM=U%%5TOUF)U#O*^1!l{mqx}0}kj~QRrCi6AWq_hZoS#Jv$-SEkU zi5W{1vsP4oWE7Ve9upQJy8&Hrmou-tQ=N7L_mB{8iR`d74Q#wVVeXs9_MRPgnjvMhG*mmyib&aIBQ2}6AQa` z`;;X{Mdyc18C)6COhd1?lP>&1elH)1v@Bm?jlZv(^TO|?-A--zET<}9h9L8GS$4{t5G)x0Wd9K^DOiJd`@H$AId?f@RHNb6`bPh4~AgV5@#GZo1sLy*HPe3 zIP63v-sGF0S&ALuKXEPQ~yUevt95X z+9nNS;qW_jfIq*bIHZg+YAho!$mLK5*B46oZ^4TRsiRiQ$I3Z!`=9Mp7{_QwlWG@v zJC-M{A$~(dPV(>KIC;Y9{J43MC7@+ffznQ^38gF4nb;O|GzxIPa4zD{X9 zw!%w_ckCw8dK~~$bDz%d#}R9IHZI-helqUH+%_mO#n!wo3OO-WM4vYDEhG_nV1Rkp zWKyMJp^BT0(3-QjWBxVFn$Mj^-6f1Yk3fDH@DZWp>@SZQbk}4xJ)VHDcr$Y<;09d;7?`8nM|YWB=Y%< z&uVF8$@y=UinAzIgrdpDEQQWm+}J8+vIq0^XY9}BV}Gl2Jd$5{5L3N%g;$5-$F?n7 zPqXu=`Uu;x9$n9ex21}X!^=5B9jMob7Rjwub@puGa(As8ec&($%sn&P03uzjB2S@- z{E_RLdg-ixE+Jw_BLwVtn?hD_r4W7GYQUddk~8hTGhox!SDTtJXhy&A&=T>OvIkYA zDCo27$tNmr5MzWw6*z1;6AQF|$@;t05TlvdPNE}U?MyJVC_u`NP0(M^DR!DC8A5_J zL6PCc@M)iFBhZYGMHh24(5JGW_xj?Oqd{VML$}S8ytOJm-?>w8Ei2%s!a$w*gug!^ z0nAPlDp@$ivHMrjY*_omk&|^epBEdEf>&Lt5s|IHV1yy5^rGr{G|%cF^P0Q}J?=s&k7Pxi$fhLn8?~TxJU`G!Oeaw_3}Ge_2JG*I@Jb3oA)5>)pJbQ?jS`BoI7b})=gM#Ahb-7$8kz*8$u#N)hT~E*^!ug*SDMFtm^Q}J zzw>BBsDFARGh#~M{ul(Pk?OEEFlkZ_Dq;#3_v8B-4SD>$GfUdPthW&O~vM)hAr z%12|$E3^6$(tJad0-OPhEax;2L?p?6_vCu$y6d?SW(oFdpcgSoGs*_aK4XfRO4w;{bn`4` zq$_YVlKaqm35?$=_u)_fLNhsZ6D^nDH1uZ?dPB4mj~;Sl+ZOw?7_#&5cM!B~6Cm|n z>gls4sk{cPtwkFBzpFZ@R9YmdOd5A(Mr8DvquU{)8^@rpR`F;{^UOzrRsF`>F@xkl zB^scxA1P-MVcsrSu+o}lcC`LgpP%Jtm_Q&iZ0EAQUqLVzcRVbA?iNU^MTx6LOW2yU zGpxW_=$;1Nc%X*Ng8>3tske&Q7^kY^8c2H%Tb*W#q+4-yc`}(4W@r?RPoHSm+OVK0 zfXhW6C_OgIA)owSmfU0^@61ziKn=VV!J83X3PF#*6lrW#Q@&6uM=uS2l0`9m^*YLPC9IC zL!7<5!bh>GI%QhV8CNLVDBOtzk?(j@tVtsFvK3%?QcWJ8e zpfmXEQB8btI{w02bL-<@<#))ZXxRF)3tZD>yl~j{OUQ5Cr5p`vx(F6j%s}350Xe=u z(;5n1Gl&Pm`~Oo6uzo+h97<=Mop|Aw<-IM4zg#k}G|3f%Uh$6!rlk5O^tnOW{(_lC z5~bkZ-2Je)W;nh)KXjVw`}XYUz_XFYLT16D09OW%rr{Jc*R~Xj4hK0XLiXcQ72C8x z(Uk6Ll zFQCX8STtzZo4DWDucI3| z0!KjI7MnCKHH20B@JDJqmm~KKZH20$lT{%H3~y59?hgS#%-B!4%w5u}fqOoLEt|Kn znV&Bn()fu^14_=ZxNNkP^!WC`ZSuVF&$8OLmfvHHV&dF2F{&k0SDbapqjx9U1$d3j z)LV$Cg8XIPEZXiY5Ow)nb5^uEjia(D%1X9d z8LNm_Vw|3xTnpPG+3642x*QToj{k1uS{>@h=@~K+r7PuEuc3@5BB*&|6;mHfQ>UaSlpCrgkt13~Z+KM{sgF(-1WvGzy>3vk z++yY^41JZvNvGQWRJnGsaZ)DoE;&N_y{~7V^?-}h?l;E|3y#-#P5+yX*B1DXsfR`B z2P&s(8P?@K#^dTH47~~0toMgjZ{NqofP-07aJ{Clsc@Ux{CI+&hLBN0>23P!J?yX6CiAL(hl=1n@*+Uz^|kCD3?&U1LZ z%nz)uNC`9Nj=^f=1LecU*38-?9ByZ)j{>?=x|NjKwk5wycDbs3SG(VVujdFpVPyzG zFga2$Z?;@*h$@jc#lA&+Zh!25e9&*wnm z^vAN|`%`MbHK{@Mn&an7Y4^jurs4bk#(9IiYaaeLwCn1=9q>ghaPJjZgk6(HrnI6G9vt~+VXOS@-2L`exFb1!O zeI*4QT*sAVpfDmu>JhAvC(Rwz(V}v0;ZX>EnXncA2eAb@i5X=OLdTTVRil!5>bT98 zlx6)^F}#z*y;b?e+eHF2$`lvF?nt|DVYXV2<%D0NS#A)r+y|pkEv9|G`?9sz2FK%w zoi_VwOXv(J-WVTN&!wQ2A^|-RuRc6qpNAG&{f6z?`;s8lrds05iD6cAwyR-Rb=Eme z8K_O$32*`hu+={%wR;v)2x29~)_vhJUsF#W|L=)Osw2HWyXjE0zrMHes1`iNRX)Ze z*GpiR5^RHd@U4N4_Lk1Nw9&e4L#Q`d!SJb)__!!Nza_xOVw;7S|E-&=e#l|ne07S--JlIoU<1(|QcT71(&)Pz`vb z$pP3*Mpu&$R5T?z8y$T;2YWP|A8VeQrtcI#4No&YuZTl5BlPVh=JJ4aM+J2}KjzGg zlzM2)eNwv3(X;|SRsv7~$NQ0B&KwTnvGUieH;`LEAL{92Z9N1#-t1NDZNH!>Y+u4w z?oJzz^ABBSgj3k|cPxWv8o7$uBgSjisUq=?2OD}JY!b0o%(PZ#CBxmJB3MiTM!I_RWer~_i*RX zdxB^+SHglRkHJeVz z2i8?wN>xppbHU&V7rG-tZyz5YhY!yO+?|zj;J(_a_YoQ=)}&Xt<8v-X>6RWSqRo+X z8@7_jRak#f9*Hi2}U5eK#SjN17M60c2+~B;mIGGZ>2JZghO3%0 z_h~0$rNq~RVk4kdPaYrrVJk2Lo(}>VE(jcP3ak#^@mb_5Q7Txf`K6Dy3 zvc>8gq~MnXlKn_z@S}I^8U?KjcT}Uq4ty?tQa_>1zqhZcuX3rs9w@cDpEJ9!LWG}+ z?K@(O#!QMz@&cV;=}>J;BWppB!81mbM9duRxjawX}Gol))r2liA{5%|;#UO7b@V%*KKa|q6~W4Ty{>lw}UkLB4F zcLG9}R-hgU#+Jrgt@64dUo5cPIC328J|12c19OIfJnaE(`tTkgdjl<;o0QbZ^9DpT zwFuOZ1x005>M5|4==Z5C@(~{wbMOrJ*g8Xv1jwqM;rVZ!I3MS~eVGO(TfyP%3|l{J zw-#$`Cv+kM52%6JjWd2VoeqJF?l=Y*IquDjg8Zi!3V%k}buVaMI*(Plz+tm}@u8fd z*BLJIs(J!DQM&oin(E@=snafgQB-Tv$rTSK2lmk*klimp>4diyajjOOdInDW72DQb zEVExrCGD-xd_4S*t(*H|8*q8_xvBVBocmX=ee3Z4uD=H0_hKw_VObtickh86|0DLR zsjd$4AGswS{G%ug>~iqHGAJC7V1gZ_^uR+gsugo3ipJh|zzE6uRdVkk3#5k&Z8xkq zFdJZBltM=-@6X{AI|a#_}TbX{PnmCBbxDm1U^%a%{tdmuDWxJJ9u$h(Y+{3^W8 z^0l_T2hPhbDjCZiV=8n@?h;zt`5~k=&|a+MOApvxLF=aK;?yY>RY=BDZi&jAMFM;B zk`F$z?XN3?VvR*IF@x}R9O$t$O(AEaEu<N&S=wt$ z(wv7(m~gRcPb7bfE*Ise_(UkhgtnKmiNeCK!{Wtok!wgxu8K#;o=|E(7(I?eBkc2@ zaFlbL9QCCMrCTnsD$;%`!xe-b2_tQIakHnPJ@1sJgGd)`ggq~LEjLJ1ueSbs`20Ni z)A?kYrqZ@Oz}5^08mvJ-psjmleb_xNld+SUYuV^e?RJt~BGAeehLr?<+6xaD)(`93 zb_uv(3~A-b7RI{-EWuVg|3ZaYA6zZ}p?fVSZb6i&uTv?+75UwMS8LWsYx-AzG5qk} z8@#OgIT?kNLX!S1N{xr2` z2=en??F4c5`CsK)knum+fghJ9#f3{`{FgzPZGll<5oArGA#4jNk(OcJE$l{SWAVwY z97s^QizLrbs&k$wtcW+%jypsMWAZDN!nkRxZpuS)<#H>96+xvhFF zq@zB2Fw$12&yeaD(_B&_Auteq4xNmY!Y_-X`&&xe*9vp-}opB4C=j zlA%o{&HISW5E*0=b?gAPF*GWaGhV`^Y(++-)sv|CaAt#GqW1;iMVm434m@lS(Pju{ zC(Ga(OSFtN8I(5mEuzFiHGgKhSPimzSxIu)h%1MlW-$CR!*$)VO$eb~>@r&H%uvkp zW~g1cS zvW+s5;plLF#3P5On@m&|5#u$E!d{%>wZ!7sHlidKK$iu{r}>^#b((p@)#G!Z;Yp>r z2TxYwk3wunS4u&51a@>-p1wRBko+esU^R&hzD_c`zw7za#_t=qU_;meku-DFT7Y2p z2e>R*<-@6nWz(d?E7kO{q*A_ZJbP}roCYDhPmAx4_0)KatQ*E^>ily|l09qzemdc& zTQ2_C2K*>|@`;1%AR#cO;_G+Wl01NMp+c?IV@}3S>7=7EN#ZF>Nn+HU9B#Il zUB$f!JANFIwtckx{X(0f69%Qeo+2uOEwz%%N1kZ)LURC7Ix`&IcHGH|zQs0Tul~%w zGes*B?T}m-eGC)Y{@CS-#pb-?Uhm*x!+3ImB&7mpYnWh@9iP#2sXSJNUD zjfa)V)dW3TD}-okO8C_w<4sS^$?&a5TAv)8fi>94&Md7riTg$fi6y~rd4)8Y2$%5D zM}^Qkc}ayk^0rT&(SN|gg4|v82}f!~lwjJTN{{`($z*`lzo*srH zKmh;jUoxz}QG3d9Oe}>MrP|>q%tQ1hwc>FpG(md!TAEL_#p;z|^z@>4aX@ym0@HPK1YT`w zE7LkXScr&}l1m4JR=dJVSK12=%xh+ZZ>*ZzBiS8&^|aM{rm~{mP8mquUW&7aCN({P z4pe%Stm|vF+??`gRE{_I$RW9rzR*^7FPMx;5e(GnaOoH^?tSTL>lP7`2qSh@uk2P> z0JC0&fe+IrNa&3yS8wW|j1>%P@@M7DxqaQcKTiniXB0g0;Bfm%UD+(8^b*AKRu7mu z?c8|WBuo*0yy<>X`DE<&fBp0M^mYsLdhrp`ec5@9`Ur9DA5&Ki%5ZN0l{ISyb*S`H zTCx4vvc8;lWwb#4cZ$SBueWxQkmm0<{*YXR?zDk9Ix=grem9ls7;yLr9dLD0vu?9y zvf+k$_59HL&Kp4+$%6SqAK>FS1+#)pn(Lek(}|BFXuAw`5syPS%&^+|N7hsWm(M;t z*H}(6p#bUgnYXIHXxRkl*8b7O<9~Tu6Y$}3@85yA?f8C^`;tRCEw~m5bY?qMBV+$5eKWc0K7x%Q zVT;7Y92A4Gm%|=CSvl;(&AfUMDWcY9l}CZ5JiUvqg@Yox!tB5r{9`rVj_Z$^trFUj zNJSpc4FOCNGo4Wy&GPy%k%B=g#dbfn)Csjy%{7id$08mE0GYGNi56Scv@>9EB*xr*mPO`_g8T&7$9!hs4+1{ zE7|j!mWrKTF2>r~Ltjdwg*>JGNh6=)k2-0XsoPM+$C__9Rd5E*DS{yZzc5ha_0#|9 z>e{CEEgubZRH~an`i`B-jI_=yS8|;e5vGE47(idS9ZDiL!A;=fX)58>e~XN^?@v17 z$uQCQadP6kX`EZZHG+W#+JpNOD`bz?ERQN)HIpU%7b4=5ITi#G?%SB^?AQT^7;2L-lHA>kvP$3SBnkTK9v?Dm(Z(2go6E+Ij~C6;P`l z5?>&7>pj#DLjxvRGV~ayMhtcCHPjN=tY>fhy~vzeSO0*j%2~Gk$MMf`9yJcxdq0elk06Ng6;I^Xy<>j?B7f2AX>jXu$aOq2*KS zgq$h{KtmDv+p9^3R~YHHE~ySkB!}=OQ7YO3cVc7=wK?e^tiJ^&F&CkDHK;a^0|l~E zHVJ^bdW_gnXQuPR?-8VL4dclF*_UE<1d!+-EAunxsCI>4vsE>s`CVK}NQpuEVCbx0 zgiCS3tPnA|uiovu6~rZ^FB@k*br8O!+L_fKs{!qY#b#mmL{(TK^kscM>ggyX z5jIA_8z=>GZ9Vf2WQ}%-lQ8w$bNKRaME7o#Gz{V#173f121vlNy2F--XL)Jz8)gY5 zcfL2<+Xo$&WXLm$rx)S{vmyGQ8?!cu#ZY#hpeAJ~o1N90faJu2Dj%Oep&TxKPDUSQ zatDyWgf*{Elw}>gq&oKo{=7zCy*8CFJwE zE`G5ia3T=I;jEfSG*|PYkENlGEksMXRbGdV=Ka(i7%=fOjb5+5yj0l7IuPqmrs4>2 z${go;Ome0W6p0+DJPopR)%ZSbph9rnkTSK@w`4ZChi{J#X5ZzGK#tk$W5EE&VMxl5 zS)N7&$y-7xHkefy3!M+**lg4TYK_psU}YI(UYoJXSZ?I(F_XfG>yC&KIzw*+9eYKu z+=`u0gpm=Tsc<&J6q=|X49_xmmByAC-)-^;Uym*kNK?ZVV3Wmerc?W~m z&FAEp@bDW4(Lyy9iNsI#@wqS_oKmgMEHY!&j#mGs${0%wz-_7EWoc}G8B)nO2pZ9U z$cXVnG`N8yy|6FETnq8y0^at8)#&X6(zO^#PY?TmPs8Gz^e z`p)2l%UHd|4bd;ay{MpQF{kJPa@w#XCQ^xWG`||zIJl5Y70!*t7Dq(di7M*yisz4 zD_+)IPuv48TMP8rHw_|8If$U>#f7`w63|QufCE-OPdUfmAc*UA5`p(%DP8L_IA{z( z5G{?x3>8{o>uasZ*j{|HcEd;Sjeh3r@_)q#cCjcyG;^8dpSUN-+H(w(XKjBYXwb&$@`TBslv=|%vO?| z`MS}ibVOntSQCy~g@%!5Z_C+zF{#p=5YcvG@8?|Cu_JXvJxZk+z8QrgofEA+eq|bW z9nrLzhYjCRKeBVbI_RekAU?Qjcs8Ft0k`P^ngAKq%D_zh(P>diou_b*Y%19AMjXvb zcsL>LSfIWTv<)VS?B-=O_!L(tv9@72R!n-;dtHGUkr1YScjNJWNu~i2&?p4ca5qX$ zo+!7;WY^ELU_=V0-7I3ON6|(Q*tfolT3Lw+Jwpgl3KJQn-nHX8F%-Ty!?dKzbYNVh z1-CV{g3D|*S}6H_zuhJ52jdQTSAxp}67JOJLioZ%C?fACpj4~;zLn32Um4x-zS{HQ?aS|8fe%EU3J8C{9Tv`W4Mxwf| zXhFoj_n6>(B2mUSNRnGW0Nj0*BZ34LSe92Dy*plWo2`zm7iq^m5JBU!r$rAm?Cy7Z z_}Kk$mg{@nM9M=D_&ag*ug~MJExm4+Bi)sA9e(#8{(@LyCbjsGCupGCKR;}3AG@IG zNVVB66lpS-rUwkEjXijfmegC@gr2HJ;6Mzap%9JG5e~VINA0jNB)|fPpgXD=k~*Pu zPZh{uXKbBO)$66lI~MvgGpVQde2$%uR!pkOmK*H%D2=)TzL(>UbJTK5=>0{Lgb+Ob z+&z@MM)YwU+h&v^Gr%vi_5z^~)T@qEqy@U-nVGYDJ_s?iNQ5-~)pwppVnToQp8;VbMENZ}KsF{&fQ z$U7Idh=|(J9QaCUP1Zloy@?4FtYU9W$Ov@ia>xw8bxS_b#uuKf>I?bl*uAZ3O=c8S zE3t=AM4egsqIU>odlb9c9J`v(CMU{v9Fl+ztI7%1UdOrhD@=+wIMyx^7BxD|-&kR> zoJ+uGzuZpHf!tkpkTx9r$VMSG94M@VhQ#f-NY)rrdt%20sxw#(M*>DmE91pYl(S+9 zMqNwa9PM)))84ryleow90a(>eV3C2rAS^-=9ZfUe21v>V&OiePKLc}G2!WTs4#t*) zp|HxtZYovZIBlz1EUNA;?2mu3!NEIl#*ue-b*wadva}2pG}iv-1=uwIGJK7lX>f< z`<+%DsvL@SY)wm61cb*Dx=Z3Uc8z~j{eD=QI^KbHb;~}4HOIJoxla0FCEt$Sn z;n>8I7!o~oub!DXM2nsAm)4rw z$){J+kMm2v;|N0?gOuGBuS?D8&xpJRDKaz~WkUUt^g)tEUGQCUN+#1*Y}%#K_f5NV zAroF*?qm#;mekaSy-F8L0)QbO0yJOE9RlG#nh+hEo>Q%5$A? zz~SbK+j2Wo}(HOKoM*aDs5yurhz5q6^y3oNyC?vT9pvf9bh#Z znXv&;tVgdkH-p$gYmsnmzzu5zICMxpWE#q@dRe!Ac{U~Y&>$4#T2Z(yD=F8q{5!P= z5$uFTI|kCjHVG7aF>$cF{YFz70?k;E&O4#%_#$~~FhV0{WrKUO0*CjqwP<9iMtWRZ z;SbB+vvX()w46P|lJaGO?VClsZ8X9)uI*@SxPLxVUbj2k3MxwMgDu5fi8w0r9;)-)U}hh*Tv0y}@JN5)8*Bg{~`GXe5@Y=^RCt zXhVH}QVa78E-Kyh0_8Q>WK#_B?E)qSU5h65ABbOTJr3W`1S|ND)S5N1Z2e_7i& zWX*P;;Mb$8Z4W7)+5>uFNAF-fLarHp4ospi^jwb)!1|jZc*>Nf0Ko5;Vdi;(#{()< zpy;y9xmkgQZX^cF|lN0n)9-C48e9HJyUek|1uLj67fP0; z-Z$wma^9lflFq2QAyh9<$du8@=Dm6aL;E*H3xeG~LrZ=<($+xgR6ReXl-*yVl)O-RT2aPU}CO{<>u|*rSMt5_2S%{+&Uut5dv5L+=8f z%abJER#q~u$u!gpm1(eL*I{CcH;gfqiEQhaBPkH!l7?E{lmW2Y-UkeZsM}Xok@n5L zXM6if6QS%vDs^FYKjanitLa{Bci`~SN}|H_@%vVDh{09H(u%HR;2hMeEk>Tum7Ff~ zN9fO^>KPdBA(}q{1UD^v?^n=Xg^jq$v{s6(&zj21asrs9cq=4A4}tX|U@-f@l?HrL zi*${x!Wl~GDt5}O#_2Osk$S^BR|C+ntKU_XWuX7@^VYlNQ2<$bzO21F>dFVv=&wcT zwzr$h?yMPpD}Kk5Z-w0G=#zFL3LbsUw{12UZ`e_w`#fv{UJj0|zr0?N= z<=0hm0RQyj7>y9CDX7)XADKd+Uob&@#RE!*NnDH(?L7kX=HoJ>SB$GpPY8Hg5?ZU< z{i&d10i-BRu2ja6CW)2sLo|I(i`O9ejb9Nu_gih>QJ%;^>$mCTL zrqsb2q&D>zlh}$Ww7D_=a{~79OgZ{46;8XII_~xV|Nrvq7q!7L_wUV^se-K>CMUNJ zht?Bl^qXLF$IzcZD`YErVvWf9p~P(@r7mk3Ddr1L?@p%h2&kOsnSb~-fl*5aBMUFg zROI4=Q?U{vKizS`!8^Jqqf`a?# z2wNruDJNq8A5Y&HR_XUZd$MiYu9I!swvCBXoow5-jhSrQO*W^=HPxN_+~5Cxe82B! zziX|%@R+2*V=F@h8y2EH2$Erhhtsd_!jk%dE)o7+vfLQwg~>TE1v+)TM4_Q>*M954l1f?KrdpFzIJg$lj|J2;X5_y$t#>tn% zbdunNmqzl%KgpIvHe}Li8`?ltDNZgMBo2pyV;jfHf!r0{M{7Z})1%*|N z+mV26yJ5vN))A0rO#7wou|ZM zQK=@Lsd;n(!^3#26>P#HuQmS@ZNe0*!{{HnieevM>l)mZ8!`#EV$9$y)f-(dedKI- zwY-tQQ2JvDOT+dF|Li-OTvM9nNqj-2Md@?e8BmqSpDQ0`zh(6gBu|z<^^gtwZ_^c< zP1-)5@QKB3v1Xf48!1L*Jx8-uZl><{f|c3eDD_}B{Y_cux+^#Jq1IeK&2JUWN2QfR zZcAf`Y{T9`b#|8_TK>8u&yTatKX+$xL8>J(xoc?mL`J#j~5|AhM7(t}0B((3!-}<1f)fw+0br zV-01KoF-BWa`Bi^CK2v7xgM6B!slLgClWH2*Od%NcY)Aj^t%_D7QU55cyS8OP+;`z zLV5c$M*1&mI~p{-i2KJ+uP7gv!Gf!YD2Jo8&74||y#31eSRI&zf8XN zOZqI1%%+qLzvctI?)yF=du8?nZdkN$rQ3D_-q(zMUZoeooD1nl?&y@FRVbEaD{Oc% z<5uf~nQY~g@f(6mT)jz7)}r}~trMUfG$1mpDhQ8~i5%(-BP<#%)T~_f^9=N@7_9M) zlN6{MnTQ!MVB(-_(VD*sdOD)o9A{=*cD!PpXWO25I|3S}j{hi+Xv$uhq^>u|B|}F_ zn9#|wM3c+lL((CM&=$j>Bl*gRAW%q(f#Z`diAXfR*n$d!AY2;{UyRe5rA!tcwe03WeMYg9 zOEuVG)W~&fN{I->Gms{!i_>ho)i{QU35E?`okczF34)D>bZA|LLP&9r+DqzA4O&{L z>o8+H7>C6QUXC3%W=igaBtA8O0sSnkFA3L5(I!-MCAb0~6YUpzONA3~iF7+|h9GKH zPam6`yBaT}eLTD!VU&7|`c+2CG&Fx>ErdcOJu*RTQuNs%cVZU44)`u2Dfp%T9K~0p>q>RE zs1}pJqarGJ2J$M~X<;8z?d0#4Q_gd&mAWw$BX4$-|FFdf=9HX!6%8t?x)3PNsYWa~ zzE0E8>|~sbeU$&%n`#bl;^JI{q3!lXel?X`>G^5ac5|8V@b-tLC#mtFexZ@i5g=7y zNZ$0|;+ zhySW??IbzZjV+;!35e>@IJF)VoL`GS63Yky$3eix>ifx<9RdE){C0lMx^69)^X!M1 zJ&@}lQR#JsBag{svV8QLeWBjxlClzX#MQgLIVKBpBtUSDC;URqQ@VED;~dNFbrpRR zMFBn@Fo>Q-J~SYwS+(C9v`cjBe{)*kfA{tC=JtK>{DZ_`$19xbo?+CV`GH8^`0aqj z|L>vjYPc&<)LOjzyNhRKW@Vi_Q&ERw=}Bli7$k=>vU zdlU#O{?IDUkbn{0wd>gr(#rLGw20_mUCd|#12L`K%}`998O$wtpe2$?{ZVkM)E^64 zXfpWby#C95K%b{Fctl~r>qr$nv?m6d8PAcer<=Y|gYvvpYd zx7SZqwT%qH?U9?>T$%s$^?iML346cxG=2tu>Akru=#FopOq9W2cIgmP5D*=BOOJk) zLCgX_K}ThY!ip=$3k?+Gk-h!8(K~HSkr;k z>1&emz?L>7TB9ah6EN}5_vBvO_JdwR!99W)33A_JpPQ_TT+_b@ynP{EEb&~sL z#Xxxy=4!6K%DBd$g11y^$oa&Sxf#Fvwt{HzKrVvT`0n^D?cejtz#=}hPR*jYe1Uk- zR8P6~O6hzC8*=S8VuMELN-LWAWa2Dj=6u6RNCnG0pU=cr7!zayDTk>jCTO)wp9eCJ z=(?BLC@xH-5y3i|ms}vnywH#aJq*tf6ej9@3ZO@mCB>>fUt{(XDi4TPtM&jzrI1nO z*TN{W`M|Ok;-o@;EKOLsm>=h&iUUYGUalsDE(B7r@nh;xo{G4iS@7=A-sgqm=aRA4 zS>R9Y4j|SV4yiIl>9j~i#1_$~w!D2KkLmJ1TN*yRzV4(yf3*|{Io!_WG~RA~Tzn|i zu^IUv5*Yve=dO=zn1L4YV$vWWD$Mqo_XU~UWABKB4{q_M)Jg5G9&a2jg*kXUd&A1Y z9@5Kd#DC{t*CxlR#ZLf+Na4$z;%X(5CCL{8*>O5%!RbmPWGR;6Vr8P;c{ruGrAGhK z^H7>WihAGjCmDf-y*YS4Hg~0?@PJmwjvG-Fmm`(abb1FR!RLd**G-U$86nmDXPYji zN!wd7D7gBu=xUHnayV?uu;Cmckv{Rwr1`a_xw+o-dUhAGk|cST<3PHm(o!sR8@QWZ zfQuf+)e`e75fyRXt~O}2O&g>`wpSQXq!fb4e3Z)(BtfYjcq`~;Lnte;?8OvRmSz_u2X9o!_(Hb?*(-Z~tFwBo3NB*zUDsLu`EZj2G?H$>j!F z7~ev(ZtS@u)G*K3%K9-bfGxrHB5Lz>i8-KcR}V^NFsi0SF|JrIeEX#Oiaoy(jeJW%iTG8`&DF@Y0P=XS8DsK%axW z1QlxH)B;`Gib49hw)>9MWOH$Ug{XVHsG+=Vd&_Ybr7(_C+u2L&55dRNu`fl$HmASc z$G<+XeZCcePMirc`<#sKcuAjvaECKfgC3A@sUFZ~BJ^XvZ_;MUN?U*qe}9aMB;WTlOVOoYp7fuE zkP1pJga*Z=RydJ}+Q9r}HC72}U(P$-`4d24CXp3)E~1n4wO*B_ZkCu?_$|HFLz|n2 zRVomQ+=eL_76sJ}38#tiC|TTufkqSp^=>tOPOz1vhC0RL1g5X3!I_Yx4dpA~J9FDo z(b=8EAChP8zG51Ah&E`_5SutOL;=%4i$F9MCU8*J*UH-G_s`XX8^piy^$p)&J9;n0 zcZb>*v`M7u2&E`&&~PL@<>^{ZpC~O7ce6x3w2Bb)ki6v)82!+zb%8?lnmn zF+lolE*#U?sM}6vh~w@^Or|DfTNvV#d|8&iCd7~hm;5mPIK+y3`^QZ05?yL^Di@_hA0(t23-7?0MUIo(T|xbp}fu@w5NFTeLqR8 z%wO`hnNJtUPGsoB&?-9A#OGln_Z1Q<6Fd;R&LZU=p-@_^?c#R6eEmD!leady&d8-2 zdDrRacWeB<$+GL*`reJx|NPDPOZZaH%4nl1B7q}5r81Nu$TmA*Z*rdjcp0jHXUL~wr+9&2Y85nl2Ga8$0=43 z_%=G4xlKs;5xR#q6l3;VL8lG5rWUl%k8f?zE z)jO_bU&G0Gi~OFixeBJ> zz9!tngxrnpfH{EqEB?Cvw)gf)xSTK*(C5(?a#>^i5f||4&es6!WU1%k?b~j!{ zDk6FAWlDH{_J$qCs1hd83N%Loga2ran@bmb}OBdVLIh9xHp~t!DnI+)0C~w(8i_`j1??PH7><+1Q*U)E=TCG0>dj7;UMhUv zfCq5b8h>o;zBlzf4(+7uUy%iwkMQyx6X?jjk--vDOgnhc3W#ZzpCm0^3~-RQH!YahBNe)NC#;Jxmkv6u%8=2ohF5Zxc4|hI{)|5icXCjpe5W%#VS`z~q5cyjZ zA~QFn8ILWx5a2^iviEqjd?#e`V4406jJcZ^U*Fp(5|!DtHmUz;!@40G#cgqXk@EyM zz<)mj^!~oMH*RiNIC)k*&-MB#upvi?r(PKCD=!zxlu!Eivf*oFtW#hpMDBO7wSSLs z_wK@n=l7GAf|nv|;VuD;IJROYhAp#rAz~)f#G$%ZE@)t6n96;-G!A5V7J<2iSpJx$59v)G%3Vef^69T>{RG4VHY`>ligQJ$wed}QV(tSgsD%AA3 zt7j!fZ5qEm;R2RPcQ#DtDq1n3u?nKExQC89bd`{P;wVl z{G<3&15d`5DIGj-^le&Qpu+8ooLPYnT!LQ|XAw}Lx_|*rYrh<3Vw2g;Ua9-Ly_376 zo%zz%-`o3h)5rDECRD0obhW@pu;=AZ#@E0H^Z&RY%x0qXhcBux=XD+y=Ac~giU6{b zR5Cc^3>wGu2d)IZwUwHZ7e>{zxT2ta6Fgv1)q`FoT-ydbKVGfr${Y1e`pW3@EOL=S zoBa2qhIiqQ$s@ihY|(pI1z{IxWt{+hm5HkfUnbf5C59ZRC&p4$(t=bW4ppN^sYO`B z^z&Ms!|FZ-UA^i3%6Mo9WaJerhCD^ka$LsRla0HEN_hBjhW%Mmx0`__zlv=?>>CrCsuU#*>r_8j-DT-{r&&+y-pK+jp~!| z_3UD&h^g3{{uaWIh%gTRVExK*^`K`Q@^e2zM>pxQhV8)oS5Vt@%Tl8_)QsfpIeH-d zcV1#d6!2Vs(i(2BqW|dU3zURaCjBQoY?a8Cknm8V%Yuv8B&yxA(=$#vYmw!n3^xr; z*GE~{Ms=V^NdWLaEaTna-7Q;TcJ!gXvA9DJ6e>?>3`urV)Nq_FWY7jcR&1fC*2mecCRPeRU z5^x3c^>F_4?VK}{#dDRFZ_ds2FkYU-+~YZ7eB1!2BGod2pjm__{_gFjzyB{SHLLMt zq`nVfr3PXuQkT%+#t0uLT8I+jJ{-LYbqil7qt=NlCaWgO*}E^YQKHox{?Yhsra{F$ zbOJ402-)cKNhZeev%i#Bt=p%}(&^?E+>NvwfUbN5&Nu!B(Of%2Vxa=VMSkCpzYHa8 zXIL3RgSHGUw6}@El?ef<$TnW;&M6ZqDtkBFZs!-wO}mHihm6$pD!Tp*wWw$v72=FC zib!ufS2zJ*=+tC;ZeHZr1BEh-0E|+27YZK+mJDWue443=V70bwHT}_puP$b38$YjK zXf%A?@B8^wL3cvn?WGZ5cg_D}U^TV;pb&p%|LEag>3Pz$?>F|0c#~cF?=-tr4lR7i zZt&b#a$5;21=mtKL=gr&a)zX|>eQN9=|u;fBZ|kAzY9Lb>B!Xx*Q2ti03$p& zjfT2y3K}J46LU(_pxz`?2=a9O=cNbid5~od441^q{tU~-q`(%fg&uVE$?Zi83B#(d*g zIB?bqj|kh@rY4fr#cdX<)Z}G;fg{kWj@O&^O&c6Fgs?dlxjAc}I|b?FCbO}IaRrwC z%`_+o6-{G~NLY#n+7;X=z87L>n&&(LE`pp45hO)Y6zVHOaB*Qi6Q6Ye2MCa90ztzX zOqA!I4T@BhkyH#Ij*RGp%}R*0M4^a@5ZEljBdS#Lw#yROiiCjU>Iy)JT(O7vdM=>R zmT|+nw`;yb?JRS@6)$83A)AXsnrZg|)B?#xy(E)I7nK+{i_dByB(@bTIGpEw6& zj4hdxO(q<{s11Ys>brM@_`br4M@!X~oifQVAeTmk(n3zJ|0=?389otXs#YQHVqHxb z(@vR$3pV939Esn)Eb)X&u55!j^_la2dznKy7YNqiE(3zx2T1Y$G{hj0y*~n6b zC*G|M?}s&9p!jz+fB)DO{ib%d8WdAoV>OZ1wz(9ahvE;pjA=vrXsuy}x$^%< zbCmJ;_W-ek@Y#{kC^0v<& zmd!AdGyhv5J%^kb+5)TK;Giq6D}>s+f`eo&E^l5*@oRJXl?TcV9RSFWt5TX)o_!Na zdaUnhnaM#~US|-;MZWZOG$bpATzl;l$LToi^Xuz5z4qQHE1Y8=_vWV|KNpZQqN6Vb}=J(|^Y` z!OaN3P?pb$hwC?MB~C8biUCekNK2o(-#^|*HjES>2DF|sjDVExu2~fn97}a(Ft#QSt_QKd& zF|;K)+{pXsWSpMf+gIF&bhRxtv->8T3=;P|&;3i(pU1Cv{&%PyXWVKLokk4PtT=fP zXbo4oxU*2F`2;=Yj=v6{o8lPCP-DUKR)ATK4i!9p@P-B{+` z9n84sTV(@iR(@AmT=CYs7?Fv$r-C5a8pR@+foo$H*GyYWL;Z&N@t6=%)UF4xI_4}z zE1?`)6CmYBUDwQ+6Zw_JAUV-2$(jKE%GVO*5(Xq$kl|)=m>rMyO-r5T3Af6ezjoNv z7Fbm9CZq0hPi~;h^oz}OX4|bGa#eXm$jd*C8mTVa!)2e+HE5?r^om>kVcGLIF{T`N zb07G}To@p)!k9O^r+(G#i*064K>33QJIpM0FAZfjQnWZxK;@UWHp#01p7fcmH3vV_ z{T+w17yzy5+CixUmYz@r%5COdJM{ouX2wG3&W8ijQKA|q%!PEC^BXa+qESDEX&#Wl z>f#};V@nnPlk>T!gBEvT!m*HYHzub!jQa>Wr^&Hg(HA#bAtwfH3)f)v=M>ACv@=?v zuky;b-t!ovv|TXVa^aKR`^OK0xvtOQS7m?g_kMMUIhW7!Cl-f-ub9(>z7=DEL!XC$ zKBvAz``C(x@5dZ-1%2h#1paBBCGQ7yQ|&z{*{ZF?ZqEHSGh&!h*nR28XhhUkwq&q2 z%5060{}@5+g1L8Nk${LsS!|iTiS%s1xkLe2Kv!5iq_LgBHvUb>@TsX;zG?est7k|sVF;+^stm`$_FM%lFROpSooY&m&a(6 z-8IyM!z7ZprH zPfDI$cCm6uM6pQ?jdi+L!aG^=Q9W}jb0G8M-W-9x#I}r{Heff2B#s%wh3?dp zOTi<&r_R`xAtQZ^Bo^PY)KwMb#Fsmonf(@}8{{YCSWPn#DPuO@zxA)pTG<2s=19YXDK$AER-Hu6=q{ic#Okge&%;hqp>Ox;* zJ%A@M)r+$nvnC1?xh??{?ocHK;x$jeZ4HNwtC(;Mf2;X!GOsn9?rHl1FT2h(M6st< zQ71K{6O9*BnOfixiKipULqEZqfVyw+Gw)Yd>(Wrmv!KAw_WV^#UA>UO1s1fecE`T2 ztlgjgA20j!m)#;xWJQtUEo3e6FMaFP>BSu3kr=65-rnAc=hG{dh@R@Alc9(pY&$>JQk*ecLGD(2b;ZMYNAppOW!z_%hqBUb=2Kz$TLX z8R)-YkNxrgP&MxR5DvJB3%snmecgEu#xoh7>a|W~ic0h)$K^I3FY!2I(@q-n2$aFd4I{7%K-~k_5U3@{P%)gx!)BQ^ z9{(aDT5M!}V8iD4gYTzh(k+@__)B0~<^dE0d*&z@ka_!_cJ1{w_&SEaR@7<3-{M~= zNPye-=OU$0P*7-xWz%c$;^U5^;A6Pp<)z^Ky~fg~w{?lRI|bVcNMof?=%eT5U~Av5 z_3Y?=zYvm`BT2%RUr83{Hiz`qiF;$?AC7)ePSw?GNw%GH1$8AJ3!IlLL5= zVk+`vq&%3%SluG7>TcI)amn%)X70s+z3;r{bgg|Gf^vs*b0LNO&7Y@}KINUqt2}Mh z?D-hF-a2BVOcMnY5PiE|x8;vhfzK&{ z=MA5_KsX91fvZ~|1n1IdbCRmNLS3m)8_$vpY`hOxaDZf6E2+!jSY5oCaicw(U4%LjO|!e^rkf$U~UuLBba zC)OeRIWfh%<16m$Z~FZsBK`Ifm*xGifZu=J#`%`z>$$=heRTE*~R}V#tm7cw-C8L zBIyc-vnVNzB;YmQ(v|f_Uqn3tC_=8CS09FIGL5cC%?xZ;nY%%rk+0DNMgcYx6%oV0 z6l15`^K|{SczbMCnk6A@R#W+`6Q6zpJlY_-vt|@1z^4Q>8QbpGnY;E{RW@OkEv?Vs zqkXyhErS)6kGrZ zVp!)Wd&$JlBN z;XMA{Y|t~kWxl~>NnB!M7VVfD6YjImtoE1Hjj9G`HO)NdDr_z3Z@H6@(Zy^DC7gF$ z;@KJHDNiyOGZ=t`Ml++!@#@bd@fh*P>6&F9@CqxUxLDHO=vLkh4S}bR5CPA`QR;;l z##&qyh9kCcP4Je}XBoMtyEWa#i1Fz2nW#Jh@)V7W4rB8sdmdoyU{aW-=S^m?cMiBa zv5zV}c4qwUfIr|bv%c_`@!z+;H}$W@`}@nGjW9lWlZj++PYCxtU!A{vXN9^~8#ZEvOM>0P)&gb|`q>v4khIPY z@oe+buBL6GDB$AI7vGyL?*y&%=|ROy(mUd--dpStA-JF>XBIMy0N3}r+k2E+1C~wW zj^_aPz{@qGfMnRmrXFvfKC@l2%jTYcT>W#FMEK+0js<2No1>5ydkojG9pt9ZVKW;? z)*p;m0IthT^3D)T1TT?rmq~QYV@9wJDQ*$g6{LjRbPy;(q0Ks+4>D7W_BX@lmQi85 zaLXk8=k#|xnJcOop9#u0*EdkCqj3WWc`d`=VueenmWm>rV~qc->@?W${-)v9o(yhy z?0C^(kJG+kDwt&6T&_i#BbVMJE6Wq46g>v-SZ!08uf7|teh}3ZBh?M01uObZpk%p! zhi+bU7Yz)KM5w{Zx8p2(UjHLAGDJ^V&a#P*sYN#fd9?FqUgX>2bh0&c2AWQWrQvMF zlC7cg=@LYoz5OtBEN;g2pFdN^9|zV#ff2&H{{do8?KiO~y4Id#|CV5W?^#YG#tl}K zz*yL(hPynX39Q^Z@tLD;FmM`kB(M!t9oVYKr`uF1n#vcE9LvR7w*MnpLsc zO`~YxrI%W%QS#+kj_fdfpmN=gl2gWUZs>L3ILCZ8t2WXtuN& zRrUQL9Kks-m;`Gh!4{*@RGVgMZw|xgdr7g>EF9Y7-W{fzK=H0JH;>IPP$ZYXnD+mDlc~H}D6{|OjmPZ4`3(L} zj3!VABrsk68NtBLFN4vNrwKUkwqtnVQ+457R6~r5DNYIH=Sm7~Fq!68?}!?Fz`A)n zODSwzlHI~CNx-qI%Qvxo9J26Cve2K$r;MGM7aehN7O2Z7k)niQByoruLWpW=g*M%; zlr9u8Smj}Zb|ia+PbgRyz8CeoM&F_-_(U@~4SJsMJO6x%^8@>ftk?HB@pU*v@@8}Y zO*h}%L7qQOa5bE5Tqga_<#|yhc3grKO$jkfiW@gBR`gp)LQ@c25$xc|{*IY}1P4D9 zt$|F0palzGrFa+AL-dIFayu~*Y$XJ?ImfIWDw7!?MmGgqAcW!r?nri}*RY>DHdU#s zpmB?!i#wY>tmSm;CFYRGd!AVO&3CR`L zD&}F9MHU|l&@W`>nh!fLr>i+$F3<@f$4G{Zc-p1_qvI+>xQ0KTSBsi?#pOupUSRDa z)XM6Q0^$yuMTE*hBxYd(c@)dTHVKsD^wP_iF8?nM_WS(KTUUq2=DVv!l5u@JmBAAD zy7qbc?(gxRZwN*83mr8!n%>fdNBR&KY7H)JthT*39AGxwij#oM1nCXg_nVp+#4#CMSFL^G=X>8M$) zIl{LTGEL8YR?Qn8S>y032T?;$RQY>1TaN}@Zi$^x(u!*@TQ!v8xeTzVm2XT`NvCdG zeQksMJ5nzHWHUGB)Pz@7OubtxE~G|Rdre_yKG$Ax%|&C@))3`U?QpV3@M>~Q_>aiP zQBKjR%q#BBZq2UO@ta@ZDaq$>;Dfmsu)SrcW$X~<>apRT;fJTl!Vk^{eZAEmBHc?! z?^>N65<;PfYvl_`G}O3lFyW2uhp;)+#*n~pBf-t?e@{H6Xk(UK^QKCFH+@hFxEsXb=IX$5vIUDu5-}j~fKaS6Qu|g0z|1cSPLbMeia%tOZdiQtrKC6p{v3|Y^eMm6&=d{X zO=jX^)C0KeL_(NAHkS^YNOzEr674l=#eYDDEocQZ*HV|rkT7)N(>8WWz|-bj zrHa#vrE?EoRvN4eh$bXM=8OvEzfZ#6-9KIX68;B&x9n$mYeIRI3RE=z6{1y>TP-m< zjM-?7;XWs>g?qr3Az)i-=b|SB-?)55so*lj{1y{D&uq4dZlg!wIF`XE!G$c|c3m3s zU01DnXvTL!2%Tm!i;j0bbep=$)-*=)uE9**507+D`OrABn{_$Vg*}9>HgGOON2}f| zXuC!`PL2ypMw&}#Fd}fJWDI$PQb!IzTzCCDodYl-{D?tyR{GQwi&W?JjIGYJ`bFs$ zo8*twkDpxQLuqvU-1;_~S#CLFC8RMpSK=(97X1V2 z1am_^G)%fD0R94OP;4wmKmVrhG3ay+vK)~x9?YJdc?X1r`WD~5oC4_@N<5LRNPGq9 z#CE99cZLpOd+tId{A+p=Aoz$T)jwN-fpL^1mtfV;38@Om=me;Cl!Te521``cdl;xJx!YBZM z7<3xH^$&0!I5r2NxzXJ_n3mS!A=#Kvbd$DV>^EBXhZzI`=I8>#VLkg#>sR3l_I>jm z%L%6!v-fPjR|1&Tr zIIzoFVS8ErW>*8O3F5?`y&@-ap}=0nBbyia<*Uo7b(bEd`KiJo6&m_ij<`WGkvxcZ zGMr9~Pwt-XM}E1EhK8dg_Dm_cVq>WjcqprSW^-xZ0FXqNUcHvTVkk{hAYsRqbF93T zZZKB!P?-xWp>aD4oltum+q%ae(WoJ^Hkf11^ii>~Dc*b6*PY8)0SgXG+xtCcx+AOl z+}vBAZ;4#?QffaJ=H2>^X~tA zqc8li7I5PHp&j^PPV!-n_P#lralB$y@qF757-x(?z=pY1CfM24wWZqWYfkYr6vbA~ zQdrM7OMoT~!_YoELN%;{m?j;BE$Zq5m|3b0I5KImaZ)Yx0Vvtgf}7FTxHFepwA%4t z|M7|DMVsiKV%FXvN_Pl3H_1n8Lrs~#;Iar1vh|x(GuGe$5Ngs7O;4Y#yiQ`8^*zlE zH;XN@OXXl$`!0#gQM4|LL~u(&1_M@#8bfmZIZpB;Rolx@-D9Bo=^2BF{@)r$u1DK=U*#(>V{e`Tp)DN~db zRx_SXP8O%#swr(gYS8h3bfbx())KUpiPl!yn5de!8r&ib17X0CuYH!BqLe9N0&+Yc z(s#@fkmZjZE)JC^o9XF_X>&jq72HT0_Zns#RkTb6G^K`BcdO3ILKmUgM26;thT9l| z&rj{tQVv15P!dUq+GqLY(iC)sAvDoQ$)7C)=2Rji1ZYBF-HOm~MWl2rv-K{9?tdY= zP6Ru$p*w<_~JmY}YQK7`g~Gqk;SN06{oMN6MERH>6VUVXBU)ec3=lL5^z(nU^TCq{#2+kd%FE^zePRZ8 zWQ)U1V-?CVlR%8E7jj8iV}kp*_Ls0O;oaAggXe&~2pN4iX7~RVakSn%%S9nh1~H+{tdt|o=Dx>%E$haY{5*cwF2BlQ}tC%Yu)UBi{Lb;@en616e-W9GFnaXCw19~}ld5bq4{Wm(1#qiYeE(md+ z*J&gW@Tgw!`AG6QAng9{Z_6}FtXv+ZC#F=g0AWZ*@oRl0Z|AUV09I|t#Ejq&3gy=6 zo22`e5P1)uO;B7|zFQRrF@+5ixxKYQdK#RO1FC)LP%Nb*Shj8!{x}q7Cpv_K6bcf0 zxO%(+RvvIc(*g-DrV#=F{%{nSDlJ9MH;H0ki>A;Elm(i?rZ)&k4*(-#&r%Q*UxzmG zocG8bTVaNoJrD36LUE&^*3s|5keXEa_Xd@+Mwb?F<_2+t*|n;DXA~oA9ju8LO(#IF za1WHAY}dg8IHW?fo?aTs!D-k|PI2r0Dy!DUGNvhCA>rJftjZm~VQ8q2ri70Up~sqC z?|-6$-|7EM!)}U>E>LYsVRyg>dK)E)GB8_sCwVLVS5IA_+hm4EsGAj4g&4h?91Oyt zL%2ncMmUo}dE421+xewr?*IN^HAaeO_~Yr? zvearGan%Ny1Ak!z7ndKMFppeXk-0lDP;+RLEi{(eSdlq9sJtgd0@`UN*S>TFtQs2| zUW}WBC@U3mFtqVWDAk+o0{-^LDpf>(DUwF7{L_L6%lJxVuc1(p6skluag6^+&2G^+ z+|QmBPaTd;Vya@@etCyX*}hjxw(y)9>4yxNiMF$fGz-$5r_wF+wUm&evsPUGJeFCn zyAghmkI{;Nn-Y@O^$n%Vp~d;tlcrn!KsRF($nXKn#=&S!n zJP1vvU;doBb9r0S@6&vg6;y;y8A904aSmQDf~tu1uShmNUBXJY7u}G3qTGv)T0I+B zA?BGL?wTZF4~mLM#oGfZG!4jf{d1L-bBL+YAU49ws%1c@n57Y$cq}h&t^dyp@TS|e zuqzHl)QZv7s5YD=Zc>PV(1;<$QwQc#i(hK}s1%-L5i$5n(4xQlrJ1pg>4aH55kC*} z_p*}rVVv{d#fHzTkNU3vG_eTd7N>nhT$hQ6(@ggIJ!(k$um(37Qh5mtEe+N5bwX6D z=HwN$d)jW~Uk-3C<|oeNgPNh&vvc+k9*p@t}{!QzYRBSiG$tG6TfLyO}u)tQY2W5 zotIuK)HGd?8mH042f4wgCJ2;9H%Df|cbK=<4Bh33zj%l%IjwvjaHltzoVxN#l*B0( zZ6**{<6L;b-arDRXGjvMT>Qg2KiZ1QzutD=`2!DyKczM$Yql4jIfhreYj1nCAJ5*34j+} z*C48qz?k?fgg`KbX>hDBOG2^ytYhbXi?QBSmvo7xVw?^8zvkEUr9Us>r)=Q&(n zOwkWDGpi-wS}>vzH+}ScJqLEdeC_-vfmzpr;;@=s2~9DEi{a$*jx8jqW=blv$R};& ztl0|XMDu1%+^sU1T@?iC)z@*^86aEj7VU$l>rE;citH|&osISxhpA?L&Jj&`>_#0} zwOQevCp7MJf;+*Cv@LA{;y`?mSshWmRKOff`#AkCS?N2`rkN}iRY#9TlLooV!!vs< zO}S+js;N11!n#6T*9wPnv^nD$%#t~}-?dF8CNmx2ws!I3F$|7OX3Gy;(sS-dHLnwr z7qS$j7j{ZQ0*MC_a9L?5x+xicl(fGtcfCH|oWDW}J_x=hj6a(K?;<|33JwX;SlTnJ zKEL~21C^ZHxH~H9ZW5=ln(?XWP!bkPmMr_5>|=BKZ;AeWxulB^l2``bk^#Hs z;C_p~Ks@D9rjJfqxnn~MQ;1hhyU8bu&@M0i^KUTZ8`TV(EIaUI-9$TG^J-l-Jm>c9MnaF(w3C(}n>@0o3 z^@Lj$!<~x4iC)iEa2x;nchO$GU1iVbe-I!bK-p(#lUDj?#yW!n^)IIQdQ>b1oH(SVEV?xf(*M(qC7)hV=$e zi?cxza$)PRsW@xIs5od@WI$0|Of|Tl2-LXuF&J?XM69-rBJ(shDX~|K8!od=8QYf$ ziSG{dV7hr$nCdjxXp2%j-r#SL-zC8C(o~^%k9UF;n`rv$kL0^L=u$gcnU-|(Toq~e z;ot4SaCKJGe=}D*jQOWyqX7zDnIBOkbvBC)rI3)I6)!K%q~PM;!PP ze%fJL?S5oL@ymaHAH{XAe-D?fPl!2gwf=8PNKcJ+7ga<0FY&{kCRAwX4 zV^v{QOH@A;%W2ZtF3FHlftI1gSs~2{k)a4ez|8-ZDKod_2bqOL602F~{~Xg7cU;~6VeJ6fj&rU$ z)!LuG^D!_jEvw6yp)As4@9~31hNq?7=}zNLSs9^~s~GP}v8&2wmQ%#*o_bYa3yqe5 zR)B6Pc4%_U{2v$yt~I$mzw9yuN}=>gI5=h%`MC0p+p0gj$=>b9Sj6r7 zN#Hv{-?|h>i`MD4_>p+ShG&vXNw`=E;>TvkC-mmMVtSDm`s}hSR;8tG&P)JvENmOc zoLx;)Mt$0hB^A5n{efuz-lYR)zAnNPfHPZ^k{<50S`FNcM++4Q!QkkI(|%P@tHhuf zXXPkphJ||~p1ELT8ysYOlr_BftbOSD-MaA#3A3GM?B1z2yk7J@EpLwc z7GQZrY(c**@MIxE>+3^gCs}FLMPi*G1?xEk#bB8vvOvN|glCD|Q&DW&)I&#BPCgaZ zEh<~cYvRP=hNb1I1*GMfr?I+B2D7L4-sMqT;MGEgg~e)|YBGifY3ljq=V65G^@D)Y zaVj^ixTA-}f60h~z=0;o_!HwvR+Xua3akY?1<;s9pfo;cGZ>jIaiv<(p>mW+h}yw_ zWRgn+$rC(cVw~}HQ z6~X4OO#+l<>CDXPUhpPpjZHN+NI6b*?V01h|C|r{leu>%p2=~-jtAkNJ^l7saCI1c zj$?16=m3E(*gWc4=D6XTN=?l}cCA~e;wH>i!uxn76}7VFFmP38QNQ=d9LwJdatu~A z{GR8ctv{CPlpcT}PPsvBKJyQoX)48BSm>a1^=1W=@*~>?vQN~gK zJ*x4L-;rkTM^ZukrFZ!IfUlt^PhfE21H5zb1M8!T{ez907fZTsxtiQVw%A5JPrx9B;c$ucTaT~b@Uh9!0?WF3! zp$sQu>OzMuO3LBULV`}D!;;)75`pL!q;s= zqcXXj8^uK89AuFWeS%|u2gM(+_5EhB|1a754ESR>2<+5`K9hNLkL8>&eW1Pfa$5h{ z-d9Fql#C9&kwH6##88Rl02o^%Y!O)uKbF!W(l)kSozZi%iq!Jn(D%CM95|Vq$=7SD z7BM`Qh&=q46Nqj%8JLFh`jddG7Lg-hlHgKqxs8DgVUfboy;dPnh9~Mu%f;$dw?Mxl z_$OeNq(>8B2?b>U2?7Dm1H!85ffVKxT7I{b==U+Xrig_6qU+&BhYibNM$ z@r1M$p+rlzL6i_eY!ETG1!fgx#!S**0)JA>*Shr^_fQd4?N~vcvq($6=nw#6iIXrI zSIMeIpp-5#lmU9&l(>3R#es0jrl@|}o?lPb$mi{jXz!ulaAZES6Jmeb;MAN=KNw3* ze`h8IRX$a=CGq^Pb9BF|Oi9R`l8kx*9lu%SA-)s8-^N8j zF6B(7z7QHwWAC?^I<5binS_{vXYCgu>#x17qIpo|vLR z)ia+j&`w5cNB@Rq#*E-``1SQfM@P~hKwGbVP$4mTFMFKjm&xBb-}C!C2EHzz6jwU4zfCgFIBEX)wE{GG=H4#J_67Jxb(0$& zqFIZfh1D|bPI8odk;9n`qc$98D*}#Z<1F2ptW`QYci}5B=)>N}u6HQ$<1jJc?6U9C z)j7~??p65j=f|91&?}4o9NyN!LQdk=?7jKgAo=PcRBI@ES9XkMLppy)L|gR+wNsIe z-T`R_&bKruW}bzEyL3R>FRq)!!RaM#97&`3-bDz&@w-f9s8%HubW4in?_aQHz2@r+ zB*$u0w8gRvY`=nIpkh`K;K6l#gx$X1EiVfwp%fuhi*<<+LcqlnrTk*B_`ygdgG;L< zF4grTVLd09_(RaJ2>b}*QmG9o&Md!DH9=Z%HZV1BB3`D!($OwgJMaU=u-NV}^`cR; z_T=!+l(ROhR16ZyxQTSEHn#{6!O7%6of$X`E7mSuF2YqC&KhzDIu9PS*JA9cOi?ItK~_l9MSQujyYC{NCOt^@eQ*=#!6vw zan=0%=vjMeyxA3hb)&L0x`s_DcBimf2g*YQrUc=huekbB+8zp$_bPu~oA-RT9IvQR z#&DT%z8Ml_?ZgBfbtX#oXkWde238d6CgjfsUeD45T=nUu3o)5b(ZVz<>UnPcpaU37eEN8<1^&F%Z@(?q z0$)RK`R25dEy{-b(o~LR;}2YkK?NbrD!vOOC$M$PvbdE zq>)Ag;-!6@pfnkRr=UWccAeIUwp>HX_Hz(B>oM#%_|}ZhNx=U6vZWm!A?k7PW*+hl zLV<@h&No{TUv$^u^ipkDtFbwb2u1%sG>3oPiP5J<*0~F8skmG=W(@>&p9u>xdc|rG zM>ck~XTD8lCKi5bqJPfzKbH5u^YncP_Y>z8X5F`Wy)RV=Kh9W#jMYb1(SJ7V5YTdY zts?Fo`Wo6Ob>Q4iv#BjI7wIJ9H^6<5>o$d(DnsS<6u(ldG>RvujcHhdGY!^y zX6h)#HM!z`!elKe!v7pm#{rT z{f%IXB=uuI%yeSvq*8)l8kC47^RB~kJ`7}5DH{R~_&yoPpEU$BRBLAGMcIo}tXr$^ z@2so*Dh}!OREbM(HBPUcX9YpyCs!Raun@+l>}0voX|XHaG=G6hNsw9M?l+2H%<4f2 z5yUyAKOCAfygzW@dZnqpmR{+LvKr7!fjiC|_N(q^#^&gJcZkft0r$SuX0Dqxm&y%J zO&o3Vm_AO*5P$FCIQ8)pMB8+=b`g(}RcnA*&Db?BXoU@bJd5*}vZGXo&S&+(5_9@{ z08*PnqVe2gZ?fS5f8m`!0)?siujk)`E*=z6#g+47ESib{0`J!;1gkyk!y}O-!dMQ; zT0$;4FC#%qUE^nav6_vrmJUYy83vqw*|z2CN19aznJ*eSJa36keovbfeb2GNpTAu{ z(}BQhPdYay8xsNwXu=i!1QdrZVPZ#o{<-sYxCMiF>)6Z`kYEO{=jwDIRrvr!qH77D zXpUpC7m|_IN0v?ls-JyIE`mv!v|{VF_FH%12B)3S=*21~vVVTAmX1qn6rHO%oh&7m z@Y099ehef8NUQcRaQwX?KP_cTu4Xi1i={DOJq^%Cj>{t-kU-3M3;HM$`rJI?FlOy_ zbPse6aCA04m|dq@o07W|=zn1OL^Xi;LksG;y%v7YH-6V9Lo3}gebgFBhtZJ7>3z|Q zql`%|<6hi0n1bj8h=FvFV~3-{8Fvwvnx7>fd1E=rWJ7VkcC;!?58-$Omm8HsOshMf zq^w;b@j^-E7QyCV+Rh|5j8e?0K@N}u_HEjxiV@VLIJD-sUr71I4%S(8gWE>O22)QJ zhhFq}$6VLn6Y?e~nJ~KPszlJx)TkmC%u^Lj3n$9J_=#8)@o3{vwk4BMR>zoa+8huz zi6)Xd!QlsxQpGrC85Vg65^2HIsX9*!BqA3H#KBTIEh(*OV!`f1Gc&Z6NhZwX*IXoq0FklY`ic;^1m=wwtX@2x@E&vx z%D%tVe`qX#^|=%k*di8`AXZnVX@8EyLeu6j^g78if72-pz5n@no%=D}e?Lzq<3JBs5jPM)p8+ zeLq4-({LnHIX-mT+za%Bz-xFE+5M1=xC@rgnW9UH8MauQPg$9hxSR z*k$XaG-T^Ek(mW15c{_wM-w&Cfi$r+I?^1M%Ab`ewjLK8=h>Ph`uupT&Qa`@()@ip zj<~{ooI%H;HC{R}jW4ks?~5l)K~^tu%DG1Oy~2M3g#Q5dg810~by^*yL7?`)+kf5@ zo^5|~BOtnYja%+YLWJ4zp)`~g?5+vDWiYk=4)Ua#rNe zZ;S1C2=`}(Cg8VwDz)UJ%4Si(5{j1CQb<-)6kSshpA0ge9J1^=8s|BFj)mEkS8KGv zplTlkRW=GMV`^hm#-*=N+~Sq|@T>KfVF zm^0LVs#q0UR1l+PT2FGhr${Hdagl&CHZt!Q={r4}6^Ji(B2);VUsPMEQPW@Y9wzK7 zP)Fk`2lQrS&?mci&{|jYE-j86iO1Pd3=-XnkrG*YAKvCahxU5&u>bcHD^PzCa81`C zZR3uoE8SAtZ9O@cP&u7G+k}o@YkJu}BS`H;zMs)ioolS#e}ySQtXm)U=&5Vpf~+(m z6Wo1;BuDZP(b4K-u=uroV+i28#AR!WF?D(rTpEZ}dZ3Hv94M3Pl&-LTS*7+3$*&uS zP4YNeq#dPUc4Y(0IZEBm{rjld^r;$q!$wKXe4{(mX93W|PG)YmjN^(Po=HAbJ^HkF z5ebMkvl0(4S-{An?0KTnR@k4aT?1$>lLu#BJB&nm#*ika3ztpnf~&Y4o<1M@U!p$` z`#%dNJ?H0?g-Jn1jEQ4vpq7$v2t?SSHmH>OE^n9^$cge}Gi+iF)c+E!_^vjM2=Sp) z_(H{|3ODILCfpQYac4z$DarZp{Ers$QFFhCJ7-$m#mHhszIF8fYKzBS#U=98F=#V0 z6k2j2GO?ReXl+#_EHxkiZCI_SXiRLsctEg%t-jUT&LFqe zIiQ;?{tf(`kdHj8I29jeSliLG=gq4B(;x7f`nlSB^B2AURQMV4|5sPR&pWdBHL|zU zOn(sGjZwB!Ms6UlK*Xh(n_PzI4;2!-9Yi=M>9qVGOx&EAjl3c#cLJem>UFlO`Zz7X z>Q_P?iA&}zBx80wXwpQOFhtbq0VF+RKu#r!LP8)pmoo%vGM8dAr#Mqf{u=}W_aOQ{ z1hyJx7o<;HtfUU!c*E&WMS21zN<=0sd_GxZm0(gEJh zP|+&gJ)hbb}h=~D7PeH%&g2e54sSCjHI%EEJ0s+W4F=Cz9Tt*{$>c$6cLB8$C;OnPGhdhu z;GI+3t8YEQ(&Pqq>j(i06GEN$yn_dKDlVKX_U<4HPhZUt$m9^ zfukM$H~|~MX)yn*syl)2zO?XWu}e((MW1bcgVUH&Cbfs(G*+`7-xo(Bo0$1=Ks%BX z`^+_gOoei4h~d%C#)&z_1HQk$7O@ySo|8KS3#}Xyi?M*K6~=&U^)vbU?C zw?hAI7e2vOzm9w0{ZnZ1Hx#rXeq<9W3km%9nOJ+nGIEbY>>5^_tRF3c$(vZ#wNxw2 z2YXOkcTlG@j8G6NA#Ta#DcU`X&x{refci3dVTc}jl6+a%Gd$a7o(ZD37CEy0*cEdd z%woY)wbnmYzpf4)Yr*wHd)p*Xrexd%zB>j+l(qFVi(5G)9?roDa2uGBhj zifwFAVA2|e+Y;&6k*~5VTampqG=-dnG!MaDR~slF+ZB_s_{Q2Ehi*ST2{^_@UL1iM zLERl*&{zqkxJ{Jm-lCcfC$rORy)7MQxRq%6!8BJBx_HS`B@L?y0F)&_l!{WQ9U*4E@>C6h}^u zWn~hIrL}^~8b`qJMmMvc&MNDQ^H(lM=EeSWK?eI}u*CM;)&T6n5%MCOaex=s_0!RZ z9bvg^0GC+J+UhCopW)oV18GM`VShfn|2_HMf17dGv8RCc$gNRCY4V=ctG^|Mh&Xx% z%?rR|PownhGG(NUj9b4|)p%utl&yZLAK1H=@WV*?9lnm-((=K zQpGE0YY-K7Mi1QUZ#W0##5T`Wl&awi5~(ZkezD$X6DSBB+&wHw>(qyl@M_0$2tZ3k z%wh1Mf)EVy5?r7_j|`@-V*^?9ZOHJ z_7>6&QI=SpD4{Hbo@6W?fqj|vJ_2l^#u1&hR*0i^xmau8eAPmZf~%ghDAg3B+Yc-@ zGJmdZ!$-9?W_xDL&~_aHwYfoF<9t`kSsyIaUpC#ZpHr!syJe>^CYlCcxGPRd@VLTt z$>8iD0pfws+WA62dZG)VW>QHCWO-O&-QKim!Ol4Ep?7NQo4 z%?Qw(&{!rSScK;mSs}Co__bRHmJ4(<3xp~JdKB;(jK@-CxDz%4U}zhw#;Uz#;%jbd zhClz1#%XHDOyV#)m}wbS}?>TtC3A#LC{9oJj$~Wn~+EUgVc8C)!8EM?UOe01$!|w2$cZ))yjEf1m_}9Gc zHU@LYEStHt*MjG1a-J$nV7OEFW~i3vhLxkJ%eOOKW)_19m*NaxS#NVP^~Z#^O?!F`FTR3IeflRnMRKs{Fg;j zagBU+urhvUk+U`nu0JGD)V#K$JI zO~k*AHQU{;cw?(`!eQ+a?bz$DcS?8UBc|jG`kJk?zaGJVhxryz)|K&Cs?M1$i|gpm zd9EMH(mP(W{`R6AgoX$4hA8wy-}p0w>=RV~GWY(zRWP%=li3idET5}bgj(arWO-{z z{3LBm57SZ(hnhs!N!d_6bzmAZNi2T@{&G29AS&&Aj#Lfxf-Ca1_T3Y^`t^ayTeLIf ze_nvEKzdqCZeVH>6pr2q5(4oG>2mq6OxqNq->UbyU~McWCUE7QfW)7$7|!J<73!(c z>@*^_BXWqGNyuMsxBv+(dMS+Lld23PN+ z=k1ajR9EuhzYjO6+_eA2Tq)iu+1pLv0j=?CP0)$(>jle4iSX;6 zL3QQ$pwrUp6L5PXvyRwbo$%GDM)>LMeL6p?d19<(rOQRRitzkCl3AB@(+6j3V22Eb z<(fAP05GZ&i_oINk_GW%wmc^@B|pZ%GO`hmu;`a&ho+&lE6d+AaO~47~c`( zgu$VXWJGu19P!oKN3tC(KyupBph8{C>6H#3b0>kmr{Zu!XzERb6NYKJnnEJ;O;|5X zAlg`X%tAy*$8o8idgcIAK}G4fX?GHc?*G2H6}WcAgsl)bANZKDqx#OyjAaUDWtxkoezj;EiS|iqIzOt!2Q7|QA6Hvy zJStxUYJawJ*S9NgmEr|Gu7CV?`rL(mH~P;ERd{qUdQ89+Ya5UY=cRj7FggYc4x%Al z!98+c)8F=U?9A3m)mOT4jBeOR74bB5$9~zW()?yt+Ihl}p9$*}V;EA78#ttxZJ!^n zK(Ge)`kJXSJ||e=+&FlK655pT3dKXyUPVd~u2676zwdrlu$kqumh4B*HCY05Gp_G+ zT9TrYfF`0o(*1E?b;To3oE{66?O>ti$;D8J*``zPf8~<@LaXf0>#I%ewqA$2QotDX z4lU@1{-OXoz!W1Z{$=a~Ywh>Xjy#V{pWBz=z0dkRi<7gyi|&uB^`KW{-|Mr!r|idr z>7Gu4Guo3m0AnlAOp0{tVau%qmGWf4S7P4tQrg)Y-y8?Fqd9oWG8u7M)OiR6t`Lp~ zOW|iRE*HLL?4nIsM8NRjO_?t>F+p-?`kFzO!--VIeQcN@RY|-=Mw0chhyfYZegZRE zSiS`fXqHF|NV2|Wz>Eh>9(D_x+CjUIP6~zX-CbS8k%idzQ^2Fb=d$&n@%4|@9~-l!N0l!fIoZqSoDf>o z{~BXvJ8_a+7RglqZ(Td2wV*+Oeh`4bexrt>0$oiK6d|3_3$tM z>dp1KWByh`_VN{f^INd?O3k#c!Z8=1PKMPLbno!7^WgV>Z_{O4Jg=# zwct2Ww;%&eBH7U_fSPHnm)ZnU%4Al{QBtm%eM+7|)z^?J1dbRH0v1L!ob_+p^Y9-W z*S|={l{oyt+9M?UFg8>RF`ALncxs0bRM&9a3rL$$(SQh`jx@v0wWl_CtfIh&*9djk zMtt=4G$YaI?a|op$f@_9%oy!;`d{y8$$Vdd7}V{?vEqV=yL!@+rt&37?#oQcdi$^@ zN9)Qdz0Z;VCeufizNQ}ydlxs~&nkdGd^UG(oFAXl)JWA`2CW$ma3}XWupy^f-$-Cl zA(q9Gwu^@dRmfJyQ;VBY$+CGipZ>!SLRrj%Fkgr#R-KBc~tHTVIe6HcivU@;Qmi}+IzN0tg0?5C+ zp^%qJ-0zExGXuKr-kvg6V!Wvp!iC7t{@LGmALNDK>MpH?FZa5^SF2ngV`2PDy(!0o zC2x+bggj|9~E^!-C)vq---eX?POST`8TxJ8nZ19daPK>9^a zc)|K51o6=cbh%y)y>=_ z1}%s0_y9#Rlt6OskQMqEm*5ZDIqud-)nFKVU>E3o&@lgKRZ0WQGrD}~E=bZbKWE=Y z^uxFMWs_gj%{)F~wQ-b`>eZo{sB>Jb^ByI`F`O~L*ja9d|1}aDO3waWT2QyBeducu zt!re2Lu7S5??^Emk)7k-loO#-|ZG{_4(oBtSls$SENh0|!BcM_25){fQG~V@Oyo#v@lA^V7zOz1%W*Yi#SBKN0miiGZS$+Sa?WO>f@Mf5utOlX z#8bDBoLC0ZGSe7%h2pS*BbBL9iMF9G zqcYJbz5Z3EP0!4$`;@EFl#E%k5=9S0gm#w359K@+;nz5cRJ4FZp^${qS{i_KH6^z$ zLU7}zIEtYgVbYzPe21`*!qb^tTp$1AP0Ck7a?C+dr6OG^`HLH-fe;4Gr7M@E1u+ml zC2wMrS0vVj2c_GE$3VEFy}v#8$ufv5t@zoWr~mG5d+%wi{e4Whhxp>Z!%*dbFVlI+ zuC@)a542zzd9$V3H3HSMZxH-Ns_0JqK2{zy)orN~jX#IZHRS3x?d6xu%uKxNR9c#!9XI_4-H3;1aLJZ6uj-}AN%z27(Q4w>Imp$_CGz~AaC57el($9_CK*_$ZZK_m*)7kGEo6kwwlp3)hAv9zDb{9 zbv7$-6^vNF9QF}aZ%q{sAT+6jTZMLXn zbld2L=$5aHaxrEoV-iY}yB9N)Xh9j|BI&99mT~^dyf85qDD76kC!cZtNo~6z=2e*QKU zkCJ1nN{&EP-Nx~ULvnD2=z^}FpeJxf&Q{@P$?p?--jW=X4CT-rsV3L zhuOK=M-B+rLi)pJutvSlL9p`*IDh^cO^M(D3mzMk3t|1YoW=Kgz2r~x1K$`6b?ol- z=hR6t=$&mpE|%^EoaygA5ea+qO|blTrDP63cI0`^W%^evuAfy1+{25WC$8%yxa{{C zsDys^DgJn+?K`vrpcb3%{^0+sSWUGXsk*zQkn1yQsF%)|1lAtbTEQ0T1v38l#tnix zQ&DD9clD*P$)I`3xQlDHE1!e&1LIqf;)TXqed(`Le7aJYcVRypBo>89m6+wcZjoc? z?Rya=@}<*+KMNp0x4$@7#I7w6SG+7t@zr%c3-|Nm7^=dVzkXRu)bhaEgRYku-gP$Eon-&JRF{-$#%i%W0E-G@ng^P?0U%&_gvPZ;f3VXP8f znrZ?HcYb`s(3oCr+SOnX@ARB;$i|B4HqX;Q0+b}2kotgs3HcJBO%`5xaMX=DU@(|W z^KbmIF&3=%VUGL-_1$M&ni+nV7Wf8w4Rj)`@4J`}$nJO6*~aAGU|k@HD50c+r$R+D zE+#p?^c5&#vRksVq}IE)B0fwiqxOB)MR9LM(UYf2!D7%TYV->>w9$b6aVlCJV(CY% z(ybVG;YI%QF5GGZ0bl}4!PFpY7q%GlngW#6n|xDtg-H08H6eEOlQoG-!~~)=T4gkb zlWKOXrf1YphptG}YhNT`0ckdZ9`}&$dXS)UfvwUcX+f3UCL`yKnZ4lv`CaOI%|Cok zY5@P>j}>F6C_KB`joif)MaCi^gFlTu%~U(mG_UT0SJ2z}=HjFG{k;bGs3HAjHi4)A zQ^4tc^z$;Pk9haL;I%z3w3f(l-^%+}ThzU6zdu;!AD!9VBm$pdjS&vj@MF^-5|RW%<<%gj6dzp z$^QF(nVZseVn=8PNaWq$nl2P! zENO~VSM=f~G=$ZuB6SQ-t)sHPp+Cp_4+m7iI)|P6i(S@)xH>9R7oyN2F00XP8$E2e zp$@bK3i+?mjXKLYY8W+WiQKLk^u`oJ+K${9)3N1jAK@|Qo{5tATFE+iki;VT>qXmI zL&ObTk#vB&wi&Oq>8Q)ssAn6_3`9Mjcjt~kz57}7Llr%oIAV)g>&|sE7Bn24;YH(P zh;g6ypPwgSmVfd*dK>fG-QButHU8T=p18}mZWS9Z+3>P2g$3k=n@4BZ^fD3Wglh5R zBpN)uMp)EEnYG+DO6feR3$8E}Iv)SNoT!pc>_8KFWu@{Hbzur1Ytu{82zlkTC!n|K z`x7#Ut&zEe(_25id738X)o?`T3z`;vR}_g}h+LRxJLc+p1|xL{EdGS#jyMNmQ3w^e zZE_xCC^VYJeg<&}l@0}@bft*mu1utr+@0l84(*Gs(dKT-1b_Dey= z-#M-ewH(}QhHlJO#MDC5vg#k|AS3MfC|W#CBZ~Z%DDkvd-y6<&gV9TFR}mfmS!0C2zS($`wU6?U_ek*iw@4jT|kk<#DMdZP2d&xc_WycO!o#1XRaEuH4^ z?(C)JCz+KwzF_J0+(G#f(c8~(wJk7eeP>GkReziH zoksTi;_h$PncY*2G>)xIES>Rob$H|TH;O{v?4<}`bEMI`+ldy~f3m-8MmT8q{;%*7 zm@(+Ks{RkvCcww-w^w1${qsQ?PJ8~Q2+{B#P}71MXHJ!|!VcA*TxGm;?`f+*_wD4> zX$R@axMYq0mRr`8BTt46F*hL)na3~)8=I?hQA<~wLq<14!&krk(Vb_$Icupa0&`bB zM3D4RazP=POd81sttA^WXW2F`w6!W!FXyBW6E(DkAoiCBAJh`7Nf1luKCF3fM2QdT zY6HVMM_x)axEI?At=_V0+pwFf8ZmxI#O?1ayzqwO_Q}92b-q^*xSfm))c45Hm+TcN z(hR8ulSMdLoQzQU;S-1wKfq;UZ{PPNO2<4B6S+;x7|kWg^9Zw;2FEqr3d{g# zAMC(IQ7c82>GDpu&>{(Uc7~rZrBAqu3SNd7Ui)FmQW&4|e_RtkU#fn3lr9%AtIcGS zgdTVa!xlYZdq*ty7=jWBJ!Zrfi1}0JfM*}g^k`5HG$H#POB4^U3KH35iNqasF?S#S z8O5tbitl7V6~D67OxD3=wm}3GI)M~ZR-#mvaMVIJ_Rf0^)&^J90N*3QH$omk9fX&C z+{WA2@}F1MfyZ7!XSB?b50=aj&+M5m&xd=pY&IN2j;yxR5r=Tm&3Qan$;DVvYlzY` zFWDO?(HbGm>zl#&em`ZMu;S=}_DeV}AVR4y;phsU=>pA}=FmbBbKQaM`%Z7PA5?Ht zueF%jQT~dRceSAgNwb7?K}@wd79c%KYz7L zaR>xJ{8nmHAH{ADV=2~m2|;0Y$zN(@$L0ACflWvzUCh1l5@QoL~H|-7eRfk6tCqWuc`?0Sg z&oDfNknNc3wLuVW_H7blMXA-UK+?t%!vt{m6<1QI4A@k)arnDq+D1&URK=hEK8f1!vMMC}%2 zh$rYUk~wUp}EEK^gf&0WUA zwDQIkV+Aix=a~mp$bdWC+$y&~6L$*`XT4tTiQgyKW$5NMAvg|dTjttAsPaR-Q5N!mt>&V_)^bxA(>ViK5oBXpYJ3|?oI^lHb>67# ziFIb-^3zS*BRx2W0VaO$dR0FV=LkL zNhghWT#u@Bg4r!wn2n|9?TfH{Sr(7e?4Iv^b9Y4tadazjh9VhVfdfR+EMx9|{-<%Gx5ZVwf<{bJ=;DRnlN_Jl;Qll}JXZ?-8aAmCj8OMm~7 z>qJ1)ytL8F0($Rl27z|r%4ZCWB`*oMJ8TB(8qY-^J}5t%l-*#-+wHh zpJHRncrneCWcz@y_U0VJOQMD;=pkK1u@dB9_4(W?-Q*I$u5D3)R-TbAZPwzre$b+p zYpi$fJLb4l>{6vQwdtnqSU1X^y zU)@MJTLZ74f}jKA?f1)NPNBnCXiJMK?$zQoQ^r;HNBD5v7a~LEpPE&qYn)yq9}~;h z7@iW)v5PuVC0ow*zh$dZTRUsp`-p83T-$YGiWY~T{1El27^ZNxYQgs!`v;(MrqPJr z%3Wjn#g_hrShy{@DoK{cGSb(+_2GS<2kQWDIrZhg?%lo`r(OG19^gm;>B|U(s;G5` z-hh`MU+_>+*)#YhSf^F3nsS4IxWPy zOT&BEyVJ59SydOCy7WyH0|@L1@xKs|sI1`9ZwMRddK6REI@ce&6kL;!eOr*uzaT44 z4Mmt+)!5!yrYGkIO)-31T*B)J%5p&G`)zc+OA| zv*ZL_HnR&CP#(yjuLLKc3DoC!m17uWcWb~dZYxm>^p+izdob>E^5zzBdwQx)T1}kO zyiu)oh5ahR)Yz_8Jb%o#z1#QC%J3^X*;O65jm(~YxDxy>d=vPN9%MXSck?g(@gEoc z=R?DR`e}o&hXZA2!QbwXfmG`ZRXlgCjNGQp*@PHHW0b}7-YuJHSo>mZ4wWvCV)@6?-OjQG9uF>9>+dQL}&upCUF@Gp};-&uH zEol1oiDj12;YC{b9=-4VvhUTS@Ex{UYZWSIfzZaXGQBMA#d~9VcFHV0ID2-w7NTyv ztaFnb(GKG)R+tSENR*uXB%VgxMR*AP&&99e#<;tEF-4tOsRkP#SVpK??8$gV)`6%o zwSmPAm?UVE1$wjP=C+7DgL+c^rCe=XO*+E` z-)p#mD8jbAzbokTca0wyB-Bw*N0U2K-nD(tv-iZ{m*tQ8vq&4$)rM*pp7n|GBO{!RBEu(Q=z4ggpfFtBiwF8eG zkL7@f#P<0Pc|gpfY*Od%89zeKSDvyscctPo@AA9^-kJxhv+0p1(H&2$7j{dWgHp}tFv3INs>2r5|n=;Gh!mpCgO zc)bYdj=B4%x0zgq?$Z|Tb-@N^rQseMUH|N_v}6Nh40hdUZr$DbKN{DF#ekal1<$Ex zSI~vuzyH7?d;Z+;%hcI!dk{Q-{RoQVTi^04bOY69nr9YPQ7bVHU~D2mK~Xp%sCr8# zq``3}Efv9|CY>)s@c%NWC{Dtck52j`LoSLl?(zj;W6xt{3DIixgqbZa8nhU7qF34Q$DXI_cy$6Ji_RSoAqXQS zE_(E9>aPRY*`fYm26RioSPe+&FHcZX>sjv`a46rPS^KF!xCXog8k22T?OONHgLx); zkK;HkKPB#8$6r5Rgr5)2?yobKCMtmloElOSIgza!8X#+^%D#caRh9W(+0EeNazBJ)9Jmn7kMxXS z$?uSzy(D`pQA2EV-xb1IbYpu-&fxxq_*}e%?W}qWMF?_>e}Vlf{DF0ZkeZ#xm7u*Y zxml^}P!(Y+?3c@pv_i>0<`*jv5y_J7XD7vclwX3N(pL;ITA@`$dL1wsy8K`=6ST6Q;6Egi{9-n= zcTFWD&Fw_);mk!Vp`~-gGZT(37@O+{4KCnmKJ0E-2rv?{o5VxWT-`YuQulJu@O$BA z8gnS`H;mnFA$X8H7jw_Kc+p$?krc#qR8GWh$#0Ew}=W z+VmID1vhKr*b*=k_ILkxmK!4?G3m2{M5h2)gf3!iSb-FArI;#z7c#}JB{)H zZ?yuu9uFHXELqUy&CqHc0f6-pVgID>V?JUqNQ?LES2>T{U!A#`McBNTw+{wb^{d)= zYji14GXQBQdPwJiJror#bAUr_<64TZ^PDF|G67R<;W``(xVA=uFO)3NioO|CL;854@p;2;>vSyRbT?W04D3Ze#D;Sd~*n zZ&_&4!_$|(xuCF*aeJs6Q86>)bJlgxy>i-@?jzU>Wb`l~e?ovbe&+K&qfCqGVauc3 zD{mz@5~MX~vi%N`cSWvFyA(!_qyYtn)7Qa$T8*U&fZO(&L(UA1q}RIVn8$3dYT-BD(xlGKoZxQ35B`bP`5tEVcxR zaJG6sPD7|*h-Og}YBGW7V0v!0rJ7w4?-UxQy4nb(M+1bd=Y%)DVc!CQmI5N@7Lo@7 z=e%I7M?(L&a_GY5`@**Vp-cPGxRFm*%0x=`M1hM`P>tujcLlF#smyP{w@*S3#eBx1 z+`-<3^)hA;KEUPmt+%#ZZ)c|f@mhEH_I74_pD!P?Cm;xX^VUlCm+T{Um#k2E@qe=P z6&<)&y8m_PscfI~>`;z&Hp5du!oNji?J;V=9a=~p%1Jca)E%tihpS*DgJSq$Pa~|{ z_b}9_6zpwE>EW{`lG*sYYngX^ z(cZhnSq&j~C^|vTM?E^n9x7X~rE~##jKD<6&Lzni-~$=<&t^!wX{>&$33Kkg*?imb zIm~0~%SmpXuqDdFT$J@^PUcgxEr?>v4 zcc-@@h^B0rZ=EZ-fR^oH&gFV2Hrsl^XC)KRB!Pv!WA|!Zp5a*2>5C^|RG3HUYLk7MNOo}- z57GCPjn3Iu4D7P)`Ix);7n)&RJCi?N(5Ubo%==z@{3HYob7pWYh@QVcM*`&49;u5e z;BwGLJm!csA(UxP&ZXz|ynexY|L*0_;U`R@v*|2^c1!$O1S^dlTq?hO#&JD_coZ_# zm0Pcmhf}l`nII2qIV^r#%UafCM3wD)o|-^YjV!+U$ZGHai&3mE-**hG;p6F*SmV$bOy}*q|eiPd^g6Sep z$W1MBYdAHuL<+EIfpdh93~6YYsRsT2Yvm?o$!)|0tSZOwRKEMl z>(BltfA;xI`{l6y!jJyxpZWVg`LF*gzwhqFr?e6TPV6U?>POUGorZH+Xe?#f+Sz2!@x|% z1Cc~aEi}N2=HGpY_L#k8`zrbfzB*+#%n*|f1QZ;$2HN9CZ}k+wBBL{W?htqBe#)up_pwqpAI;f`%B`Jtm7-X z9uwkBP1f>BA@Bvt?Izqk18eSpbp?Q#G?PFZ;aQgis|%(c%}H0)^^Ebz(5z#cO-i#( z_gZhfBgGjNJ)TaZ-3TNy##8U;^=b*8y`X$zN1TnzqdGJv?>oRQH6C|A?(>Ocd+yf$ zFI~*c2X)c<kv>+tCcM(o#w3wjLKhKCX|C4`0o8h7hL*ZJ{kVfyb;S!|K>VMKGfXhY|>6 zhQpV1lgiy8YHn)p78@6|61eFFk@5Qe2fy#X(z6Bsm(BXz7k}paf8am+J-_4kf7f?? z4_ydXA^URBM^z0_LIr&HNZ+xanbl~wZ+{d8{k3@Oh-x>;lpa9ctR0Mx05+!SXS zj0P#QlT4k-68%hD6+x@123Dz)Myo(Ygm9)6WWp9gU>V`h63jedWGc@9Gd3vzW<0nR zLBqd0y1Ri!tID(VKrX{O4>-`roLJZ(BO2(i7r;FUs6hr4>6{VKPmHtL1G6bkPzN9k zBe4^J)iJ^@^+2-JbzKhY;l<%_c=6)B+nc*m>v3Jy<5CN(kPCCkzw{%2;&1-gfA@SI zKCkEX41k~4*W=n9o)hjEMXW9J4A))N=RhTZ6UL1?o+cZObFvGtY89rjmTDvri; z!`%jo+IxUzEmP_m!=$fChEiU`hcB@-gWDW^BYPXrC>z7=TfAFHHF9~(x8TNl8dE@MClsLZH5pC zW6_APqVG}mk{YcWbg=ruwn8`LhzOc#U4laZA2P<Oc3fW^ z9^2!?SHCp+Vz`>NWDfN-Mr&e6rlU_y@25?w9ic8G3kd z9mK@_`H%mBKlJ+N-aMb`zpPe|_Q(IsAO8Foe)c#1^S`woZeg;5F!WO_YcZm@mAcqt zt!3%kJU5Fwc%v(X4tWdjW?%16M> zM)G0^CRFfI`A#IXffedGRNL)A+dcYg=BDUS04X(E6+m->W^Qhzd8wt;!{P3DynAu? zYCRn9?(Wugy*Vy*smOC#s}_9e^;iD%fBOgDe)S(bpMlToc|8N*=k;}qfcM(k(Jp&Q zv)UQht1tK>ir%SAVo$xYnXa^V8eQNO0L&Q1U8DTOYdmzt`c9nQ4CFBZKQ&XEYB0jmVKTHZA3}5i)*}7K~3T zzwS9Gl3r-VyjZD5cXKlvX)j)i)9A$`R2N?s6Kw#b1+)nCaHK+vG{K`Q!3egFtudz| zn|q%6mJNU~cYNbN^Luap9{CVDmB<_u^Aj63Lue2}3>3R%n-tQ%ux%{**aZm>imkZ^`7;-{=e3b{`Eii*MH)F``3Q^_x+k* z{ZHjmdI`GX+zM!3N)&h7&ZX7D^J6XLd@iJQbiCaB!~?jVy^qE|Qxo!^mo01!fZQAy{Yx+?kP&nIo<|+`3o*j!bEw zhbX0{vTMAskQ_J@)ZD9d(SZ&)Sdoun0fdZ%6zA+cw%2N}+22tjBJ*slup6O``5(wq z)>5jMQoJ6HFK=(|)^&aH^3}~@J>1;f-mD{@4hMUe_kZiJ{I7rIFaOc!v+j94uV(=K zyuQx&`pCT4fZmgYcnP}rM1_}oQq@hY8Ta>89iN~wnrsj0-^_E zyLeM+xafqhsnD85I^&g0?0l&xdV<|am?7fQyq<5Dxye!=Ka+q1Vs3F&jSSRIpF&zdF_h2{FvLUWaji4&yUGQRkX1j%JSmq z)ooq98cQilvEnFB^Rl?iy|}s2ix;DrFU5;nU95Pi#fB2M6wqjvqa4Z#&5%$gqXWuF zL}c$738iw#5HN6}0gBkaN1&7usL0l1eEBjX5*XfkQ*;QFdQWC#2qC7>BO(<-Pi+moWk>cF=ZNaJD_B7hSFm|JzSm_{Kdh?G~qZQua` zHK4s$Zy&pewMPkLwue@8j^l_Fis6(HJtZ4q1gHz&6GDiNNVyc9EEK_`+bMH4i=qIJ zsVG1t`$*U$A`|f8#5Xi)OsUv4-3DD{d_M!{&{-)XooIQvl$t?{RPMgAYKNt3Nv?rvW)fW?2^%kMPswBUm`_j>o3Mgw2;rT>+G`1sIsJ)nj{ zSPqJ0c8_FAy&jYeri2-zHBn%gMDqs`nvu~aL?Kw~N94s#l5HWfFKkQ52UsLF}fqDMSF z0L-3<9$S-?y~j8w2as+Nt*eD1a%<6rZHse{$E`h_a){igD@37rKh!?T9YczkJ`ae< zoS6z5)oufgJsAze90bhYB#@vCwiZ^hmN^b^OXJWrZJjZ$37HWYk=Zjs84*2Wi)fkg z|0l!jygmH!|LMQ^^so4~-}O6x-zPu*&09g1jBfoTODpwImm2+e*pBDNhxKrLcxT0U zI%R4DR`f@jAu|LjM{gnGF`AoK8O>(PUP?eycJT}pV55MDNWvDWAZg_$lVCt(IVwga z(g>au(ozkevH?lSd;w|Jh3am-_whj)Az6yh20&H`L8eJZxBd1VvPHS2#5hw2K#`f- z+ib7V9~qBQg9HQ!xC7yK@Z#>Z_-e*=y;;}ex*iXQ^~K$*ySo>Mwcgy`xTP@?ZDaq* zzxTKP(x3lB5!>^|@_9Y4X8`=XzU~R&$u~}$rmhvcCZ)Mu@s;+hDMB+dX`&~)KF$+f zbt2s-df=}uQZlo=BuPz}W0#cW88w zo<@>Ah4(uHy!R?i02q_<-huB7tDdlV4*`&-X9&G#YuX}Crwnl0;;3baC4H z+jlxQ5IijFivw%nQgC-HhxeOgZ$42asVdVI>=}zeliHx2hD$%#zC%2EbMVF#S z4`!-G_R#sPp6ZcXSI<0c`SwjeHNINM!_p>`j8AD-}H)iq;e{ zWDC7@%gi3px+2vhBhfa6^4zkuY^km1{X@KaNC9r^iButqR0gX?s zw*WITLXvt!WNc?7qW3s&ifHY}AGV*rDc_l|ZV~1D(6xCxWvxr^s!OTM`uOnHP4C{O z)TDsAa>;mDm}kxlp&>JY*rMN?6I^Opq>hN8X^ITeBip!V6<>PX(-y!-7IgMAY*7SI zX%0auAV+I~AaHSZv6vy0mb9XXwwELUNSY(kONr>to~gm{O8p{4OD^j7WQjN<&vZx@ z=MNMc`def`XPW~G(%g*h2lrCSTI;$l$6D8QJ-m4N{^79R-W(5y!@BsomRgY6<@paj z_kVr%vw!IE;VaKu%IEdGo&oUl`p1O<<|S8n4_@A(ByGT-*=_s8?>k}C(DY(zlCPfF z35FA}p;DarNi#HfmV`_OICt@{l4g082T#lmk>-2G)uj#inx-_(x@L+TK{> zKy!Q7lCE6x6>VPgw0zbMO@1}^ASiH0gM%HIyyajzuW40!!r%f|1{=9&bzL3JxYFc1 z3Z{68%D$EpU7L0>+0LuL8}=k(>*5){wEJ3SI+fcyXeAOJ~3K~(zB{4WCGEH^8cf?By0 zn6bK-!rP_3JhB+wWIjBj^PRiRF0L_FaUaZY4C;-PStnNx&?JsFoy+ zWSW6*3T-X96&*cdOqXeJQ{4`~JCq`gi=RU-Zxaxc>aQeX>K{YJ!3cGFqED z2o*Bs7nY}NgpqzyKA;oNCut?%q@ymz~P{4?9*>-PAl`*VNy zpdL8#W8e5K-~QgO`sDg?OMEVW{>%NVXPh7{5Bbd>yu5j9QbWcZXil0kI8@7$tdMdu zD~ZgkwdP*Q7Ev;lq9S9=M@GG!D*~x(4H1kUEwpV&#n#cf&YQNbb8BDtC?8G&VCFZ~ z4hySWaV%Btbhqm1#j3-NUf_fo+^zVan#W8qVKmTO9857&@M16nDcW#@2Ml1&sBF({ zgP8@CF-LXdvhPF;^jThJc?+pWA}?dCBwb$?TFaBvAJIcS z)w&)}@ospuU#U3Aq z=cQ+EQllD@V?1P_5lulU<3YWsEG%`GJb=uILE)aDAl;ZbS^(6^nsG8)kQ;GOo`q^w zU{1J_Idel~wg}Z6%5LVQMLz+TnQtY1Xry~@yDPv_jmjK()EL46SGG_jYW6p1kJ-

|zviw>#V7)PQiOB?TO@Kv zhqIe^a*5dKz^6!i;-}G3KRaq+7yM~6wCV{s91o1?(cM8DD9LQ3RVBPAcHo9#H^!x- z=n1YTo@70bgbYoK(Qv~?rpTBS?w;m!k%sR`@nV17?^HXpx=Wz}l7a`F-4tS1UzLUDo9gKWO|k1=XWZFeirXzQsw_d!IPAv*W)Z5Vd?+4j`y zG(L&L-cz%14R)T?_3Tr=#B)s^Yk#BTb?j;0BT8C>pW*!M9=EWim|3=9f-P`%y2EI0 zX=XMU-2yW!X0_N-tQI3ss|a(4ds!;H!i+|`St+8K51Pe9!d!qtqb+0j3z1X3jBMy# zks7wuNVJx{A(D}Ux>W0VZp_5CVcXQYdPeK&fnjzWc}{^j?-I!^mixJyFJ9bV`rSgkI1(aac0Ne-cRQ1zZENGztiqguzfP_&q+MjA;B${B)q zl`rkd6B1&4gGd@x+}zR)M$%9iG}cm%)z+#~xj2inR=QzXP~6>EoVCcDhe|JFP$+XN z4s$CEIB;To{_yq_r!O6ojC15UktkN&4{lOy3*L`7lNtS?c*$%93MeEWlxa5g2jxv> zGy34FQ!z{?TRRskr5+A1*SeOv-rT&p zxw(7!^3~z6-rgRMhouxNj*PAMQ+e6==C9>fe&Sc%;n#fTlf$DvbnsMW7Zvl%6{!Rw z)FV^l=0auWRLk%Eg!_=+2F-rCie7y6^DGL%#{0eODaM|pxUJ50eoascinBW0Y^`bL zYlXYKXbhW*(`|TGlrb7FGBx%^ZK!yV5$HVtC<8)7=7@xdkkESq*}4=NDfQmPY&}wn z-dZY^2}PIGdq&S_kvadPZ;?HpG5<3F{-sGUjtEdA#^7+wUkSRwcmB(s9MOir$zbO0G#@I$w(pNq7$q4tjgqvXuM`}RhSx5p z`7_77SppRyr2&zmVhC%M(}8oLXrLHw(@YT>`Mi)2eK_QDgat=T)E1#}6CPSe!Q7rS zAPH9$sB;(b0vDa|B@38WpgXZs&i0+rn=`wD#|N5` zmzTEp-~yNTrHg;~5_iQ5XMEv)FKp%vYMQVlE-qqm3a2kMx4fh)KZ%asDOJr(XkDY9 z_h_-H3g*|~c?bqd5##=?#GWXo-9o$rCgmv}NeXO!d3UR#+3JSlL#j1Mq2Z+mLlrJ{HgC}H+Ca|D4sJuu_zQR!+HLeD!0rHfCvCKIvPzEuws6kLx!r{ZM zsc}0`u;lhAiIeSgjxx!VgpAm~Ws;cN#va@@qZCkUeQUODdff2%(BC~oq>*-9)|W@D zRdr!4wpK0+-Q;ddg&AwLT2Knz>7K$U`oZ)bk5=kIdHGO64)bJ)ZU(}Pn#u@7YB%T4 zU9`KAVd-iuBeZp9#Hpce z*_EfWT92*e{X>6qKN`0EW+^X@<*;z6<(qCl^#{K9^zrtAzF=Y)Di3b!0=&!99Kn&1 z;{GMVoRA5cDQ(WRYZK_`LIF^0ImU`GM!#ci>L;9t0wr-4*Cx;T#fpTAPLg-r0ILAU z<4UwK_(LYp)eD9!*&-1#ajI+u9Qh7FrG?UEwk@vNuC9=4DE_x5^0`L6Qm-nX_rI{5J~ANu{9s0a0COPP9}JxvXul^QN7 zsj8{d(Tp3TPnL5WYzg~0GgUD24w56$PtlP%zGxYly^riPL??)dIY^3B?|E)&EgsM4 zq0_d#d6%aR0NmDBM?bDSEH;qsvalFR;n4QG!hrEA?tKBU| zw>nKd9M+NtXbnQ;Hm4sLVtPG)1Ijk}?$? zsT?xw?CQ!M+)e~?>qupfJfG16ThG&DJ~ll*#+!GF6u{$pc>l(Z6=ku-xh$+MFTNCc z9jSGb4>!xPwHOzL(fMUIjXB(}Dl~{AJ`G`R!$6bv-WUB?4qtii?FTo3=%+?iTCr-! zyW2AJ$S$@=znW zogaXqaB=fx?2CYaOst^|C|w2$aDhVEGcFT*WCvI?8|c!bhU!Su%#poif7JF`@xVMG z`w-hhow8C6%d)QP%~B6{FFtlWytui!yS=-+y}h}+y;;_BsKpBuEwh!I`&Zxey1sl3 zeiHo61Y#ex@A9rO2q2VZK85;HM8{xXhS6-nKNNl45GQsP2Sk&jD!g1ZbIW! zUCr4eXa-2;8R+yKbmi_I!?W4Ex?olx(55wh=vvdL%gIn$yewUd-&b#7u zvXDZ~dG-8(-1j{DU!RfUPZI&gCmFcJ{*8&tzC)UiGaoIlzx?G$@{>Vc%14SIY5IX9Qe}QNthQv zBh-BizvlA}X(gGF1v%U%rPdh+GowmnrH&o<1P&lG4oXFMMn?Li5dytgkJe>v%X3p^ zo;IFO*t#O}{t?^8QGAhernTn_lWwdIFXI@{U2ZHsZIPv#(cEZcDKH8&9~=k)$lctA zOlZ)sgVTeNwI)$)w};Y_BLsDPpF#5poNOku&f*<&4i~8bEW)gBH7|rv`T7p{?oM`1p{g^XLmmAa;-JOr5f! zA2Yhcxf}lgchWQ_4K`F&6ghx;jL;_cfA9UfD{A4Mb6bqC?e(GZ9PY>09tJqd@l zU9mi0+co-{*TkzqNKEQtRvwM~n=7%ZIi;DrGcSDU*HB@yeH(=*Ky=Wpm-)aGcsRAq z+zrXoo^4m9@$P4ykpdWnieG;P6L@P8TFsdfz(J-I)UBQaE7pqkx zv1uInOe!ZsC8>AhP$p+anvlIKBU?xB1fqww^CX#+iqzI5F*5u7hGJ{6HAZUumDX|I z7%}{^eT;!F@^vGZf~9&flB$L7Fk`jxic!p~%gbb>7bltKRtDmg8{kf}#E?DP_-+DV z867`HvrJhj2@G@EXvQ`}vcTwOR@?^7MVtD~Oj9aa_E1DdaHmETU4!Kt^2q1{5e*3q z?rW$;CXykutB*k&b2|^_5WOLUVVLZZTjTkx)|7Lfu}^{a1LekASPbTOzx7)__I=;$ zFP7K#RZ9bTTW(HV-edVleWOw~I9Sc8R|%atCt)%=fEhVVBPfJ{u-l_z?e9E#&z_1i z?*jo_1bv#vquu?aHUKNakp(=^H{Jf6UXVsK;t|lBMC?!86HPE#r~r`^pVmomnlbQUB95j)T zITG>*>=>-FA|UvXjR@mrN2FRGNfKk@ySoN*$myppqcs~-mwi_{=iNqM3f5vvWpNg> zrI;IQ9in2Zj#6bbH`$(MSB&mStJO8`l4EaR#=#7`bC*^PzrARp!CewX_BOr!8l+ZG z+C!l*rT_}f=Db1rN|@#X4AOXPv3raD?qTu3a)gl4)svAxBB7Kr5kX*zQ(8wRduH@8 z5?1d3a_iP3S~q-6fdj?}t++464wX(U&SkMuR4ScZ3X6NKm{%?5@~Yqc>W_Z*=HYcP zy*)G~l==Af&D*bj@y%C%?*82er}LZWr=AgNV}9BcNklo8!>f9@@lt4%wlPQG*mOaF z4X=Q^@A4&!%n?`x_>f!C7Bd%|wtz-kGX++#2o6v%&{8HWz>J?86P(1zp@87~J$~eq z_l%BgdH#ypYxQ@`4eHfPW;0qThgz0pJ>J~B|LWB@+}*vnyL<1&i4hKywS4naKf*bBwp8A+Lc3e?w)PqtW54gT}XS1lMDXqQ;-*%(9>N z!AwR*#9q>28nJ3%Y#Vn3JdV}4{V5X(4JXJTxQ3uIQmL77x^F3mip_79dQ%Fx-He}nCn^E2rh9e^Or;X1zL<*{&?XzuQQheG-C1wD?egQQ_Sc&Bq*bmZw%rx zDMiYl_WzRgE6zE_Ue?;@d|yOHWL9Qny2{mMV{BvD>TV%1pj+JnA+^MW83P6k zV1m^00Es~l5q|*#7$PA+0*M{~637CiZYf7$Xy967u00;hyvCosYQ4NVBjJJ|ZLZZ~nXg8tO64!mY6iM5KIfqN-YESumH! zz^h87;0IMFI1_!vADEz^%woDj;58DY*Y>==NIrGA$SEG?JgcI1`2^1NAWENx*b;|I z`q)Noe@hJ^Jkt*eNb^!oj53BhA>Fsg@Swzukd?ivRE5a6->LUx&KECo-{XEq1eh(i#=?MU_NYltqY%q!9}-GC48NowGb+3a)@>N|`y1 zF)q>xRJaHx&MZc25j5tCc5$_5}JV)`)W zr{vfi7sY~NLSiG5>n8~$F)cm&;+H+%y$c%*qNVA}YkT=fUCL^&&Nw@S&VXzjsnk<~IVhHl=OF1RJ3fn-m2;iOHfF@L z9$d61s_xned8X&3XzIKxA~|tKFH2B$Z1d8pXXAVV=>B-UIz8&j^8P#f`~{m zyiDwQNJ??+zn@dc&o|K zTsMFBo`xq8l=bxsdA!K=!i_;PZ;&FS0%C5ByCkuQI-vEC7XmkM?bqg=yY`PX7ga=4 z=2ejzgQ-@Q_#j0;J#P`m_MXmcJIw*Nq~5Z)2pVGUy2H)tjjjJtWZf<5Qf^bni!`I6 zn7eZUtw&w@lfrq7dGWHKi1dIvjfe=C`+fDV-^6`o<`kC4g&!8~ja8BK`go(Jz)y3$CVi8(BN9^I5k?{hAB2HG zGadUztHUeM|4Z`vGkW=T?gZ+9!U6h%FQ3ul|7K5uTjGJR*O%-<`E*uf6A%RGp^ZjT z5~Sf1%i9*WN5=Nt_jVX)h+pLXhAy5Nn40q~zj;NU?y-OAzbQnX1YB3&_t=n5b+ZRK z{CDk7{@?8n9{2*}jm&DV6m##IEOkF43na79ECR@Mr+KJ3gFLf7PDmg;%v9JayZ>ry!j^0K@>!n*AMjgsEvad!djO|W>zNVB-X~Y zJ_}-E5-yYg3r#N=&gqwGsa;mEdR-{PSt&P5@(M0ZU5pq+tuN-m6r%0$B0!{7UbiNC zJTEWlc-;VyM{<2hr+A(S%Sd=c21Q*1A~HGL-P41CFwgMg!fTad%0!MqZW)0{^6-85 zeT%p6;=WcDcYAr!kFF$|AYy?EgtNTR0I8<12a=hR*p>DtdGlKzUz`5q<^JLqf@QmH zwpl3A<=P)!t>Z@Hc5JCR(t)bXrL_B4BM|-if&wfA1Q5bv?io|}nm5x7kc4?2qe|0$ z^cr|LM-q`Hm`F1tX-Npc0ZB-L%K{C)hOdC+JU%q1p2r}W=W-UL;9cx*;P1)r$Q|j- zAGv?S#3HS!w5Bf}U%h(u>BpaZ`q^h+yng-adRGCi-;MSEl9x|G3Iq3i?#=*b0?85X zp5|2bel?O66+eQC+-ZS*#J1aQwV!_DPxsn#=!=ftLKQ)@1>AWD?gJzc1~D^8^?5-| zd~_Zbr*j^7^U1?32w_$t5}n>YrUFlZg(-u;s(Iw=h)INIQ(b@$zbmVjOLi(QOp$fn z;AdYZk%*pVKVq)rRxweh+Cze=vT3nh5lGvr7sH3c1|n>$?N7#i)feCY3w32B0&{7V znKVIv#Ec|BiKKT<5ZNHl+_t!{z7N#+z7N~>*!H;Jv94*6Pfz~tU93AiyiTns(#m|Z z4@+zcah$VELI^f(W?<%eFPNxm`grN86t(4I5kevRnVGcJky-vd77CX7bWvNt^JC3S zBr;K}hukV6lYgjjVOctqxCV`uRfWCl0(x6(N_7vN0w>#V^vyiCu zZDxYUF=-Q$NSV$}MkT|K%^3s`A|forB#K34S%?&O!?-80^hPQvstwL2(lL@pBVuR> z$$0nWMof%0aq9rrYf%cgf2m)&ZQ?(L<2&Cbogfbr+)uFDICKJxwl`nt5>Faof*i%HQ~p|HEJV)xY-Nzxz)&y#1m4#ozP4AB;OffHece zUuKSsh6t8SKJyZI{_@BHY!?6kAOJ~3K~#Ex7?L?+7GrUO5brPH1g!CnlY+KdWOc}| z$fqy)f`otqnJ@zVv;5I3y1Yx<5QJNz0alyOb~c~^Kz6!tzVLisi4l+}u4{Y^TJApX z`~lJr^-|D)yuet}ZmD*d{@(iUe#GDTGyew!ihN`&BmEw437khb!qP@=5z&YT>=DZ~ z+X{ORi_*7>kPAwMV}%>e0$b zPBPnJFlCRX1?_PaDhs{wroo>xOWJ7U!->y&Zf))5mp7W^hP35qx76BsXCp=gM_JFRYF8! zCh(vEJS=KsUYTv%cZ$Gg zpT^iT2xfB=Vd1;M2i#-ZuO-6Hdj4( zs-o{qsf2Yxk~Ha(IjU3}dQpMURr-}}KwuU@Hu5KIxir;f||hyG9gUXFJ*2E89O9O}MLxMSI9pU?V=*Uc-( zaVaW0I^}bqjENA!Ozg_-;et!!rSqlB(s@}RLYE5(pl!Ka9S&*QS{5Bcny5%$k|C-P zWoeECk(o;-MNqnXA~V9LLPLav`(RKlr{ry@`fjYU?a_A-EtpvP~|4}49~PMVUBaR7-{BJUxyki zGV7&nb4|d`#Mjn8dZbH}$1C+F*DE(cS6UX5hNj#asZvw!3q*BUoC%^yJX1Dq^(T#^ z&2S!c1=pG3Xp6{N$K%L}xR=AlGSkfo32+7>wg`kX1S0g|a)|_k zJdBdugG+^Si>TtVS+Q7bx)Q&`97G7u2qvbc!X(!PuU@zZC72~VeIK#!bi4bTclp(y zg?SS9k6$jIz0wyi`-^3H@dAC})@50wckNxY1)zNqrV&VxyBCz7PjBQ=`r;7`uuznJl7-dKeOXx?25=K^{_uhcF|P zGKUW&gg8CYcEmcFuj!fSrNu|?xBG*#JX|9(A2DM+NPwdJ$q_-4HfSHd?^w6|<{N+W zCf+|~q!G2xzw_|jPvpf5y7YK}5>x{wo$o<8rJp|lb8&FY@$*T0I>>Eek7Z=7?H^wx%H*BL&Z4Ye^~sKi!*%J?i!qBbBD|2cnUl=yRYa62 z%n=#;h_Qybdiu7;Htc?ncklL}eCcoBd1MkjeDX>^c_}X*_2GfK%Chisk!2xmBL+`15UHqzb5%A2;db~S%I*vc zKi;i8ETHTWEDeGL@|x$yPV(bw?5hSdv z?t_SZ;*pHZ!v+uY?QZvthuiID@7~2%KlN=xk`wjsylS6()LuN$96Ic=yiC zlOp26m+$r;-t8Y|y3i%a0(XZAEt`jn3l)QB7Ls{l1w9fn>>kb}l$^j5{iCn^du#p) z;YDqz-r0K`G7gcz!qT!f(Mg%OW4qAuiXI-*Zx~Ox#{|ns0t~4ZoqLQcUnEi?1FkM# zy!m+ftN&6ho0C8!Ag%{dAlvf7#=T^PBgK0h1|@(npQl2k3vr|iODa4=nJY--8GQP2 z=y0EaM8w_dTAIi0r#Rd7@ne?AmrHNISe87k5`iPjn#q6u)B zGU|`UQr47;X3SHDTUFuKmf16y2qeO6nN7qG!%yX;MH&$$hii2rkW8m;odwRUWh1K_ z5wjpq&QlckIKx4wr#H$3apEISG&Qpv+)5^9WLUUR*~?6b-ACopBO=Vw2Er-DwX`}NbG{Ov~n%<|%g?>>9wV$L>V+x))X-|okVoAZctch9^9k;h=^ z#Bs!k4j*bjRrj$S@t_H535rYxMj^5)LY3f*d$H9sOWiF+y^eTJ5vC;4y<&s%-*B-56Q=q1i@elt>#s_8d<8=3}?p&EG%KeH%ae)9uTz zYjxdTJ<4}p_17=4ETP;km+T7>dEI$QVt&2^6kwfEXF^t#F%WAgOTt)~Sv*qoLW~pv z!jhUu5Cc$=;U=P!g33(Cq*GtFh+t;3!Au14oHdFBSUt*09t>yc5##JHakq0hLO9*q zAfiIPoj0?3G|HJg&1sG%l1inyo5)hUp|P)FhOr?c#y)O0jNR6a_A#Dr{`TGeldtmq zEj^s6zkZaL5Ax8_RoW8e$P*3#EH&|IQ-H9u?K^Ap+)8_-x#_FQ37z83B))Q~ya5Oc z5!1i>AsZ17gBH@#ub9gE9tnLJ2j+f8_nOjAsOzc&@cfF`8iL}dN z05cb*iI~y4E`9f*+NFs!O~Uozae4V5m)}Ckq7#y`S4o;YPo89qn8#FAzxo(q|k!c<>CFAN7 z37OJY)^euo=)z>4^^(@(J~E4jW3Y!C5+FFJd0=|em5nFW4M8y{?+$Wrw(!KdU~7o8J-db|nK zdR>OPlO=UJtk0kGs2%0{{Y!$aFG#7c%oJXVUF5T7b1%tkb74-7D~nI+JO zvWRq;JdEPb3A0wfl4)aleEstJ@hiM|eEG>IAtINHUM|T(tx;ck@6-_s+aNJ&_k%!N$6>?X<4r{S%qRzWW$pHMdyT7?#^MwpEhC zU73al^Niv%(TMEAOytMcbo00f53l|t2u~IoV%V{h$N%&GpZ%=-@-NU|`px|Bz#4QhYH^rs6rqR!hdf5? z=Le0E$rp@f=B!FaeC5aB=&|7fW2qIR%PY zg9Jr>iuDgo0|!O@K-8-|GCeeYxcT@6u2%2(7uQs5Waw=LO z6-2EHw2I9y#JhVJ9)55WGV4iP`?3j2$P{7`@N97 zyY0&C!)$C4nPWF|-`3psv2C%h`_q#Q%ljH*#2DlK6NdZ!CNun4(Lh*6V?L6)@JJ%+ zOjZv@CaX1YJ&5%&zxS8c*Wdr66+imZ|L@B``{VcTzFP0^%tszYKM4<1{Pm*kDk7Dn z3?Gr1VZ^hI?4)Sam7qKgc|`hV>@Vmskz@uC$bo8a z%5kQ6rVFubvn)%$ynOkcFTVJ>?|%NXpMCcE~TlO5f}Facj=zId)&y@$M~# zjdi6lYQgO5%Gq*#%I$8qdzQ63iHL@~FnflQSj=r~W8T~^O?MA1j3F@G8}shoq?r%Y z`jRLkl-Y`|pQufc<*cFi46PIca(FZ*&n#`BWF!y;&_(u$#uFDa5}jd@L8MFPuF`rC zsc?vh>hNg2x7G$xTY6TCLFIaRFhmfw)`?QJQ(s6~E*H9VzI0x?^lMV;!y~u8A3s17 z_4Q75ppV`6Ep5O9Nolw3-4pE_!fjm{9Oe@4>zeKoY340+AA|PY?svX#X;!i1QT`F4 z$gEfIAZFKIX_^z6$P&BzRmCGCvJ2OHvAH*ypdTySW<5g)NGRbXS$ITUDgwBu)-NKV zGPh$yyfogExM`?H@3eH$MylHSsGXM^_3qj#!rEImk;c5+KwnDbN>L?1L{+K2_#$*! z=wZ>zwOy7S$$jCz)UBBbwrz-D_c8VyyWQW@82i)vxZQyCF__zWyEls1_o}svZAE#% zw-t$f-*em9d~EyTzMJPf>*s3Oj|+p%l_fmUnJv>IPS6j}myzecQt7_~Lsr)S?qhb`?|6_-EX5NyK-yY%d}pd5TfP*glHq;Np}?y)=J%iB$1`E zdcH=VhhVmDLe^iZp4`g zaCZQzEi>I+nA3I!#aIjYP)i*~!eeZA24I%!9s9Pu`MNzUJiU2MB)HzwlN6*eN9!9-F>3PhDrOk55s?2og znHAxi&zIx(dQQVmb<8KgIFncqo_PSFpIdx$GKJ65-V@bZLtQ@8rE@wq^F!5}&uiP6 zn=M2kfk?zXj-W5)Nid`j1?~A?q!T?oZ)No1KH&%+Pnz*mzqu^ahh#mK*t)pOBnT#z zotsaW^}%L7UEOnzrP?n{3wM#U%EW<;dK8>QEjnNjbL}`U97WY4`mo4&{&7y{ccn7p zd>QZ|4I5|C<_XH38-;0WpLu*v_rLt>|BjNFjcs3rcwX#?;6ApUGi~ew_!wcv5k7{k zceVjfTkj0A_wUBjQ@(pgfVqQcd3fOK1-&N`wJs`P0TWsWleV5Bp-Rk=$xT%{M1nco z+U3fsR4cH;RuPM~N_j*ESR%8v1w^8{+i2SA*XAxwErKh`OGHy?Nh07i zE`N!+p#Cc?!%JjVT&W`#3ka}USUdu;3e{z;iVB8Zmj#b3N=*DJN|;k-ORIY87J z7`$8}Lzb&v783dF^IwegzdQb`p!gU+^Hu!FiI_MlY5@A$eniw0 z(AQ%*a|Xks+*JgA#4_Mcg&m)`?jO_pDK;iv<6)#3Vo)&9z)W2umE;mK(*N>LzjOKZ ze;dRQk=8tuTPK9{hd2rM+9QTYPiEFm0EtYG2TOvOg_(!#5uwuDjR+zw!VeDFEcbOE zM?{8a6VGGtr!6X77?Z} zV`6h3+pRHE(KORD()SH@Eep%B7v9s>8+rPAV-I1@F?`#Qz!>Y-KjmS}Jj}Uid;L*+ z@wi6z%i>JbFS_(xmP&P^FQgrmv|NhuOZ^(@qTLA)ZEZ=Ss5p^|SGiFEnWb^vo#w)H znm%%-QE)srC?Z6pK1RY^hrh6#iKU!u+u&!yHzCu*h@`5HnOoU7BLbN$67EXmwu3mM z(8O_^WfHbmv^!#=ItfWpPFVRSe(fLr<3Ig-f9JRU$shmz+c!Viwl!jpv;=#O0-Ovt zkt;K2dcxi(q?N!($P)efMo$`XzlZc&s-@j<;`ZU9~l2YFE7e zq<{1gE{m%yk1yzQfpEV*IDvZS)=5?R1*%}GYjbc_5`aXR*~2p-A`vOV!%ReqPf?FH z28J6ER9b{HtGf|V38XWwOlO`8Ol)pC36Xr5BW9yZA$ullZ(eL9(+ObW@G5TbDxog_ zNQTE4k~h-`-p<^c|~2>_5f!hd`Kc=Rs%|=?OS;{5aG8cMN*2p zX#~RDoI%x$z+>3$4cDW!HGX}0`0gj6Jyi6?OYYruk!7K#teyLXM3XsLK-Bsrj=s~Ge>yY zai%C#1!}~?Ji_~OJyxO4#2FzVCeFzeh%v^jVJ*?8sv2S-v75kSE|blO^~n(l|>SPbJCU5lG%i1*q#|?qqlxsRf9=JMp;d!Uzxey>u~FB zv2b?_w@2+h;{NuVV?wlu6e62U+P#AC^@cICyD5o{T|^LJ`_7bM*4pCxhKOmAbH~1c z#mqIGh{t;8h_FF{`4Ka2`>?1fPQy6*uqT8_#*UKEPm}fLRJ0v4r!tbWe^~prE9+o zRWRwpBU-mayDSMBMBG%DtGTf@(YDv?tO}NpHV7nYiPl=NrVzEck$_SPfTumoy=fg3 zTg|J)5=WafvkV(rN!=P~nVaZl)>O;2JCFyh;3;kQ8|Ypi`+LHv*U|?7aC>K)&r36e;73^Dt^MIk{X8+kR}hj~ zMKA(<4?IXImXVRGkMix;jWLd7{=q$%-^@KQY!DPg=6iy`+EVI*->4Dzj^P;{8k7&z z@3|N8>nYbqS{`anP6u%dW>9xV(yRARx8Ht$Ct~fyP}PAV%?R*SW^(H?At*wkbtVQS z69-bYdwOeE_nnwTItVuD4}4a>MK}}JOUu&S28){QOoD^e5Vf;RQf(f_tOP3Z6|)@C z$*S+6+W8U_O|%k6I+J9&wo69ru@jNm&aB0_a|{SZ1UHUI&xqDZmW*2Qq@}U;FfaZV zi{@^Wv?UA0V5JPr*wzk7dGUEPUTzxqZm9go-6TB^$B!dl^82?lF} zkX|0@NR+-f0SwU|ETS6WdbtMDKvmh1q@oHI5q6wQ0Gd`qW@1j3$Y`Po1T(RYv5IH{ z$?Wc#2qF(7Qc*P<1gN%x`VvcV+Y%fwcWc@V7MUt7!-Y8_EMqT6HYhm)LexrS@XYMW zmF8fH@PhNvyno~K@Xx>a=&%2kU-;8M_|gCV8~^A3@(+IJn>RmM*C+R#fK1DR2#2#= z2$F>qow#F0`g4%yJUGAY6`Eeh$^@aPH3Ily2|7cL0x)2})A4WC|IM@H0Vuz;(!teB zfQcV2{nO9B`2H6^_~oDb-Vc81(~rOR$|Y6l?fX3u3HA&Tsc5V)Z|3ICR6A05?A5rV zsuA8>A7fROZR|u6VU>Ad?wKLHc%7ltGh^x3VY5hAS%xl51SKo5+=UwRHU@VV6=rFz zXSy;+T3S>nl8`oRQF+>jgzskGxNqxykrrdqu%~TXz}UBOdvEO$>uS8a@B7moKo`NA zKehhV^~D2UF4`Bc?1}fXzERds(t0lWBk=9e{!oSt-tTB#=uVtVf>^jeQF@3a1G72pbqf z!ztXijrJ{u#r>A+y1)5p_HJ+AVz_TR-?q0u{u;ga>m_WV8u#okT z@*}LP6_HgfL4dUt=QB!;RIOhe45Cbe_9c`XlWn8CJ+u74%srY&mSPM#$AugJSI4r$A zJm}JCS)@1VOEpTBDT_&pAMX3cZt0f$mgZz5LB6fkHM~GCYK-mMBRSW>t~&9AyOPNPZ@ZP)1UdX|Rk$Q?2x% zwl?-16JR&jJVothkpgk=<{93#xqDr&*?mL`!Q3HQ(X<$*!i7Gs%(hn`1|a6Y{)->a zE~^h_pbx9NjzYC_%zZ{mo%`A2f;aDcha6kapeRLs>bi7DvNiQlRi$M$4UyI#4IdhA5`2UJPK}Fn+=+zx3j8=PGR=golQeEp55BV@BV>SJ7@JNtjb(-%=vt-m}is0 zS$agzIwwB(t@3a(pMl#vN|#O`)(p`cCH7}u(5V_7+U&Fac~X}TzSp;2COS}F2R`7e zvpUsfdTvQN&{)U4oR2c2S`s|}1Eyr?nEEQ+iz4|~P83pkdZNp18ncWX^?lm(#b2slI8DYTg-jPf+JTg;> zMC3MX5$4Pt=q$qIM7Hnc$BBq0LP$&OKx0Oxil_>Sw1+ESE_}Jra=BbDrma6dq$n>7 zsitT$%jIH;%k?^#oVd8_hr5H6kQU~HOM2Qzq;urH-pSm@4i5(0yta*Shr8|j*w*#^ z(<9~k+ntzb_8r&${x|1nn&y1h2`3@EdA1*cxON&`+eK@C1RL`7m^ei z&V|^FYU@mQyNC=A684C$LZArmqM)qqlgS8R>Oy%ajqcW4w+uQW(N%I)^-<*?65(Ve zq7dRlj=k22!W{0DJ^~rRtvizlB}+q&ZDi&mG9nX65quDTU77$^CwO8qKI=cuMTlrb z*?V&lU77<;#oNLvU}j}VXBEnz<>Kb;dQBjOr8T`?bh!*S=?klJYk^2s5y@l^rv+;@y&>m3Gv|IPjFyJjQSfylA%5yiZ6P28P+vf}sNJpJC6Z+`nvzkGjt z6EUhMm8daah$M1kTB#4n_^0;A`QefI!FYV$M$VfF=N#{o3cfh6*Z2U%emcY|_*QUM zB`Cnk{NmxoFMRjYAHIJ4?&HI&ye)A*wZvo+kVSM+LkhDnK{|0pTllhYYyEP)N2u!M za!FE8ljV}kTnBzx#zIkwh0vvIT^aSH6}Or^v)8;1*9OhGdC_u z1Ovpq+N1^3+S-VNnLw+Lg-Dq92$>eJ?5(Zt?k=K@F+8=1#3I?c*+?@IDeGqiCR?OnQt zC{&V3druMROCq)RB!b9n$zrK_LRu%)VY_IrN=y20{)hjP0C#%*ANhmjGiI@uM+<+Qb4L%*=L&LVIUDcf3PLZt4Z<=Z%-moYtTBA(H-6)m<_J5nUu+J~`3j52T*afC%{ z1TE4AON$u9%q&0r&ENR*LD}gvJ_o)$DXbsd;?7L%N#G$CseaKc-#ETi``OO z-{Lr~mL)|S(}-~Mv98JRK$%QUtKu#9cHEPX`uzQp9xPiwOFB?%A_6*Dg`Bh6+9#_?O#qp-+8 z)qWw&M@ln`WD=RJX+)}uM-)Pmrx%QwVP+x%Pv#7dbQ9+PpRu>;m2JDyw8qz*D%Slh*)cm4;o|48F5@7xR$TZJvw`z*b!^3IX>R^d3E1wn~q_s;7aKg3lm~DqrxZx z6?%7=8Y2aQ4`pI7$8a2bbuj2^i!!u5g5_S&MrbH4Y>NlpKjF6>HTeM*QiFn$4IK5yZtU6k2>VXc3b%keXhD2u8dQVUeG1_oM5MgFYdMzr_r|-@NAwh%)%~a3fshqSSO^$spB7Hz_;${ib4|S3Tt_TQj5FygT!rfeziN~>b z76-E9Xnot?-@X+~hcald6^IU;%hOS1@cLzd73SJYzyfglnoGfBDVIgmF~& ziy(;Y^|bwm|L~9g{tv$S!S}y;VQ^Eyuwx*LFdq=-)@9o=5Q-4PbVLww+q#_2hD6&5 zxdW4QF(B>&C>~C&0kb1=Ym5j>36Q5P%)Oo<#6$q;%u9n<5Mj7y;z>H0 znaRzdLLETOav4uZOlY3Fg8=X}aj5}Qm{}7c0M(I?IyEFv%^>s008~d{49y2OAz|e3 z(;(2s&bWr$rGgD}3`4+Uj79|MomWF5+mC<%N4Fy@c0C3}B(S;URH}erUht!E1Lj6( z$$5#0oU*wA1f@?qFK;&h5)nQ2gqgZ2NaIFr+hpt17?`CAvT$q2m5A0_4`OaiEE(3C zJ3mCq=UM6tsI-TS&yViJdLd4)%yJhIwinAdKL`Do#ln!oFmKns7Lg*BxmC>b`Vi<1j>JTg#=TpC&apQhI%jE@` zsdodw|Nfu;cM&0l8i)BQb9&rEhaQI>hu_}qb{A;@NWJmacxxyk+ZKe^^|A=ik64O2zdtiAQ5??bf0(~@Z?w{jxa>3+XhRA5D_IRc1NO=Jkj(R zPv=vrUxG>8E&r}{4W%A^CC97=n4Ta*L}b;qL;Z~fOnB@V&7D?b29R9z+_0K^_DhKA&W3Iblt|M0yBk{#bI1 z!p#6tP4l&<7|Ak6(XF14VYM_%(x&NbS}0uewV7!_fSA(fDbjPis;0~mGie@+E?b}p zV$M7ZVm4J`cDLXEZ~u!)lX|d5@ag>}e5m(UD*8H0fjGuUGCJ@(n|4~`hYnAng5TOaYhum9;|WA0!mc_xg5da@!YId`&vHA$2Y97wRmb1 zY4)-CYxgProhiao9zNZ|utI{PP(BsugwL({3So|=e4aFH%mZNByQXRz9{F5Pp6kT% zq}h4B(eMaNn$rp(s@(=YtW~)DRS#jv8mO;ChtEK5& zv-HxZE+ZWuiNVt`h;!|YV+;adCO1!)QJysB9^N*n!2g$N`{ z)7rYW@;nZ=rBpCgLe%3(zW}3{YIwM+?gyBWgWeAvgIZ7dI$usGz>T@Jz>vOqgft06 z=^GNd8&JlOflk*}9^L>neE;Jg|4)E;yBeQ^$Y0@W>8E?!@5&a%)nqv(JzpZ;%SHi+ z&3q3)bIq`y6H9{~{O0;O!h~h);Eot1BoU4n(R=FZo;4tZYW4-HiQhN!^p zhuZDXG*$ZZyXdz!{o6YT)L72|wVqs#5jUrez~QBBXFhMxPOZOeFCQ)+zy9z& z5h0Oa7Gi3W8aX5m#K-|?1d5=?K1R#@$#f_rSk5gx#u(+OpJE%Fl|xN+-NBuhQ-PmN z+%fi)>=LG1|8VuzTdHvKK8iGl&A|h6>G{?muQzruPaSWC#dr?BFeerHHI$$ecWON6fdAllp-`|IUwJ z|MD+>32w|=F0Qkm1P@npRfgE_huwEmuc%~1C+IJmY@I|<1PKAj!vTX>q_v81GMi#5 zX3l2?Mx>ROQsRV!ZpzYeVJ1?5&Fp4GWafyeZ1b5tK^{&>9;i9-_Z(%SYDA!pku>{b z40lx3-n*)E+QK7MzH3PB=X1Z`HP5KyIGD*DnA3U&YbRkk{`Qx@{Pwrs{mtL}>%aQz zzxvCc{p=UN_*YZy9KX}YKE~+qh!lICNq_mD|F8edHw3@;)%X5`fB5hJga7Cs{oDWU zkAL|4Km54$NG9tzeBbqUjpQ%tLDJg!(l0M84TXWlQj*acWCkw=5_=+#!vQFexNtBf z$3>zX#%BgB5qrQG2LPF8y$5QJi0Rh0h(?t8aYF|)L6C)U{Ppl%;>G69n!2@35QrK`l`56{$R|UVI|oqU`ihlyogX0-DDn( z$yUrnCP_ycB3+n=8A1?C+P&3=a1Dr4^K9l2VQ!M!gtUyRf~oGixR3jurI@MqCfFOd zhLUmLf`qMi$|YQ)PxEHal``jY4k#V7_1B_@=SMT02RKvoAeC&m>ccOtO7LnoVlmsE z=Kbq8Bjq*#jzB4q^Y8!=LiY?-UCWl$L%A?p%IIojlU#R8l65ZXOZqqSCc~@GdSr_L zK&0bWnZ?YOekB_suZ2^r#_U1-nU!KS^D%!tXJu}cq2*>^uD6>XBksGYx!E!H^VeVT z)@9qc2~ghtT&}so_RPuF|L|Y`mn4jUvTbh3L2+WqKR2URDGQ*AXG%=#ByOkKHIX)E z3XI4IgaII}t80enV)nk=2#JN6WPVNn0Os{qPx?YR3-e{f0_dh8dBz~&x{LssIA2MA zBRN$LBwXt)6bpTwlR);srgVwKzFa0!MTevQ!EE{ zxQ^Ok=ETIT^5#U=PiJP7Q=IhSm;(f{#B^wzDF8|n9fO%OZ#=($hBKLtaU3EN8NLD# z?!=N7YskkzYe7gt{k+L`!YsI%EWGzE_5GzBrrMcTSup!C&-yiuG&!rQeyEa%B5^w~ ziCMW>fk-L<;bVa*((-k#)-xi${;&RXz%^l53(VEa(E?w=^LD=e_8AL=Ur>u5!`iw6 zW_IwGQ`|2E$dS|QprkSpJVqLjj}@#1Y_<0bq5LJ0cztuCZ1J+=TB%nMkrBZw)3_F| z4^)jW+Or1|I51ZTMLkcE2;6)Pn;xX(hjzJuTnV^e5lV~tkR>xebr=@9`Y+&qkU^^mJA4KD(zQ|ev;pLPp*@jER8 zTX=*siKV{|05b55(#rwnT2MK+oDyQi`A|eAF`^{rCRr=hGyovEjx~CSq*DYGrouyY zuyogfm?8HJ;WiXJh{eVcX6~MdpfFoW&fL3?F^)aFj({L+t%n;DQ)_O@L?{vw+bXh;4k4HAr+@EH{;&RvU;XVTX!zl){;|J=Zh!m! zZj;xULcj<>W&;ch2aKVv4n&{^5q_xt<{*f#{-B?}q0=c&8xx`KoU9)NQjPcfzVBcJ zQ&57nUPrp_Zg=w?TrFxy2-Io&gRkZM;r)HL-NC{~fPoKy!{bgcvKOZ`E%RZ(bB5j&VC&&F?n$gB?Y< zNfTyadF>Y#*;+qsCy#h}`OummB1;pIm&@fCNADX6_rAH>`En*CVYZ0AZK>qp*5;*- zlhX+g*Epm%MBv^%=?UR!|6>t%C22LX zCPd0*ae-9KOv+b@5ol&CZNJ~M8y_0Ua!IKVGpiPZC4UezQ!~U;j+kk#B4*cJGWHyq zYkJ23n(%%Mgcw8jU%%iMLXcq3n0*IvwEut|7iwMy^1`v5T zQOg>Nm@A8(uw0!m*MJZrt@05w3yn}%B77yyL>Sfbdgi|{ZdL>o$op#vlII68CkVqMkE z+!TpjM_#b`+T6_DIHko90AOR_tiXsl@kw$zWgr|?%^cF&ECRXW>JeAeXQ2mZj_ET# zhh?>C$dn{4mKI@9?N~gBxw%mz&Tz*xCo{JQ$Gl&@Nazf#c^s}yMl2*M;S-gnq@$EX zdVu^WixN{+7)=FWa$qYil!ze`-;z?<@-S2N|1=BK`qaX0=%}J1o2h^>!@w{NcRL0K zf*FzI&cNK~aL1p0dD(FwbBPSdpr*63x zce50`<|8k6dH{gTI3+ICYh?dTy!N-|8h~oUAxH>_GVBf@)N=4|1dt(FA_9TTT-CoC z61v%159a$t#N*hpvaoGEI2O)#tslMKh@z%F7Fn}ztA3c)qRypt9Y-pq~-gN!bqY_tIoY`tP?J;E*U#MWDe6NMYo_B>sNN<54t&s>a)t#6m^E@VJ0#iVsN9K@2M7R@>=@A|n zf!WWDQ4<`Gy5rJsnSYVjKqWkoGja$^p`FyCIo+F!9c7j&iFafUvpj2C@2a}w=vlku z3Chv}B4rVeTMNR>DCyR!^{x^{u^1sCu{q8QcO;zQrZoUyX*LEx6q~04OVPtIAcM+F zkWh9Wyy5l#grEKKU;nSSlhY=rSNZ&Ns|-4ZK*U#$j3+k;2yB((jRe4m1l;2I^?<_= zf*Cufv=C1J7UOT34vG_f{%m>;luR4~<9XmzGk|(Tj*JUcfZ-!T!43cgZ~{dK8G4K{ z#&?Ikt9^IGRpV}80hvA02yX6g=7)P~NX=X``PGx$Q3NvcRO~}di(|nN9#Q5+4WIu7 zKgv)fQ>Ox_|!M&*!o4_qR72VayPR&aZRhjpayh+xI9)Epayp-7+gLadxXE@tmrF*NiKI0oLgpOP&(uxy z0A|VenvrZA1eoYoCU!MbH65W20Il_mNhD%7g~)0_kr;`XMaQwS{Y=fnvz%C0c+p&z z#Bn}A63+t@2}VR!xF;3NOt#H5_x>eVTm1YP9}vgw7M|YBsu_sMv(c5QF`HYOieSHkoqZpsDUz5UJLkmiM({jp zO*J4?hq(K5kFfam06Io$*@zkc9Q zaiBhi7)ik<0LbX0{LiH7!!xZm3FFm_dOoDgG^-!NiM@%433kqMr?p-oqEOvz>io$#$V|wLcg|+ZAwU>~LxdQOnS=}wm{G`?k!#?aDUCslgxQM* zabVI<(UBt4+w!Uj006;-$^YW z*mb-0?#IBV{ETWKW0idfE6k|U+aOtqFfI4+P+-??bWSp6f%tG?Eh{l23$+$VxSjc{ z@5>+j`2FJ#`p0j0+tLMs#6Cq?$Z3CK3+LABzCbWlVsUUp6zOW3T1P-YtZ{fv;-_TB z%kEacPSkCFfa(6oGq}QpX1L1|+T=7E9;qF$QnpHZ0KrJP`^swUIo7475bkSPT%#zY za7zVB%sJhpy+nGn!aXVm#vy+v*_&yqYb`tHJgXk=ooPfTW>d?bGTib#v|35cmGT;C znH7XH=fQ+I8N`{bo%u8snwsIOb^fg>E+PPl+a6R|7>p8U({ZegOI#Ku2g0Be0boxa`0HNHKLzjKs;E2?w`uO`%Lg z-ZCEfvx0|b>_K&+Wk;{BbeiDJd|FhVz=ioea*z=)X@&*p#wnWm^5F$YOYELJ$ei~R z^@c1Ej$@!1Aqrw+Y(hemydG(?U3xn)Z!E(7(y8Ic4`-z~y`CM%-EPRx2xM#IF1;}j zKx}eqgIz;!hlLJ2@XdRZVL?hUfC4$%>+APkUcO@N=ko^uJimN|h`ya#-`or-*Z-|C z=>#qy5sRpeG4?U`*2RGkfm_d#CLR04LNieb5z`))3y7SnncLcm=NMX~M@4R0RtQYI z%FsGqGu_IK=2@9et2}~N9hHlPRFu$sWP3@X*0&1`a=~I| zbzV;wn25N|0MVF3jV!zK2C3PcQ;F9~c%x%fk4ki~*j14}ZB5pg-orz=1X>uC$q z^QGO6RM7}AngbD+{r&X^@%C*7b+y)G7z7PN|GQsYzW;-KB9IMN6>f0c!xTMyr~skc z%lU(deEt3Je|SA_r_;x8zUkX`dAYRSFPE3T_1=5yorRew^G&lV%eFRQ+S4Xl=;N$) z^ZHDEe^uH`s|hnAd0tehNz8S0OkAjNnq{z%ZE2~*NVRaOms#9ZS}%`Sj&cA*M2o>r zOp=R$>c*dngr&k=xXny}2LL8CvuW5_*&GFuNeApIs3W`%Vz)eSy&R6U9<{lJj>mI4 zW+FWSf}1W?X)-vbpNMi?n`$Y1+Q6+%SUFE;Z$(&$qScA4M(ztB%;Pwy^2n$CbLkhe zM4A0$WU~PC1j&Q@(Z{84bVjADml^<)<`l#*(=ps_j6sABuH!)FbQ{%}fGAB*S?;nl zB2M}GtTAy(bm!Wj(sP!f73yu*xg}2Yl0CVr>dmYOtCISzz@#t!S!UAONV_lTwR_;d z_=|rrRWUxvuhsTE$vx{T$#*knLIL&H3rlsptFFy@Yi0A~QyU)URMR#rS>AP zgPgU@5{GjBQMS^2UOWWx0FQYCEsF26E0{A>AWBF%)%|NjU9+_L>6RVZ=7mzL{O}Ct zOv(2!O_L#)Nh5OZ0*Gd2ruVUD4hjl-4Q zsk`ae(Z<-Xyl8>T{J0oUbsqJ8DzbPBefLS>qp<{lSG@G1CWe5)d&q2<#E@M35 zCFu*$)vQlS-W!07^Fc;amx52^s>wI-n0Deiai*O)@Zl<7!r^IXcRdb3lO{wG$i>jE z;9rM1E1BZtTCGS3g#A&!1|aDH%iS~s;08PN_&}`g{zuR`Ua2=JN7UO zQ*az_pG92yCHGgZ4m$kp9p690`xOIn=>Saab)yd#Y)zT~OWFy71cfn&u>?RI+=w|c zZJQc05eslb?isrx+lO{~#nu&x+l8c2ox7M(a5IT20I-N537nr*ax6c2;S6xW_hYTZ zpRnXJ8u_VG2XO4DDItpIO8kiP=XytmAX%;hS=6Ea~=8JRda|C53o;XcLz0d^eV;i@cR zIszv91cRj`p|mZROYWAdfSjz)tn$YIxAbPxJRVQxq#jg+SQKZXH9^Ab@&5Vv?rLvW zz3zH@*Z1A;4x#9Xrlot+;;lTM@!6a!BD)O;1uzJ6S5R|wzc(2D{`uGY&5oOm!%TB_ z6gCV2$6@JGv$dZvn+7=YY|t1{PJ!WojRBi{IHAj?(mtMf2llNsVQC;usL@v^d=DT1 zCJ+Wfpq%IhGDZY-jGGz~hC^f0%&hVtK#OJfN`Mgw;fY#MOZYqj2m*krgT>%t42Z&r zaU5f(aC0A?JTsyoG#6wacD;S8@9zxU+okcw+*xF6r>&oQKQnihw)Gc6xojU&AurNc zPKY5SfS6~EOX@R0M&8W=+d+36lQZnr1#q!5}x~| ziwO57EiHVAv5&(e8nbX?;;nVzR@O~-b2HOJ$J_0i8FlG4J8zd$Kj)K&Nrnv_!_3ax zX=@u3K}5ptV8sg|<@cM?8C27;-w1i~O@oMZC;%M$E!=IW-tV_>za9Jieto~aza6*h z{r>*d55MUjFW8#K03>y_2p|TC?c8pfzJ1LrrH)iNH3oGL;9NEvAcAMT%G6Syj6i{a zklqdOP>(?3N<4yqFpx6=gxvx%fWv@V&diB`S&r=R!-6)7orx91%n&T4s2+iS5T860 zA&@VOag1vo$HSc1xAX7m{Zn|D9a;ECWCW)JFoIrA=iNYUH2I)*CzL=ui{JMh`4nNM z2NMu=0ASwC!R#0&6vB8HkpSrJ6c8!KF}Q?%W)k;-;6U4{(J17=3G@@bCsYw&*Zq`~$*)x>!ZEhyb3T2X?3V?%oSnDTZs$?1# zZjwce*+6c>HKZbL*?CSkY2L*Jc1n558fSB|3;~$3t;lVm&qc-zI9vwC@VVOr4`-IA zoixsb=O-HN>?x|k&mQDaEjeN?T`^;~W@;o(VL+0l9_Rj&hOO=f9tNxW_mW&Zb+gMT zyS^3Hq+yN!Q)j*wOLI7pe2x6_R9WU({BW{FG)Ld>mY@Is=*XxaLfrZ0c+e9+CB#GmYqi6oAqTF#AWOnWdsjbqjd znvl#49)TVYR2|_Ww$hU?KbA`@=IHLJAq^_R0-78CfD@3lleK*(xJoOX1>d-I-?x_==D90s~)?3{;eNJ?6*(_ zkJ#^^8v72WWR`Y*d--^5mmmClfArlS|BoXBtj`@koM~e?Z!QhH@V22awvAhZx#%M% zW~mh%OQw8f!Y8-~V#blp-O7~KE~F36eBe28_hLS+JfPKhERa*_ck{+E^8iBtMs^PX zlh$W;+IreP*v)t$yhl1vVQv**(YXA1m7b zfb#!9MeHKPnA@!|PyS|LVjeFXc~dx3d10R+(He*eRqywWA3BO z!83;-66EecMifLLgq`Dbf`;v@4fpp0I{`=SfpK8 zwzj>1Y%Cjd10-fyXXG=Id-(vNhEtXoJS62hHm8Sq&(1||&8mnvzcV41g&0eyx>j40!-s%mrUCVXFtNIto3LcOd0r$C zP}1;xcDWOhnJ&a26{|d8u8Y#&l?PbG9*z-aDg9s;$xIp#B9h*-{?6;P7GL?+C5DtR zBDl30*G`_I=X4)Smr)BVfCrjjzL``S^-}hwWIghY!E#qPn1ctx@IylnKd$yRe2lo? zW8Y&Bh!Mj>4Xic-B?sYvqg?gpE#7wwM6EP0%~-We>z%F{Mm&oM;qVyd1~{fWA}J*7 z00byvPKcmM+rxAcM-K7;11a;6G!U3FR1U#yHgL`Y9BUIH~mOx+=t}zoym!8u}Gxdo3aX)lqHQ87^QYWH0b5t`TNu>Y+h-AWJJ#w0C z%tfZ`(nTcPhv})G@)TAxcY|<326ti3+P+CoQ?cqWRUH~`_v?G8j{OLS>vx~XV}E}~ zv+Jj~JkUi3BZUX+r+5uNEpMzIlD4Iz!TyD(JYj6?dgsr zh=7H5&9F`*G$vLvz$R!G2mr#(gAuIprfzOK@fpA=osS5J;xRb$Y}|xT0g0_~M)HhQ z24n7ybn2`|LI4q$?E{#GX(vAQvoM?5e!r7zmu8wnG$x?)>1EqGvxrFRz4a|wst<7- zg4NnBQq{O@PabTW z*Q}$y4A^+UqSj`0&5qaFZ|OUrG8Agdk#O6Igrv31%hPsZZn8CafOwyV>9Z?>00=Ad zKUzg97L_e+0ELuGT^Qh85Yjr$g?T2U5fdSCR-su8q=3}j+zmY3EZj}iRT(+VL|Q^R zsMIT*>bBXq7F-AbS}znoOczFlu|+V8x#h@aYG!K;o@m>NybS;xdI-y05I&~Z+3wY{ z@aelYD<(!O#&LMKngWKoLAA;r2+YDF#3Is|g^641 zSWO>JM*-&Nk^1La)z-;Md9JT}OjW0qfb$Ao`hhP8NHtEb*BNeU?w^abDWHNTRWGJ@ zqx5#}HotRTYWRhAaDtZuxHN>pEcwRfjp1fQoZ_pRKc%7o5yD&F#<7nvq)ARj3yGXd zKc;jZ7O*GBiV=G3q3XvS?pM^IpTE<$@9g?!$FA?60>G|sG4{}bfWF_rG~BDTGY!Pu zgMa)-+xLHnZ{Obj)z9Mg9v*^dCH}0W;N`GgvmOI-2#YWzV8kxIwb%qsJ$j?gyq$tM z^KU>p5~U}Tm@-;AZ%`KDT%mYIAw|h<(qLpc?9AK3bH+r3jLgg{R%k`!atI&3bD&j$gFo57Tf=H9pLxRK7ba$GE;IJOGF> zoQNE-dQE0`2kE)P4;JcYI-L-iE+1e!w~yZd3yQdOKA-vW!7)$-h`2S*%$1h0UgZct zE{3mrCdxWPMI2}7_j2p-9YIcm8&zTdEtID<|Kwf$e_x$&EzRX-G zQPn2xxZg6=v~OGfI$x5hLJ4|p>^q5+5?wf-S0d836C$v*Dk)NiC$h{bUI8id?8rBk z5Ru$8;MspcK83PAKZpedL?&}{Hx%@+qC^r}7!X9bg(;ZFK;*mK;Dop9=figeJ>Uok z3snq8Izkb{8-b<$8ZvmkpnnDs2A=uim@&H<3hLTmvIBMZyMzmc7|;80z2z5uvqf7=YXf z8+kX~GaJiYsaTgD9)yA(Kon|(o5vt&s6fboz?YYX?ZmI#8aWXNlQcdtGIKxm58HM@ zWC7Xwg@6#aNoNv_nYkyzTas##OB@NiQ2s$bKK=YVYyfI8n@TO&BK@kQt(2hBwW?`L=ZmGI%Yw(gb|O|nVP`#O%#`I>I3uF`>`+g z-%?*!b?BbnST{GfCXE1vlS`XnjTL;JcRfI8jzA!o*i_SCUlJ_<4|j7!Qq@^gn49Yu z<2ZB-J;wWYpN@T()@&TV{Og~;{r0ntTbS>+JG0#0-w=bjvB;-i{xUoe(-*b_@Z78C zJlqIbE8D`?QGi6|N+k4Anq%B6+;mmkP!vJ1BeL%JCZXqhMKe6 zKrhMA3lPLL|OeUHQJc8lHoy6fjR-vK=^MLz+-4JlD&vb}sj@b>a@{`h)% zy=)&ooL*keA71+T(obh;(ob7138cxa6U*ZZfW(y5djf!R(^|!RWm43zmAD*~>1&7y zd`${ejfvgL!HWeRUlO*Gj+P}5R%e^(WjW29@++V7YjXFDMX+hT$+ESyY2njQM)63y zq*5~xdl)0fWS0~awvZo?O*m&9<2t+Zl3pO_=ktAPR}sHV*_c))ugn5iKYp?R%xeK= z^W;|@2p}Z2%5b+abPO=3BoyTF?PgXvCCTo}8a^Yn=5p9u?>YvHWYr-}GF^xh(mC-T zt5S6xBr+S+lD@9wOEXRVFA=ABR(hwWK4W247J}zRKQnFIpc*N5MU_X{Oa_wYJ8&E)bgc<=mEY`o0k>Gl)k)-O7`vfdaC~Va;i*YN{-QNM8tl-g}X@a+ZOJ^EwO%y&RffKVxFHu0x-*x1WzvCN3}h3 zi4tGo(?JAlQ9lX(rdqP_BwZ811Z$_3V0u3utMN6PP#vjkj0gL=o@LW6U*XNV?-_$^ zB#DG=zxjo2kOs!`;VTWO5JiDsvyNXZb~+euo!F zXC=)pIVwKVVdwi*5NLP`2@1@>JM;jH7=S_GOm^<^dWP4FyuRSpdF#|Ww~o@92!H^9 z8n|ifcRY4512=Nx08n)^R3}#e3bSx$4=@9BS3@@)hI+6XsD)X02q6Ioz!#xG8Pgnq zkBH-kGNj71%>A0aXqYBaT-_{yMVV}v=E(P9O(_;v7OXQTajuDP~|ygm9lo*lZ&60bprBAg1HElZa=?WQOudV-e;K z2*S-FsaWQwZc3FRCWI6oO`6w(H|qjS=?czlYI>675K;O#a$L2r1&;u*B16<*Z;9A* zPG_cToBA|3r<(p+6&Xd#C`EDIxg~RgU?Nqkk!Z0rBDv`KfXaseGn?v~-=%mYtD?NR z*0lKfQ?BQmNVw-}dYwo4{hABGv4YHonJ5*$efv(Wo^5AeIb z-SqRY>jAs^efZsJ>~YiB)$WdN=zbjLX%#1M4-9ld5tu-k^%)_7=)vRyP;-!EHJD&h66+sr~(Z51n6N11V%ssUbYMM z#i0xbM9Pm@8pI$ok3HvQ!NiV;!GewHC0q?vk0VP?$kx$=96T9(2n37)cy4shA_j?M z+g=H{%jLu`V1~erPb|{q()l9XI-iAm@<1*Q*EEu(999{q7!N=5>S)1UZhK5zP#N0j%_~73B@YnZ9QeJ7EFL z2o7$Oc%v@&s6AXVL>KlLrInvG&Vk9^^?)kcFy& zaHJ040S+K&gk89G>Oj(hfJJ~2h>4kmfSZV; zY{-rjAk3Yqg=`2cu4uDLBdC5 z$Pf|CC8UEh61MKHnNc)BoORPMiz6T*34zDumeciTg zJD*x_Z95TB+d5B6d0mY;&@?9|PwdWYV(}S>JL}A~%zRK^5uTE4+j|H9001BWNkl#oO{Q(t15JFg4S>o*y!`xtY?` zr8bJnUVS9-7DBj0gR_}0Gh;@Cj=bf7z}!VzAP}+YVd3LAFk-*GyDC~xpm6K`l*=}e zhB=PUbZM5_N>?i$Ppa>Paq{yVd>z51Un|~KaRHW4d~t==P<|E$u}I^q9axRV(iqp< zGWX$Y>9wT6i)&OLYZUM@|D3t)_gn89Gh;hv^hn3sjK;U!fLG&DkEy=mKA=-v|ZKG>}j9G1}Vi>_n3{= z$e*Xo1{Ge4t65($zw1IY9f>nFTjcjSXN5>I*7WPL7e3S%?kmDYT9vbiWl2vMInnyC zE#rcp#yN#Ie=u3-q3MSQuxGi2^WDpvu#C#?5q^xr-S^|JV}zTl<<9@*XPWv5)QbY<+%8)T1dW@xjU1&<2ZUqGJndduAEbw&4 zXA*Te?HSMk8RLs6-n9~iV#lO}De!6C`M7FQa0`LR6c~Vk{>YOGL^J?`iP$b$8-@D= zaEc2FG@EmpOw@_9ti>z|!v~4;RUtW*CY$Q^2ValMwsh{C!ABf(kbP!WNO(lKBr`@Kbc3}GtuBNPD+)> zEV9ccgR@qc$SWlB95X-t=JOyQ3&i5gVu{+Ghz?j%>L(QIJZRJY!~wpZw|VT9jQ^2e zU4rmu*5zUuWQsZgdh*|z%cayG<6%` zHEzKff?z%nJ#2Ucnz>n^h9ATUpAR1+$RRY)!*&D%0Z>PTAaq1zOvmoi%*-(ul)#MJ z2^=GWK-BjjfTQv^!Ge&HD5~EfLe7{a;)cW>J2Er%E@wueU}h2MLff`|XuLJqw*J!j z6yckk5qWE;@HG2jHMfXib$mZv8x_t~YPX3Em)0^0AtCKEr)28gr%xe2{1hdvX~^$# zQ%bUgDFF6$TA3!aA@{2J%tB&wg#d0XONC?XInjpcKzGh%4|6Y2lh2zG+nO{zj{W+cOnVz)W6!lWl8iCJ zRGO$cxTRcdg#&<%{I5Vz3TSk?yuSAK`B%RhLn+LHAoEnhXXIoA5~LHT3|2h1=M^0gV4QS?|_kOR{BYjX9^8yRQ}dlIK<`DBgMG zjlaO3<^drjBoK&Oqf*h`G~H^bs#AG#-y+s>w|U`Vnwfj-qmxQy=E>YUB9^<^yo~V; zct8-5X~%ilA<-at!6eX!ge%vVs2`VlFMM0-eUY_r4XPCkSqfF*veuS>l%*`|_Tdk|UteC9+jjr>alLIUQrCs02vb>> z-1RXJ5rjt$8M96JiQzP@u<4e`>vhaMV}v}P!Hrp!cY;(Zs#LGPB6Vr1ofP{4Nt(;v z7~`P{vAgOerxR&{m#NR-=%)tcG#KaJWsG9G9#b{P;o6RN>^WDJ!s=dXIgW!R#Z*~55fRCe2T3}eB5GX;U@6;nV_^sV_S>(YK74%n_<62< zuKtH1L^(f;b51h=afo+v0v->>PYD4;Xvgv6+t=gq_W2KgTGq9{Gp3v{gRZnRGtE~# z;4<)OSz8d1TXSt;>UOvudOX}T+}t&6&!I=pMz$k}F@V7Vu;i?FbS6Zlr;Ltmu=j2A z2}qra_cY2<$Ndjps?V49vK^%>!f5=&dR}2%b$jQw7^!(Wi}{?1pu2}hbg+Z?tO>f( z*HQq?J@5?x1+h~fRW{R%=7PJMhFYlFb4NR(wfx)S4sAG^JRf*Gus>@+B6w^Ug=cEh zX*s>hcj+9U(r9hrm#9RaLXU27rs#G%N<(}9kzL-VyyRl2Ix_GwKtVwwsFfCRVO|PE zq7*62>lRYD<9!5Dz(A>;m@S1;pHmkihLrmG?^qVsr@j7DU00Hgq>6}0DRq;w2B<9e zQdUUevJ#h+%FM;@1rFce2A=tQbF9-X{ins~LYTbnAE=4@Wp#gat~FruZ(p{RB5!)g#bA za3*qcXlpw$;9x2z8i2BX=P_RzZx01gW=EGU=w-mk5gp*v*P8(_35^7>B#qD8i~_*{ zz*t~`K5U8%h$fKaXCL`-XO^S80O6J{DTar-nTA{9$g>6sqi_vXgu^Vt!kga=-*$u> zlp{)AyYE_7mKum<`&_^bDRL{kG8jUe+}7ozpe*IKtse@NrQU^vxd;iYHy{IC93nMc z20xWkxz!huYfMP~0Eg{d0diA!Gt&UvHAhBLYl^2v!e2y0UDhcUN`nRy_v&THINvWi zrjj{osq?1DrSa54UkwfeOM3}uBjVwZ8kz`9^vktd$c@UiCW#)OYv&|rs4mqsQ%IR) zqVGDtgGJ4L;txf{(RMTQ2s7J{{m|Xbj@Gmthc>u9o^NiUZC5onJq`^sZM%EuaS;4v zyM6if*9fx5Yg3PAP}So=3xDjkw=jcI`sE2l?SOL+Q*r}=L7w+}#_~;!Q3sU;PEKqf z3|#spo%yOgAi9rDK?KMsPbk7Xa*o0XID&zok`qGbAYoa2{kWBvy1YnPSJYBB;mW8t zt|axw5+Mr}=0aRnVlXq66#++eZ;srjWg49e7xYFN;~jO(wam!dj4+}SVPgc$nJhtM zfG|k}1riiO2xD3T8gn!pP}iD*aCc!z3fJ&6_Ynv%K_50E5_py=Op)4nM3k~*Ar!C@4jQkmrx%Ao#T4U%J$u*lY^~J7a9%B=w-%|CL#dSmSWF#>_K`H&dfT#kx8pQ)fq8DlH*iXp1tmr z4UZZ6*Z@yP^l3NF-*hU>=Ycly(J`E-XR|OmDH;hq??77}!a8yX+$W$a?T~%YoQ}@a z3A&qG&dBn!PLiLS@dx?T`d@}MrJHa4RJ#Qcm)wMmhpfZ>hvDrq&Ot53rYL`Gj7KkxNwp@J> zDIi1zVKhh})SV+Oyr<&PqAkXZHa*GG9$^(#G}0&r4uh)@geE3XozgcK^mwE#kesT2^S zNLrjBOj1b5D8Xa76%(WwDCK#~H6D2&h6z4$+w_h`E#^8?nf8OMJAHO0uvNfNEWmrIbm(T_`>j!rmvNGGPo8Z8iVR zY4@FJ^>p$6U5WoG*-Zf9AmMir)>G9qo8( zC=)A=!&(qP)>9*ef$(lxcSlV9O;WIoeb=@${!s2Mju4pZ?emMi9f5p2rjaw}YQ%{m z7(Z{8o_$Tk9-Wh&=SB?cLa)m}fteg0PC{Wp1#l!ZmjPGmf0fvaET6|^2?w}rWIKAK z`Pi;^e&@{f1cYi1O4MU?QVH<^Gb6HWW(t^jG1+2O{jHQ54)vWyn2Jb=Ln~J|D_}}3 zAFmX|#KNU;Wsp!|un@yhq)t@|5M0>Y5>c5SvAcp8gjzRd2{@P#BvND~R}=MXtA`Tx z)0n4nI9E@DqR?~Zd!_fYM?diH85@ehu{?J_4gA-Bb!JKCKbeF0%hcx*BzZ2rB30Hu z8+iuNJz;bw@2B5h!#g%q&GQhTfj{jF#8Ld@gguXf2@^TAxm&Yl4%I{5kG3~E%w5gA zhe{(tn>9iPkZXY58>MBp|MC){Su#dqmE8!WrnzZ}2)I(fRn@{RLZ9Sj>WJ`mAlUSP zb0Xv2Bhn>< z&jDZJJOXuSFM*scK^!*58A73;>gb0-hxYZm+7Ox39ze&Vm}mMLO`el`V=g0okcgru za6yB%eB&c>0ZhyWB;aNvDBu=_NeP_Iu=mO~2cE>eCXwvD)s)F{ff{FBnL=pH<0#pd zOr!Y$B2>6CX@JnNU9uDw7ACHxFv(h%+j6U=2+La6S}Ld-CIb`PEcwvxZrXZ; zL3q=>=~HztwMwm0YN;!Ulu}bGDpG1X7FiPelxEr)f|K@&ar-k)*;+8kwG|4y%a@8v zv)bcSBf5WKELG#w9@9t(xdQJ`k#>Tk=KnvuwViF;3mErVh03Vjr;|4G*l_Y*GlX(b z!A9ChkAs?Fild#OlO;AXhA!*C85}**e0}~Fsm2|@E1g9DG4<;#kLl)d1JU3AyT9Lv z|MpM+X(@H^#yq zALr#oopz&B07^7}$Al=(f2|kFGd}bD+Yu3#C6vd=@aU0{sbPrl>|=R=th?krT~A}U z;DZ|F?%TTc1+rxYY_orwqP7{wG?!W#=cTF#x?hqIAYTx~y8R7(ZZ#dsaQ@!)V#z zK0Qn>Q`!Ir*d6mLNpkCX8w3vvLINUrdL#z0ZT881Fh>MpPw{6&AaCNBgE)AEwFrV| zfOZUat4A)11LO`jm{N`uOeqlNXbq}oA1jnnK3 zl2UJdDb$0_k*pCZmCJ2?`9s@Zt-Y1yPExrnQa6?w0I}4%T$eUu*mw0fMCuo&u>TB2 zIB^M`os;__BBV)gEHQ}bl9cfN)W7NraxmXdp}r9{oW)AaYc(A!b09c$4?cA<*K$6Y zw`T5cM>}%$R%>nTm|-)6s*r@(gnwiK9a9Qaq@JE+<`hu~Wo~N!Mzdsz2*A}E5iC5d zj6|e*q+~-~^VJcN5pg6;oWmy0HDE+7S%MS~=aktIrySqi#-II?ITrJGpp6!td z6U`5EdpjOsM~kDD<*=tC461HUv73Iv?LYhd-~E5>pA1e!4omkyfP_2qGk~PzPP6PG{-1Z5-}GjRv>pPG!W?%<%2x&{WU=YN8O9bhm3p+XTr`F;K4*W)*JaohguwqY>2Y)N=_W%t$CQ*(@a;nZE~x$Z1rBK)a52piqsjG-Sh~Axph4N zVODZ1rAxQ^!XnOgmpNg}-KIk650G=zK6b4aLB|Lo2haGi*;}5?^Cea##y^?m(_g7S zveffyyGxhc&a;l16ZSE5OHC_XIXX0FpeKQF)=0e#U>>3jAEM6z5FNO9&=A(STcGKY zFT0vHZHKm|P2E+Sx_ZF$*tG?ez|GWLX>4lLjge!J7(cy{8%F`KdmGZXys0Ik^G89# zMKbLMpe#$m4hn=B8IvhY!wja;jNDX#W4G5g|M9T5y?y;*uTKs!HHQR>MU<`bN^+~b z2(JQRS-0TAB_KjYz(m53%7s{ji$H{!Qc1+jLBv8qg+#K+?PktC*I-0s@T^BfaF_4I zLEur-cPZJ3y_DfX6~IO*QUVQ$yd0xEPfG-9H#&y`9ANGPv@D5hQfm>KKxxV289Dtg zIsFDGuU3}D{h78Nz&pz|=Hn!UCvl~K69f(_49^X#FnORz6{1=S6D@VEQnqC)Qr5ai z?|w^q!fIKPGMl}kyE98Z@6)N54yH?UWjt2FGx=}$ZG$^fI8Kl2#bnnfwyMlzZtk9D zqP*8kjfq-o?bzXArfGz$Wntn{Yf`SWZ%k2SZfnw&IV-CHuj)lzM8X0*MXAFT#UkwE zEuOsVnJhK_Og^G*a;MehRK!GQtiyD1j<@^Wwk$QtN#X8hvqA667ExwO!bNJ;vy=vs zHiinFM&<|$4<7Is%uO2haDI-*n8spg^t3-YN+OcBfcHcB3mf$!^ZrQ>Ha7NgVZ@w` z@AQ6-bJ7RZsDJyN%Yq2&8Aa~3)a$M;0Q&F$xBvFOeGo3fQn;2CKE1S;SIW8UTLYxKuAczCF4JASGUctZxwXCm?Czc@XRF$%b! zdgClB=QgIx0>ks{cDKpZaChmA{n>=iyd_o5UthIx7ic-9_soX@Z^!_;)!KuIl6sm7 z>bZxx0;}i}cRYU2kry>e>g(g35CXiLRtyXN`_E`fpfUHlF|gHUiTH?!V}AzX@#DL? zsvgh%6%q~$qU7b4y4Gclh+5ZDsz?r_ zPMk~K{(p_p%0M)aT`rBhm}CzyMIaf}-HsR$8BCoGvnt%X>c9+ck)($dp16e`?nJI` z0c~LpRWyyG#q-Ik)Eb(x8+-IVG9YJ+`xq@oWOOiJ6Nyh_3H%IZ$n0&`cAhdM1bs1R-U@ZRHQEZ7bh5-d4UXQ5P;1Qcy}HN|HHuI0-)}Qe@Ju zz+oYIDUR|hW2diX+Hqt_kT8jJ;rc=hks6`I71pFIB&95OE-On3mW)TDTnC5@+HlW( z;7+^oGY5;_#fQ>)w#Hv))}L3@1+XzwzNfKgGMr8(By)I}Cp#)~>a5zBc^&Nv1%Lqmk2Z7K|wH03$3c%=7%Q=9xW~XQ$;uXK>F6Et%&UF!TTpH}iM~n%ffs zkrKjESE{Q-+4xrY=FQsvT4-UexB6k>`y$)DeyXyTSk`iWI zm`i_ji87M?#5Cqr+x7q5yL(RI-B~yXV`;-lx9|W>`uubE^rpj$>8i%Bb&2T=BIL$6 zfK2cjZ+*{4lp%|otaW==Fm#DokABGk*}%#CY?K(8XID7<`#tci4RV#u(}^~0dokN` zzfd7D(ov_a41jsh^o@wU?T0o1d)tq8G;{Z+9v)q2W^U@o%y#s!d{Bwu5D#NYFrdQS zw6O<=Q*z7A{7|@gGjFPoCmPyszs1`gZ$~^E3^0#o5v0xC%{|<7hBhELLbKIF7)5|< zmPJl10JB&~pl6&5i)S1Xz_KJVT*y7Rca;5nOO*rxPE@l*7VwzNjOoIS`l4;MDKmPT}_d5-idL+ zOg6;D%P{?ncn@%XXHvN2IP{V}tCG{?IF=Oo3eNnR2=}m(gS?@vOnrKOYJKLOU2gnr zCcTi5r(6Xi^H1itMnfx}_tL$?q zncsdXWtPp^yTu9OJ?D@CT%+Aje_9g_TwF!cADpBcb#slVcVNmY`!C-p6vu|Yb~kw z?bus8_Q$JQv#`hWn|V9z2_Y(cd-lwq?03%a-!f920ijGT>(Ic)%^(O!she zJibdL_-ND`EDeU4E!ynxx+lVZk|^P^0(sN zo6x018-VDkAs7pJuy9CqP)M=xATj{!>f6HiGcC=6001BWNkljK7*y!l}iPYaFJ3IU4z->T$VPc zm3azGx-!#y8s~U}@}g4H-U>!UL}4+@49^7eNqE-?wRZF$21Y=XJ9Q0b-&S?qc;D+} zv<6G59yXSRXWGOZ%Ff2shw*R1FRyNn>xn&64JImYQbW=u#od}kGgWvdYrDl9V;nlKpCG zz1Lsl^{W$w=hMzSUT&rcQyY(urS&fDy?(0rJ98-jOLZ~#@^nlZh=t6-NrleJ$dVRm zo-xK|VH(;v0BZqfVF;MQ6T$0&=Evjd$5DhFKa3tBJ8BS=mP+K2X$XNJAry|13bHVG z94Lf`hmG{bZ~{j}iYuk8ejHLNgNcQx)L2TnRVt2P1EUJ+a^J*iq2<1OX2@1wgqA|3 za9zrlPW4h2fXw{){9dZu*H5J`GZL&{WPKyuUE-?E!)slp*deJWEM)?9s3(+k0F1k{ zh^vmM$u1fn_nqXPDmhzMq{|jK~^tWSVw;)7n zjGal^0Xj|d>>sEXV4!LGz2FzF>;Ss%3`P+JLboHSYW_Rw+2h)w$F7H(9;UlK)oj?wyiT9Ce4w?+KL>LQ2<~z(_!RVHvegkny=G+)1arC9PGD&pJ7rPsC4R0}~ zwVVtyS}B4kdePKb58b8bAg1EJ6F4VG0b(k^9;k%q(C?FfKQuLQuq$S&W>@N=13uXxXSdd0Ba3lHe*V?BXN^g&}t-hWw_> z3$c-axPU=YnVF;{b-b$?0FnwQ$qmrNUuRL^UI;C~6wQJe%3K02ED>5n5LRVjX5q4w zA|xWTE;lCH>dH*FHF8y?F1LkjM*5fuFf{PAT55%IHci3>tZYJe6P!|9G5EUtkQg7iO-N%x`LlN zKX`nDCrdIl)w!0lF_Iqo70)&~B23kzQ;mCRArhB`_O;R;n?fU|%%-_WRQlKBJta=Y zs-I7AblRZ$DDYgloZ=lKO(}Z^c8&nu4)-36;Tg@-Q`6ntnyI-RZGU}y|ML3f+mGLl zw;!<|)%h;leR+9#`Mj<-`oI4EU-hA8Hp-Udly>a<{(S!B*MI)YU;Z&fpZ-LNSme_$ ze|)*Uyxf2Jc>BzNqp zRf8$aUcbl#-(v z3>HkVm{yQ$y3*t-;SwyLRdeo?`u35h*YvW}zW7EaO05?{G}5rol-&p)^UKLy59IC# zbEP>t?Ks8ZNxh7I$_$ZsXNrw5V9W_bzVGKb9wB`@(CZ1D9WZg~g2tb_hBi%0vu4fz zFgMt7`tQv9{-OJviLu;hyT^L>Wx;wkW+#p!QCRM`*fwly*;Z$!bp?yT!dfOTmi5k3 zmi1Q3ZM%JL=11FAcRij0TGxB@#IiMy-06}?AS@HBD$itK^x>&UF>NyfK9{SxUY@t< zyT#4`un$?(g?4M2Bn$+GLip@XPf^Tpl>8*UnK?!=dG*7((wRfKWSgt}AvEe)0y8;) z&I7ZHm&QYx`HDF_7AbkeF_EU>is($VNMXeoSxy-dombi!$vDiAc{I(@-udU`9DE%y&7?d|wYYB{jin*>P`lSkvqsdjv)7VMkrX}fed&rL{)+VTS+RoTVP?*=0PF| zM=25#jB&s*!6_)lrkSA`XBNuxsjwNTFe|OOYsh@d%jT!^EYU$lG^uRYa<*5N`Ei{98y-|k3ipwE4Io&g z>pZWUfU!lH&ZY?s$;Q6-3~o5dBtars@Rd@{wUp}9Qb?Gv-0bJmtiHFy15J0;rbi>! zFikYuwrqNswgxp0(S3))o3`ifEh0jnM-ZOh-yYu{Z(n}fUw3QG%~^^a&*ah}|5jW0 zfsE#Lb8lnd1w>)rpds8^Gj2rKc*wf$N z!zDt&H^dteARoe=py*T-NBIo@5eSkS`B#>e$cYXrHDIU&K4fKDWchF-X>z-PT-Jr8 z5OoM*FSb$#^hUNR86mYgJpJBC+Rk~W!#UZ zvGt_^AsQ0S4v7>V*~QUetNGGRbj(j+fE->&%vD0~2x!!MF$M4HEe$U1^YCl^W1U4-ni)?j-S2Ilo`{Y@198a5sY?4(*HCC z2zMz(q^LH!fZ#wB-NonDTI%fEzJo~HQJ3{No@OfjX%2H6W;2&k)8*UKfV<|h^koip z4aQjJ+emy-ZF^{YJih<-0iIF8lH~|NQ8ij z9n6xMod_>o&_1L8cs7&+Xmw5<27CTQ2lm=~%IgPg%6#zR$2^fPs5B-pkp&PjO0s^q zO^Y)61QFTp`pCKB?u6$=XL7H@&;=@)5bGA*rS?7^4<`tL&P=#Vx6}u9!B<_&yjO-j zXeylZt+f#fAs2JY6yD8dKKoKL?+fKIw7QNnFMHJCtRENH;4t+V8wV_D2@%c1iJT~E z!NMq&mI|h*m6w90xQMfG6(25Bl$AuvvKk=E2IjKfnTyo5Zg(n6T|d;@2QCXsAx?24 znOkcIBHFPZ$J5$Dz_C9oNhy^^;yT=yWhGF_0O3T&Nl7xfl$u8kQNNikQ9UuNYJQdSBttPXZ_E@Hl3CX{Rk)oMYq&|h6 zBeuzSjFBUh*Li-maru0cs^#+(8u)V6HU^h5Qui*?Vx}pmo?4-vyEXy{5;xsRsskyM zX6EoPQ!|Zjv#{fM9_{gXe1E?E`1bYB&*xV?_UHcf(5Kl!6%coJu5#!r34+|iNI+Gb zj8YdQaCLC^TezAN;$RjcUL`6Km@B9<2T=uxm%_pzCRth7AzbornWa!I&O|Jrj1y%C zyJu#n!xRB>Lm1~JZAdUar>3;EQ1zqf(c*DzOJHeU3^1ZVCE5y=K;5DcRtobdCE6hc zo@8kTS)zKl+qnu)7a3Wq@rXj)10GxoAp#}B;Y=K)h}^~}VULN5z(S%b4&ha51yQAy zrAU-28yw_dmO^Wb<3stKaNX+5BDZ__w9vZNn@}N8p-Nx`QpD)PV4u&bj7==1KhyDZ z1ALifU%j|j8$t{;=6uXjgn9O{%x&LOud&|lS#(-XS6uT=*Vc%7%2BCxWM-b>_I-{s zd)myZAWB4S?p01h**VM{$QvK?992^dx8pd%t!Xn;Ak-QcQQe7I-Ab(iI2_Ei)@8jX zE)_&+N*cl-Hg|J3)28ZX?xxxv&o?+!n`zUg`}589P&ID{g7&vJZ)UB9sy_BOG*tEN zz~20E*kgw|(Gu+d5xAPRfP=&Vw*%-_Be#ROM%XC9r(7$=bS1(N$yLeOX}U%Mx|#i8 zuIZko4xmzyFqghV>bEF}i+ck}gb5cy0W>^+&8OY+^cUo9D{`R$ zVN~*?GS9qKb1A*48`Tb-{#beku)2HhM0yM0*dx~(rn zC}kb`ZZ_3EH#F-pQP~fg&L}ki^soP)f9(TBJ!%rVRx=k8S5_oFP_ge5M#K#eYIdl4 z#E<7|Ywg?X_qWIEcij@w5UoWd^Ib-^b3qqJbnT;W`{g&_xb%uDKpDH7ggc&Aw%ZoNhuvlQOb0p zPe8@Qk@HBJPP<}mF!}&H!=2qZP4gl>DQZl~rlomqr;}6H16L9c(D9ObN*T>&W->G9 z=010@?bt~q$5+Ix?Z^XAtpTXZ1_~Ro*FIsGnIbC;u$00ggKZj^Z?Px8{-9S_9k9hsTZ?EMz%Jad73a821;T+KDM2iyh;Yu~NSGyg(ZbSYPZ)9$8i#pzA)FY$ z%vzO>f}{dsDpjOH79uLkU6zf@QtD{g_ z>(JJYy|o?gdhGl2O<1%wYe$h1=0!@VuIr6~x@_45my+J}0tg$r@1RN1McsEODq z0v=2r;oUhvS8sfP17TqvaQDykMPt`6ka#G8nO$UIVdCN(JyjMF^XJJ)s;5D^?yW#J z2M9z$m^)tu8WHBu2J^5+rld|yVJ$37om~k5vmhte&>aB$0Go3oIzkk#DB~)W%4Gp+ z1`PzbLo%WeWzZ-|P6&zh+X!$alwstfOazSgmY4b2*1cFOR1vLnc zkWx!s*7bI2V!4#ki3h|}y)~CPLm9{(#w=qI6ca~}NXlU!8NLzTrV!9UdT$OwcV8KFYKJ8oZM4iHCD%Duo;g-(pUzR8AGy zyoDRf&Fx^>LLWp0To7)$6BDq8E8x`mg%y5)4SgTeuKEW~xR-#6fmcnb&s?3bH@}F@=$=?#ckkJ>revK^q|C zKfw2}A5=fP2gNIOiarrLp%Gq*iDVBZi1OkufBSJiz6CL)$!#sSdyq!?$aQfcDj!Ll zn2DECSN9_zr7R*feZRt73vbJdaMV(2S&G!97A7eorIaFth?iQrV}wA&!tCK9(w%*C z5#n(j(x=F3VxKw)HoW7+hpQ{8J!;L}Ra;?EQ&lZpB$5~)ao5Ss>FkM!(wmwP?(u1P z!lY5T)+LW19?e_6bUEbh&G1;Ro*Qfoh0CkU5+kN+&mTYyPIt1KE)f&<75U;4VRQ+ zYH7eeO~P)fs>g90`;V_*UcY~Rd;Rv~_1nMv^S|Rt+s6+dKL7sXFMqn${lowL52J@+qVtiOCg#(gK4W#6lkSDF zRK&aNAHOK?2AU&nFCZxnQdz3uz47hc=%G6$+gBMTy*4mduF0^7%1_%Fr;%> zl`pD{c(E?X9Q|H)!s#jMQ}9eAPbo-54r6+-)_4X_c3%uvyk!Jr9Q%X95TX0S+B4kS z@tSY4?yt;jZ8vLSM|j@B-q*H0;lZ*y$#}s&X6`hR3;`k|@d)Q)dbs1+p71!Ds;ah@ zRRBL;`SHq+$7)uZ%JWg(C9n{4I1kn^4?JsEl0=3H4+AZo;kq`Jq6CYxxr_*5GM_(r z#3>W*W;Y&<^YC_pubJ`_ICKfK8=-$VwL8f@x){83sXLlgI1oil2ut-^P}pmgDj^&q zU~(3&m5PKEFsElSxrUf-9A+w|WCjTn5toE}j4ZnBkh`(Gx6VDQaHgJ2uUd{geRR2j zU=XJKm@?WqCapn6Ovw1rFd;Q^KyPaWl7x~Qv+%NUS!De{r5Z@qd-SuN=Q1a^j`@wY z*0QXvbsgUMUK05wWAH-4I1#UL(ggbzp5BB^#}aQ7v>@<)fMSM?##L4%`=CDbHqO5g7PlRY1d#gl9_h&ovM`5i}-LU#e0); zrkkccX{gA$+zXRoezBpCEO$>3MeiKOhQ-nv6P-aam0~&68_^W*W~wdg0M(WiL(`|3 znj4oAVV*rOprQG10z^`X>9Acy0s$s-1@mG1Zcm35)Q0_`52x_3bOZ#M2l5Q*dw|r5 zWNH!mcaE7+Oa>yi{5I$k51#El(~P`Kjd51&afUakJHjX+GNu6N+Swu^rM4Kbj2KBs z2{!O?kEfVLxTkv#K@-9N4r~1#%3`bvn)Ymx@m&x&Aml-Ta6)B&=<3U@ef<9AKkV{r z083dVYBYzMhf^36hZ_NUJYg1Qp@+9;v?oz8Aulf}W!XMTUBPl&e+l!tekigo%S+*< z@bYr|rIwArLL#Lyh^1t7TVBm$IWs-3d_^_{qDUcdU6$1Up9vH_%yZIT`hVLqa+%X; zXXYb!^peylGIz*H4*{U6$zX5Ck(MQGZQmcDXze*Cg_ds$b0C)WMl7XN5)mm&S@Ue+ zF)Wr&?mV6O=2~-k8$+*jA?huvF^bugt$|ZokR6+ci^o{saz=B$cgdA9& zvPro))}w5Kx~nxu>62t-(W0`P7?? zWz`sybo!uskz%H$43Gf^ga*j&8VE_lHcmdFVwxk4=NYy3|sh=TZ);zy3 z40G1RXb=$efa$Q_5N}F z@cI7vm%84{x`ub1ZSQg?PlV16VFpsZZ|%C-6@aM@6Jwgi&BL=I-1i5K*q$@^Ys`*^ z+&5Q5Y;Ji75K$?`M`%Yv(s`_`2k(^59D~U#8h|Sv?&bXTr?KGz&BjQb_W5z=x+V*$ zIq{fyt?B6N-D&szkD7_jz@#{*-Tf`fs;yJd^0mLjA$FPi)g?IczDK)q%04esn0a?Q z0&cCfV}HJ?9Al@+lN{fCR*1UfOWgiT^2D*DQ^8`v;Y7g07*naRLq=DLt0>_AcZ<#RQf)aXr>d7 z?y_eV4JiHwTafX~3GNz*oDDv_cM<8)>ze~-VuZUDBosBmryF4M=7e~T~hIaD^ zHI49Q&0L!`3stwb7bG-0BkDtA{~ zr7%ELgqT6w_MwzTxR^FK4Y%O<^!aZ@DzQkZ+wFx#SR`!F8KQ3+1~}GBt!Bm}loWuQ z<~&r}vA_Q4Am}k@fqo2yco10mrdftVixx0JeIC5#$g{BrKD1KmH zF}6v4+2xb%Kz-gs1ac72!(eA_yappNNTsuVJCL-pOG!dYR$Q&lHq%TTS zMh6cMLRi>N4mX7Zwud3SP?$G`?J##$L=D~TXbkavCsThsqp?4p@o;}SS_@14)(v5) zDJA)ASR%sFB4h|WB5)g^DRsINoMLx+A$Yo!rFj_agTq z%XeA`)uOCagX)dz-K9Y8EO!zxznHs0BC4nJneK!)U<(AGKoD<;S3m$oFkk=+DQI(V z%#Kh{AqqyzP3e%l13gT_4B3xwImPMHB|IgOD^~koR=~>@t zj5#ABbFIC19lG1qc4IffmJqhY1wumHaK~TZZ}6A+0Z3fH6<9)TscBRki~s@MZLD@< zcUSGTR%S-ToMU_kF1|5l#BRG(wX0MsS7v0yoMV26_kCWe5k@3StxOaEIU*;-Cn(g| z&m61EM-0!Pw~ldQX51W1xa`xI$joGB6F#@cPesRQq!U5>)0`B1eDS0g%~dm)d+R$5 zZc~EXG3l*`?H`_#Ghyzue=-m!bCY!OjQ$YkY&yL>mL2wZp8D|hj7a1NH7#6nPR?8q z!pxW@g`eX=6Or=Lfn|>vTDE|kmB%Dev;iN8UOP zif$b8vxgS?5luPwyzmI=I-+5&t?|(*%)XNJB=bKmRsnk=E%2GU{)+Gh{ zJn^v1B2rS_N3+J7%lyHa9LZgCSe5N!P9+C&24;Bod>B92ROw)x?{LC?V17;yDas+#}T)AK0R2e^jJ|n-0?TQ&2S=L03oMbv$TT zFe6Wgio0vSZ?~?UiG>*ug~iNT-$eufi^x(}LN?dD$8Ot4*S^#xi^UB4WiD<#Os#G1 zroDlySr2UCj1yFmCt5dI}CO{0uS%ip*d~SwApWBaehqc0AUt5AK(z zpO!K{Z)_^V03ibefE39P70Dtx;HI{Bz3*Gwce{Pi+ZX@xqu(}hmjF5>_O&cOe5#)= zBGeOCedIEF=||i`BD}=Zr1|CGQguABCaB__Npd&tJPm?;|RRf~+y=(va{-yQ(di%Iu z-?!_R{ryc^hud~JeXh%CIbV>awVjyvwzs}x@$Y~3NzNCk=cS$lX{qORJ(scymvN3F z;iJA8CO{i7XPZ$mIMCJzLp<^XvrR?Gb1?TooX+P(Lg3Z<0Uc<%2Y`9swVC&(`);lK z-d)Z6ZY|J~H6IWT3P6w{V>pr21R1YQgaMEo-jPMa**E|a9_iDgWJv*yV>+`596>xK zDl>qFkbn`)lS$>Cc6v`AQDpje;_5POHxs^-aT1xu23gK{Hs#5`!H6&*VRR4-n?RN$ zPLm{tp(`lg(wVJH7zrqzrzHY7LT z_wDN0pO{xm{l06+%(B%tUhs%WkU9*K~Hy`R(eM?&F9;TxeK)AtAPQ_f}Hu+P3kpE-YH zVK~9X@Bni@u>4Pk=fjvaYPKf;0UjghZkDgGbq(;Y)_TjBkgYdDy6tyD>}Kx0nG?Dt z7AM>w2+RTqTwN*A)=1&zgCG&pErbq0GX`u*rWj%5QO&XEV-=9*M0ZU>;;b~X8XmD; zFbp9R%Q%)lsZ1^a-6Myj$HOAygm{P}kRTk9a$QdVfgJ8>JIKEa0S-0-zh@2>0uH)6 z3P48yKnZUa1i)^0g2i~5hHuK~`qhP|8h=(m2oZU5@Gx4*hZ1Au}Mfz7D~ zf2p5O>vx4u!l#v%Q&|e|1u>9L>SqVEcroVyP&g$EXO0pJgd&nh0}zNEu>=r82Vek3 zXdXM#3r0gE2M*tuIG{k>06O9&ToJe2?M)#Y1}K0sN)9@N1tOMGB8<>dl6qd&!j&0o zslrlNnCY~f(mGdI(vwtJQa>nC@W4eL_3a3n!O3$S#?gbz_>4+^p7=~Y)?+k(Cd&JhZ+x^4cYFTorHPU*CBcf|hxJH7L*0L_kNx0;)`ms=%!|SP& zqQlygo7IB?HO0oyGL9LW%KRKJlpW(SCC>-vF?gEEaDgz~tXXuKD9$H`4(|4E4=g;TY z*Uz7RcDelUa`|puUurqkddmIZZShAQ3&KMGg%M6X%RZ(KA8MAyjTq|SS$!1!lsCEEwwao0wdu*<7=~z-7In?RB?!E2X)_dRX*Zb|dx9#Kl z?c@Hn!*0-^@OnO7E}z!*^>Y5S)YE1C#4JlW3709~#AoPe;=U4F6)C19s%eoz#3`aX4E_+% zwdYGZv=+w(J3NjV$HUtJGs7yv&CGfVLi@f&g!RqcRGVAZo=6fS0Mi!M5im?M{{jHr z6@el(OL-!U-ZlQF(gWGSeJo8dA}qoK!WDqr6=qul5j0Z>6X=BDrUo8p8s-oIYGKBv zpaxyNci0=7u^Fflgk%+yPVbf4eV>A2)3%zo1VrS~kw`X*U_>5;(Qu_aX0bp7aRkOf1Td#i_Awhe zP}q4T)0kF_CO_2^8OZ|x36m%*P$j8=#Oo!>sh3lfmCKT-fcbdlJSzvge^kSR(fSDH z83^J`)XcK-p=~B)b=~$Yk4JMOVj<3nQUJJ_!-!ZTA|YOu_0Vf%k(#sZ^cSeAnl8(l z<1l=hj=P(h5>hq{iOd;Po(NTSw-k;eU@f&sb@w7A(LTv?%*esq3g(pBOnYnETWkBi z-!qh`EX$@lgu8VQ3j_;O-veN&CoVPTHFa45a9z%8J*BcuxBvtpGcy7*i%*eA&ewTp zNpdhsKVb|;WX?g}rTMvArk5ow-nzQ(eY3D;`>uBl+qG?aRWNsTNKH5(k%fgjV)z)z zjPjjV)GZJZ7mSYC(dIPB)l>{974f?EJF)FA># zesvo3#$1RH+#m}kM4YnNCng_En;67=IH8*ZO;z@srsjD|1ZD_e9>@fbcu54vNSU#I z&b?)rB1_Uk2FuR_nGrB%vS7YS2g)evbm9O22NG0*Fte4`ywUZ4KHfflWH8faW)zHp9R70pzDixV0>E0&wX8Wvl)7ND zE9#Oly!k|PGwppy=p)>1-|hhsdBs`_Ol3Q;6e*?aE@jQSAk&y~N}j-&R9t6~P^9L) zMl|EF#@yx+vEw!r!bge;(!7SY}#+D*I zL75+JHY7abd7V3IjexHE*0q~K#Mbww>K@H@0Bn69GtLyfdq-rCmc&ML!v~xqD^$xk zS;PXML+Ep)(9S4ggxuz3N$L17XJUX8F6KKTm;(|cGL9$!&w*#mup*D?k{JjV02B`! zWTb3a6GfSXSAcvBVJaZNF@aL!+2w>0NC0RafD+y_vH(IeOmXCr(E@}-3sN8&V%2_E z3wI!E;R5~LdynR}?{@vIzJJxXP2cW*)wroQ0%NcOK<+6*UL84T1t>rUpj<8nS1y7A zAt=KB;7OsI$iuHYopc5PmITD%{*A0i)5l@%Spgn>bdj3WiGwirOl~(bGu`{f zB+NW$xQHThc;-Dkp|eCh7EUwkTUhc5p5}#z9@cW-i-XpF@Sg!P%yk4GJS5Hd45B?0 zsm~)SqNj=55w`TS9?zd3EF%*vBXVYY4~H^qD!tPHg7g1RQH;8&bv0Gfd%NHF`)$8o zx9i9CZM%N6`%ToP>j_E?KA*o^PcNsJ*X49B>$#rJ5s~&QB1-6Hg!k6xW-c<9$gLxB zYuf;E;n3G4)g@0_kB81Q71xL$Y4;`0rc6rBDkz0BgETa9wo))SGDi;BE6?6IKeY!# zBumF%|L?!n-uHHYJ-;4`yd)^lM4p%_1c*p@B0EMz9VN=P|Q)85R~O;wwl_P&K1AoRBF`_|UG7x%Tj13dS5f764h&WtgQ*S)B%Yze(z%wm?9`vl?(Vb@424l{TV5IKl_8!6Q(60=YG(KJV zL*4CMAdJdNT%ep#7G#lnLN18ZkvG6V(tH>XOu!fbGf_&I5RjRdy5Mw@Wc`<4nFSfo zsq@^^j@M&TD3QC+usd)naXR*{-%gv>(3OFd1HH6dUi5OSV!>Pkd=-vL3j zBVo6me#P!Buk~h~5S!m~^3ubEuvr`N@Y%+OS&n#M2uY^jB;Pn32N!}n2%(3=5NH7) z?M<(n|Mg%0_@Dmj-~H?Jj~}`x?>>fsJ;f&ZbTmW>aPR;|B!P%b7#(2;!$mfTjzcOj zoEvn!7Cg*XhyFja+(r$65rL6uE&!1p;sGugR8kB)NQUEkjz%27VWgs>0bryv6T>ptW_a00XyL_JVX+4 z>_~PC2+*GFW$3M?hPG=n)quRhySeWByJK*vNyy1dH~UzT>S0orfGAQ+S$M+sgiDb+ zQHOY#wP08m(>v>j+8F={dFlfnuGzVAeYCrC?-Wxj0qJIhp}EV@X;;;(_|;Pt0`AsS z_rB!`x~mdKQ;o1};oZ$c0dwMv<5&yDfxwJB67w07M&HAdGHl2|fDG{f?c!s=mFLND z1$$72ROENVxg@}c)$8f6=iM>BYRHw+f$1YW*a0O%51OF`F$MxTIJ3~8_ZMbHE_pSX zv)Seft_uB{zNgr`Z*TqM*5AIy+ooTy?c44cEcNu>`LGf_3ddF6%;dK_)Qy30Zd@E zNGVc;B?)r}CVDOv;E-jRTW)JJN88bW%iggWLEn@ z?j2ieIJD*Xa5)@Y-)YT_$T$x(&+GDW5kF0!ZH6ixKpL1c-H|{YkBHGj8dBQo?zjQMfw382 z3P1})Bp`4AM^r#%YQnZuKM9=*o))NuD?`7*wPr&5jT zdw7_2PD<9ueBuaiZO=@y9RCe-b`tE6h^Ul0u(!-(2RW`e)ZRxjP`24>$K9D9*S|64 zzi5M_szU-?PJ@oO>f;BU>JJNp=&)a z%f&&FtCY%uiS@Q2*l<8}a6nRTK+xR*p_{9!fC@6RI7TCjh#VtVf|Ks2LLw)hzz!rpne5mqbhFT&5U;*gIy+K5Na99x15{cpS@Hkxd zgZ?&clmn_U(z-|glcxgOkT2)rkphCoU~lNRiP$YibCd)uLJkc83RO3^rs(GCs2x

(#q0=jij=v(rBQ8#8GDU1+Y5xg$1Qcqp)RbC3MwVsNc7Cr+b87^|E%LzQz z<%MuMlFW0!50i0{K&&(%97AM4%KhpSN+W~02N7qIt_IF??q(AY|4>vuqQ#CvZD_4L zLxV>s!)(<|m7c;WBEnQtfuBZ+S?7)FaAa&EW=w;r5>abAi)ilw;o+^_6Gv#F7U7ZX zvmh1^bxg2e0fc2aBXKE}r7+6@AzYVJiYd%Yq|W{!r-~uW-RuZUJGQ=AL3+U6_XzZe z+jd2yZrvkVZ^_8)wr3E#njai#$RKF+9M7n0UgL8h2xAEh=$SVS0BJgS*p+kQKkz0V zj}^iOMIdG#HVYD#g(x%LSS=99-McDSgRoHb z=Gt~>d%wN=^<%a5{a^gW$N%>=-W#|;Cvb2t?x7(b0VqjWLj(f`szQwskWwZnfDXu! z7-|Hhm+%ir;vNu&TPy(NQ1bMMP=qBH<4Axg0VPl{XALmyu@C{YB9-uld;%(Y4W`B$ z5Rn8!fluJ~z%p%Y9Y_!WqC>O{FGA=J2!0z{aRl%?L=OlgaW~-(C=i-rGsKQeA{}KZ zwXQFxzw;0O_Amb4@BP1j`e%Rs-~RWXfB)~FUl%G+FK0kU0$GHF%etuU_4HaQ5etc! zcR*rcHATWw*JMEf(NZcPAz?0-Q_>X>=k?{Gvq*IZah@2*lj3O}jboZTQci|ZF;}?R zbPS?ZYKX>iSaYdMQ(Q0sI_ZNsm=}ko1*iFuX?lDftHmi#o9=gdFe?DfY;q#QEHC4s zw>>cOxiuWe9dXjOvKrw5vO)k*Q;*0kuDNZxZ~Oh;wyz&wzP+GQ`9sxyDfpDbw z%z2DG+%lOoWuEbL@jxIzLP~w=4USEbo25BhN@W%nKEn47Q}GmtPxPv3GR?f?P?Kx|~k6uAg6j_HzEdNL}i=@WL#@ zwMv~L;RgWrNC=#4{bRurL#m!JixYn}#Hcfl*biz9qDK^#yd`Gs`-ll;V)aSb8*G&! zB2^pK+4+%0&JWV2JK$e=-wlDWk1(yw6de#Ogg{|-)l~0^l#~NN1R+Vu;6V!0jQ@2r zVtH!R!&I%coXqw64FIe)Ya1f<`xV^6%$O(1Hn1KVpjF|>b50-K+B9H8#k+3||gAnF;Z*qz8 z!Dl`)P+^8eK|a*ogO7n)9P-Aoqwz=h`Pf1o#1Hi;NCwbZu<1bR43%AeOCj*N^GU&T zu1ynTZxO`oNFA{OD5F);bM>`G{#&S4p&~4@)KaA&agjnp5KIgx5FAK==s}MbCni{F zia(R+g%E)QNBa~={D=;I#K8@5L2BA!s&`=eihXv&6EiVz_<%!7D#?A#?w4zh))dK+ z9Kxvv5|AJlbCDc)H2?r007*naR1YALiqnwC$f}z0ar^yNq%L(SWi4eP$T;COk5Lgx z#48EA8kds%*k@2Y4kvaXVd7id>^KlcTnYrhPgGckst4v)JFQ1!J>}N+7U8aH))A;I zO8~eUU?z&IK}t54_GB88<)Kt;w5bE=I1ftls_Q=(K@!4o_zAAV*EI1**{e!tz{ z?%Rjm}03O!=ab%2v8>55=1VZH84-if7J>0wpnMLp99;%=ku5Jdd z<{hEAcZcr%4x|9V1%rYL2Lv#i8F2A12t*QBMPTUZXAX1%1crjcuS^8BE*I}zup-hT zX96ts9Ojqv_vpwZMV8b0>gH=b=QfX77=@UbB*4Rhh=_%mwfBdM3?b3(d86_0@;%3O z#vE71q(C1zh462h86B~iS$rHs&2O}nYNTL83u>%CdncK@*G=DlkJ4q^l%s*?cldGVpDnKxx-DgE{fu?jxQvvde7&3}GG)DZh`N0n)?8H6;^- z9zZdi^)Um?AfOPKgO1t+kdPy6j`R;W$w9e-h?KY$Ko@2t$#C638HO^Ha0uVsqqjSB z3w66+ZEOAG!`{F4FYo`abpU#R;D{qE?gn&%*r$1i0fktQ#nGG@s1(`+%~_Yv<^E-ZCZQY1caTnGC=rGM zNZ)~9gFagTdJkLyY^p*G0Kxa5bJ!bTrPzVa!@A2WAQ3O-+jv8L2Pzl}s0bB^!`?6y zi2)15o%rnbK}Z29R=NXJLIVIG!}ySL4gex0+~o9K`1bq%@DKmJ|KvaTr@#K6{;&V~ zfB2oh`)_~x`75GdzWY>{T2AZfaxTkK)@501UDmYKlu{x>isYc0m{X|{!#V^2puMq( zn`&2g>%FD2AZ0!d(c0at6N`uS-s-YO1TrJSz;KkBNRF(fWImmVScV}vElyYm$&zE@ zdk;-huAu)$G_1Q%sTWVk(a2w%kvwCMl?7ui#!`~`jOUqtQW!1LBirvMalR=Q%e!5@ z@4an(zqi}hk1zXnz2C2Td&jNOu8YTt%PObK>-WogI=_6Xr%PSWNK}@!ENgn2Q#}zg zMLoN%G{KG2CudkQO_!uZ#{EDLi}uc?jD=G~xU)$1DdS~7F{NWrGhwFi(2Ab$2*Vm=KEcIMu5iY_dKRcPw z^W1Lb+I$#(++$3X6%w)?LokVmU0V{8RC@%NHgm%OwJyZHcZe|UMC@if#wsW*;ZA_H zuH%0b(})(t%#O+{@_>Mhpfh3*OJ#LLm>CR#S;kxAP;@XK`%at!GdHt|iO+ipV%!fA zA*BL9_O9A?aJPL&gfMe$psL#~-1L6yA8+2a=sm1QZ_#_WM%yB_;-=m^hC>7i>%LQX zra?k_wmov>JKa$+Ze|#$hpB5AOK*pfpAmSF8-XTkK6^QO2pkTR-owjJL()T!ZQ8$< zk%JH!io%E%fzSA|CwAGy*(6ZvSUWC}Kz_ z6JLOllFk$gxd-N8%yP9x*n^PB+!WB=JkEAru?$4nX=P>0@o z@69uYExCO}s{P1{E45~vI1F4I4ykHdNttqym^DcnNpKeTS)$HeS^iFZWUHEK?>*;c z*}IM%e7*oYf&*q^xoh(9;24}AI_-=}H>EU&rLv2eGfI6BpLL3?ec!iyf6NsLweNes z_qG8>)4jF(9=EpLx^3nS0=hMiuGX6EgkadgG=v;fy@L@Ts41c(Ubr_yBt&KqB4J@h zsigpll!Zj7u+$}(kQoazAuur)m{V;)G(ZQn?8K66gy}U%M>Mp+7G2RDySiGaVK|t&H79r9-5P*J+#EY%06KAa1TAC%PLY~aSommA&=a{eUCI`K z6kr$}hENcigChzf3WNf(4P$f~XW)*qMM4d65eRLcnSL1VE?y zvXm3E6kcjQ5fTK}vO)k2i3F#0C@r`-4?1$6=H=%z)C5Ler4Ml(^SH1_9X5*)cqDZN z9T1%(K_Nz*!9*z2p;3&8QtLzyj5R=dL7z;0ainOn$RUn9;toch0zTyrPx9yyO*}Ry z^OixPV>&;ekGTUU0{5e^8EFoH;Fj%#9TLrHDa;VxJb`-aeeb>PZEGLjzJ=rU{YP!_ z{_QQm47z~@-UEAZpg6Ll6k^0ukcAjX3J^O45|Pw=Isyq2r2i|MVVe>lbBCeBJ|db1 z4m!uOLKF!FAcTzHfn1B}n05tw-^tzg-M0_j+^*mJ*6jMOZ&!c6L1Qoj4fJqF_Xw=+ z;h0Op#gO6fruC;$LhAyRIF0TI>!D&PV^NT={S06SFz0(6K#EP*Rh1sqTeDi`z{fFi99LZHYM ze1|Y7KZHB@Mu>q6_&e|ly1RTry90K_0$>ocSP3@|97BnKlv~I@$dfOzw^6) z@3Neh)9VW{o?c$0*1E3CX#v3Hd@i*hAydL?h5=_r%w!-|S_@@L6qAaRaUMQqVKl1K zm_=Pum!Gs@k^M8}@Gc;?Wxcg5K24RFdTYHmAT(_$u1{)4w;pZ|&QgHLtd+Xj2)vC5 zU=bol=8=gw+=3+lgqT=zL4Bx#iHPZdX*oSL!|(*Q*xGKc+27vV{&D+u-*4~Nud#2i zH@I)L`w~(^E|=G(o?n0V%jNu1*VA%-;aUNpF6#{D#h+xDAA#{m2Ua>q_7lOQX-ysN zTLei)9HaJ@YhhE(KJk&;5>B(7#(CZnQd&g-L8N4e)Iqh(_X+^&Ko!4`hOA~Bk+Z3! zu|PiR;XKTb?Q0&iBY$zI5NT?Aj*Y^TlzRT%IY4}@n%o`#$3Om~>&N?cyMpP0+_rmfEz2WUJ-@v6-fF2?bz!CgB4$e*gA;>wS&qf` zO!Ot~iU^)(=WLtDb^^xtd16-Mh=+!sgb$09Yz2?MJL=c*-BNBm$~Ensr5ss4c_|D= z+{jrRG`SeJSzf{3d(TJLzTMruw|0B~w%>1WUw{1g`1bAFZ$Iwu&6`{nSV=BRU6)eF z+goHsz#2qq6#*>P<&x9ZecRUMv@S27zx#P6E+m0Ajkw3`#Akf*h<`96;*=|l$Lgf% zLLBkXa0sgNJs+RoF?wOiGbP74svQ7{$D(b7i$+GFRzPqwH|uSG`|&s1?W5F+#0c^7 z`3I?0O3Erclbnwo_^cLS{%CizFa-d;-)y_P8HVfi-S*AzAHHt@;oGLycj$X)1GCug z;qIoG;BeESpvWxHfNo&G{G2&L>;?nbnKOB?UnJUv6Yb0d+95Eb!K z=~VGl>bfwINM0CVj;`iiNfq`Uy`dVK$KJ8+xHmB`tyc)K;IUr<0(6q8&CXYeC}IiO z0x!%ZoXUx%a9xI}^>mTf?~obmD(4HYXJ1eE_iwVSr_<^D%U^L_cs=bz|LlMMWBjB4 z@(=#A|NP~5e&v%w$;_$G9V<{AkCW8v9zeo)lC1mKtQr6&S0jh4)7COf{M!4Fa@ibq zGCvPW9|*2-aeBh)#-r|uk#zLWxi4krEFH{Dd)MCf{a#A7-jOJizfv7kN{y^Ah!f_Q zzx8Nq0XX}LvF3^auX0N^PHHdHcud6EuFTgQ^LaniUsHJIxs7$R*^^N3dv-E;_^?R! z@TMKdyWQO#3Eab7vo*SJ*9b80=GNO?wQcQc*4}Sl2=TtZxw}PE-Fh_Ou)aIGMGtUf z1hn3j7#)lRR6RJD2?-6%BTxor)HCOP+&KAh6HI+Y^gtuu9J8d{06S4gDQh9YA|%46 zg0)huRB9wDyt}Gx<5Xc2r z07OWwMAgApJTnRauyf(nJyyOHTq2;#LL@Ke?~5!nUh$Ofv`l9p)H?#{D><{#dE&K6AUhnQ1Lk?g#*ZUJQ7Cg zuMK^yskw!!_PvEi+gopHYWr=|W*^@^z~T0Gb<_9Xeq$+Z+uD7%*4@$iUHctraotrL z0zhnT9v%=ok^?fbt9KxS60#OPp{%7|3ZDgQ<%(Ds>*7o%0wi2dh#bfk-uJ!VuJG~B z*A1^5UpKh!*t++j@Ks~8aCZ$+RU!!>lr?lmmIy#%a6_u#5ma(XIydly>A-bEEbdLp z$sIy(9!N}Pfe7JgCFn?Nghw>MQ`nujs_lbY2cnic;XQl|q8W>00lx(>0yr)h3UmSh zj25^;IMUhOAzC2kL>)o{0I@*%1)ziP01a_PesSADKB3={&+d-2VC+aI00+!C*$7}j z2oyvCdk1|9+^bJoyD~SMh`cnSYCeqtAFr&RPc8H*tXW&yKa@>bUt-AI$d5b zpHAo3*H1tHTu)0~7Fp`DR;d*cwW-uXNCUFsHdFUtY^Rt+YS!NAZOk&Ulma7@j}B8> z)^5b~i0{ri-z@1ihlVtvP2o<2W(JtMAMhi{H%2OWW|XiBb zsSoc)YBM}i=8c4IE+X-8Aj4eP@^-r-f~n0IkdS4G2$7Q795-9fmmF%CT8=P|CLAA8 zMi>zfc^bm8XGS_~h+*dG<(fHy_?(*+Q=T@?y#tOAkVpt59trcm{^Ng=5;J!%b(tId zIEJjIn(4SSXy9~!A`ZmM1GY3xV!4GFW7ip`JY0fiWAV+5qrf`SZt0kzdU#f8`FP7l zD?h?xCzE@Q%mL0V((U%~@$ugG`{m_zS=Lgjlo}Bk-|D7F*!Pw`)$|qh-f|!`0IE!C zeP|bmkU0<{%pwSpn7i&Q)gk@&b*XCxXXV4PmW4??yp&?@Rq8O)B4wh$-kX~3+V1;y zyV)${q_O*VA;2RyLxZYcfWnO zwoujUJB3@@AtJOfMD&*MZoPgSoFtf)I;8&@LT~`9CWJDz=7wuJ;DI22rMPm0!xjhL@itk6akT7 zK|&M>CX&J;ECNi-!o;cN7(~`bJUKaH06VZES%3wYxmxS0X13=9R;_Qh*zSIPhmUvq zc!Q5C-gZ(e?hHUkbvZL4fd?S7AhCS^i(S=9X4BO0OCg|HcM9XW1sxQ?-}`)Gy5)Pdde74&9PLbTzI-?^N1dRA9tkjZSxuDrbMjT zpDXVig5K{pfY|SMB21)EZhcFsr_*I@yr-d&M3MzE*VLR-!ckQWZ($taisN}PqkTsJ zQm&dY%^iq4;K7BOHpFpEghyC-c=T{nRJN{wWSapH zfv{Ajy3|^DWf3WrOF9Y-R&0a0Ysvza~No$=B`5lR|iC9=;+SBX-JtA#pMu^iSQK{!|O<_Ls=;6Oka%(7s9UIFXnhZWD- z_bMmAIMwfJSqs+O@vh5@a3v(+dOE)pt`LwBks_6)(9Hfh6v+9w&x=0*5;H!iNRt*$ zgl2y1C`PT83eYT+N7*nmbvZ0_PkQ`8dWR>h3m)>h(E%N6*lCPEvOrTYae!fRNQCoW z$22NquJg(BH-E}pJ2PjQL`QR;KMEf#WtdrP52(+v{m2Ubz^34&>uEQ4H?`h0JoasG zTib8DskN=|w|n23cH2I-zTLyE?Y6()+;?pT(ZbQ%yW4g4F5O`#3XLFf7v+=ind`u6l3YT5Jx@Y;Qtqn1KT#15h|1NzekWjul*;mas;=gu7`AKqj#p-=ykNAE7zl{8A}!%}@CLL3m9QPTpxpp-h^YXM z5P^)*01Kjp?*I(o5FNm&eEYZl{U2U_dH#z(`^&%h-~M{R@5}N#WCqZ{-x#+5hp@7q z!i@zY1YkpSsfbi$k-AnQDC<(!ibUtn--~cPm(%MDGfOG!1Uq}2SHJj#w9 z!Oa68+$Q({52x<2={|PI2O^h;q>GQ4ooP>v49!h|>1fa*pwv2>`Jq;Wa2jEd1N56) z%z<8h>U&3}G*8KqYeRz|m=^#}Ei)10|N84cL8O#Q4}vHYr1ilxfmOL_oGlaNCin=v zO_|)ob2A73+2o8r8gM@xd}i|;ZThSkaX#h7vU{omvTfFVH#coN0JME)Nt~B!0-2c+ z_3Q29{r$_btmo5tSue~I2&FD&s_si&^N~ZuEG0#HB2uKzy*#NK0#vCp%8w3HH;{+t9oAJX{O$IkX1DwG+xu7DuG`n& z^tUhZ_9I=dMZKt=POs%KX-@T5ZeReAh*P`;k90DMrwwyn`eQ(DHWD?a2m3iRIO!s87OBxU zv$hkF?HibS+uhXn&9k#7F|fJEi|%D?|Lwnf`Mtjb$g-?$zt_`+m@}yfA}G&m;v+vG zXGt+*(g6pj>X?h-C_R0~YY$aYOu*^k0sg6ze~dh}x0GAkfJTglQ!`j@#-<&7$n;I6 z<0yR}o?xo&9=)F~FDNDH38gMYhUbMuvZ+qW0Hp~cFXM?|FtwDJ{tZs!kztMF7l%et;eMas%mxGpFG zOkAonA`^*lDTofK0w4%P4G#dg7(8?MgiwHy9NlbLE)Yn}Nv8($h%`~dsNf>KZ!3nL?br~k9}*} zwC%08et*Af>uWaz`&Jb#(UiIo zOqd`sWyJr;kUxPD5=fK^bx~@d)hY%^{i1-nbsrfS5$EB)_cmJ(2Fq-_XHP;mGV`8? zyKl4g_`csKx7WJ8s%@gx&;)M0aQ^U=&v^OtVLH#{{Bb^iAof(whG^kjO9h9~rYVTl zDx&T!rp%qkW&`;U4|D)7=z*-$!RCGS-mPH~2So#Nj-3++fs#h=T0sJ2LI;`vnwz z1{23jNKP57IVSXq=>k@rGJvBsPtOR3-rN;*2cChLBGbZ^h(NcXWVvksUsJmBg1ivV zTVj6yhYi}3=fbiLds@A|>^R&1lGayYVo5?<%FPuZyCe!oBGACD^FX!cQ z0itP{=kuJW836KBqV>&F0e~{kZpMjykQ(EhH?>Ici3p15iv3W|BQkTXb!=iqeExVoeaK~6<`0O-luKE{0_ndTY{e!bTuy45 zx(K}IMfKq8=p$m{Okory9h|@T3Oz@?C+}e3!BjQE$!L%!BYG-e1Wg(ftL&y$>xuwU z>xje)BeehkAOJ~3K~zqx1VqGbyT#PNL>N;plh&3}2`$)wI4}}%%DGHks|%2)$;`lv z2Grw#A_rQCnB#vq_~D<5-W4B$tiT<)`?TYm*@L-tlpNTlaH=7k3H6ys3x;BBI#Vuj zw&TD0-~Z--{zuU-(t=9bX$`8xDaMe|`iAmZMN}kYayG29t=5zi00a#+SOLKbR&5Sm zTQwI^;glVGubYEY;+!XSTjqtBi4#(u%9K(C5! z4j&6#iVAdpTIBblh!8z&*dKV}ag=IgD|d&aA4OhJ%b7^WfP_Xo{V~vunBOBjCh`+S zWpo0lCXW`lgHzpMl}LnW4oq|eFYB1>hJn6n{3%u1F7>uvw{5-N-r9E4^?m>LMZf+r z-`;02Heb%4N}kUje&Ff6eE1&B#J zz?r(X6hPe>M{~*~T|?~M#b?GN(5NSELeD4c21@`Pi&;9XYycWO4FFR!1aVLVPpng+ z^Xzk>Im0wTN;nnF$teMo6H#K18j+bfrId(}nUR>eBnodK5qd;PD1ZT~c?)Db+RRLt zx<`7f6pqF95E%lF=R~6t@8G%s=J60>jkF;*B-D{rbnuYOec&8{`eM`KZI9<5VAtA} z4VWXli#mwX9AMjS zNPN4#$?Nx0^6BHBgz&);LyO@ZcjwXdan4eA&V`5nV(fkihFIfRpZmy4NhyWuGQgF` zw(1v1TkGhwy6-9igxcyjnVmQ}I65#yD^w~U)P{5q)rDJw#TlDr;chOLL6}s z%+ZL(jyGJ*ZYG_$9w#RqN}}h|!7XjuZcXL& z^$*wU*S)>JZ(ry6%w?L&`Luk<`IK@7CXbCKA*ZCE$e1}Rnt?ik#C{l>x;a|2)`LS2 zbop@U4$J{`mINSm@*EtvyDn$~#7~RW+!rk405lqbq>~I3d#0@Y((3$N> zQX2x4F;caj;(I)*2!LP~(O^9d8v%iw5hI7vfH0XkhhD^tfHWlT3T6hX-qc0C?ogXm zVFTT(ZJW2v>Mm`w{i-IUEu17&7y&FH8o-otny?f|36Xi4DCLx=h$LW^I*-dvgmZd+Z~?RMMV-}ZICe*dyw zuYdS||INN{hHrezr;p3@;luL$aXMd~K7LN+L^+?&9|1WIH=<+?Rhl_Aspik-cgWAAH>ZML z-Phmzr?>JkfBX5j*WYdMUWosR8>&|D6=*_x0ZfGAu(-Z)SzK2x3)rg}Of#w>XHKb^ zN}5m8QaC5mJvr7nK-2oKbN^6V#+z?0thKn6rXvT!u-i8g6LV* zsE5O)Lm$+)0$o*Vv6eira1UAuJ2H)qUI01tBNWe6&@p@<bCB;t4e$S z{`30w`qSV4-FE%{etoUpaL)6`bGppsbSkIi^7MSU{4h`Fd48Iw(>y;RU?~elay8Nc z-s>4`gV`a{8-$V64V_RuMqfv79P3Dg{!AT~=|gYDM50o1v5|BrDsd1U*xg7o#=$j! zPDEnsnNp!cSLPJit1SR{;L*RRYLglxiX-uKv{&FBBwgup)Bx%97(P=KX*+_o)-XEw zwj(92d*%XWBF$A@4b%t|sHQX#l8co2gh(kDO8EeEbIy?%Z~(nhnZl?{d`Ch=>dbML z#w&KLBxB)^fB8TB_oljSYtKt;dk{I)l{q3Y6)C9+AQ6+g5~swenUrZsDbLeMO;gG# zWkgJ=MB*zE<&=A-7GPurA7HJ+%rmYW13)H3(DdOWU!4*4_a_+$N;pRp#>uC&x82`ry^cNvwQ&H8m z?hc?PO?7X3ZTs7LeZRf8?I!Dcd;99wcX)lFw^!WiiF3(ixqO;VPv>9#+4=LY%W~%V zG@Z`GDa2uAUSgsl(n6hhpwGjzcyJ3sMjp%FR`;Vu>1eTo{Gk>VziHQCBcPd#ow;di z5$EfwgxGFZ0Jrt#(oDqmwOwC*y~bMD+wR*6b&D#%_7(cPpP*F_5r#^**}_>rEXTtx zI#P(t9q+2+u2O)m+;HrCETAFYwT|@Y1PqJh3Iqoi2W5uLR6VNwoZsBnVoL;ni2HtPw9(!o;kO&H+f$XgBv6RG~ zX4Ap41FseNh~YYR1G3)J_EZ!Bx``C5r<`4pLUR{q-T-$U77HJADCmj6_wwAkI5E!6 z9;h}X+IOkN(Hi@mvE4huPQ2{kDY&BuMo5x(8zV)k#DR+KV3Ra)S~wNtOq_wUni?Q+ zuF_&_aobll+qc`gUibBVzrEM%TYdT2u2+0}<*Lj4^z`A!`TXJX`B!B*Q%ao6bh-dw zo)=`!WdZ~P6lu4&m%3hqj$TU6)2S?{SPMtucNhoa;C{s24_kQ8TZ)=E=FLoe%&kY? z3t?NpA+l+`ye~I@Q=U^_kR&^t57Ic7OS@k3@6F*!JmaBr@Z_R4c9sTy+R`Jcv zKA;saVwxRh0|I1sGkS6?U^mYnUAOpjZsXL@wR8q1$ahgs@LPX2+ z=W;>>oK9yVn9e5v&t+bo7EUFX42We4-(1JVrJTBjYWT=Zm2(RHf}0|TZ6V(7P_gP5 zNWmB|O+>-U?R@Edy|sO7`@UV@_U(3i`TqLz?{Dv~KY#gM(~3(ePs{W) zmGh}wo|n_KoPRtmr_*v-<_{?q;#~6VjydH}$N?Zzv>0KmfI#TE8z2xRGo=Rsi$Cm~ zB45N*MTHL5%#Sew zUX_t;9;q}Ign9S9ZHZEq7V6H{Duk0CsMZbLO=aJ1L=38?65?P+CM1`pV(4D-tW8TU zM9D;nxJ)xKFc#IX`~+V+%Td zK{ym~IEvPhp8^qYHC{CbfOX$r-d_LB-~BsXzkd3V|NPJYvuV1NJVjC1;|IJ;i(^|G zsb4MVqu{aK1^4*3$BreE!AdHfdl5G4p1!3Ky%kq{Y=)z&Kcvx7+{6`?IE6|c5Y?2s zP7X(&%ge&a(bRWe9EV% zRHkw|BN6A^cfTX%>M&y;z)3SbLa~qVSLk$dOF4HqR4o2fZ*MQ!TH9_SVk)|?04TS2 zS#QhphfKt7C}M9f?b}Zv&DVEtJBWar*4+X^hUnVD^c!tK*V6WgDH8)`b`=Y@Gk};G zxVl#dWAc)GDsY;-WS_E?iE@ULC?zPx5h0h%giMJOr{KY+h^2#QS;LeL9;0oJE~RhM z0g-&5)Q<8h;e9i4kgX!ekUE;h{Yd-l+G^}5%^^b7zG#gdbAL>S3A=;Fewh`0SJVyL zvcg!WhCRvp+8e7XqKg@0N2C%)E_a;o=13TQDRf!q_~Ap@c2AY=LCo%iU_b^?fxucq zkDkdP;*J1p;L?B)I-=~DnnhsPIDb8SO==qH2fb~G=_yi3jhTzO5+wvcO4vh13m_-X z$O({`Gp9nF!^%OFqDxRSb0g%YZG3ucZ*6b;>-AMtTHRmYzV7Q))@yx#ZExS@?TxlA zDV8*S`teUsAAiiJ^S*5)%X_Y%q zA(cGp#$$Tp-3B?77d8l)Lmo&T6itz#G=@M@4VfjH87DUIZC&4Aeh%Csr9^;bn!zFE z62aMcaPzqOL{!9N*R5$Y7d5F?0iaoHx*G&5uXXp$ zV8jDNOsS!hcpxhy84J|V}*Dc-x-{&qPKM1+x}A48%D?Ccmj&M2)v zL{aV_QanHIhM~vtaesCVKwlRdGJ21%K!hMNLl0aX_J8mZSQWMpa#xBoGT8FOk#jz5 z9TfFZ&m#r!Xw3Syn?rccJrk&JZwSx<7?3H&B05oK$ca+MRCo%hjhZEo;SQQ8I<&T% zXhv-N)@n1Ax?cD7s{3ZGCcr7pr|HZ<%jp9;%%{t=TvE={>4Gs3${CSTnK-4C^LUBy zK8W+XJNNF(80=*jkta5+SO-ae0C>XxHB3zG^ZEgGh_WkGWcK!B;-@d*45eg~p=`^QhTArTMRL)NyPfwr9 ze4eIdp3W&vQ(gcurGm&r#9RnlUs|Wfdc^Wp_J)3U@!y%L^a_aX_z8+myx44HkaPW zk8OuLnF}D%24(awf>&3U&jC*P0r;h7o3)o=Q&RWiBHd$oD-K)rm5dh?xkcTCW3fUq(^Ea5~ZBSd=MZw z@V(vYk)NS+Mx@s2opJ}RB2xE#y{X9h{(61+_V(rX-~aI2_t$T=?dyJ(l=Abke3<8_ z({y=0J%2i#pQq`3xqM#c(^O8S%!pjl6ibRDR~!MmTG%zj(gTOl`J@l*(>_nLhjd>Y zkx1i!wy|Y}`{4k0A^_ZzCVJ#jOq?T4O|&82t(sBmdOY&6j5R$L5;}MUaj-KJA&ObZ zKQpJ4ruY$}*$%y}jXB`4ejoIkgE-KMo<~pD)hpg>IWQi(WmFbh{8>tP;cMT3f}4d-$8b{cAH%oYaho z66d~Y1XQ=^-@3j$^syZ?YatAdyx9@q(;=cbb`G%z78P*FaXEEf1&WG+Sq#gfF{9u7EXQFWD zX3h`EejbyL_(gglb)V>%btK04IQJNC?itGusbV>$!}{|;D(i1#G}Vt20XSIUgK6o} z9*pnT-|vV;4}2%@HuJ?~@_o#LT^FzW<$|dYeJs2s3 z{s=0q151wh-}l%0`dzoH+^+5V)^6|DpZ{2;ZM7=+?q;Bns3xM*vOGQKc~RSyk#m8R ziIH+9PRwM8ArvM`F=_|ooH!a;zsu1i%u?9Zjx^+INA%1w^nl1tJoLIDlco;7A_wOq zTd1d$^+S=MZ)0(+m!m(i4rlJZUmMl-!G9j6lwO$;m?QOvF;3W+p9~uG@tBM-mFWHB z!aj-vz_B;JQ$`=9f_OxCM8k{^vBmKi!N~naJH~hb)(7)GUIYikgk*=n9)XAW9NY|! zoXy_&b2N4_VEjdvJRMTgp#mKWVPqP1Y9tb-S+nD;0vNlzA>WP4kk< zyeyab^h`w4d?MmfPMmVsbrUgBV#3Usxdf~zPPP#RL5Lh)OFQIA51}+eA9+7cM+Y)^ zWA2Ew)orBM#A|L+?L^~#-3}ko@e(SYMPzC}9Ix^LeqDVpHF zZ(n||rtjarD3~QMcF&gPG@Z|RndkFmUM|z}Jk94)mZ>cBbSC1I3K1tR7&=ZOAAx0I zu0@2X1w)#VIE%KU40-T#bvUBBfx{m7PjfTfT6MRr*0sv(`nIlb6B(h;sgy!a)3usd z6?5?F;|d?yfdYMLVc_xX2plj>2S6Xmh&Zkn?T+ARc82~TA?p}##d4KmCK}O1jzq`a zm=Hqi*S|Gk42hWr#j|e;?>vt9Pyqp|8}-r&)gg_&qM86A0Ry7Dgzgj(t>Y{h++#TA zz+J9HIm9u5?B=oVMqqV7s6@p*uFT?o14xLBsNl&UI{^9ySPE;c7&ac>^r&>J~A0@oMkX-D(XfA&7f3-5uyMK_>hD z^{4gQZ>Oi* ze*gZ{AJ*?*uHV0}@894d0EaMpP!zepMIRm`LsNxl%uT3 zX~G8v$K7H;6!-q4ZxJKVYy1a89U{FC>cx&Xsb-c^5ve11MaLcs9uo}*$(|mq8DTn5 zm8dR{d>|1?oYa(uTI67hjF5%iImcFPIFhVMRkc>@_4eW}rA+73^E53-8%0F%6ZY2u z(7}gt){oXBUW9P`9=Gtgouif>2;@+x4c^$1hn%{lqR0Gp57!W@k4yEA^b5X&4Wg|F z2=o3m`?(kEkb&YyRef~4#ypw&FFUU1P;vi@|MOpnYLgnLcx`*hQ@jyJ_umuq`mf~f z#zO@cVYX^kQiid$cQZ9DxrA^xbaCK7M`inpr`g>OI8^)}U(0GH|X^y8nNKL2`p`nY`feERfbnitL`43hzEzrDVMEzrTuq_xcv zRBO{JCTb0skaL6z5;GGaVahYO0+_m|2#7|U%Ivy_^e^Po0PZS%FgR=y*muz$(nRWN z#lAQ*J1Cs}17_X5fJZlY#OfXqL0z6O{JrB4h&2vj-(X_g-Y~H0agv$;BEmPwz(;I_ zKY-WJ$He(Q&pvh*NS&|Kqmq0mg#CELhwiek$cCQQ-41AUoHyPz%H4>2kINcsi>@gM zd?NHE(vW^ys4acSlJBmr*z*7|f&;{cHojC0b(se22hkh|4rX=q|FKMZuK$%alEF~G&-NO2IQG{?`#X=2JjR-r+0P}7tOM+8e3eL%QXBMJO?(&OPe zq{UrSa!G@ZG3dGCvM>D6CRGO-8*Z1{y-M9CBzcrIIPn40T zl%{N8WuEdhr7|ze^KyC)>GCw6N}dxZ=5)juIzTtD5W^8(Pie&Z^_^#UL%4@1VCaN@ z@yHB&YnV{t5$;z*C=VbA2uBnr1fD&`<<}nw8>=)_(>^)wdJ+>8HK|e^Y;CK#s+(9< z69;IvBU1C~pv{C39n9jJx*JMIrlS(FBLH?HcN`X!!f6)f-B@R-I8y{z^bb6R2-(aL z`})}3zzO^OhX5(yZjlvd!Ei?~xA1qyncy1wARAkSzR4WV7xmyC3*rP|+~wWnI7Y+$ z=sUClWbRzhb_;iLSB-~e1gv`${CKutNKyb147#(D0L{9IGa&_I-T-2^7FSyWLds+q z=ADEoA!jNSq$ErRUa*ujWy%0fK#4NP<%k^KJnRkU7}7<2r7MjSI4Q%D!D&tM9L0t+n;-n~R_tw#LqxOUW}+x_tN{m#NGrN;#Lw z-8pA-Q_=nQzOT3I>&xx!<#v7Bw|8}UefxqlrzMs1dAgkE^JO_dr!t>U&t-l-O-m_D z&J%MU8p5cvdmS9{T7;B1$3aH@7&)j4-7{jQJ^8Y4v%7+9w7bv~%RH;p$mRe5AOJ~3 zK~%Kgy@yAj)~P6JZmK4#Vk#<4v`Q0gYu#h^xVN^p?WfnT+uPS)oi1~-JS|2jn42j; zQL6y)!2<=oS{xVGfWRTp5x57gu@@y|06nxV zqxj|+$q=Bqjp;4BI}@oJV~n2$ZwqjYhC{`~?iG+Bgc=b64PY*a88I~NJs%j5$9Nh8 zKNe%T@JA4205LFxY(U`6!5P^g6S%n`766TczcZyf2-*x1Q3h9sj3BY=0)R0i5Ql=8 zfr?uNK%(Mc?o1%w6VHNBqhov>wwM~{6=E?nA^}$fHoty2r6u$G+t=&Y-?zP{%WwGf z5h$QyN8usniYSDPsEoi|630odMC{=_~3<%2Em)`Ih8S5>y1pbb3-=E zViGg2oN@@HyMXt>+Ht^-RCTNS&)>iN-T(8iKV8m$^7$Ve9i)`ePwg+w|mrLnU;`uzjV5HJ4WD>))3q9ar7yxyp$L>t~? zmhl0D0zptRGY-)WLb#eEha&F5!?lNh{jdJ|K$}Eq5= z_Ikbk^ySO#_4RkZ{lEA1r_&Ss$A9r({Mm2*=`x)GspJx`-IPnzj%o@A{}wP7(}_6J z$eFn(1rKa;%%L4zREHoLxa$h~S$`;j`+f%cIB8YCn*bjB=P2glt&I=dfvFtz^wD5b{}vJP+zn;$7@`d$ z3)&Ag#lSnow~o<{1AKq^_V54wzyA2)`O~M*=hJ1HPNCY5cF_ix%gn^oOsj~yi8eJ8 zZCl-M+x2>Tf4jbI`|46bg&pK})!VJUyve@5zx=GNHZwr1ZaAe6zxnagZ+`Xh^XCsw zAE)_zT0StPxtvQ{ro0ex5VnUOT5H|G6u?DVlZ^mXo5}`2Dm#KV6>t>^%9?iSu1c8m zyXrb>$9O^ADRPF)IXYHA?8Wq5$3EP@qvi#_KcjbQQ?C}iTTc%_6JRf134h@x>*4}~ z?gIxC9IUmz_QF1HIc9Ii{x+Pt45%^N9zPC&X;16u{A~wADq^oVTH&rr%q{KD0`$9cTkTT24gU`$uZO!FA+UB9R|-Su-AQ1 z0C(L!4z!SK*Kvo60l*cB%?*(ctzEovj)*`23dnJ!P%1~{IT0h0IplJR=M;$nkT`S7 z2#B2HJc&4^aYx?ePx0_l+&x69lGD`HeZA@;bIU2moiu)tBiao{00YG(+5v$NXHJNC z%QNr$iClfySCd^zx`aleW#|;`y;1qyEWASI8D#z%je67A3mMV zr}Oz~UOr6al=H~}=W@zv2EZww5Q!+#&H)kQNIXuAql_L6m8YJ~KqJ2t58y3~oTaKes5f7$~jZ%RZ}b^|w; zU3Re=l=)`jpz2D9;?2P&q*H|EGO*?@<_R!LT#KAdiumipd+rG2y}xyIw5U4AC<>Wk zJ=ACV2~3EXyNV|qrZ|{5)3`V_==gA!i0&Gfgg~U%v zG09^Djn#@tLj;5Pz=qZY{V2i?Udy57>*>3WgFmBy9Quk77jy|e5P&l#2M9E_yD_DN z41fqJ5ob&Yz{nHLh?FU1E`(GtBU0f)&|@J9nIAk3HlS`@=F#aG%7IAj7x7&^*k!op z5zX0WE+dly0Agw!Ym;!Xx`SvF)v8TY*0$Z+eyz84U$=c-xApq^?fuIi-hcnw{p+_m zFH0%&bYd<}Y2Q~d&F6CZG(CTOD$6v@^E5q`a+#;6DW7thkhq^D5B8<*sq0e1G}w5( zHaND1)kjffSb)Urc=Vd6hH;=Nq3H-d zscm;aGYbL>B2}rPBD$+KrZEY{h?0`7Kz13n#os#Vm-$T@f9CN1;d#@rBt< zywe_C4d|GELjvCc#*7G3NS%i_oy-lv0P~$+-6uGy54wheDSByiL7KgXW5oL_2nA6c z64TUmW*#}?i@AcU12d9($H=1#V?#os5<@L_r(jlMGQT!4G|p5pT^z{05}{)jSsCo@ z303{aUScgbmrc8M^R1db(< zl~jllB9xSwQeuw3#@X9c08QQ1W!JXWy-D5n>)Y%1>-Vp(|M1)EA6}^LWX1+eiE<{I z^Guh|m!}`j<$NYKD$l8$%arH&>2mp;Qz~UHd5MWwO2t%TImyJw^!B*s?g>5*WPx91 zjs0Tpbxd9RgWMY`hkIGxH+E(kCroEwxkaWujKB`>O`|_(H)6pXep08=>&6*KouQ>; z_vky>=6`F+BAoklmwGq2^hK{!AQAgc(<_PW(2IkJd?@~&9#6@Y8mtA~k zM?c?VCi}{^LwqCmJ8XdCYsMh{u4N8o0sb%l@~`d;iI_T%lvBd)KMgVP;j@itYkYtX zq~VdtE+W_UDq6u65au#3(}Kh$O*Xdehxj;i>e|QvBe~s`;zQZnbN~8S&W|NS?Db+l zj)-cKxrhl5lqeujU`qyhpO7EtG$9={w?rJR^-+8r7wu6X;`pv3Sm@YYJbd(j5g|KV zmN>#-AKK;vb`@j(A%_@3Yyb<-!(nvm;G(f^g2O=FYt1`wdw}4o)R?BIskzs>|MP$T zKmOBy@}FETPXg!;sv@nmx`9LC%#_HXn~^oh!AARU|&gVfFS$s zW_4So?rq=ddfV6A?e*LH*FSFWFE8J|?5?XdK~Lvt{`@q5d@h$cElVkRo=@|9p31UJ z&zUE}M1V{g0o{~v9>O|!cpXQt%fDbMjz6X%eCa>^0q&7BpQ`c5Y0VAl=F2AX%?gLMHC z#=ck7s#a4qa4~iBrnR;T?jo}9w{3fCb#M8-ec2LFa!Npj3&)?xIhAQTrId-7m~)xS zyu?Av2akFTbSaIx!+NIh=A}7J>zcd2m;WyM>Xm zNe%1GF*k~NygiQj&B%FAf$f!KN)f@O!&CxItLBNx_=!;O=NZuCD69 z=;AuG*VYq^3DA3)u4YWA7Q3s#UI55IH@_l$RCgNOhwDA)=sq6?b0Umum~AX``$+`` zfCS`VfM7^W?oI>ntoO0{pgSDq7jSgYo|yz4SnHq2B&_!i|#n;b#QQyUYG*{gfPSaJ&^&pIwUYc6pQ&F4yzEO3ljm61G?dG=ul^# zkDM5B=jdX_Y?u3O+kHg`L^uBGAcVUc+I^b%+1Wnb5GkCBY)5w5{%Y7!; z9cH8nFaanbIc7IOVuyqPZaeYh!Sd)wk2xfw1hxWlYz53Vz(g4OM<*0F8@vq!Ysdxk ziafb)0L_6SnJKd{l6eN-P4=8{o=)m&R!cfNRKi5)qRvDBo;j&oW=<)erX?^629{GM zqOI*!>)N*4cD>bYzrM=8+q&8NtA4-w%bUH|Bs0~vOg|8&>GGTBfAsY9>(A$p&&$)f z%oCTA66a}pFqayqWUAo6iAAIv@ytY8YrCqLDKgc%I{0mUSF7fZU^Jt&xtPmLmkGKZYvNc?F3naUQ)7nrr6lZp?X1vAM5rMr3*q`WT!) z=0v^g>}30p6EpE}^TjSY-i5pN)(@ugSSBB!A~hl6%=uAH6}oHD#>Dtn|Hoe)jUPTl z`GylF@Xp6*-^>yxv7qgRQKzS7CGPXu2rQCg-5jRQeSLlZd8@bQ^W`+3&!>-5o?#fk zjt=l)hyq7vc+3MvuN!lAcIf@{!xX149Ae{okL&1jxUP&pF3TSOb*QQPP7EKHt5JB+ z!?t~pIXw!No)m+3pZ5S<*)hEsg6BKOrJHge=1B+BJmykjs%m@Nn^ZF~Q`Odg5i^k{ zttQIgo+-H+6BA&fG);3f@PW|elp+g@6946Y_uu}r|N6iA@Zsa>@v-Zaq=jJwmipRHel{TSMv@DZ7coA*Z#bGMNezOWoZxEQr?H{{H&jnKV#vKJ;*DQ{jvOAT#`UJ>545cHFr#bAlV(Zox20zz|qVnDuxMFFLgS-j}+4B4%(1 z;e2F_dqcv%5%aM&hs$Qa*KHEFyDuK<3OTI}j5iC+4hX zDP>1YoW%`*G=e+(1~_~fblY!D>Q>i%Tlf9CtykGrXcg58P^L0*A%l53f%^IRhw#QT zbK*pt^0cIBN@a>{Bxy~hq3Lq@FrP1XwnUFb?;*I4ijRlqv=7=$b&MvUD=C>Ha_op& z@ovbE9D8*DX;r(#L$=#>y}qjIx?cCXwzl22SB0h|kQ@?H$$840r!p;1r%xZBeiI{0 z<_QrpPl*c=G3Bt0gh%EO@2D9Onma=1r9q?txSB;D+mGuZs6JfQ%zW(F4^dgX$(eK0 zYHraCi#D-Vr6RhTnd>e)AvLQAsG#Z^x0QN3xNM5Y9zJNCY^H&cI-qfpZVfINrFle) zV%L6-w+uqzozr-#$WV|CmobYuBo8Ukc|7^`;3A7Y#@ z(T)L*YWijKLDj9Jv-mVQm;*P|%)Aqz>Kvw71Ox zZ*{%Z^>uwSt$V%IeOvcU>t;8VKFv1mGd%PPUlbO%QFJar&FF5=0wD0UQ$Xl zex?a^qUCj5}_6p1jyQ2U(<(U)D%lUNrlsKK1=alA@a>;W}nK@^k0Guc>rEoLG zeGy|qQBgH@Ya&f_Z!HL_O>CDnQE4J#BHCg~y#RD%?LqeGH*7JKk}5J*j% zSR+hFWgLTCCITez$kmHD0vtF!j6rb7iz1#3iQJKp0fU8MV~`tzF!ta@TkEMvKmg4n zDJizcnBw0$=KhGsg9`;avEm3QS-^IrtY$QpepG!Wv5QYf{+B4F!X{O zV0I_;6)*uLM?z=-1(3ljfFotMoq0B^5I$`zh+w*c6Zq8}ktTF=Q1Ax8fC}EIs{(8I zyxbjdMtCRa7(4*T`2ui5A|`Z@oaPujE2z0use9d3WnWiods{bO-}U7cugzax@CGCk zi;>TH{*b0id49fp{&f0Srpw3W>5__{o}S7)rJUo$$|@W)WE_y`Gu zE2(;f`5g)-cd#DYqcEWS;2z*fRcW=l+rHk2leAq_h`F`R(QDnrcG1?fsa3IN-o$nw zc682DDKk=@=L>UYrm36~mz+vWwdS&@N_dMpgq$;Brz-$<3ssQ$Vx-a4B0YEJP>qEr z)ZA^YtE!sWw%^{a@AbB3qSG`#J%1!l=E|uYJDa#E_O^>O1Y<%#nx}J>8e6oIODR*y zb2O+&G!G2f^Kk8Ir}F|LMIJ;RaqIzsN>P{{ER`O`>|LT8Ax~XREPEI3W8^PrN+p)z zOneA6k9MpVz^XB*%_%o&5urDzV_if$1eJz0UmX}_jI{K zHLV)mtRMQl&RDmY>xihRX=}Afb5l3n>)z^$KqX}`&s-4D&8C!@GnnU;nV60^9A*d4 zxkQv`*Zt#>K8wfrJ3cWsI{9D9_w5M&{@vgG+rRy*|LITu;-7u`{JBg^E>oEoM2r}5 z7}ymz1BX6C52#<<6{7^Fg>hCeVOS7#PL}1z(+;bhKOM5#L%;YddQCt zAk@$y)4l17M3o1KP9KOw5jrGoQGdq^7`gu@p1ih_nIBOH1rv#jo&@yS#jC- zPW~9`578#t9Shj~5e_t3dlBWFUn?kCKI)5;+7BP1PJ! zn@BTNYt8DW`^M6s3RUGM#V{kEFF(vrA9I<@>5`{YUQR?jEvGck94QDyoHKa8q&tt` zo*pz#veDHZn|-`f83t4Xz;wjb40vq3x#8Xk?NbZ|1_rZd9*QUqyQrx~=*uYk%!eQ&Z|x3|5%MW4(WF_AHI zVoGV6PV@Yc&8wLclSozXGh;JR6YHZSw7v=kA`DWF_LQ0^^gZDAFxuE~ z77=tdmG&7^2tEeWi;e{AfWCL9%aEjXpL~dG91R_!n;lq-vD-ujFi>}b`?fZ)aXl8Y z8_kc9pg1+UNZO8?3BZk_`=+jpVkT4X>%0DsW87;4%-$20$h!n1hEvp6;oZxCK!|K6 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

Qk~Xa9;hX0yT<*cxP^cJJCjAQpzIh^ZN4n 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/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 literal 0 HcmV?d00001 diff --git a/Docs/Usage/index.md b/Docs/Usage/index.md index b333e2bb2f..e3a838d8c9 100644 --- a/Docs/Usage/index.md +++ b/Docs/Usage/index.md @@ -10,4 +10,5 @@ - [Scene Formats](./Scene-Formats.md) - [Scripting](./Scripting.md) - [Render Passes](./Render-Passes.md) -- [Path Tracer](./Path-Tracer.md) \ No newline at end of file +- [Path Tracer](./Path-Tracer.md) +- [Custom Primitives](./Custom-Primitives.md) \ No newline at end of file diff --git a/Falcor.sln b/Falcor.sln index 2ec2c30ef3..f81ca5e57f 100644 --- a/Falcor.sln +++ b/Falcor.sln @@ -84,6 +84,8 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "MegakernelPathTracer", "Sou EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "WhittedRayTracer", "Source\RenderPasses\WhittedRayTracer\WhittedRayTracer.vcxproj", "{431C3127-E613-424C-B964-FB53DAA87789}" EndProject +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "SceneDebugger", "Source\RenderPasses\SceneDebugger\SceneDebugger.vcxproj", "{B1715F7A-6EFD-4910-B271-7423AB6961CB}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution DebugD3D12|x64 = DebugD3D12|x64 @@ -214,6 +216,10 @@ Global {431C3127-E613-424C-B964-FB53DAA87789}.DebugD3D12|x64.Build.0 = Debug|x64 {431C3127-E613-424C-B964-FB53DAA87789}.ReleaseD3D12|x64.ActiveCfg = Release|x64 {431C3127-E613-424C-B964-FB53DAA87789}.ReleaseD3D12|x64.Build.0 = Release|x64 + {B1715F7A-6EFD-4910-B271-7423AB6961CB}.DebugD3D12|x64.ActiveCfg = Debug|x64 + {B1715F7A-6EFD-4910-B271-7423AB6961CB}.DebugD3D12|x64.Build.0 = Debug|x64 + {B1715F7A-6EFD-4910-B271-7423AB6961CB}.ReleaseD3D12|x64.ActiveCfg = Release|x64 + {B1715F7A-6EFD-4910-B271-7423AB6961CB}.ReleaseD3D12|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -249,6 +255,7 @@ Global {8F6B5FAB-30FA-45C6-B5EA-BCD1D26781C0} = {935D7586-B55D-431A-A0ED-338383DE1A1E} {873F13CA-A9C7-47BA-857D-8848C5E7F07E} = {D16038A7-B031-4181-B4A1-2C416C02330C} {431C3127-E613-424C-B964-FB53DAA87789} = {D16038A7-B031-4181-B4A1-2C416C02330C} + {B1715F7A-6EFD-4910-B271-7423AB6961CB} = {D16038A7-B031-4181-B4A1-2C416C02330C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {357B2AE0-FE30-4AC6-8D41-B580232BC0DE} diff --git a/LICENSE.md b/LICENSE.md index e8709a4736..29185689fd 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,16 +1,16 @@ Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of NVIDIA CORPORATION nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, -OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; -OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, +OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; +OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 422dbec72f..060b9e18c1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Falcor 4.2 +# Falcor 4.3 Falcor is a real-time rendering framework supporting DirectX 12. It aims to improve productivity of research and prototype projects. @@ -13,12 +13,12 @@ Features include: The included path tracer requires NVAPI. Please make sure you have it set up properly, otherwise the path tracer won't work. You can find the instructions below. ## Prerequisites -- Windows 10 version 1809 or newer +- Windows 10 version 2004 (May 2020 Update) or newer - Visual Studio 2019 -- [Microsoft Windows SDK version 1903 (10.0.18362.1)](https://developer.microsoft.com/en-us/windows/downloads/sdk-archive) +- [Windows 10 SDK (10.0.19041.0) for Windows 10, version 2004](https://developer.microsoft.com/en-us/windows/downloads/windows-10-sdk/) +- A GPU which supports DirectX Raytracing, such as the NVIDIA Titan V or GeForce RTX (make sure you have the latest driver) Optional: -- A GPU which supports DirectX Raytracing, such as the NVIDIA Titan V or GeForce RTX (make sure you have the latest driver) - Windows 10 Graphics Tools. To run DirectX 12 applications with the debug layer enabled, you must install this. There are two ways to install it: - Click the Windows button and type `Optional Features`, in the window that opens click `Add a feature` and select `Graphics Tools`. - Download an offline package from [here](https://docs.microsoft.com/en-us/windows-hardware/test/hlk/windows-hardware-lab-kit#supplemental-content-for-graphics-media-and-mean-time-between-failures-mtbf-tests). Choose a ZIP file that matches the OS version you are using (not the SDK version used for building Falcor). The ZIP includes a document which explains how to install the graphics tools. @@ -26,12 +26,15 @@ Optional: ## NVAPI installation After cloning the repository, head over to https://developer.nvidia.com/nvapi and download the latest version of NVAPI (this build is tested against version R440). -Extract the content of the zip file into `Source/Externals/.packman/` and rename `R440-developer` to `NVAPI`. +Extract the content of the zip file into `Source/Externals/.packman/` and rename `R440-developer` to `nvapi`. -Finally, set `_ENABLE_NVAPI` to `1` in `Source/Falcor/Core/FalcorConfig.h` +Finally, set `_ENABLE_NVAPI` to `true` in `Source/Falcor/Core/FalcorConfig.h` ## CUDA Support -Refer to the README located in the `Source/Samples/CudaInterop/` for instructions on how to set up your environment to use CUDA with Falcor. +If you want to use CUDA C/C++ code as part of a Falcor project, then refer to the README located in the `Source/Samples/CudaInterop/` for instructions on how to set up your environment to use CUDA with Falcor. + +If you want to execute Slang-based shader code through CUDA using `CUDAProgram`, then you will need to copy or link the root directory of the CUDA SDK under `Source/Externals/.packman/`, as a directory named `CUDA`. +Then, set `_ENABLE_CUDA` to `true` in `Source/Falcor/Core/FalcorConfig.h` ## Falcor Configuration `FalcorConfig.h` contains some flags which control Falcor's behavior. diff --git a/Source/Externals/args/args.h b/Source/Externals/args/args.h deleted file mode 100644 index f784538abd..0000000000 --- a/Source/Externals/args/args.h +++ /dev/null @@ -1,4283 +0,0 @@ -/* A simple header-only C++ argument parser library. - * - * https://github.com/Taywee/args - * - * Copyright (c) 2016-2019 Taylor C. Richberger and Pavel - * Belikov - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - */ - -/** \file args.hxx - * \brief this single-header lets you use all of the args functionality - * - * The important stuff is done inside the args namespace - */ - -#ifndef ARGS_HXX -#define ARGS_HXX - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef ARGS_TESTNAMESPACE -namespace argstest -{ -#else - -/** \namespace args - * \brief contains all the functionality of the args library - */ -namespace args -{ -#endif - /** Getter to grab the value from the argument type. - * - * If the Get() function of the type returns a reference, so does this, and - * the value will be modifiable. - */ - template - auto get(Option &option_) -> decltype(option_.Get()) - { - return option_.Get(); - } - - /** (INTERNAL) Count UTF-8 glyphs - * - * This is not reliable, and will fail for combinatory glyphs, but it's - * good enough here for now. - * - * \param string The string to count glyphs from - * \return The UTF-8 glyphs in the string - */ - inline std::string::size_type Glyphs(const std::string &string_) - { - std::string::size_type length = 0; - for (const char c: string_) - { - if ((c & 0xc0) != 0x80) - { - ++length; - } - } - return length; - } - - /** (INTERNAL) Wrap a vector of words into a vector of lines - * - * Empty words are skipped. Word "\n" forces wrapping. - * - * \param begin The begin iterator - * \param end The end iterator - * \param width The width of the body - * \param firstlinewidth the width of the first line, defaults to the width of the body - * \param firstlineindent the indent of the first line, defaults to 0 - * \return the vector of lines - */ - template - inline std::vector Wrap(It begin, - It end, - const std::string::size_type width, - std::string::size_type firstlinewidth = 0, - std::string::size_type firstlineindent = 0) - { - std::vector output; - std::string line(firstlineindent, ' '); - bool empty = true; - - if (firstlinewidth == 0) - { - firstlinewidth = width; - } - - auto currentwidth = firstlinewidth; - - for (auto it = begin; it != end; ++it) - { - if (it->empty()) - { - continue; - } - - if (*it == "\n") - { - if (!empty) - { - output.push_back(line); - line.clear(); - empty = true; - currentwidth = width; - } - - continue; - } - - auto itemsize = Glyphs(*it); - if ((line.length() + 1 + itemsize) > currentwidth) - { - if (!empty) - { - output.push_back(line); - line.clear(); - empty = true; - currentwidth = width; - } - } - - if (itemsize > 0) - { - if (!empty) - { - line += ' '; - } - - line += *it; - empty = false; - } - } - - if (!empty) - { - output.push_back(line); - } - - return output; - } - - namespace detail - { - template - std::string Join(const T& array, const std::string &delimiter) - { - std::string res; - for (auto &element : array) - { - if (!res.empty()) - { - res += delimiter; - } - - res += element; - } - - return res; - } - } - - /** (INTERNAL) Wrap a string into a vector of lines - * - * This is quick and hacky, but works well enough. You can specify a - * different width for the first line - * - * \param width The width of the body - * \param firstlinewid the width of the first line, defaults to the width of the body - * \return the vector of lines - */ - inline std::vector Wrap(const std::string &in, const std::string::size_type width, std::string::size_type firstlinewidth = 0) - { - // Preserve existing line breaks - const auto newlineloc = in.find('\n'); - if (newlineloc != in.npos) - { - auto first = Wrap(std::string(in, 0, newlineloc), width); - auto second = Wrap(std::string(in, newlineloc + 1), width); - first.insert( - std::end(first), - std::make_move_iterator(std::begin(second)), - std::make_move_iterator(std::end(second))); - return first; - } - - std::istringstream stream(in); - std::string::size_type indent = 0; - - for (char c : in) - { - if (!isspace(c)) - { - break; - } - ++indent; - } - - return Wrap(std::istream_iterator(stream), std::istream_iterator(), - width, firstlinewidth, indent); - } - -#ifdef ARGS_NOEXCEPT - /// Error class, for when ARGS_NOEXCEPT is defined - enum class Error - { - None, - Usage, - Parse, - Validation, - Required, - Map, - Extra, - Help, - Subparser, - Completion, - }; -#else - /** Base error class - */ - class Error : public std::runtime_error - { - public: - Error(const std::string &problem) : std::runtime_error(problem) {} - virtual ~Error() {} - }; - - /** Errors that occur during usage - */ - class UsageError : public Error - { - public: - UsageError(const std::string &problem) : Error(problem) {} - virtual ~UsageError() {} - }; - - /** Errors that occur during regular parsing - */ - class ParseError : public Error - { - public: - ParseError(const std::string &problem) : Error(problem) {} - virtual ~ParseError() {} - }; - - /** Errors that are detected from group validation after parsing finishes - */ - class ValidationError : public Error - { - public: - ValidationError(const std::string &problem) : Error(problem) {} - virtual ~ValidationError() {} - }; - - /** Errors that when a required flag is omitted - */ - class RequiredError : public ValidationError - { - public: - RequiredError(const std::string &problem) : ValidationError(problem) {} - virtual ~RequiredError() {} - }; - - /** Errors in map lookups - */ - class MapError : public ParseError - { - public: - MapError(const std::string &problem) : ParseError(problem) {} - virtual ~MapError() {} - }; - - /** Error that occurs when a singular flag is specified multiple times - */ - class ExtraError : public ParseError - { - public: - ExtraError(const std::string &problem) : ParseError(problem) {} - virtual ~ExtraError() {} - }; - - /** An exception that indicates that the user has requested help - */ - class Help : public Error - { - public: - Help(const std::string &flag) : Error(flag) {} - virtual ~Help() {} - }; - - /** (INTERNAL) An exception that emulates coroutine-like control flow for subparsers. - */ - class SubparserError : public Error - { - public: - SubparserError() : Error("") {} - virtual ~SubparserError() {} - }; - - /** An exception that contains autocompletion reply - */ - class Completion : public Error - { - public: - Completion(const std::string &flag) : Error(flag) {} - virtual ~Completion() {} - }; -#endif - - /** A simple unified option type for unified initializer lists for the Matcher class. - */ - struct EitherFlag - { - const bool isShort; - const char shortFlag; - const std::string longFlag; - EitherFlag(const std::string &flag) : isShort(false), shortFlag(), longFlag(flag) {} - EitherFlag(const char *flag) : isShort(false), shortFlag(), longFlag(flag) {} - EitherFlag(const char flag) : isShort(true), shortFlag(flag), longFlag() {} - - /** Get just the long flags from an initializer list of EitherFlags - */ - static std::unordered_set GetLong(std::initializer_list flags) - { - std::unordered_set longFlags; - for (const EitherFlag &flag: flags) - { - if (!flag.isShort) - { - longFlags.insert(flag.longFlag); - } - } - return longFlags; - } - - /** Get just the short flags from an initializer list of EitherFlags - */ - static std::unordered_set GetShort(std::initializer_list flags) - { - std::unordered_set shortFlags; - for (const EitherFlag &flag: flags) - { - if (flag.isShort) - { - shortFlags.insert(flag.shortFlag); - } - } - return shortFlags; - } - - std::string str() const - { - return isShort ? std::string(1, shortFlag) : longFlag; - } - - std::string str(const std::string &shortPrefix, const std::string &longPrefix) const - { - return isShort ? shortPrefix + std::string(1, shortFlag) : longPrefix + longFlag; - } - }; - - - - /** A class of "matchers", specifying short and flags that can possibly be - * matched. - * - * This is supposed to be constructed and then passed in, not used directly - * from user code. - */ - class Matcher - { - private: - const std::unordered_set shortFlags; - const std::unordered_set longFlags; - - public: - /** Specify short and long flags separately as iterators - * - * ex: `args::Matcher(shortFlags.begin(), shortFlags.end(), longFlags.begin(), longFlags.end())` - */ - template - Matcher(ShortIt shortFlagsStart, ShortIt shortFlagsEnd, LongIt longFlagsStart, LongIt longFlagsEnd) : - shortFlags(shortFlagsStart, shortFlagsEnd), - longFlags(longFlagsStart, longFlagsEnd) - { - if (shortFlags.empty() && longFlags.empty()) - { -#ifndef ARGS_NOEXCEPT - throw UsageError("empty Matcher"); -#endif - } - } - -#ifdef ARGS_NOEXCEPT - /// Only for ARGS_NOEXCEPT - Error GetError() const noexcept - { - return shortFlags.empty() && longFlags.empty() ? Error::Usage : Error::None; - } -#endif - - /** Specify short and long flags separately as iterables - * - * ex: `args::Matcher(shortFlags, longFlags)` - */ - template - Matcher(Short &&shortIn, Long &&longIn) : - Matcher(std::begin(shortIn), std::end(shortIn), std::begin(longIn), std::end(longIn)) - {} - - /** Specify a mixed single initializer-list of both short and long flags - * - * This is the fancy one. It takes a single initializer list of - * any number of any mixed kinds of flags. Chars are - * automatically interpreted as short flags, and strings are - * automatically interpreted as long flags: - * - * args::Matcher{'a'} - * args::Matcher{"foo"} - * args::Matcher{'h', "help"} - * args::Matcher{"foo", 'f', 'F', "FoO"} - */ - Matcher(std::initializer_list in) : - Matcher(EitherFlag::GetShort(in), EitherFlag::GetLong(in)) {} - - Matcher(Matcher &&other) : shortFlags(std::move(other.shortFlags)), longFlags(std::move(other.longFlags)) - {} - - ~Matcher() {} - - /** (INTERNAL) Check if there is a match of a short flag - */ - bool Match(const char flag) const - { - return shortFlags.find(flag) != shortFlags.end(); - } - - /** (INTERNAL) Check if there is a match of a long flag - */ - bool Match(const std::string &flag) const - { - return longFlags.find(flag) != longFlags.end(); - } - - /** (INTERNAL) Check if there is a match of a flag - */ - bool Match(const EitherFlag &flag) const - { - return flag.isShort ? Match(flag.shortFlag) : Match(flag.longFlag); - } - - /** (INTERNAL) Get all flag strings as a vector, with the prefixes embedded - */ - std::vector GetFlagStrings() const - { - std::vector flagStrings; - flagStrings.reserve(shortFlags.size() + longFlags.size()); - for (const char flag: shortFlags) - { - flagStrings.emplace_back(flag); - } - for (const std::string &flag: longFlags) - { - flagStrings.emplace_back(flag); - } - return flagStrings; - } - - /** (INTERNAL) Get long flag if it exists or any short flag - */ - EitherFlag GetLongOrAny() const - { - if (!longFlags.empty()) - { - return *longFlags.begin(); - } - - if (!shortFlags.empty()) - { - return *shortFlags.begin(); - } - - // should be unreachable - return ' '; - } - - /** (INTERNAL) Get short flag if it exists or any long flag - */ - EitherFlag GetShortOrAny() const - { - if (!shortFlags.empty()) - { - return *shortFlags.begin(); - } - - if (!longFlags.empty()) - { - return *longFlags.begin(); - } - - // should be unreachable - return ' '; - } - }; - - /** Attributes for flags. - */ - enum class Options - { - /** Default options. - */ - None = 0x0, - - /** Flag can't be passed multiple times. - */ - Single = 0x01, - - /** Flag can't be omitted. - */ - Required = 0x02, - - /** Flag is excluded from usage line. - */ - HiddenFromUsage = 0x04, - - /** Flag is excluded from options help. - */ - HiddenFromDescription = 0x08, - - /** Flag is global and can be used in any subcommand. - */ - Global = 0x10, - - /** Flag stops a parser. - */ - KickOut = 0x20, - - /** Flag is excluded from auto completion. - */ - HiddenFromCompletion = 0x40, - - /** Flag is excluded from options help and usage line - */ - Hidden = HiddenFromUsage | HiddenFromDescription | HiddenFromCompletion, - }; - - inline Options operator | (Options lhs, Options rhs) - { - return static_cast(static_cast(lhs) | static_cast(rhs)); - } - - inline Options operator & (Options lhs, Options rhs) - { - return static_cast(static_cast(lhs) & static_cast(rhs)); - } - - class FlagBase; - class PositionalBase; - class Command; - class ArgumentParser; - - /** A simple structure of parameters for easy user-modifyable help menus - */ - struct HelpParams - { - /** The width of the help menu - */ - unsigned int width = 80; - /** The indent of the program line - */ - unsigned int progindent = 2; - /** The indent of the program trailing lines for long parameters - */ - unsigned int progtailindent = 4; - /** The indent of the description and epilogs - */ - unsigned int descriptionindent = 4; - /** The indent of the flags - */ - unsigned int flagindent = 6; - /** The indent of the flag descriptions - */ - unsigned int helpindent = 40; - /** The additional indent each group adds - */ - unsigned int eachgroupindent = 2; - - /** The minimum gutter between each flag and its help - */ - unsigned int gutter = 1; - - /** Show the terminator when both options and positional parameters are present - */ - bool showTerminator = true; - - /** Show the {OPTIONS} on the prog line when this is true - */ - bool showProglineOptions = true; - - /** Show the positionals on the prog line when this is true - */ - bool showProglinePositionals = true; - - /** The prefix for short flags - */ - std::string shortPrefix; - - /** The prefix for long flags - */ - std::string longPrefix; - - /** The separator for short flags - */ - std::string shortSeparator; - - /** The separator for long flags - */ - std::string longSeparator; - - /** The program name for help generation - */ - std::string programName; - - /** Show command's flags - */ - bool showCommandChildren = false; - - /** Show command's descriptions and epilog - */ - bool showCommandFullHelp = false; - - /** The postfix for progline when showProglineOptions is true and command has any flags - */ - std::string proglineOptions = "{OPTIONS}"; - - /** The prefix for progline when command has any subcommands - */ - std::string proglineCommand = "COMMAND"; - - /** The prefix for progline value - */ - std::string proglineValueOpen = " <"; - - /** The postfix for progline value - */ - std::string proglineValueClose = ">"; - - /** The prefix for progline required argument - */ - std::string proglineRequiredOpen = ""; - - /** The postfix for progline required argument - */ - std::string proglineRequiredClose = ""; - - /** The prefix for progline non-required argument - */ - std::string proglineNonrequiredOpen = "["; - - /** The postfix for progline non-required argument - */ - std::string proglineNonrequiredClose = "]"; - - /** Show flags in program line - */ - bool proglineShowFlags = false; - - /** Use short flags in program lines when possible - */ - bool proglinePreferShortFlags = false; - - /** Program line prefix - */ - std::string usageString; - - /** String shown in help before flags descriptions - */ - std::string optionsString = "OPTIONS:"; - - /** Display value name after all the long and short flags - */ - bool useValueNameOnce = false; - - /** Show value name - */ - bool showValueName = true; - - /** Add newline before flag description - */ - bool addNewlineBeforeDescription = false; - - /** The prefix for option value - */ - std::string valueOpen = "["; - - /** The postfix for option value - */ - std::string valueClose = "]"; - - /** Add choices to argument description - */ - bool addChoices = false; - - /** The prefix for choices - */ - std::string choiceString = "\nOne of: "; - - /** Add default values to argument description - */ - bool addDefault = false; - - /** The prefix for default values - */ - std::string defaultString = "\nDefault: "; - }; - - /** A number of arguments which can be consumed by an option. - * - * Represents a closed interval [min, max]. - */ - struct Nargs - { - const size_t min; - const size_t max; - - Nargs(size_t min_, size_t max_) : min{min_}, max{max_} - { -#ifndef ARGS_NOEXCEPT - if (max < min) - { - throw UsageError("Nargs: max > min"); - } -#endif - } - - Nargs(size_t num_) : min{num_}, max{num_} - { - } - - friend bool operator == (const Nargs &lhs, const Nargs &rhs) - { - return lhs.min == rhs.min && lhs.max == rhs.max; - } - - friend bool operator != (const Nargs &lhs, const Nargs &rhs) - { - return !(lhs == rhs); - } - }; - - /** Base class for all match types - */ - class Base - { - private: - Options options = {}; - - protected: - bool matched = false; - const std::string help; -#ifdef ARGS_NOEXCEPT - /// Only for ARGS_NOEXCEPT - mutable Error error = Error::None; - mutable std::string errorMsg; -#endif - - public: - Base(const std::string &help_, Options options_ = {}) : options(options_), help(help_) {} - virtual ~Base() {} - - Options GetOptions() const noexcept - { - return options; - } - - bool IsRequired() const noexcept - { - return (GetOptions() & Options::Required) != Options::None; - } - - virtual bool Matched() const noexcept - { - return matched; - } - - virtual void Validate(const std::string &, const std::string &) const - { - } - - operator bool() const noexcept - { - return Matched(); - } - - virtual std::vector> GetDescription(const HelpParams &, const unsigned indentLevel) const - { - std::tuple description; - std::get<1>(description) = help; - std::get<2>(description) = indentLevel; - return { std::move(description) }; - } - - virtual std::vector GetCommands() - { - return {}; - } - - virtual bool IsGroup() const - { - return false; - } - - virtual FlagBase *Match(const EitherFlag &) - { - return nullptr; - } - - virtual PositionalBase *GetNextPositional() - { - return nullptr; - } - - virtual std::vector GetAllFlags() - { - return {}; - } - - virtual bool HasFlag() const - { - return false; - } - - virtual bool HasPositional() const - { - return false; - } - - virtual bool HasCommand() const - { - return false; - } - - virtual std::vector GetProgramLine(const HelpParams &) const - { - return {}; - } - - /// Sets a kick-out value for building subparsers - void KickOut(bool kickout_) noexcept - { - if (kickout_) - { - options = options | Options::KickOut; - } - else - { - options = static_cast(static_cast(options) & ~static_cast(Options::KickOut)); - } - } - - /// Gets the kick-out value for building subparsers - bool KickOut() const noexcept - { - return (options & Options::KickOut) != Options::None; - } - - virtual void Reset() noexcept - { - matched = false; -#ifdef ARGS_NOEXCEPT - error = Error::None; - errorMsg.clear(); -#endif - } - -#ifdef ARGS_NOEXCEPT - /// Only for ARGS_NOEXCEPT - virtual Error GetError() const - { - return error; - } - - /// Only for ARGS_NOEXCEPT - std::string GetErrorMsg() const - { - return errorMsg; - } -#endif - }; - - /** Base class for all match types that have a name - */ - class NamedBase : public Base - { - protected: - const std::string name; - bool kickout = false; - std::string defaultString; - bool defaultStringManual = false; - std::vector choicesStrings; - bool choicesStringManual = false; - - virtual std::string GetDefaultString(const HelpParams&) const { return {}; } - - virtual std::vector GetChoicesStrings(const HelpParams&) const { return {}; } - - virtual std::string GetNameString(const HelpParams&) const { return Name(); } - - void AddDescriptionPostfix(std::string &dest, const bool isManual, const std::string &manual, bool isGenerated, const std::string &generated, const std::string &str) const - { - if (isManual && !manual.empty()) - { - dest += str; - dest += manual; - } - else if (!isManual && isGenerated && !generated.empty()) - { - dest += str; - dest += generated; - } - } - - public: - NamedBase(const std::string &name_, const std::string &help_, Options options_ = {}) : Base(help_, options_), name(name_) {} - virtual ~NamedBase() {} - - /** Sets default value string that will be added to argument description. - * Use empty string to disable it for this argument. - */ - void HelpDefault(const std::string &str) - { - defaultStringManual = true; - defaultString = str; - } - - /** Gets default value string that will be added to argument description. - */ - std::string HelpDefault(const HelpParams ¶ms) const - { - return defaultStringManual ? defaultString : GetDefaultString(params); - } - - /** Sets choices strings that will be added to argument description. - * Use empty vector to disable it for this argument. - */ - void HelpChoices(const std::vector &array) - { - choicesStringManual = true; - choicesStrings = array; - } - - /** Gets choices strings that will be added to argument description. - */ - std::vector HelpChoices(const HelpParams ¶ms) const - { - return choicesStringManual ? choicesStrings : GetChoicesStrings(params); - } - - virtual std::vector> GetDescription(const HelpParams ¶ms, const unsigned indentLevel) const override - { - std::tuple description; - std::get<0>(description) = GetNameString(params); - std::get<1>(description) = help; - std::get<2>(description) = indentLevel; - - AddDescriptionPostfix(std::get<1>(description), choicesStringManual, detail::Join(choicesStrings, ", "), params.addChoices, detail::Join(GetChoicesStrings(params), ", "), params.choiceString); - AddDescriptionPostfix(std::get<1>(description), defaultStringManual, defaultString, params.addDefault, GetDefaultString(params), params.defaultString); - - return { std::move(description) }; - } - - virtual std::string Name() const - { - return name; - } - }; - - namespace detail - { - template - struct IsConvertableToString : std::false_type {}; - - template - struct IsConvertableToString() << std::declval(), int())> : std::true_type {}; - - template - typename std::enable_if::value, std::string>::type - ToString(const T &value) - { - std::ostringstream s; - s << value; - return s.str(); - } - - template - typename std::enable_if::value, std::string>::type - ToString(const T &) - { - return {}; - } - - template - std::vector MapKeysToStrings(const T &map) - { - std::vector res; - using K = typename std::decayfirst)>::type; - if (IsConvertableToString::value) - { - for (const auto &p : map) - { - res.push_back(detail::ToString(p.first)); - } - - std::sort(res.begin(), res.end()); - } - return res; - } - } - - /** Base class for all flag options - */ - class FlagBase : public NamedBase - { - protected: - const Matcher matcher; - - virtual std::string GetNameString(const HelpParams ¶ms) const override - { - const std::string postfix = !params.showValueName || NumberOfArguments() == 0 ? std::string() : Name(); - std::string flags; - const auto flagStrings = matcher.GetFlagStrings(); - const bool useValueNameOnce = flagStrings.size() == 1 ? false : params.useValueNameOnce; - for (auto it = flagStrings.begin(); it != flagStrings.end(); ++it) - { - auto &flag = *it; - if (it != flagStrings.begin()) - { - flags += ", "; - } - - flags += flag.isShort ? params.shortPrefix : params.longPrefix; - flags += flag.str(); - - if (!postfix.empty() && (!useValueNameOnce || it + 1 == flagStrings.end())) - { - flags += flag.isShort ? params.shortSeparator : params.longSeparator; - flags += params.valueOpen + postfix + params.valueClose; - } - } - - return flags; - } - - public: - FlagBase(const std::string &name_, const std::string &help_, Matcher &&matcher_, const bool extraError_ = false) : NamedBase(name_, help_, extraError_ ? Options::Single : Options()), matcher(std::move(matcher_)) {} - - FlagBase(const std::string &name_, const std::string &help_, Matcher &&matcher_, Options options_) : NamedBase(name_, help_, options_), matcher(std::move(matcher_)) {} - - virtual ~FlagBase() {} - - virtual FlagBase *Match(const EitherFlag &flag) override - { - if (matcher.Match(flag)) - { - if ((GetOptions() & Options::Single) != Options::None && matched) - { - std::ostringstream problem; - problem << "Flag '" << flag.str() << "' was passed multiple times, but is only allowed to be passed once"; -#ifdef ARGS_NOEXCEPT - error = Error::Extra; - errorMsg = problem.str(); -#else - throw ExtraError(problem.str()); -#endif - } - matched = true; - return this; - } - return nullptr; - } - - virtual std::vector GetAllFlags() override - { - return { this }; - } - - const Matcher &GetMatcher() const - { - return matcher; - } - - virtual void Validate(const std::string &shortPrefix, const std::string &longPrefix) const override - { - if (!Matched() && IsRequired()) - { - std::ostringstream problem; - problem << "Flag '" << matcher.GetLongOrAny().str(shortPrefix, longPrefix) << "' is required"; -#ifdef ARGS_NOEXCEPT - error = Error::Required; - errorMsg = problem.str(); -#else - throw RequiredError(problem.str()); -#endif - } - } - - virtual std::vector GetProgramLine(const HelpParams ¶ms) const override - { - if (!params.proglineShowFlags) - { - return {}; - } - - const std::string postfix = NumberOfArguments() == 0 ? std::string() : Name(); - const EitherFlag flag = params.proglinePreferShortFlags ? matcher.GetShortOrAny() : matcher.GetLongOrAny(); - std::string res = flag.str(params.shortPrefix, params.longPrefix); - if (!postfix.empty()) - { - res += params.proglineValueOpen + postfix + params.proglineValueClose; - } - - return { IsRequired() ? params.proglineRequiredOpen + res + params.proglineRequiredClose - : params.proglineNonrequiredOpen + res + params.proglineNonrequiredClose }; - } - - virtual bool HasFlag() const override - { - return true; - } - -#ifdef ARGS_NOEXCEPT - /// Only for ARGS_NOEXCEPT - virtual Error GetError() const override - { - const auto nargs = NumberOfArguments(); - if (nargs.min > nargs.max) - { - return Error::Usage; - } - - const auto matcherError = matcher.GetError(); - if (matcherError != Error::None) - { - return matcherError; - } - - return error; - } -#endif - - /** Defines how many values can be consumed by this option. - * - * \return closed interval [min, max] - */ - virtual Nargs NumberOfArguments() const noexcept = 0; - - /** Parse values of this option. - * - * \param value Vector of values. It's size must be in NumberOfArguments() interval. - */ - virtual void ParseValue(const std::vector &value) = 0; - }; - - /** Base class for value-accepting flag options - */ - class ValueFlagBase : public FlagBase - { - public: - ValueFlagBase(const std::string &name_, const std::string &help_, Matcher &&matcher_, const bool extraError_ = false) : FlagBase(name_, help_, std::move(matcher_), extraError_) {} - ValueFlagBase(const std::string &name_, const std::string &help_, Matcher &&matcher_, Options options_) : FlagBase(name_, help_, std::move(matcher_), options_) {} - virtual ~ValueFlagBase() {} - - virtual Nargs NumberOfArguments() const noexcept override - { - return 1; - } - }; - - class CompletionFlag : public ValueFlagBase - { - public: - std::vector reply; - size_t cword = 0; - std::string syntax; - - template - CompletionFlag(GroupClass &group_, Matcher &&matcher_): ValueFlagBase("completion", "completion flag", std::move(matcher_), Options::Hidden) - { - group_.AddCompletion(*this); - } - - virtual ~CompletionFlag() {} - - virtual Nargs NumberOfArguments() const noexcept override - { - return 2; - } - - virtual void ParseValue(const std::vector &value_) override - { - syntax = value_.at(0); - std::istringstream(value_.at(1)) >> cword; - } - - /** Get the completion reply - */ - std::string Get() noexcept - { - return detail::Join(reply, "\n"); - } - - virtual void Reset() noexcept override - { - ValueFlagBase::Reset(); - cword = 0; - syntax.clear(); - reply.clear(); - } - }; - - - /** Base class for positional options - */ - class PositionalBase : public NamedBase - { - protected: - bool ready; - - public: - PositionalBase(const std::string &name_, const std::string &help_, Options options_ = {}) : NamedBase(name_, help_, options_), ready(true) {} - virtual ~PositionalBase() {} - - bool Ready() - { - return ready; - } - - virtual void ParseValue(const std::string &value_) = 0; - - virtual void Reset() noexcept override - { - matched = false; - ready = true; -#ifdef ARGS_NOEXCEPT - error = Error::None; - errorMsg.clear(); -#endif - } - - virtual PositionalBase *GetNextPositional() override - { - return Ready() ? this : nullptr; - } - - virtual bool HasPositional() const override - { - return true; - } - - virtual std::vector GetProgramLine(const HelpParams ¶ms) const override - { - return { IsRequired() ? params.proglineRequiredOpen + Name() + params.proglineRequiredClose - : params.proglineNonrequiredOpen + Name() + params.proglineNonrequiredClose }; - } - - virtual void Validate(const std::string &, const std::string &) const override - { - if (IsRequired() && !Matched()) - { - std::ostringstream problem; - problem << "Option '" << Name() << "' is required"; -#ifdef ARGS_NOEXCEPT - error = Error::Required; - errorMsg = problem.str(); -#else - throw RequiredError(problem.str()); -#endif - } - } - }; - - /** Class for all kinds of validating groups, including ArgumentParser - */ - class Group : public Base - { - private: - std::vector children; - std::function validator; - - public: - /** Default validators - */ - struct Validators - { - static bool Xor(const Group &group) - { - return group.MatchedChildren() == 1; - } - - static bool AtLeastOne(const Group &group) - { - return group.MatchedChildren() >= 1; - } - - static bool AtMostOne(const Group &group) - { - return group.MatchedChildren() <= 1; - } - - static bool All(const Group &group) - { - return group.Children().size() == group.MatchedChildren(); - } - - static bool AllOrNone(const Group &group) - { - return (All(group) || None(group)); - } - - static bool AllChildGroups(const Group &group) - { - return std::none_of(std::begin(group.Children()), std::end(group.Children()), [](const Base* child) -> bool { - return child->IsGroup() && !child->Matched(); - }); - } - - static bool DontCare(const Group &) - { - return true; - } - - static bool CareTooMuch(const Group &) - { - return false; - } - - static bool None(const Group &group) - { - return group.MatchedChildren() == 0; - } - }; - /// If help is empty, this group will not be printed in help output - Group(const std::string &help_ = std::string(), const std::function &validator_ = Validators::DontCare, Options options_ = {}) : Base(help_, options_), validator(validator_) {} - /// If help is empty, this group will not be printed in help output - Group(Group &group_, const std::string &help_ = std::string(), const std::function &validator_ = Validators::DontCare, Options options_ = {}) : Base(help_, options_), validator(validator_) - { - group_.Add(*this); - } - virtual ~Group() {} - - /** Append a child to this Group. - */ - void Add(Base &child) - { - children.emplace_back(&child); - } - - /** Get all this group's children - */ - const std::vector &Children() const - { - return children; - } - - /** Return the first FlagBase that matches flag, or nullptr - * - * \param flag The flag with prefixes stripped - * \return the first matching FlagBase pointer, or nullptr if there is no match - */ - virtual FlagBase *Match(const EitherFlag &flag) override - { - for (Base *child: Children()) - { - if (FlagBase *match = child->Match(flag)) - { - return match; - } - } - return nullptr; - } - - virtual std::vector GetAllFlags() override - { - std::vector res; - for (Base *child: Children()) - { - auto childRes = child->GetAllFlags(); - res.insert(res.end(), childRes.begin(), childRes.end()); - } - return res; - } - - virtual void Validate(const std::string &shortPrefix, const std::string &longPrefix) const override - { - for (Base *child: Children()) - { - child->Validate(shortPrefix, longPrefix); - } - } - - /** Get the next ready positional, or nullptr if there is none - * - * \return the first ready PositionalBase pointer, or nullptr if there is no match - */ - virtual PositionalBase *GetNextPositional() override - { - for (Base *child: Children()) - { - if (auto next = child->GetNextPositional()) - { - return next; - } - } - return nullptr; - } - - /** Get whether this has any FlagBase children - * - * \return Whether or not there are any FlagBase children - */ - virtual bool HasFlag() const override - { - return std::any_of(Children().begin(), Children().end(), [](Base *child) { return child->HasFlag(); }); - } - - /** Get whether this has any PositionalBase children - * - * \return Whether or not there are any PositionalBase children - */ - virtual bool HasPositional() const override - { - return std::any_of(Children().begin(), Children().end(), [](Base *child) { return child->HasPositional(); }); - } - - /** Get whether this has any Command children - * - * \return Whether or not there are any Command children - */ - virtual bool HasCommand() const override - { - return std::any_of(Children().begin(), Children().end(), [](Base *child) { return child->HasCommand(); }); - } - - /** Count the number of matched children this group has - */ - std::vector::size_type MatchedChildren() const - { - // Cast to avoid warnings from -Wsign-conversion - return static_cast::size_type>( - std::count_if(std::begin(Children()), std::end(Children()), [](const Base *child){return child->Matched();})); - } - - /** Whether or not this group matches validation - */ - virtual bool Matched() const noexcept override - { - return validator(*this); - } - - /** Get validation - */ - bool Get() const - { - return Matched(); - } - - /** Get all the child descriptions for help generation - */ - virtual std::vector> GetDescription(const HelpParams ¶ms, const unsigned int indent) const override - { - std::vector> descriptions; - - // Push that group description on the back if not empty - unsigned addindent = 0; - if (!help.empty()) - { - descriptions.emplace_back(help, "", indent); - addindent = 1; - } - - for (Base *child: Children()) - { - if ((child->GetOptions() & Options::HiddenFromDescription) != Options::None) - { - continue; - } - - auto groupDescriptions = child->GetDescription(params, indent + addindent); - descriptions.insert( - std::end(descriptions), - std::make_move_iterator(std::begin(groupDescriptions)), - std::make_move_iterator(std::end(groupDescriptions))); - } - return descriptions; - } - - /** Get the names of positional parameters - */ - virtual std::vector GetProgramLine(const HelpParams ¶ms) const override - { - std::vector names; - for (Base *child: Children()) - { - if ((child->GetOptions() & Options::HiddenFromUsage) != Options::None) - { - continue; - } - - auto groupNames = child->GetProgramLine(params); - names.insert( - std::end(names), - std::make_move_iterator(std::begin(groupNames)), - std::make_move_iterator(std::end(groupNames))); - } - return names; - } - - virtual std::vector GetCommands() override - { - std::vector res; - for (const auto &child : Children()) - { - auto subparsers = child->GetCommands(); - res.insert(std::end(res), std::begin(subparsers), std::end(subparsers)); - } - return res; - } - - virtual bool IsGroup() const override - { - return true; - } - - virtual void Reset() noexcept override - { - Base::Reset(); - - for (auto &child: Children()) - { - child->Reset(); - } -#ifdef ARGS_NOEXCEPT - error = Error::None; - errorMsg.clear(); -#endif - } - -#ifdef ARGS_NOEXCEPT - /// Only for ARGS_NOEXCEPT - virtual Error GetError() const override - { - if (error != Error::None) - { - return error; - } - - auto it = std::find_if(Children().begin(), Children().end(), [](const Base *child){return child->GetError() != Error::None;}); - if (it == Children().end()) - { - return Error::None; - } else - { - return (*it)->GetError(); - } - } -#endif - - }; - - /** Class for using global options in ArgumentParser. - */ - class GlobalOptions : public Group - { - public: - GlobalOptions(Group &base, Base &options_) : Group(base, {}, Group::Validators::DontCare, Options::Global) - { - Add(options_); - } - }; - - /** Utility class for building subparsers with coroutines/callbacks. - * - * Brief example: - * \code - * Command command(argumentParser, "command", "my command", [](args::Subparser &s) - * { - * // your command flags/positionals - * s.Parse(); //required - * //your command code - * }); - * \endcode - * - * For ARGS_NOEXCEPT mode don't forget to check `s.GetError()` after `s.Parse()` - * and return if it isn't equals to args::Error::None. - * - * \sa Command - */ - class Subparser : public Group - { - private: - std::vector args; - std::vector kicked; - ArgumentParser *parser = nullptr; - const HelpParams &helpParams; - const Command &command; - bool isParsed = false; - - public: - Subparser(std::vector args_, ArgumentParser &parser_, const Command &command_, const HelpParams &helpParams_) - : Group({}, Validators::AllChildGroups), args(std::move(args_)), parser(&parser_), helpParams(helpParams_), command(command_) - { - } - - Subparser(const Command &command_, const HelpParams &helpParams_) : Group({}, Validators::AllChildGroups), helpParams(helpParams_), command(command_) - { - } - - Subparser(const Subparser&) = delete; - Subparser(Subparser&&) = delete; - Subparser &operator = (const Subparser&) = delete; - Subparser &operator = (Subparser&&) = delete; - - const Command &GetCommand() - { - return command; - } - - /** (INTERNAL) Determines whether Parse was called or not. - */ - bool IsParsed() const - { - return isParsed; - } - - /** Continue parsing arguments for new command. - */ - void Parse(); - - /** Returns a vector of kicked out arguments. - * - * \sa Base::KickOut - */ - const std::vector &KickedOut() const noexcept - { - return kicked; - } - }; - - /** Main class for building subparsers. - * - * /sa Subparser - */ - class Command : public Group - { - private: - friend class Subparser; - - std::string name; - std::string help; - std::string description; - std::string epilog; - std::string proglinePostfix; - - std::function parserCoroutine; - bool commandIsRequired = true; - Command *selectedCommand = nullptr; - - mutable std::vector> subparserDescription; - mutable std::vector subparserProgramLine; - mutable bool subparserHasFlag = false; - mutable bool subparserHasPositional = false; - mutable bool subparserHasCommand = false; -#ifdef ARGS_NOEXCEPT - mutable Error subparserError = Error::None; -#endif - mutable Subparser *subparser = nullptr; - - protected: - - class RaiiSubparser - { - public: - RaiiSubparser(ArgumentParser &parser_, std::vector args_); - RaiiSubparser(const Command &command_, const HelpParams ¶ms_); - - ~RaiiSubparser() - { - command.subparser = oldSubparser; - } - - Subparser &Parser() - { - return parser; - } - - private: - const Command &command; - Subparser parser; - Subparser *oldSubparser; - }; - - Command() = default; - - std::function &GetCoroutine() - { - return selectedCommand != nullptr ? selectedCommand->GetCoroutine() : parserCoroutine; - } - - Command &SelectedCommand() - { - Command *res = this; - while (res->selectedCommand != nullptr) - { - res = res->selectedCommand; - } - - return *res; - } - - const Command &SelectedCommand() const - { - const Command *res = this; - while (res->selectedCommand != nullptr) - { - res = res->selectedCommand; - } - - return *res; - } - - void UpdateSubparserHelp(const HelpParams ¶ms) const - { - if (parserCoroutine) - { - RaiiSubparser coro(*this, params); -#ifndef ARGS_NOEXCEPT - try - { - parserCoroutine(coro.Parser()); - } - catch (args::SubparserError&) - { - } -#else - parserCoroutine(coro.Parser()); -#endif - } - } - - public: - Command(Group &base_, std::string name_, std::string help_, std::function coroutine_ = {}) - : name(std::move(name_)), help(std::move(help_)), parserCoroutine(std::move(coroutine_)) - { - base_.Add(*this); - } - - /** The description that appears on the prog line after options - */ - const std::string &ProglinePostfix() const - { return proglinePostfix; } - - /** The description that appears on the prog line after options - */ - void ProglinePostfix(const std::string &proglinePostfix_) - { this->proglinePostfix = proglinePostfix_; } - - /** The description that appears above options - */ - const std::string &Description() const - { return description; } - /** The description that appears above options - */ - - void Description(const std::string &description_) - { this->description = description_; } - - /** The description that appears below options - */ - const std::string &Epilog() const - { return epilog; } - - /** The description that appears below options - */ - void Epilog(const std::string &epilog_) - { this->epilog = epilog_; } - - /** The name of command - */ - const std::string &Name() const - { return name; } - - /** The description of command - */ - const std::string &Help() const - { return help; } - - /** If value is true, parser will fail if no command was parsed. - * - * Default: true. - */ - void RequireCommand(bool value) - { commandIsRequired = value; } - - virtual bool IsGroup() const override - { return false; } - - virtual bool Matched() const noexcept override - { return Base::Matched(); } - - operator bool() const noexcept - { return Matched(); } - - void Match() noexcept - { matched = true; } - - void SelectCommand(Command *c) noexcept - { - selectedCommand = c; - - if (c != nullptr) - { - c->Match(); - } - } - - virtual FlagBase *Match(const EitherFlag &flag) override - { - if (selectedCommand != nullptr) - { - if (auto *res = selectedCommand->Match(flag)) - { - return res; - } - - for (auto *child: Children()) - { - if ((child->GetOptions() & Options::Global) != Options::None) - { - if (auto *res = child->Match(flag)) - { - return res; - } - } - } - - return nullptr; - } - - if (subparser != nullptr) - { - return subparser->Match(flag); - } - - return Matched() ? Group::Match(flag) : nullptr; - } - - virtual std::vector GetAllFlags() override - { - std::vector res; - - if (!Matched()) - { - return res; - } - - for (auto *child: Children()) - { - if (selectedCommand == nullptr || (child->GetOptions() & Options::Global) != Options::None) - { - auto childFlags = child->GetAllFlags(); - res.insert(res.end(), childFlags.begin(), childFlags.end()); - } - } - - if (selectedCommand != nullptr) - { - auto childFlags = selectedCommand->GetAllFlags(); - res.insert(res.end(), childFlags.begin(), childFlags.end()); - } - - if (subparser != nullptr) - { - auto childFlags = subparser->GetAllFlags(); - res.insert(res.end(), childFlags.begin(), childFlags.end()); - } - - return res; - } - - virtual PositionalBase *GetNextPositional() override - { - if (selectedCommand != nullptr) - { - if (auto *res = selectedCommand->GetNextPositional()) - { - return res; - } - - for (auto *child: Children()) - { - if ((child->GetOptions() & Options::Global) != Options::None) - { - if (auto *res = child->GetNextPositional()) - { - return res; - } - } - } - - return nullptr; - } - - if (subparser != nullptr) - { - return subparser->GetNextPositional(); - } - - return Matched() ? Group::GetNextPositional() : nullptr; - } - - virtual bool HasFlag() const override - { - return subparserHasFlag || Group::HasFlag(); - } - - virtual bool HasPositional() const override - { - return subparserHasPositional || Group::HasPositional(); - } - - virtual bool HasCommand() const override - { - return true; - } - - std::vector GetCommandProgramLine(const HelpParams ¶ms) const - { - UpdateSubparserHelp(params); - - auto res = Group::GetProgramLine(params); - res.insert(res.end(), subparserProgramLine.begin(), subparserProgramLine.end()); - - if (!params.proglineCommand.empty() && (Group::HasCommand() || subparserHasCommand)) - { - res.insert(res.begin(), commandIsRequired ? params.proglineCommand : "[" + params.proglineCommand + "]"); - } - - if (!Name().empty()) - { - res.insert(res.begin(), Name()); - } - - if ((subparserHasFlag || Group::HasFlag()) && params.showProglineOptions && !params.proglineShowFlags) - { - res.push_back(params.proglineOptions); - } - - if (!ProglinePostfix().empty()) - { - std::string line; - for (char c : ProglinePostfix()) - { - if (isspace(c)) - { - if (!line.empty()) - { - res.push_back(line); - line.clear(); - } - - if (c == '\n') - { - res.push_back("\n"); - } - } - else - { - line += c; - } - } - - if (!line.empty()) - { - res.push_back(line); - } - } - - return res; - } - - virtual std::vector GetProgramLine(const HelpParams ¶ms) const override - { - if (!Matched()) - { - return {}; - } - - return GetCommandProgramLine(params); - } - - virtual std::vector GetCommands() override - { - if (selectedCommand != nullptr) - { - return selectedCommand->GetCommands(); - } - - if (Matched()) - { - return Group::GetCommands(); - } - - return { this }; - } - - virtual std::vector> GetDescription(const HelpParams ¶ms, const unsigned int indent) const override - { - std::vector> descriptions; - unsigned addindent = 0; - - UpdateSubparserHelp(params); - - if (!Matched()) - { - if (params.showCommandFullHelp) - { - std::ostringstream s; - bool empty = true; - for (const auto &progline: GetCommandProgramLine(params)) - { - if (!empty) - { - s << ' '; - } - else - { - empty = false; - } - - s << progline; - } - - descriptions.emplace_back(s.str(), "", indent); - } - else - { - descriptions.emplace_back(Name(), help, indent); - } - - if (!params.showCommandChildren && !params.showCommandFullHelp) - { - return descriptions; - } - - addindent = 1; - } - - if (params.showCommandFullHelp && !Matched()) - { - descriptions.emplace_back("", "", indent + addindent); - descriptions.emplace_back(Description().empty() ? Help() : Description(), "", indent + addindent); - descriptions.emplace_back("", "", indent + addindent); - } - - for (Base *child: Children()) - { - if ((child->GetOptions() & Options::HiddenFromDescription) != Options::None) - { - continue; - } - - auto groupDescriptions = child->GetDescription(params, indent + addindent); - descriptions.insert( - std::end(descriptions), - std::make_move_iterator(std::begin(groupDescriptions)), - std::make_move_iterator(std::end(groupDescriptions))); - } - - for (auto childDescription: subparserDescription) - { - std::get<2>(childDescription) += indent + addindent; - descriptions.push_back(std::move(childDescription)); - } - - if (params.showCommandFullHelp && !Matched()) - { - descriptions.emplace_back("", "", indent + addindent); - if (!Epilog().empty()) - { - descriptions.emplace_back(Epilog(), "", indent + addindent); - descriptions.emplace_back("", "", indent + addindent); - } - } - - return descriptions; - } - - virtual void Validate(const std::string &shortprefix, const std::string &longprefix) const override - { - if (!Matched()) - { - return; - } - - auto onValidationError = [&] - { - std::ostringstream problem; - problem << "Group validation failed somewhere!"; -#ifdef ARGS_NOEXCEPT - error = Error::Validation; - errorMsg = problem.str(); -#else - throw ValidationError(problem.str()); -#endif - }; - - for (Base *child: Children()) - { - if (child->IsGroup() && !child->Matched()) - { - onValidationError(); - } - - child->Validate(shortprefix, longprefix); - } - - if (subparser != nullptr) - { - subparser->Validate(shortprefix, longprefix); - if (!subparser->Matched()) - { - onValidationError(); - } - } - - if (selectedCommand == nullptr && commandIsRequired && (Group::HasCommand() || subparserHasCommand)) - { - std::ostringstream problem; - problem << "Command is required"; -#ifdef ARGS_NOEXCEPT - error = Error::Validation; - errorMsg = problem.str(); -#else - throw ValidationError(problem.str()); -#endif - } - } - - virtual void Reset() noexcept override - { - Group::Reset(); - selectedCommand = nullptr; - subparserProgramLine.clear(); - subparserDescription.clear(); - subparserHasFlag = false; - subparserHasPositional = false; - subparserHasCommand = false; -#ifdef ARGS_NOEXCEPT - subparserError = Error::None; -#endif - } - -#ifdef ARGS_NOEXCEPT - /// Only for ARGS_NOEXCEPT - virtual Error GetError() const override - { - if (!Matched()) - { - return Error::None; - } - - if (error != Error::None) - { - return error; - } - - if (subparserError != Error::None) - { - return subparserError; - } - - return Group::GetError(); - } -#endif - }; - - /** The main user facing command line argument parser class - */ - class ArgumentParser : public Command - { - friend class Subparser; - - private: - std::string longprefix; - std::string shortprefix; - - std::string longseparator; - - std::string terminator; - - bool allowJoinedShortValue = true; - bool allowJoinedLongValue = true; - bool allowSeparateShortValue = true; - bool allowSeparateLongValue = true; - - CompletionFlag *completion = nullptr; - bool readCompletion = false; - - protected: - enum class OptionType - { - LongFlag, - ShortFlag, - Positional - }; - - OptionType ParseOption(const std::string &s, bool allowEmpty = false) - { - if (s.find(longprefix) == 0 && (allowEmpty || s.length() > longprefix.length())) - { - return OptionType::LongFlag; - } - - if (s.find(shortprefix) == 0 && (allowEmpty || s.length() > shortprefix.length())) - { - return OptionType::ShortFlag; - } - - return OptionType::Positional; - } - - template - bool Complete(FlagBase &flag, It it, It end) - { - auto nextIt = it; - if (!readCompletion || (++nextIt != end)) - { - return false; - } - - const auto &chunk = *it; - for (auto &choice : flag.HelpChoices(helpParams)) - { - AddCompletionReply(chunk, choice); - } - -#ifndef ARGS_NOEXCEPT - throw Completion(completion->Get()); -#else - return true; -#endif - } - - /** (INTERNAL) Parse flag's values - * - * \param arg The string to display in error message as a flag name - * \param[in, out] it The iterator to first value. It will point to the last value - * \param end The end iterator - * \param joinedArg Joined value (e.g. bar in --foo=bar) - * \param canDiscardJoined If true joined value can be parsed as flag not as a value (as in -abcd) - * \param[out] values The vector to store parsed arg's values - */ - template - std::string ParseArgsValues(FlagBase &flag, const std::string &arg, It &it, It end, - const bool allowSeparate, const bool allowJoined, - const bool hasJoined, const std::string &joinedArg, - const bool canDiscardJoined, std::vector &values) - { - values.clear(); - - Nargs nargs = flag.NumberOfArguments(); - - if (hasJoined && !allowJoined && nargs.min != 0) - { - return "Flag '" + arg + "' was passed a joined argument, but these are disallowed"; - } - - if (hasJoined) - { - if (!canDiscardJoined || nargs.max != 0) - { - values.push_back(joinedArg); - } - } else if (!allowSeparate) - { - if (nargs.min != 0) - { - return "Flag '" + arg + "' was passed a separate argument, but these are disallowed"; - } - } else - { - auto valueIt = it; - ++valueIt; - - while (valueIt != end && - values.size() < nargs.max && - (nargs.min == nargs.max || ParseOption(*valueIt) == OptionType::Positional)) - { - if (Complete(flag, valueIt, end)) - { - it = end; - return ""; - } - - values.push_back(*valueIt); - ++it; - ++valueIt; - } - } - - if (values.size() > nargs.max) - { - return "Passed an argument into a non-argument flag: " + arg; - } else if (values.size() < nargs.min) - { - if (nargs.min == 1 && nargs.max == 1) - { - return "Flag '" + arg + "' requires an argument but received none"; - } else if (nargs.min == 1) - { - return "Flag '" + arg + "' requires at least one argument but received none"; - } else if (nargs.min != nargs.max) - { - return "Flag '" + arg + "' requires at least " + std::to_string(nargs.min) + - " arguments but received " + std::to_string(values.size()); - } else - { - return "Flag '" + arg + "' requires " + std::to_string(nargs.min) + - " arguments but received " + std::to_string(values.size()); - } - } - - return {}; - } - - template - bool ParseLong(It &it, It end) - { - const auto &chunk = *it; - const auto argchunk = chunk.substr(longprefix.size()); - // Try to separate it, in case of a separator: - const auto separator = longseparator.empty() ? argchunk.npos : argchunk.find(longseparator); - // If the separator is in the argument, separate it. - const auto arg = (separator != argchunk.npos ? - std::string(argchunk, 0, separator) - : argchunk); - const auto joined = (separator != argchunk.npos ? - argchunk.substr(separator + longseparator.size()) - : std::string()); - - if (auto flag = Match(arg)) - { - std::vector values; - const std::string errorMessage = ParseArgsValues(*flag, arg, it, end, allowSeparateLongValue, allowJoinedLongValue, - separator != argchunk.npos, joined, false, values); - if (!errorMessage.empty()) - { -#ifndef ARGS_NOEXCEPT - throw ParseError(errorMessage); -#else - error = Error::Parse; - errorMsg = errorMessage; - return false; -#endif - } - - if (!readCompletion) - { - flag->ParseValue(values); - } - - if (flag->KickOut()) - { - ++it; - return false; - } - } else - { - const std::string errorMessage("Flag could not be matched: " + arg); -#ifndef ARGS_NOEXCEPT - throw ParseError(errorMessage); -#else - error = Error::Parse; - errorMsg = errorMessage; - return false; -#endif - } - - return true; - } - - template - bool ParseShort(It &it, It end) - { - const auto &chunk = *it; - const auto argchunk = chunk.substr(shortprefix.size()); - for (auto argit = std::begin(argchunk); argit != std::end(argchunk); ++argit) - { - const auto arg = *argit; - - if (auto flag = Match(arg)) - { - const std::string value(argit + 1, std::end(argchunk)); - std::vector values; - const std::string errorMessage = ParseArgsValues(*flag, std::string(1, arg), it, end, - allowSeparateShortValue, allowJoinedShortValue, - !value.empty(), value, !value.empty(), values); - - if (!errorMessage.empty()) - { -#ifndef ARGS_NOEXCEPT - throw ParseError(errorMessage); -#else - error = Error::Parse; - errorMsg = errorMessage; - return false; -#endif - } - - if (!readCompletion) - { - flag->ParseValue(values); - } - - if (flag->KickOut()) - { - ++it; - return false; - } - - if (!values.empty()) - { - break; - } - } else - { - const std::string errorMessage("Flag could not be matched: '" + std::string(1, arg) + "'"); -#ifndef ARGS_NOEXCEPT - throw ParseError(errorMessage); -#else - error = Error::Parse; - errorMsg = errorMessage; - return false; -#endif - } - } - - return true; - } - - bool AddCompletionReply(const std::string &cur, const std::string &choice) - { - if (cur.empty() || choice.find(cur) == 0) - { - if (completion->syntax == "bash" && ParseOption(choice) == OptionType::LongFlag && choice.find(longseparator) != std::string::npos) - { - completion->reply.push_back(choice.substr(choice.find(longseparator) + 1)); - } else - { - completion->reply.push_back(choice); - } - return true; - } - - return false; - } - - template - bool Complete(It it, It end) - { - auto nextIt = it; - if (!readCompletion || (++nextIt != end)) - { - return false; - } - - const auto &chunk = *it; - auto pos = GetNextPositional(); - std::vector commands = GetCommands(); - const auto optionType = ParseOption(chunk, true); - - if (!commands.empty() && (chunk.empty() || optionType == OptionType::Positional)) - { - for (auto &cmd : commands) - { - if ((cmd->GetOptions() & Options::HiddenFromCompletion) == Options::None) - { - AddCompletionReply(chunk, cmd->Name()); - } - } - } else - { - bool hasPositionalCompletion = true; - - if (!commands.empty()) - { - for (auto &cmd : commands) - { - if ((cmd->GetOptions() & Options::HiddenFromCompletion) == Options::None) - { - AddCompletionReply(chunk, cmd->Name()); - } - } - } else if (pos) - { - if ((pos->GetOptions() & Options::HiddenFromCompletion) == Options::None) - { - auto choices = pos->HelpChoices(helpParams); - hasPositionalCompletion = !choices.empty() || optionType != OptionType::Positional; - for (auto &choice : choices) - { - AddCompletionReply(chunk, choice); - } - } - } - - if (hasPositionalCompletion) - { - auto flags = GetAllFlags(); - for (auto flag : flags) - { - if ((flag->GetOptions() & Options::HiddenFromCompletion) != Options::None) - { - continue; - } - - auto &matcher = flag->GetMatcher(); - if (!AddCompletionReply(chunk, matcher.GetShortOrAny().str(shortprefix, longprefix))) - { - for (auto &flagName : matcher.GetFlagStrings()) - { - if (AddCompletionReply(chunk, flagName.str(shortprefix, longprefix))) - { - break; - } - } - } - } - - if (optionType == OptionType::LongFlag && allowJoinedLongValue) - { - const auto separator = longseparator.empty() ? chunk.npos : chunk.find(longseparator); - if (separator != chunk.npos) - { - std::string arg(chunk, 0, separator); - if (auto flag = this->Match(arg.substr(longprefix.size()))) - { - for (auto &choice : flag->HelpChoices(helpParams)) - { - AddCompletionReply(chunk, arg + longseparator + choice); - } - } - } - } else if (optionType == OptionType::ShortFlag && allowJoinedShortValue) - { - if (chunk.size() > shortprefix.size() + 1) - { - auto arg = chunk.at(shortprefix.size()); - //TODO: support -abcVALUE where a and b take no value - if (auto flag = this->Match(arg)) - { - for (auto &choice : flag->HelpChoices(helpParams)) - { - AddCompletionReply(chunk, shortprefix + arg + choice); - } - } - } - } - } - } - -#ifndef ARGS_NOEXCEPT - throw Completion(completion->Get()); -#else - return true; -#endif - } - - template - It Parse(It begin, It end) - { - bool terminated = false; - std::vector commands = GetCommands(); - - // Check all arg chunks - for (auto it = begin; it != end; ++it) - { - if (Complete(it, end)) - { - return end; - } - - const auto &chunk = *it; - - if (!terminated && chunk == terminator) - { - terminated = true; - } else if (!terminated && ParseOption(chunk) == OptionType::LongFlag) - { - if (!ParseLong(it, end)) - { - return it; - } - } else if (!terminated && ParseOption(chunk) == OptionType::ShortFlag) - { - if (!ParseShort(it, end)) - { - return it; - } - } else if (!terminated && !commands.empty()) - { - auto itCommand = std::find_if(commands.begin(), commands.end(), [&chunk](Command *c) { return c->Name() == chunk; }); - if (itCommand == commands.end()) - { - const std::string errorMessage("Unknown command: " + chunk); -#ifndef ARGS_NOEXCEPT - throw ParseError(errorMessage); -#else - error = Error::Parse; - errorMsg = errorMessage; - return it; -#endif - } - - SelectCommand(*itCommand); - - if (const auto &coroutine = GetCoroutine()) - { - ++it; - RaiiSubparser coro(*this, std::vector(it, end)); - coroutine(coro.Parser()); -#ifdef ARGS_NOEXCEPT - error = GetError(); - if (error != Error::None) - { - return end; - } - - if (!coro.Parser().IsParsed()) - { - error = Error::Usage; - return end; - } -#else - if (!coro.Parser().IsParsed()) - { - throw UsageError("Subparser::Parse was not called"); - } -#endif - - break; - } - - commands = GetCommands(); - } else - { - auto pos = GetNextPositional(); - if (pos) - { - pos->ParseValue(chunk); - - if (pos->KickOut()) - { - return ++it; - } - } else - { - const std::string errorMessage("Passed in argument, but no positional arguments were ready to receive it: " + chunk); -#ifndef ARGS_NOEXCEPT - throw ParseError(errorMessage); -#else - error = Error::Parse; - errorMsg = errorMessage; - return it; -#endif - } - } - - if (!readCompletion && completion != nullptr && completion->Matched()) - { -#ifdef ARGS_NOEXCEPT - error = Error::Completion; -#endif - readCompletion = true; - ++it; - const auto argsLeft = static_cast(std::distance(it, end)); - if (completion->cword == 0 || argsLeft <= 1 || completion->cword >= argsLeft) - { -#ifndef ARGS_NOEXCEPT - throw Completion(""); -#endif - } - - std::vector curArgs(++it, end); - curArgs.resize(completion->cword); - - if (completion->syntax == "bash") - { - // bash tokenizes --flag=value as --flag=value - for (size_t idx = 0; idx < curArgs.size(); ) - { - if (idx > 0 && curArgs[idx] == "=") - { - curArgs[idx - 1] += "="; - // Avoid warnings from -Wsign-conversion - const auto signedIdx = static_cast(idx); - if (idx + 1 < curArgs.size()) - { - curArgs[idx - 1] += curArgs[idx + 1]; - curArgs.erase(curArgs.begin() + signedIdx, curArgs.begin() + signedIdx + 2); - } else - { - curArgs.erase(curArgs.begin() + signedIdx); - } - } else - { - ++idx; - } - } - - } -#ifndef ARGS_NOEXCEPT - try - { - Parse(curArgs.begin(), curArgs.end()); - throw Completion(""); - } - catch (Completion &) - { - throw; - } - catch (args::Error&) - { - throw Completion(""); - } -#else - return Parse(curArgs.begin(), curArgs.end()); -#endif - } - } - - Validate(shortprefix, longprefix); - return end; - } - - public: - HelpParams helpParams; - - ArgumentParser(const std::string &description_, const std::string &epilog_ = std::string()) - { - Description(description_); - Epilog(epilog_); - LongPrefix("--"); - ShortPrefix("-"); - LongSeparator("="); - Terminator("--"); - SetArgumentSeparations(true, true, true, true); - matched = true; - } - - void AddCompletion(CompletionFlag &completionFlag) - { - completion = &completionFlag; - Add(completionFlag); - } - - /** The program name for help generation - */ - const std::string &Prog() const - { return helpParams.programName; } - /** The program name for help generation - */ - void Prog(const std::string &prog_) - { this->helpParams.programName = prog_; } - - /** The prefix for long flags - */ - const std::string &LongPrefix() const - { return longprefix; } - /** The prefix for long flags - */ - void LongPrefix(const std::string &longprefix_) - { - this->longprefix = longprefix_; - this->helpParams.longPrefix = longprefix_; - } - - /** The prefix for short flags - */ - const std::string &ShortPrefix() const - { return shortprefix; } - /** The prefix for short flags - */ - void ShortPrefix(const std::string &shortprefix_) - { - this->shortprefix = shortprefix_; - this->helpParams.shortPrefix = shortprefix_; - } - - /** The separator for long flags - */ - const std::string &LongSeparator() const - { return longseparator; } - /** The separator for long flags - */ - void LongSeparator(const std::string &longseparator_) - { - if (longseparator_.empty()) - { - const std::string errorMessage("longseparator can not be set to empty"); -#ifdef ARGS_NOEXCEPT - error = Error::Usage; - errorMsg = errorMessage; -#else - throw UsageError(errorMessage); -#endif - } else - { - this->longseparator = longseparator_; - this->helpParams.longSeparator = allowJoinedLongValue ? longseparator_ : " "; - } - } - - /** The terminator that forcibly separates flags from positionals - */ - const std::string &Terminator() const - { return terminator; } - /** The terminator that forcibly separates flags from positionals - */ - void Terminator(const std::string &terminator_) - { this->terminator = terminator_; } - - /** Get the current argument separation parameters. - * - * See SetArgumentSeparations for details on what each one means. - */ - void GetArgumentSeparations( - bool &allowJoinedShortValue_, - bool &allowJoinedLongValue_, - bool &allowSeparateShortValue_, - bool &allowSeparateLongValue_) const - { - allowJoinedShortValue_ = this->allowJoinedShortValue; - allowJoinedLongValue_ = this->allowJoinedLongValue; - allowSeparateShortValue_ = this->allowSeparateShortValue; - allowSeparateLongValue_ = this->allowSeparateLongValue; - } - - /** Change allowed option separation. - * - * \param allowJoinedShortValue_ Allow a short flag that accepts an argument to be passed its argument immediately next to it (ie. in the same argv field) - * \param allowJoinedLongValue_ Allow a long flag that accepts an argument to be passed its argument separated by the longseparator (ie. in the same argv field) - * \param allowSeparateShortValue_ Allow a short flag that accepts an argument to be passed its argument separated by whitespace (ie. in the next argv field) - * \param allowSeparateLongValue_ Allow a long flag that accepts an argument to be passed its argument separated by whitespace (ie. in the next argv field) - */ - void SetArgumentSeparations( - const bool allowJoinedShortValue_, - const bool allowJoinedLongValue_, - const bool allowSeparateShortValue_, - const bool allowSeparateLongValue_) - { - this->allowJoinedShortValue = allowJoinedShortValue_; - this->allowJoinedLongValue = allowJoinedLongValue_; - this->allowSeparateShortValue = allowSeparateShortValue_; - this->allowSeparateLongValue = allowSeparateLongValue_; - - this->helpParams.longSeparator = allowJoinedLongValue ? longseparator : " "; - this->helpParams.shortSeparator = allowJoinedShortValue ? "" : " "; - } - - /** Pass the help menu into an ostream - */ - void Help(std::ostream &help_) const - { - auto &command = SelectedCommand(); - const auto &commandDescription = command.Description().empty() ? command.Help() : command.Description(); - const auto description_text = Wrap(commandDescription, helpParams.width - helpParams.descriptionindent); - const auto epilog_text = Wrap(command.Epilog(), helpParams.width - helpParams.descriptionindent); - - const bool hasoptions = command.HasFlag(); - const bool hasarguments = command.HasPositional(); - - std::vector prognameline; - prognameline.push_back(helpParams.usageString); - prognameline.push_back(Prog()); - auto commandProgLine = command.GetProgramLine(helpParams); - prognameline.insert(prognameline.end(), commandProgLine.begin(), commandProgLine.end()); - - const auto proglines = Wrap(prognameline.begin(), prognameline.end(), - helpParams.width - (helpParams.progindent + helpParams.progtailindent), - helpParams.width - helpParams.progindent); - auto progit = std::begin(proglines); - if (progit != std::end(proglines)) - { - help_ << std::string(helpParams.progindent, ' ') << *progit << '\n'; - ++progit; - } - for (; progit != std::end(proglines); ++progit) - { - help_ << std::string(helpParams.progtailindent, ' ') << *progit << '\n'; - } - - help_ << '\n'; - - if (!description_text.empty()) - { - for (const auto &line: description_text) - { - help_ << std::string(helpParams.descriptionindent, ' ') << line << "\n"; - } - help_ << "\n"; - } - - bool lastDescriptionIsNewline = false; - - if (!helpParams.optionsString.empty()) - { - help_ << std::string(helpParams.progindent, ' ') << helpParams.optionsString << "\n\n"; - } - - for (const auto &desc: command.GetDescription(helpParams, 0)) - { - lastDescriptionIsNewline = std::get<0>(desc).empty() && std::get<1>(desc).empty(); - const auto groupindent = std::get<2>(desc) * helpParams.eachgroupindent; - const auto flags = Wrap(std::get<0>(desc), helpParams.width - (helpParams.flagindent + helpParams.helpindent + helpParams.gutter)); - const auto info = Wrap(std::get<1>(desc), helpParams.width - (helpParams.helpindent + groupindent)); - - std::string::size_type flagssize = 0; - for (auto flagsit = std::begin(flags); flagsit != std::end(flags); ++flagsit) - { - if (flagsit != std::begin(flags)) - { - help_ << '\n'; - } - help_ << std::string(groupindent + helpParams.flagindent, ' ') << *flagsit; - flagssize = Glyphs(*flagsit); - } - - auto infoit = std::begin(info); - // groupindent is on both sides of this inequality, and therefore can be removed - if ((helpParams.flagindent + flagssize + helpParams.gutter) > helpParams.helpindent || infoit == std::end(info) || helpParams.addNewlineBeforeDescription) - { - help_ << '\n'; - } else - { - // groupindent is on both sides of the minus sign, and therefore doesn't actually need to be in here - help_ << std::string(helpParams.helpindent - (helpParams.flagindent + flagssize), ' ') << *infoit << '\n'; - ++infoit; - } - for (; infoit != std::end(info); ++infoit) - { - help_ << std::string(groupindent + helpParams.helpindent, ' ') << *infoit << '\n'; - } - } - if (hasoptions && hasarguments && helpParams.showTerminator) - { - lastDescriptionIsNewline = false; - for (const auto &item: Wrap(std::string("\"") + terminator + "\" can be used to terminate flag options and force all following arguments to be treated as positional options", helpParams.width - helpParams.flagindent)) - { - help_ << std::string(helpParams.flagindent, ' ') << item << '\n'; - } - } - - if (!lastDescriptionIsNewline) - { - help_ << "\n"; - } - - for (const auto &line: epilog_text) - { - help_ << std::string(helpParams.descriptionindent, ' ') << line << "\n"; - } - } - - /** Generate a help menu as a string. - * - * \return the help text as a single string - */ - std::string Help() const - { - std::ostringstream help_; - Help(help_); - return help_.str(); - } - - virtual void Reset() noexcept override - { - Command::Reset(); - matched = true; - readCompletion = false; - } - - /** Parse all arguments. - * - * \param begin an iterator to the beginning of the argument list - * \param end an iterator to the past-the-end element of the argument list - * \return the iterator after the last parsed value. Only useful for kick-out - */ - template - It ParseArgs(It begin, It end) - { - // Reset all Matched statuses and errors - Reset(); -#ifdef ARGS_NOEXCEPT - error = GetError(); - if (error != Error::None) - { - return end; - } -#endif - return Parse(begin, end); - } - - /** Parse all arguments. - * - * \param args an iterable of the arguments - * \return the iterator after the last parsed value. Only useful for kick-out - */ - template - auto ParseArgs(const T &args) -> decltype(std::begin(args)) - { - return ParseArgs(std::begin(args), std::end(args)); - } - - /** Convenience function to parse the CLI from argc and argv - * - * Just assigns the program name and vectorizes arguments for passing into ParseArgs() - * - * \return whether or not all arguments were parsed. This works for detecting kick-out, but is generally useless as it can't do anything with it. - */ - bool ParseCLI(const int argc, const char * const * argv) - { - if (Prog().empty()) - { - Prog(argv[0]); - } - const std::vector args(argv + 1, argv + argc); - return ParseArgs(args) == std::end(args); - } - - template - bool ParseCLI(const T &args) - { - return ParseArgs(args) == std::end(args); - } - }; - - inline Command::RaiiSubparser::RaiiSubparser(ArgumentParser &parser_, std::vector args_) - : command(parser_.SelectedCommand()), parser(std::move(args_), parser_, command, parser_.helpParams), oldSubparser(command.subparser) - { - command.subparser = &parser; - } - - inline Command::RaiiSubparser::RaiiSubparser(const Command &command_, const HelpParams ¶ms_): command(command_), parser(command, params_), oldSubparser(command.subparser) - { - command.subparser = &parser; - } - - inline void Subparser::Parse() - { - isParsed = true; - Reset(); - command.subparserDescription = GetDescription(helpParams, 0); - command.subparserHasFlag = HasFlag(); - command.subparserHasPositional = HasPositional(); - command.subparserHasCommand = HasCommand(); - command.subparserProgramLine = GetProgramLine(helpParams); - if (parser == nullptr) - { -#ifndef ARGS_NOEXCEPT - throw args::SubparserError(); -#else - error = Error::Subparser; - return; -#endif - } - - auto it = parser->Parse(args.begin(), args.end()); - command.Validate(parser->ShortPrefix(), parser->LongPrefix()); - kicked.assign(it, args.end()); - -#ifdef ARGS_NOEXCEPT - command.subparserError = GetError(); -#endif - } - - inline std::ostream &operator<<(std::ostream &os, const ArgumentParser &parser) - { - parser.Help(os); - return os; - } - - /** Boolean argument matcher - */ - class Flag : public FlagBase - { - public: - Flag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, Options options_): FlagBase(name_, help_, std::move(matcher_), options_) - { - group_.Add(*this); - } - - Flag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const bool extraError_ = false): Flag(group_, name_, help_, std::move(matcher_), extraError_ ? Options::Single : Options::None) - { - } - - virtual ~Flag() {} - - /** Get whether this was matched - */ - bool Get() const - { - return Matched(); - } - - virtual Nargs NumberOfArguments() const noexcept override - { - return 0; - } - - virtual void ParseValue(const std::vector&) override - { - } - }; - - /** Help flag class - * - * Works like a regular flag, but throws an instance of Help when it is matched - */ - class HelpFlag : public Flag - { - public: - HelpFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, Options options_ = {}): Flag(group_, name_, help_, std::move(matcher_), options_) {} - - virtual ~HelpFlag() {} - - virtual void ParseValue(const std::vector &) - { -#ifdef ARGS_NOEXCEPT - error = Error::Help; - errorMsg = Name(); -#else - throw Help(Name()); -#endif - } - - /** Get whether this was matched - */ - bool Get() const noexcept - { - return Matched(); - } - }; - - /** A flag class that simply counts the number of times it's matched - */ - class CounterFlag : public Flag - { - private: - const int startcount; - int count; - - public: - CounterFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const int startcount_ = 0, Options options_ = {}): - Flag(group_, name_, help_, std::move(matcher_), options_), startcount(startcount_), count(startcount_) {} - - virtual ~CounterFlag() {} - - virtual FlagBase *Match(const EitherFlag &arg) override - { - auto me = FlagBase::Match(arg); - if (me) - { - ++count; - } - return me; - } - - /** Get the count - */ - int &Get() noexcept - { - return count; - } - - virtual void Reset() noexcept override - { - FlagBase::Reset(); - count = startcount; - } - }; - - /** A flag class that calls a function when it's matched - */ - class ActionFlag : public FlagBase - { - private: - std::function &)> action; - Nargs nargs; - - public: - ActionFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, Nargs nargs_, std::function &)> action_, Options options_ = {}): - FlagBase(name_, help_, std::move(matcher_), options_), action(std::move(action_)), nargs(nargs_) - { - group_.Add(*this); - } - - ActionFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, std::function action_, Options options_ = {}): - FlagBase(name_, help_, std::move(matcher_), options_), nargs(1) - { - group_.Add(*this); - action = [action_](const std::vector &a) { return action_(a.at(0)); }; - } - - ActionFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, std::function action_, Options options_ = {}): - FlagBase(name_, help_, std::move(matcher_), options_), nargs(0) - { - group_.Add(*this); - action = [action_](const std::vector &) { return action_(); }; - } - - virtual Nargs NumberOfArguments() const noexcept override - { return nargs; } - - virtual void ParseValue(const std::vector &value) override - { action(value); } - }; - - /** A default Reader class for argument classes - * - * If destination type is assignable to std::string it uses an assignment to std::string. - * Otherwise ValueReader simply uses a std::istringstream to read into the destination type, and - * raises a ParseError if there are any characters left. - */ - struct ValueReader - { - template - typename std::enable_if::value, bool>::type - operator ()(const std::string &name, const std::string &value, T &destination) - { - std::istringstream ss(value); - bool failed = !(ss >> destination); - - if (!failed) - { - ss >> std::ws; - } - - if (ss.rdbuf()->in_avail() > 0 || failed) - { -#ifdef ARGS_NOEXCEPT - (void)name; - return false; -#else - std::ostringstream problem; - problem << "Argument '" << name << "' received invalid value type '" << value << "'"; - throw ParseError(problem.str()); -#endif - } - return true; - } - - template - typename std::enable_if::value, bool>::type - operator()(const std::string &, const std::string &value, T &destination) - { - destination = value; - return true; - } - }; - - /** An argument-accepting flag class - * - * \tparam T the type to extract the argument as - * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined) - */ - template < - typename T, - typename Reader = ValueReader> - class ValueFlag : public ValueFlagBase - { - protected: - T value; - T defaultValue; - - virtual std::string GetDefaultString(const HelpParams&) const override - { - return detail::ToString(defaultValue); - } - - private: - Reader reader; - - public: - - ValueFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const T &defaultValue_, Options options_): ValueFlagBase(name_, help_, std::move(matcher_), options_), value(defaultValue_), defaultValue(defaultValue_) - { - group_.Add(*this); - } - - ValueFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const T &defaultValue_ = T(), const bool extraError_ = false): ValueFlag(group_, name_, help_, std::move(matcher_), defaultValue_, extraError_ ? Options::Single : Options::None) - { - } - - ValueFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, Options options_): ValueFlag(group_, name_, help_, std::move(matcher_), T(), options_) - { - } - - virtual ~ValueFlag() {} - - virtual void ParseValue(const std::vector &values_) override - { - const std::string &value_ = values_.at(0); - -#ifdef ARGS_NOEXCEPT - if (!reader(name, value_, this->value)) - { - error = Error::Parse; - } -#else - reader(name, value_, this->value); -#endif - } - - virtual void Reset() noexcept override - { - ValueFlagBase::Reset(); - value = defaultValue; - } - - /** Get the value - */ - T &Get() noexcept - { - return value; - } - - /** Get the default value - */ - const T &GetDefault() noexcept - { - return defaultValue; - } - }; - - /** An optional argument-accepting flag class - * - * \tparam T the type to extract the argument as - * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined) - */ - template < - typename T, - typename Reader = ValueReader> - class ImplicitValueFlag : public ValueFlag - { - protected: - T implicitValue; - - public: - - ImplicitValueFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const T &implicitValue_, const T &defaultValue_ = T(), Options options_ = {}) - : ValueFlag(group_, name_, help_, std::move(matcher_), defaultValue_, options_), implicitValue(implicitValue_) - { - } - - ImplicitValueFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const T &defaultValue_ = T(), Options options_ = {}) - : ValueFlag(group_, name_, help_, std::move(matcher_), defaultValue_, options_), implicitValue(defaultValue_) - { - } - - ImplicitValueFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, Options options_) - : ValueFlag(group_, name_, help_, std::move(matcher_), {}, options_), implicitValue() - { - } - - virtual ~ImplicitValueFlag() {} - - virtual Nargs NumberOfArguments() const noexcept override - { - return {0, 1}; - } - - virtual void ParseValue(const std::vector &value_) override - { - if (value_.empty()) - { - this->value = implicitValue; - } else - { - ValueFlag::ParseValue(value_); - } - } - }; - - /** A variadic arguments accepting flag class - * - * \tparam T the type to extract the argument as - * \tparam List the list type that houses the values - * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined) - */ - template < - typename T, - template class List = std::vector, - typename Reader = ValueReader> - class NargsValueFlag : public FlagBase - { - protected: - - List values; - const List defaultValues; - Nargs nargs; - Reader reader; - - public: - - typedef List Container; - typedef T value_type; - typedef typename Container::allocator_type allocator_type; - typedef typename Container::pointer pointer; - typedef typename Container::const_pointer const_pointer; - typedef T& reference; - typedef const T& const_reference; - typedef typename Container::size_type size_type; - typedef typename Container::difference_type difference_type; - typedef typename Container::iterator iterator; - typedef typename Container::const_iterator const_iterator; - typedef std::reverse_iterator reverse_iterator; - typedef std::reverse_iterator const_reverse_iterator; - - NargsValueFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, Nargs nargs_, const List &defaultValues_ = {}, Options options_ = {}) - : FlagBase(name_, help_, std::move(matcher_), options_), values(defaultValues_), defaultValues(defaultValues_),nargs(nargs_) - { - group_.Add(*this); - } - - virtual ~NargsValueFlag() {} - - virtual Nargs NumberOfArguments() const noexcept override - { - return nargs; - } - - virtual void ParseValue(const std::vector &values_) override - { - values.clear(); - - for (const std::string &value : values_) - { - T v; -#ifdef ARGS_NOEXCEPT - if (!reader(name, value, v)) - { - error = Error::Parse; - } -#else - reader(name, value, v); -#endif - values.insert(std::end(values), v); - } - } - - List &Get() noexcept - { - return values; - } - - iterator begin() noexcept - { - return values.begin(); - } - - const_iterator begin() const noexcept - { - return values.begin(); - } - - const_iterator cbegin() const noexcept - { - return values.cbegin(); - } - - iterator end() noexcept - { - return values.end(); - } - - const_iterator end() const noexcept - { - return values.end(); - } - - const_iterator cend() const noexcept - { - return values.cend(); - } - - virtual void Reset() noexcept override - { - FlagBase::Reset(); - values = defaultValues; - } - - virtual FlagBase *Match(const EitherFlag &arg) override - { - const bool wasMatched = Matched(); - auto me = FlagBase::Match(arg); - if (me && !wasMatched) - { - values.clear(); - } - return me; - } - }; - - /** An argument-accepting flag class that pushes the found values into a list - * - * \tparam T the type to extract the argument as - * \tparam List the list type that houses the values - * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined) - */ - template < - typename T, - template class List = std::vector, - typename Reader = ValueReader> - class ValueFlagList : public ValueFlagBase - { - private: - using Container = List; - Container values; - const Container defaultValues; - Reader reader; - - public: - - typedef T value_type; - typedef typename Container::allocator_type allocator_type; - typedef typename Container::pointer pointer; - typedef typename Container::const_pointer const_pointer; - typedef T& reference; - typedef const T& const_reference; - typedef typename Container::size_type size_type; - typedef typename Container::difference_type difference_type; - typedef typename Container::iterator iterator; - typedef typename Container::const_iterator const_iterator; - typedef std::reverse_iterator reverse_iterator; - typedef std::reverse_iterator const_reverse_iterator; - - ValueFlagList(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const Container &defaultValues_ = Container(), Options options_ = {}): - ValueFlagBase(name_, help_, std::move(matcher_), options_), values(defaultValues_), defaultValues(defaultValues_) - { - group_.Add(*this); - } - - virtual ~ValueFlagList() {} - - virtual void ParseValue(const std::vector &values_) override - { - const std::string &value_ = values_.at(0); - - T v; -#ifdef ARGS_NOEXCEPT - if (!reader(name, value_, v)) - { - error = Error::Parse; - } -#else - reader(name, value_, v); -#endif - values.insert(std::end(values), v); - } - - /** Get the values - */ - Container &Get() noexcept - { - return values; - } - - virtual std::string Name() const override - { - return name + std::string("..."); - } - - virtual void Reset() noexcept override - { - ValueFlagBase::Reset(); - values = defaultValues; - } - - virtual FlagBase *Match(const EitherFlag &arg) override - { - const bool wasMatched = Matched(); - auto me = FlagBase::Match(arg); - if (me && !wasMatched) - { - values.clear(); - } - return me; - } - - iterator begin() noexcept - { - return values.begin(); - } - - const_iterator begin() const noexcept - { - return values.begin(); - } - - const_iterator cbegin() const noexcept - { - return values.cbegin(); - } - - iterator end() noexcept - { - return values.end(); - } - - const_iterator end() const noexcept - { - return values.end(); - } - - const_iterator cend() const noexcept - { - return values.cend(); - } - }; - - /** A mapping value flag class - * - * \tparam K the type to extract the argument as - * \tparam T the type to store the result as - * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined) - * \tparam Map The Map type. Should operate like std::map or std::unordered_map - */ - template < - typename K, - typename T, - typename Reader = ValueReader, - template class Map = std::unordered_map> - class MapFlag : public ValueFlagBase - { - private: - const Map map; - T value; - const T defaultValue; - Reader reader; - - protected: - virtual std::vector GetChoicesStrings(const HelpParams &) const override - { - return detail::MapKeysToStrings(map); - } - - public: - - MapFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const Map &map_, const T &defaultValue_, Options options_): ValueFlagBase(name_, help_, std::move(matcher_), options_), map(map_), value(defaultValue_), defaultValue(defaultValue_) - { - group_.Add(*this); - } - - MapFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const Map &map_, const T &defaultValue_ = T(), const bool extraError_ = false): MapFlag(group_, name_, help_, std::move(matcher_), map_, defaultValue_, extraError_ ? Options::Single : Options::None) - { - } - - MapFlag(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const Map &map_, Options options_): MapFlag(group_, name_, help_, std::move(matcher_), map_, T(), options_) - { - } - - virtual ~MapFlag() {} - - virtual void ParseValue(const std::vector &values_) override - { - const std::string &value_ = values_.at(0); - - K key; -#ifdef ARGS_NOEXCEPT - if (!reader(name, value_, key)) - { - error = Error::Parse; - } -#else - reader(name, value_, key); -#endif - auto it = map.find(key); - if (it == std::end(map)) - { - std::ostringstream problem; - problem << "Could not find key '" << key << "' in map for arg '" << name << "'"; -#ifdef ARGS_NOEXCEPT - error = Error::Map; - errorMsg = problem.str(); -#else - throw MapError(problem.str()); -#endif - } else - { - this->value = it->second; - } - } - - /** Get the value - */ - T &Get() noexcept - { - return value; - } - - virtual void Reset() noexcept override - { - ValueFlagBase::Reset(); - value = defaultValue; - } - }; - - /** A mapping value flag list class - * - * \tparam K the type to extract the argument as - * \tparam T the type to store the result as - * \tparam List the list type that houses the values - * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined) - * \tparam Map The Map type. Should operate like std::map or std::unordered_map - */ - template < - typename K, - typename T, - template class List = std::vector, - typename Reader = ValueReader, - template class Map = std::unordered_map> - class MapFlagList : public ValueFlagBase - { - private: - using Container = List; - const Map map; - Container values; - const Container defaultValues; - Reader reader; - - protected: - virtual std::vector GetChoicesStrings(const HelpParams &) const override - { - return detail::MapKeysToStrings(map); - } - - public: - typedef T value_type; - typedef typename Container::allocator_type allocator_type; - typedef typename Container::pointer pointer; - typedef typename Container::const_pointer const_pointer; - typedef T& reference; - typedef const T& const_reference; - typedef typename Container::size_type size_type; - typedef typename Container::difference_type difference_type; - typedef typename Container::iterator iterator; - typedef typename Container::const_iterator const_iterator; - typedef std::reverse_iterator reverse_iterator; - typedef std::reverse_iterator const_reverse_iterator; - - MapFlagList(Group &group_, const std::string &name_, const std::string &help_, Matcher &&matcher_, const Map &map_, const Container &defaultValues_ = Container()): ValueFlagBase(name_, help_, std::move(matcher_)), map(map_), values(defaultValues_), defaultValues(defaultValues_) - { - group_.Add(*this); - } - - virtual ~MapFlagList() {} - - virtual void ParseValue(const std::vector &values_) override - { - const std::string &value = values_.at(0); - - K key; -#ifdef ARGS_NOEXCEPT - if (!reader(name, value, key)) - { - error = Error::Parse; - } -#else - reader(name, value, key); -#endif - auto it = map.find(key); - if (it == std::end(map)) - { - std::ostringstream problem; - problem << "Could not find key '" << key << "' in map for arg '" << name << "'"; -#ifdef ARGS_NOEXCEPT - error = Error::Map; - errorMsg = problem.str(); -#else - throw MapError(problem.str()); -#endif - } else - { - this->values.emplace_back(it->second); - } - } - - /** Get the value - */ - Container &Get() noexcept - { - return values; - } - - virtual std::string Name() const override - { - return name + std::string("..."); - } - - virtual void Reset() noexcept override - { - ValueFlagBase::Reset(); - values = defaultValues; - } - - virtual FlagBase *Match(const EitherFlag &arg) override - { - const bool wasMatched = Matched(); - auto me = FlagBase::Match(arg); - if (me && !wasMatched) - { - values.clear(); - } - return me; - } - - iterator begin() noexcept - { - return values.begin(); - } - - const_iterator begin() const noexcept - { - return values.begin(); - } - - const_iterator cbegin() const noexcept - { - return values.cbegin(); - } - - iterator end() noexcept - { - return values.end(); - } - - const_iterator end() const noexcept - { - return values.end(); - } - - const_iterator cend() const noexcept - { - return values.cend(); - } - }; - - /** A positional argument class - * - * \tparam T the type to extract the argument as - * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined) - */ - template < - typename T, - typename Reader = ValueReader> - class Positional : public PositionalBase - { - private: - T value; - const T defaultValue; - Reader reader; - public: - Positional(Group &group_, const std::string &name_, const std::string &help_, const T &defaultValue_ = T(), Options options_ = {}): PositionalBase(name_, help_, options_), value(defaultValue_), defaultValue(defaultValue_) - { - group_.Add(*this); - } - - Positional(Group &group_, const std::string &name_, const std::string &help_, Options options_): Positional(group_, name_, help_, T(), options_) - { - } - - virtual ~Positional() {} - - virtual void ParseValue(const std::string &value_) override - { -#ifdef ARGS_NOEXCEPT - if (!reader(name, value_, this->value)) - { - error = Error::Parse; - } -#else - reader(name, value_, this->value); -#endif - ready = false; - matched = true; - } - - /** Get the value - */ - T &Get() noexcept - { - return value; - } - - virtual void Reset() noexcept override - { - PositionalBase::Reset(); - value = defaultValue; - } - }; - - /** A positional argument class that pushes the found values into a list - * - * \tparam T the type to extract the argument as - * \tparam List the list type that houses the values - * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined) - */ - template < - typename T, - template class List = std::vector, - typename Reader = ValueReader> - class PositionalList : public PositionalBase - { - private: - using Container = List; - Container values; - const Container defaultValues; - Reader reader; - - public: - typedef T value_type; - typedef typename Container::allocator_type allocator_type; - typedef typename Container::pointer pointer; - typedef typename Container::const_pointer const_pointer; - typedef T& reference; - typedef const T& const_reference; - typedef typename Container::size_type size_type; - typedef typename Container::difference_type difference_type; - typedef typename Container::iterator iterator; - typedef typename Container::const_iterator const_iterator; - typedef std::reverse_iterator reverse_iterator; - typedef std::reverse_iterator const_reverse_iterator; - - PositionalList(Group &group_, const std::string &name_, const std::string &help_, const Container &defaultValues_ = Container(), Options options_ = {}): PositionalBase(name_, help_, options_), values(defaultValues_), defaultValues(defaultValues_) - { - group_.Add(*this); - } - - PositionalList(Group &group_, const std::string &name_, const std::string &help_, Options options_): PositionalList(group_, name_, help_, {}, options_) - { - } - - virtual ~PositionalList() {} - - virtual void ParseValue(const std::string &value_) override - { - T v; -#ifdef ARGS_NOEXCEPT - if (!reader(name, value_, v)) - { - error = Error::Parse; - } -#else - reader(name, value_, v); -#endif - values.insert(std::end(values), v); - matched = true; - } - - virtual std::string Name() const override - { - return name + std::string("..."); - } - - /** Get the values - */ - Container &Get() noexcept - { - return values; - } - - virtual void Reset() noexcept override - { - PositionalBase::Reset(); - values = defaultValues; - } - - virtual PositionalBase *GetNextPositional() override - { - const bool wasMatched = Matched(); - auto me = PositionalBase::GetNextPositional(); - if (me && !wasMatched) - { - values.clear(); - } - return me; - } - - iterator begin() noexcept - { - return values.begin(); - } - - const_iterator begin() const noexcept - { - return values.begin(); - } - - const_iterator cbegin() const noexcept - { - return values.cbegin(); - } - - iterator end() noexcept - { - return values.end(); - } - - const_iterator end() const noexcept - { - return values.end(); - } - - const_iterator cend() const noexcept - { - return values.cend(); - } - }; - - /** A positional argument mapping class - * - * \tparam K the type to extract the argument as - * \tparam T the type to store the result as - * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined) - * \tparam Map The Map type. Should operate like std::map or std::unordered_map - */ - template < - typename K, - typename T, - typename Reader = ValueReader, - template class Map = std::unordered_map> - class MapPositional : public PositionalBase - { - private: - const Map map; - T value; - const T defaultValue; - Reader reader; - - protected: - virtual std::vector GetChoicesStrings(const HelpParams &) const override - { - return detail::MapKeysToStrings(map); - } - - public: - - MapPositional(Group &group_, const std::string &name_, const std::string &help_, const Map &map_, const T &defaultValue_ = T(), Options options_ = {}): - PositionalBase(name_, help_, options_), map(map_), value(defaultValue_), defaultValue(defaultValue_) - { - group_.Add(*this); - } - - virtual ~MapPositional() {} - - virtual void ParseValue(const std::string &value_) override - { - K key; -#ifdef ARGS_NOEXCEPT - if (!reader(name, value_, key)) - { - error = Error::Parse; - } -#else - reader(name, value_, key); -#endif - auto it = map.find(key); - if (it == std::end(map)) - { - std::ostringstream problem; - problem << "Could not find key '" << key << "' in map for arg '" << name << "'"; -#ifdef ARGS_NOEXCEPT - error = Error::Map; - errorMsg = problem.str(); -#else - throw MapError(problem.str()); -#endif - } else - { - this->value = it->second; - ready = false; - matched = true; - } - } - - /** Get the value - */ - T &Get() noexcept - { - return value; - } - - virtual void Reset() noexcept override - { - PositionalBase::Reset(); - value = defaultValue; - } - }; - - /** A positional argument mapping list class - * - * \tparam K the type to extract the argument as - * \tparam T the type to store the result as - * \tparam List the list type that houses the values - * \tparam Reader The functor type used to read the argument, taking the name, value, and destination reference with operator(), and returning a bool (if ARGS_NOEXCEPT is defined) - * \tparam Map The Map type. Should operate like std::map or std::unordered_map - */ - template < - typename K, - typename T, - template class List = std::vector, - typename Reader = ValueReader, - template class Map = std::unordered_map> - class MapPositionalList : public PositionalBase - { - private: - using Container = List; - - const Map map; - Container values; - const Container defaultValues; - Reader reader; - - protected: - virtual std::vector GetChoicesStrings(const HelpParams &) const override - { - return detail::MapKeysToStrings(map); - } - - public: - typedef T value_type; - typedef typename Container::allocator_type allocator_type; - typedef typename Container::pointer pointer; - typedef typename Container::const_pointer const_pointer; - typedef T& reference; - typedef const T& const_reference; - typedef typename Container::size_type size_type; - typedef typename Container::difference_type difference_type; - typedef typename Container::iterator iterator; - typedef typename Container::const_iterator const_iterator; - typedef std::reverse_iterator reverse_iterator; - typedef std::reverse_iterator const_reverse_iterator; - - MapPositionalList(Group &group_, const std::string &name_, const std::string &help_, const Map &map_, const Container &defaultValues_ = Container(), Options options_ = {}): - PositionalBase(name_, help_, options_), map(map_), values(defaultValues_), defaultValues(defaultValues_) - { - group_.Add(*this); - } - - virtual ~MapPositionalList() {} - - virtual void ParseValue(const std::string &value_) override - { - K key; -#ifdef ARGS_NOEXCEPT - if (!reader(name, value_, key)) - { - error = Error::Parse; - } -#else - reader(name, value_, key); -#endif - auto it = map.find(key); - if (it == std::end(map)) - { - std::ostringstream problem; - problem << "Could not find key '" << key << "' in map for arg '" << name << "'"; -#ifdef ARGS_NOEXCEPT - error = Error::Map; - errorMsg = problem.str(); -#else - throw MapError(problem.str()); -#endif - } else - { - this->values.emplace_back(it->second); - matched = true; - } - } - - /** Get the value - */ - Container &Get() noexcept - { - return values; - } - - virtual std::string Name() const override - { - return name + std::string("..."); - } - - virtual void Reset() noexcept override - { - PositionalBase::Reset(); - values = defaultValues; - } - - virtual PositionalBase *GetNextPositional() override - { - const bool wasMatched = Matched(); - auto me = PositionalBase::GetNextPositional(); - if (me && !wasMatched) - { - values.clear(); - } - return me; - } - - iterator begin() noexcept - { - return values.begin(); - } - - const_iterator begin() const noexcept - { - return values.begin(); - } - - const_iterator cbegin() const noexcept - { - return values.cbegin(); - } - - iterator end() noexcept - { - return values.end(); - } - - const_iterator end() const noexcept - { - return values.end(); - } - - const_iterator cend() const noexcept - { - return values.cend(); - } - }; -} - -#endif diff --git a/Source/Externals/hypothesis/LICENSE b/Source/Externals/hypothesis/LICENSE new file mode 100644 index 0000000000..63c372d442 --- /dev/null +++ b/Source/Externals/hypothesis/LICENSE @@ -0,0 +1,21 @@ +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Source/Externals/hypothesis/README.md b/Source/Externals/hypothesis/README.md new file mode 100644 index 0000000000..7a6e859a38 --- /dev/null +++ b/Source/Externals/hypothesis/README.md @@ -0,0 +1,7 @@ +# hypothesis.h +## A collection of quantiles and utility functions for running Z, Chi^2, and Student's T hypothesis tests + +A variety of quantile functions are needed to perform statistical hypothesis +tests, but these are missing from the C++ standard library. This compact header +file-only library contains the most important quantiles; it is mostly a wrapper +around a C++ port of the relevant functions from the Cephes math library. diff --git a/Source/Externals/hypothesis/cephes.h b/Source/Externals/hypothesis/cephes.h new file mode 100644 index 0000000000..2bad421784 --- /dev/null +++ b/Source/Externals/hypothesis/cephes.h @@ -0,0 +1,404 @@ +/* + cephes.h: A subset of cephes math routines used by hypothesis.h + + Redistributed under the BSD license with permission of the author, see + https://github.com/deepmind/torch-cephes/blob/master/LICENSE.txt + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include +#include + +namespace cephes { + static const double biginv = 2.22044604925031308085e-16; + static const double big = 4.503599627370496e15; + static const double MAXGAM = 171.624376956302725; + static const double MACHEP = 1.11022302462515654042E-16; + static const double MAXLOG = 7.09782712893383996843E2; + static const double MINLOG = -7.08396418532264106224E2; + + /* Forward declarations */ + static double pseries(double a, double b, double x); + static double incbd(double a, double b, double x); + static double incbcf(double a, double b, double x); + + inline double incbet(double aa, double bb, double xx) { + double a, b, t, x, xc, w, y; + int flag; + + if (aa <= 0.0 || bb <= 0.0) + goto domerr; + + if ((xx <= 0.0) || (xx >= 1.0)) { + if (xx == 0.0) + return 0.0; + if (xx == 1.0) + return 1.0; + domerr: + throw std::runtime_error("incbet: domain error!"); + } + + flag = 0; + if ((bb * xx) <= 1.0 && xx <= 0.95) { + t = pseries(aa, bb, xx); + goto done; + } + + w = 1.0 - xx; + + /* Reverse a and b if x is greater than the mean. */ + if (xx > (aa / (aa + bb))) { + flag = 1; + a = bb; + b = aa; + xc = xx; + x = w; + } else { + a = aa; + b = bb; + xc = w; + x = xx; + } + + if (flag == 1 && (b * x) <= 1.0 && x <= 0.95) { + t = pseries(a, b, x); + goto done; + } + + /* Choose expansion for better convergence. */ + y = x * (a + b - 2.0) - (a - 1.0); + if (y < 0.0) + w = incbcf(a, b, x); + else + w = incbd(a, b, x) / xc; + + /* Multiply w by the factor + a b _ _ _ + x (1-x) | (a+b) / ( a | (a) | (b) ) . */ + + y = a * std::log(x); + t = b * std::log(xc); + if ((a + b) < MAXGAM && std::abs(y) < MAXLOG && std::abs(t) < MAXLOG) { + t = pow(xc, b); + t *= pow(x, a); + t /= a; + t *= w; + t *= std::tgamma(a + b) / (std::tgamma(a) * std::tgamma(b)); + goto done; + } + /* Resort to logarithms. */ + y += t + std::lgamma(a + b) - std::lgamma(a) - std::lgamma(b); + y += std::log(w / a); + if (y < MINLOG) + t = 0.0; + else + t = std::exp(y); + + done: + + if (flag == 1) { + if (t <= MACHEP) + t = 1.0 - MACHEP; + else + t = 1.0 - t; + } + return t; + } + + /* Continued fraction expansion #1 + * for incomplete beta integral + */ + inline static double incbcf(double a, double b, double x) { + double xk, pk, pkm1, pkm2, qk, qkm1, qkm2; + double k1, k2, k3, k4, k5, k6, k7, k8; + double r, t, ans, thresh; + int n; + + k1 = a; + k2 = a + b; + k3 = a; + k4 = a + 1.0; + k5 = 1.0; + k6 = b - 1.0; + k7 = k4; + k8 = a + 2.0; + + pkm2 = 0.0; + qkm2 = 1.0; + pkm1 = 1.0; + qkm1 = 1.0; + ans = 1.0; + r = 1.0; + n = 0; + thresh = 3.0 * MACHEP; + do { + + xk = -(x * k1 * k2) / (k3 * k4); + pk = pkm1 + pkm2 * xk; + qk = qkm1 + qkm2 * xk; + pkm2 = pkm1; + pkm1 = pk; + qkm2 = qkm1; + qkm1 = qk; + + xk = (x * k5 * k6) / (k7 * k8); + pk = pkm1 + pkm2 * xk; + qk = qkm1 + qkm2 * xk; + pkm2 = pkm1; + pkm1 = pk; + qkm2 = qkm1; + qkm1 = qk; + + if (qk != 0) + r = pk / qk; + if (r != 0) { + t = std::abs((ans - r) / r); + ans = r; + } else + t = 1.0; + + if (t < thresh) + goto cdone; + + k1 += 1.0; + k2 += 1.0; + k3 += 2.0; + k4 += 2.0; + k5 += 1.0; + k6 -= 1.0; + k7 += 2.0; + k8 += 2.0; + + if ((std::abs(qk) + std::abs(pk)) > big) { + pkm2 *= biginv; + pkm1 *= biginv; + qkm2 *= biginv; + qkm1 *= biginv; + } + if ((std::abs(qk) < biginv) || (std::abs(pk) < biginv)) { + pkm2 *= big; + pkm1 *= big; + qkm2 *= big; + qkm1 *= big; + } + } while (++n < 300); + + cdone: + return (ans); + } + + /* Continued fraction expansion #2 + * for incomplete beta integral + */ + inline static double incbd(double a, double b, double x) { + double xk, pk, pkm1, pkm2, qk, qkm1, qkm2; + double k1, k2, k3, k4, k5, k6, k7, k8; + double r, t, ans, z, thresh; + int n; + + k1 = a; + k2 = b - 1.0; + k3 = a; + k4 = a + 1.0; + k5 = 1.0; + k6 = a + b; + k7 = a + 1.0; + k8 = a + 2.0; + + pkm2 = 0.0; + qkm2 = 1.0; + pkm1 = 1.0; + qkm1 = 1.0; + z = x / (1.0 - x); + ans = 1.0; + r = 1.0; + n = 0; + thresh = 3.0 * MACHEP; + do { + + xk = -(z * k1 * k2) / (k3 * k4); + pk = pkm1 + pkm2 * xk; + qk = qkm1 + qkm2 * xk; + pkm2 = pkm1; + pkm1 = pk; + qkm2 = qkm1; + qkm1 = qk; + + xk = (z * k5 * k6) / (k7 * k8); + pk = pkm1 + pkm2 * xk; + qk = qkm1 + qkm2 * xk; + pkm2 = pkm1; + pkm1 = pk; + qkm2 = qkm1; + qkm1 = qk; + + if (qk != 0) + r = pk / qk; + if (r != 0) { + t = std::abs((ans - r) / r); + ans = r; + } else + t = 1.0; + + if (t < thresh) + goto cdone; + + k1 += 1.0; + k2 -= 1.0; + k3 += 2.0; + k4 += 2.0; + k5 += 1.0; + k6 += 1.0; + k7 += 2.0; + k8 += 2.0; + + if ((std::abs(qk) + std::abs(pk)) > big) { + pkm2 *= biginv; + pkm1 *= biginv; + qkm2 *= biginv; + qkm1 *= biginv; + } + if ((std::abs(qk) < biginv) || (std::abs(pk) < biginv)) { + pkm2 *= big; + pkm1 *= big; + qkm2 *= big; + qkm1 *= big; + } + } while (++n < 300); + cdone: + return (ans); + } + + /* Power series for incomplete beta integral. + Use when b*x is small and x not too close to 1. */ + inline static double pseries(double a, double b, double x) { + double s, t, u, v, n, t1, z, ai; + + ai = 1.0 / a; + u = (1.0 - b) * x; + v = u / (a + 1.0); + t1 = v; + t = u; + n = 2.0; + s = 0.0; + z = MACHEP * ai; + while (std::abs(v) > z) { + u = (n - b) * x / n; + t *= u; + v = t / (a + n); + s += v; + n += 1.0; + } + s += t1; + s += ai; + + u = a * std::log(x); + if ((a + b) < MAXGAM && std::abs(u) < MAXLOG) { + t = std::tgamma(a + b) / (std::tgamma(a) * std::tgamma(b)); + s = s * t * pow(x, a); + } else { + t = std::lgamma(a + b) - std::lgamma(a) - std::lgamma(b) + u + std::log(s); + if (t < MINLOG) + s = 0.0; + else + s = std::exp(t); + } + return s; + } + + /// Regularized lower incomplete gamma function + inline double rlgamma(double a, double x) { + const double epsilon = 0.000000000000001; + + if (a < 0 || x < 0) + throw std::runtime_error("LLGamma: invalid arguments range!"); + + if (x == 0) + return 0.0; + + double ax = (a * std::log(x)) - x - std::lgamma(a); + if (ax < -709.78271289338399) + return a < x ? 1.0 : 0.0; + + if (x <= 1 || x <= a) { + double r2 = a; + double c2 = 1; + double ans2 = 1; + + do { + r2 = r2 + 1; + c2 = c2 * x / r2; + ans2 += c2; + } while ((c2 / ans2) > epsilon); + + return std::exp(ax) * ans2 / a; + } + + int c = 0; + double y = 1 - a; + double z = x + y + 1; + double p3 = 1; + double q3 = x; + double p2 = x + 1; + double q2 = z * x; + double ans = p2 / q2; + double error; + + do { + c++; + y += 1; + z += 2; + double yc = y * c; + double p = (p2 * z) - (p3 * yc); + double q = (q2 * z) - (q3 * yc); + + if (q != 0) { + double nextans = p / q; + error = std::abs((ans - nextans) / nextans); + ans = nextans; + } else { + // zero div, skip + error = 1; + } + + // shift + p3 = p2; + p2 = p; + q3 = q2; + q2 = q; + + // normalize fraction when the numerator becomes large + if (std::abs(p) > big) { + p3 *= biginv; + p2 *= biginv; + q3 *= biginv; + q2 *= biginv; + } + } while (error > epsilon); + + return 1.0 - (std::exp(ax) * ans); + } +}; diff --git a/Source/Externals/hypothesis/hypothesis.h b/Source/Externals/hypothesis/hypothesis.h new file mode 100644 index 0000000000..31b1cb4fda --- /dev/null +++ b/Source/Externals/hypothesis/hypothesis.h @@ -0,0 +1,355 @@ +/* + hypothesis.h: A collection of quantile and quadrature routines + for Z, Chi^2, and Student's T hypothesis tests. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include "cephes.h" + +namespace hypothesis { + /// Cumulative distribution function of the standard normal distribution + inline double stdnormal_cdf(double x) { + return std::erfc(-x/std::sqrt(2.0))*0.5; + } + + /// Cumulative distribution function of the Chi^2 distribution + inline double chi2_cdf(double x, int dof) { + if (dof < 1 || x < 0) { + return 0.0; + } else if (dof == 2) { + return 1.0 - std::exp(-0.5*x); + } else { + return cephes::rlgamma(0.5 * dof, 0.5 * x); + } + } + + /// Cumulative distribution function of Student's T distribution + inline double students_t_cdf(double x, int dof) { + if (x > 0) + return 1-0.5*cephes::incbet(dof * 0.5, 0.5, dof/(x*x+dof)); + else + return 0.5*cephes::incbet(dof * 0.5, 0.5, dof/(x*x+dof)); + } + + /// adaptive Simpson integration over an 1D interval + inline double adaptiveSimpson(const std::function &f, double x0, double x1, double eps = 1e-6, int depth = 6) { + int count = 0; + /* Define an recursive lambda function for integration over subintervals */ + std::function integrate = + [&](double a, double b, double c, double fa, double fb, double fc, double I, double eps, int depth) { + /* Evaluate the function at two intermediate points */ + double d = 0.5 * (a + b), e = 0.5 * (b + c), fd = f(d), fe = f(e); + + /* Simpson integration over each subinterval */ + double h = c-a, + I0 = (1.0/12.0) * h * (fa + 4.0*fd + fb), + I1 = (1.0/12.0) * h * (fb + 4.0*fe + fc), + Ip = I0+I1; + ++count; + + /* Stopping criterion from J.N. Lyness (1969) + "Notes on the adaptive Simpson quadrature routine" */ + if (depth <= 0 || std::abs(Ip-I) < 15.0*eps) { + // Richardson extrapolation + return Ip + (1.0/15.0) * (Ip-I); + } + + return integrate(a, d, b, fa, fd, fb, I0, 0.5*eps, depth-1) + + integrate(b, e, c, fb, fe, fc, I1, 0.5*eps, depth-1); + }; + double a = x0, b = 0.5 * (x0+x1), c = x1; + double fa = f(a), fb = f(b), fc = f(c); + double I = (c-a) * (1.0/6.0) * (fa+4.0*fb+fc); + return integrate(a, b, c, fa, fb, fc, I, eps, depth); + } + + /// Nested adaptive Simpson integration over a 2D rectangle + inline double adaptiveSimpson2D(const std::function &f, double x0, double y0, + double x1, double y1, double eps = 1e-6, int depth = 6) { + /* Lambda function that integrates over the X axis */ + auto integrate = [&](double y) { + return adaptiveSimpson(std::bind(f, std::placeholders::_1, y), x0, x1, eps, depth); + }; + double value = adaptiveSimpson(integrate, y0, y1, eps, depth); + return value; + } + + /** + * Peform a Chi^2 test based on the given frequency tables + * + * \param nCells + * Total number of table cells + * + * \param obsFrequencies + * Observed cell frequencies in each cell + * + * \param expFrequencies + * Integrated cell frequencies in each cell (i.e. the noise-free reference) + * + * \param sampleCount + * Total observed sample count + * + * \param minExpFrequency + * Minimum expected cell frequency. The chi^2 test does not work reliably + * when the expected frequency in a cell is low (e.g. less than 5), because + * normality assumptions break down in this case. Therefore, the + * implementation will merge such low-frequency cells when they fall below + * the threshold specified here. + * + * \param significanceLevel + * The null hypothesis will be rejected when the associated + * p-value is below the significance level specified here. + * + * \param numTests + * Specifies the total number of tests that will be executed. If greater than one, + * the Sidak correction will be applied to the significance level. This is because + * by conducting multiple independent hypothesis tests in sequence, the probability + * of a failure increases accordingly. + * + * \return + * A pair of values containing the test result (success: \c true and failure: \c false) + * and a descriptive string + */ + inline std::pair chi2_test( + int nCells, const double *obsFrequencies, const double *expFrequencies, + int sampleCount, double minExpFrequency, double significanceLevel, int numTests = 1) { + + struct Cell { + double expFrequency; + size_t index; + }; + + /* Sort all cells by their expected frequencies */ + std::vector cells(nCells); + for (size_t i=0; i sampleCount * 1e-5) { + /* Uh oh: samples in a cell that should be completely empty + according to the probability density function. Ordinarily, + even a single sample requires immediate rejection of the null + hypothesis. But due to finite-precision computations and rounding + errors, this can occasionally happen without there being an + actual bug. Therefore, the criterion here is a bit more lenient. */ + + oss << "Encountered " << obsFrequencies[c.index] << " samples in a cell " + << "with expected frequency 0. Rejecting the null hypothesis!" << std::endl; + return std::make_pair(false, oss.str()); + } + } else if (expFrequencies[c.index] < minExpFrequency) { + /* Pool cells with low expected frequencies */ + pooledFrequencies += obsFrequencies[c.index]; + pooledExpFrequencies += expFrequencies[c.index]; + pooledCells++; + } else if (pooledExpFrequencies > 0 && pooledExpFrequencies < minExpFrequency) { + /* Keep on pooling cells until a sufficiently high + expected frequency is achieved. */ + pooledFrequencies += obsFrequencies[c.index]; + pooledExpFrequencies += expFrequencies[c.index]; + pooledCells++; + } else { + double diff = obsFrequencies[c.index] - expFrequencies[c.index]; + chsq += (diff*diff) / expFrequencies[c.index]; + ++dof; + } + } + + if (pooledExpFrequencies > 0 || pooledFrequencies > 0) { + oss << "Pooled " << pooledCells << " to ensure sufficiently high expected " + "cell frequencies (>" << minExpFrequency << ")" << std::endl; + double diff = pooledFrequencies - pooledExpFrequencies; + chsq += (diff*diff) / pooledExpFrequencies; + ++dof; + } + + /* All parameters are assumed to be known, so there is no + additional DF reduction due to model parameters */ + dof -= 1; + + if (dof <= 0) { + oss << "The number of degrees of freedom (" << dof << ") is too low!" << std::endl; + return std::make_pair(false, oss.str()); + } + + oss << "Chi^2 statistic = " << chsq << " (d.o.f. = " << dof << ")" << std::endl; + + /* Probability of obtaining a test statistic at least + as extreme as the one observed under the assumption + that the distributions match */ + double pval = 1 - (double) chi2_cdf(chsq, dof); + + /* Apply the Sidak correction term, since we'll be conducting multiple independent + hypothesis tests. This accounts for the fact that the probability of a failure + increases quickly when several hypothesis tests are run in sequence. */ + double alpha = 1.0 - std::pow(1.0 - significanceLevel, 1.0 / numTests); + + bool result = false; + if (pval < alpha || !std::isfinite(pval)) { + oss << "***** Rejected ***** the null hypothesis (p-value = " << pval << ", " + "significance level = " << alpha << ")" << std::endl; + } else { + oss << "Accepted the null hypothesis (p-value = " << pval << ", " + "significance level = " << alpha << ")" << std::endl; + result = true; + } + return std::make_pair(result, oss.str()); + } + + /// Write 2D Chi^2 frequency tables to disk in a format that is nicely plottable by Octave and MATLAB + inline void chi2_dump(int res1, int res2, const double *obsFrequencies, const double *expFrequencies, const std::string &filename) { + std::ofstream f(filename); + + f << "obsFrequencies = [ "; + for (int i=0; i + students_t_test(double mean, double variance, double reference, + int sampleCount, double significanceLevel, int numTests) { + std::ostringstream oss; + + /* Compute the t statistic */ + double t = std::abs(mean - reference) * std::sqrt(sampleCount / std::max(variance, 1e-5)); + + /* Determine the degrees of freedom, and instantiate a matching distribution object */ + int dof = sampleCount - 1; + + oss << "Sample mean = " << mean << " (reference value = " << reference << ")" << std::endl; + oss << "Sample variance = " << variance << std::endl; + oss << "t-statistic = " << t << " (d.o.f. = " << dof << ")" << std::endl; + + /* Compute the p-value */ + double pval = 2 * (1 - students_t_cdf(t, dof)); + + /* Apply the Sidak correction term, since we'll be conducting multiple independent + hypothesis tests. This accounts for the fact that the probability of a failure + increases quickly when several hypothesis tests are run in sequence. */ + double alpha = 1.0 - std::pow(1.0 - significanceLevel, 1.0 / numTests); + + bool result = false; + if (pval < alpha) { + oss << "***** Rejected ***** the null hypothesis (p-value = " << pval << ", " + "significance level = " << alpha << ")" << std::endl; + } else { + oss << "Accepted the null hypothesis (p-value = " << pval << ", " + "significance level = " << alpha << ")" << std::endl; + result = true; + } + return std::make_pair(result, oss.str()); + } +}; /* namespace hypothesis */ diff --git a/Source/Externals/mikktspace/README.md b/Source/Externals/mikktspace/README.md deleted file mode 100644 index 7704b7edcb..0000000000 --- a/Source/Externals/mikktspace/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# mikktspace - -This is a copy of [Morten S. Mikkelsen](http://mmikkelsen3d.blogspot.fr/)'s tangent space code written during his master thesis. - -More info on [wiki.blender](http://wiki.blender.org/index.php/Dev:Shading/Tangent_Space_Normal_Maps). diff --git a/Source/Externals/mikktspace/mikktspace.c b/Source/Externals/mikktspace/mikktspace.c deleted file mode 100644 index 2b952f4ac6..0000000000 --- a/Source/Externals/mikktspace/mikktspace.c +++ /dev/null @@ -1,1890 +0,0 @@ -/** \file mikktspace/mikktspace.c - * \ingroup mikktspace - */ -/** - * Copyright (C) 2011 by Morten S. Mikkelsen - * - * This software is provided 'as-is', without any express or implied - * warranty. In no event will the authors be held liable for any damages - * arising from the use of this software. - * - * Permission is granted to anyone to use this software for any purpose, - * including commercial applications, and to alter it and redistribute it - * freely, subject to the following restrictions: - * - * 1. The origin of this software must not be misrepresented; you must not - * claim that you wrote the original software. If you use this software - * in a product, an acknowledgment in the product documentation would be - * appreciated but is not required. - * 2. Altered source versions must be plainly marked as such, and must not be - * misrepresented as being the original software. - * 3. This notice may not be removed or altered from any source distribution. - */ - -#include -#include -#include -#include -#include -#include - -#include "mikktspace.h" - -#define TFALSE 0 -#define TTRUE 1 - -#ifndef M_PI -#define M_PI 3.1415926535897932384626433832795 -#endif - -#define INTERNAL_RND_SORT_SEED 39871946 - -// internal structure -typedef struct { - float x, y, z; -} SVec3; - -static tbool veq( const SVec3 v1, const SVec3 v2 ) -{ - return (v1.x == v2.x) && (v1.y == v2.y) && (v1.z == v2.z); -} - -static SVec3 vadd( const SVec3 v1, const SVec3 v2 ) -{ - SVec3 vRes; - - vRes.x = v1.x + v2.x; - vRes.y = v1.y + v2.y; - vRes.z = v1.z + v2.z; - - return vRes; -} - - -static SVec3 vsub( const SVec3 v1, const SVec3 v2 ) -{ - SVec3 vRes; - - vRes.x = v1.x - v2.x; - vRes.y = v1.y - v2.y; - vRes.z = v1.z - v2.z; - - return vRes; -} - -static SVec3 vscale(const float fS, const SVec3 v) -{ - SVec3 vRes; - - vRes.x = fS * v.x; - vRes.y = fS * v.y; - vRes.z = fS * v.z; - - return vRes; -} - -static float LengthSquared( const SVec3 v ) -{ - return v.x*v.x + v.y*v.y + v.z*v.z; -} - -static float Length( const SVec3 v ) -{ - return sqrtf(LengthSquared(v)); -} - -static SVec3 Normalize( const SVec3 v ) -{ - return vscale(1 / Length(v), v); -} - -static float vdot( const SVec3 v1, const SVec3 v2) -{ - return v1.x*v2.x + v1.y*v2.y + v1.z*v2.z; -} - - -static tbool NotZero(const float fX) -{ - // could possibly use FLT_EPSILON instead - return fabsf(fX) > FLT_MIN; -} - -static tbool VNotZero(const SVec3 v) -{ - // might change this to an epsilon based test - return NotZero(v.x) || NotZero(v.y) || NotZero(v.z); -} - - - -typedef struct { - int iNrFaces; - int * pTriMembers; -} SSubGroup; - -typedef struct { - int iNrFaces; - int * pFaceIndices; - int iVertexRepresentitive; - tbool bOrientPreservering; -} SGroup; - -// -#define MARK_DEGENERATE 1 -#define QUAD_ONE_DEGEN_TRI 2 -#define GROUP_WITH_ANY 4 -#define ORIENT_PRESERVING 8 - - - -typedef struct { - int FaceNeighbors[3]; - SGroup * AssignedGroup[3]; - - // normalized first order face derivatives - SVec3 vOs, vOt; - float fMagS, fMagT; // original magnitudes - - // determines if the current and the next triangle are a quad. - int iOrgFaceNumber; - int iFlag, iTSpacesOffs; - unsigned char vert_num[4]; -} STriInfo; - -typedef struct { - SVec3 vOs; - float fMagS; - SVec3 vOt; - float fMagT; - int iCounter; // this is to average back into quads. - tbool bOrient; -} STSpace; - -static int GenerateInitialVerticesIndexList(STriInfo pTriInfos[], int piTriList_out[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn); -static void GenerateSharedVerticesIndexList(int piTriList_in_and_out[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn); -static void InitTriInfo(STriInfo pTriInfos[], const int piTriListIn[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn); -static int Build4RuleGroups(STriInfo pTriInfos[], SGroup pGroups[], int piGroupTrianglesBuffer[], const int piTriListIn[], const int iNrTrianglesIn); -static tbool GenerateTSpaces(STSpace psTspace[], const STriInfo pTriInfos[], const SGroup pGroups[], - const int iNrActiveGroups, const int piTriListIn[], const float fThresCos, - const SMikkTSpaceContext * pContext); - -static int MakeIndex(const int iFace, const int iVert) -{ - assert(iVert>=0 && iVert<4 && iFace>=0); - return (iFace<<2) | (iVert&0x3); -} - -static void IndexToData(int * piFace, int * piVert, const int iIndexIn) -{ - piVert[0] = iIndexIn&0x3; - piFace[0] = iIndexIn>>2; -} - -static STSpace AvgTSpace(const STSpace * pTS0, const STSpace * pTS1) -{ - STSpace ts_res; - - // this if is important. Due to floating point precision - // averaging when ts0==ts1 will cause a slight difference - // which results in tangent space splits later on - if (pTS0->fMagS==pTS1->fMagS && pTS0->fMagT==pTS1->fMagT && - veq(pTS0->vOs,pTS1->vOs) && veq(pTS0->vOt, pTS1->vOt)) - { - ts_res.fMagS = pTS0->fMagS; - ts_res.fMagT = pTS0->fMagT; - ts_res.vOs = pTS0->vOs; - ts_res.vOt = pTS0->vOt; - } - else - { - ts_res.fMagS = 0.5f*(pTS0->fMagS+pTS1->fMagS); - ts_res.fMagT = 0.5f*(pTS0->fMagT+pTS1->fMagT); - ts_res.vOs = vadd(pTS0->vOs,pTS1->vOs); - ts_res.vOt = vadd(pTS0->vOt,pTS1->vOt); - if ( VNotZero(ts_res.vOs) ) ts_res.vOs = Normalize(ts_res.vOs); - if ( VNotZero(ts_res.vOt) ) ts_res.vOt = Normalize(ts_res.vOt); - } - - return ts_res; -} - - - -static SVec3 GetPosition(const SMikkTSpaceContext * pContext, const int index); -static SVec3 GetNormal(const SMikkTSpaceContext * pContext, const int index); -static SVec3 GetTexCoord(const SMikkTSpaceContext * pContext, const int index); - - -// degen triangles -static void DegenPrologue(STriInfo pTriInfos[], int piTriList_out[], const int iNrTrianglesIn, const int iTotTris); -static void DegenEpilogue(STSpace psTspace[], STriInfo pTriInfos[], int piTriListIn[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn, const int iTotTris); - - -tbool genTangSpaceDefault(const SMikkTSpaceContext * pContext) -{ - return genTangSpace(pContext, 180.0f); -} - -tbool genTangSpace(const SMikkTSpaceContext * pContext, const float fAngularThreshold) -{ - // count nr_triangles - int * piTriListIn = NULL, * piGroupTrianglesBuffer = NULL; - STriInfo * pTriInfos = NULL; - SGroup * pGroups = NULL; - STSpace * psTspace = NULL; - int iNrTrianglesIn = 0, f=0, t=0, i=0; - int iNrTSPaces = 0, iTotTris = 0, iDegenTriangles = 0, iNrMaxGroups = 0; - int iNrActiveGroups = 0, index = 0; - const int iNrFaces = pContext->m_pInterface->m_getNumFaces(pContext); - tbool bRes = TFALSE; - const float fThresCos = (float) cos((fAngularThreshold*(float)M_PI)/180.0f); - - // verify all call-backs have been set - if ( pContext->m_pInterface->m_getNumFaces==NULL || - pContext->m_pInterface->m_getNumVerticesOfFace==NULL || - pContext->m_pInterface->m_getPosition==NULL || - pContext->m_pInterface->m_getNormal==NULL || - pContext->m_pInterface->m_getTexCoord==NULL ) - return TFALSE; - - // count triangles on supported faces - for (f=0; fm_pInterface->m_getNumVerticesOfFace(pContext, f); - if (verts==3) ++iNrTrianglesIn; - else if (verts==4) iNrTrianglesIn += 2; - } - if (iNrTrianglesIn<=0) return TFALSE; - - // allocate memory for an index list - piTriListIn = (int *) malloc(sizeof(int)*3*iNrTrianglesIn); - pTriInfos = (STriInfo *) malloc(sizeof(STriInfo)*iNrTrianglesIn); - if (piTriListIn==NULL || pTriInfos==NULL) - { - if (piTriListIn!=NULL) free(piTriListIn); - if (pTriInfos!=NULL) free(pTriInfos); - return TFALSE; - } - - // make an initial triangle --> face index list - iNrTSPaces = GenerateInitialVerticesIndexList(pTriInfos, piTriListIn, pContext, iNrTrianglesIn); - - // make a welded index list of identical positions and attributes (pos, norm, texc) - //printf("gen welded index list begin\n"); - GenerateSharedVerticesIndexList(piTriListIn, pContext, iNrTrianglesIn); - //printf("gen welded index list end\n"); - - // Mark all degenerate triangles - iTotTris = iNrTrianglesIn; - iDegenTriangles = 0; - for (t=0; tm_pInterface->m_getNumVerticesOfFace(pContext, f); - if (verts!=3 && verts!=4) continue; - - - // I've decided to let degenerate triangles and group-with-anythings - // vary between left/right hand coordinate systems at the vertices. - // All healthy triangles on the other hand are built to always be either or. - - /*// force the coordinate system orientation to be uniform for every face. - // (this is already the case for good triangles but not for - // degenerate ones and those with bGroupWithAnything==true) - bool bOrient = psTspace[index].bOrient; - if (psTspace[index].iCounter == 0) // tspace was not derived from a group - { - // look for a space created in GenerateTSpaces() by iCounter>0 - bool bNotFound = true; - int i=1; - while (i 0) bNotFound=false; - else ++i; - } - if (!bNotFound) bOrient = psTspace[index+i].bOrient; - }*/ - - // set data - for (i=0; ivOs.x, pTSpace->vOs.y, pTSpace->vOs.z}; - float bitang[] = {pTSpace->vOt.x, pTSpace->vOt.y, pTSpace->vOt.z}; - if (pContext->m_pInterface->m_setTSpace!=NULL) - pContext->m_pInterface->m_setTSpace(pContext, tang, bitang, pTSpace->fMagS, pTSpace->fMagT, pTSpace->bOrient, f, i); - if (pContext->m_pInterface->m_setTSpaceBasic!=NULL) - pContext->m_pInterface->m_setTSpaceBasic(pContext, tang, pTSpace->bOrient==TTRUE ? 1.0f : (-1.0f), f, i); - - ++index; - } - } - - free(psTspace); - - - return TTRUE; -} - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -typedef struct { - float vert[3]; - int index; -} STmpVert; - -static const int g_iCells = 2048; - -#ifdef _MSC_VER - #define NOINLINE __declspec(noinline) -#else - #define NOINLINE __attribute__ ((noinline)) -#endif - -// it is IMPORTANT that this function is called to evaluate the hash since -// inlining could potentially reorder instructions and generate different -// results for the same effective input value fVal. -static NOINLINE int FindGridCell(const float fMin, const float fMax, const float fVal) -{ - const float fIndex = g_iCells * ((fVal-fMin)/(fMax-fMin)); - const int iIndex = (int)fIndex; - return iIndex < g_iCells ? (iIndex >= 0 ? iIndex : 0) : (g_iCells - 1); -} - -static void MergeVertsFast(int piTriList_in_and_out[], STmpVert pTmpVert[], const SMikkTSpaceContext * pContext, const int iL_in, const int iR_in); -static void MergeVertsSlow(int piTriList_in_and_out[], const SMikkTSpaceContext * pContext, const int pTable[], const int iEntries); -static void GenerateSharedVerticesIndexListSlow(int piTriList_in_and_out[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn); - -static void GenerateSharedVerticesIndexList(int piTriList_in_and_out[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn) -{ - - // Generate bounding box - int * piHashTable=NULL, * piHashCount=NULL, * piHashOffsets=NULL, * piHashCount2=NULL; - STmpVert * pTmpVert = NULL; - int i=0, iChannel=0, k=0, e=0; - int iMaxCount=0; - SVec3 vMin = GetPosition(pContext, 0), vMax = vMin, vDim; - float fMin, fMax; - for (i=1; i<(iNrTrianglesIn*3); i++) - { - const int index = piTriList_in_and_out[i]; - - const SVec3 vP = GetPosition(pContext, index); - if (vMin.x > vP.x) vMin.x = vP.x; - else if (vMax.x < vP.x) vMax.x = vP.x; - if (vMin.y > vP.y) vMin.y = vP.y; - else if (vMax.y < vP.y) vMax.y = vP.y; - if (vMin.z > vP.z) vMin.z = vP.z; - else if (vMax.z < vP.z) vMax.z = vP.z; - } - - vDim = vsub(vMax,vMin); - iChannel = 0; - fMin = vMin.x; fMax=vMax.x; - if (vDim.y>vDim.x && vDim.y>vDim.z) - { - iChannel=1; - fMin = vMin.y, fMax=vMax.y; - } - else if (vDim.z>vDim.x) - { - iChannel=2; - fMin = vMin.z, fMax=vMax.z; - } - - // make allocations - piHashTable = (int *) malloc(sizeof(int)*iNrTrianglesIn*3); - piHashCount = (int *) malloc(sizeof(int)*g_iCells); - piHashOffsets = (int *) malloc(sizeof(int)*g_iCells); - piHashCount2 = (int *) malloc(sizeof(int)*g_iCells); - - if (piHashTable==NULL || piHashCount==NULL || piHashOffsets==NULL || piHashCount2==NULL) - { - if (piHashTable!=NULL) free(piHashTable); - if (piHashCount!=NULL) free(piHashCount); - if (piHashOffsets!=NULL) free(piHashOffsets); - if (piHashCount2!=NULL) free(piHashCount2); - GenerateSharedVerticesIndexListSlow(piTriList_in_and_out, pContext, iNrTrianglesIn); - return; - } - memset(piHashCount, 0, sizeof(int)*g_iCells); - memset(piHashCount2, 0, sizeof(int)*g_iCells); - - // count amount of elements in each cell unit - for (i=0; i<(iNrTrianglesIn*3); i++) - { - const int index = piTriList_in_and_out[i]; - const SVec3 vP = GetPosition(pContext, index); - const float fVal = iChannel==0 ? vP.x : (iChannel==1 ? vP.y : vP.z); - const int iCell = FindGridCell(fMin, fMax, fVal); - ++piHashCount[iCell]; - } - - // evaluate start index of each cell. - piHashOffsets[0]=0; - for (k=1; kpTmpVert[l].vert[c]) fvMin[c]=pTmpVert[l].vert[c]; - else if (fvMax[c]dx && dy>dz) channel=1; - else if (dz>dx) channel=2; - - fSep = 0.5f*(fvMax[channel]+fvMin[channel]); - - // terminate recursion when the separation/average value - // is no longer strictly between fMin and fMax values. - if (fSep>=fvMax[channel] || fSep<=fvMin[channel]) - { - // complete the weld - for (l=iL_in; l<=iR_in; l++) - { - int i = pTmpVert[l].index; - const int index = piTriList_in_and_out[i]; - const SVec3 vP = GetPosition(pContext, index); - const SVec3 vN = GetNormal(pContext, index); - const SVec3 vT = GetTexCoord(pContext, index); - - tbool bNotFound = TTRUE; - int l2=iL_in, i2rec=-1; - while (l20); // at least 2 entries - - // separate (by fSep) all points between iL_in and iR_in in pTmpVert[] - while (iL < iR) - { - tbool bReadyLeftSwap = TFALSE, bReadyRightSwap = TFALSE; - while ((!bReadyLeftSwap) && iL=iL_in && iL<=iR_in); - bReadyLeftSwap = !(pTmpVert[iL].vert[channel]=iL_in && iR<=iR_in); - bReadyRightSwap = pTmpVert[iR].vert[channel]m_pInterface->m_getNumFaces(pContext); f++) - { - const int verts = pContext->m_pInterface->m_getNumVerticesOfFace(pContext, f); - if (verts!=3 && verts!=4) continue; - - pTriInfos[iDstTriIndex].iOrgFaceNumber = f; - pTriInfos[iDstTriIndex].iTSpacesOffs = iTSpacesOffs; - - if (verts==3) - { - unsigned char * pVerts = pTriInfos[iDstTriIndex].vert_num; - pVerts[0]=0; pVerts[1]=1; pVerts[2]=2; - piTriList_out[iDstTriIndex*3+0] = MakeIndex(f, 0); - piTriList_out[iDstTriIndex*3+1] = MakeIndex(f, 1); - piTriList_out[iDstTriIndex*3+2] = MakeIndex(f, 2); - ++iDstTriIndex; // next - } - else - { - { - pTriInfos[iDstTriIndex+1].iOrgFaceNumber = f; - pTriInfos[iDstTriIndex+1].iTSpacesOffs = iTSpacesOffs; - } - - { - // need an order independent way to evaluate - // tspace on quads. This is done by splitting - // along the shortest diagonal. - const int i0 = MakeIndex(f, 0); - const int i1 = MakeIndex(f, 1); - const int i2 = MakeIndex(f, 2); - const int i3 = MakeIndex(f, 3); - const SVec3 T0 = GetTexCoord(pContext, i0); - const SVec3 T1 = GetTexCoord(pContext, i1); - const SVec3 T2 = GetTexCoord(pContext, i2); - const SVec3 T3 = GetTexCoord(pContext, i3); - const float distSQ_02 = LengthSquared(vsub(T2,T0)); - const float distSQ_13 = LengthSquared(vsub(T3,T1)); - tbool bQuadDiagIs_02; - if (distSQ_02m_pInterface->m_getPosition(pContext, pos, iF, iI); - res.x=pos[0]; res.y=pos[1]; res.z=pos[2]; - return res; -} - -static SVec3 GetNormal(const SMikkTSpaceContext * pContext, const int index) -{ - int iF, iI; - SVec3 res; float norm[3]; - IndexToData(&iF, &iI, index); - pContext->m_pInterface->m_getNormal(pContext, norm, iF, iI); - res.x=norm[0]; res.y=norm[1]; res.z=norm[2]; - return res; -} - -static SVec3 GetTexCoord(const SMikkTSpaceContext * pContext, const int index) -{ - int iF, iI; - SVec3 res; float texc[2]; - IndexToData(&iF, &iI, index); - pContext->m_pInterface->m_getTexCoord(pContext, texc, iF, iI); - res.x=texc[0]; res.y=texc[1]; res.z=1.0f; - return res; -} - -///////////////////////////////////////////////////////////////////////////////////////////////////// -///////////////////////////////////////////////////////////////////////////////////////////////////// - -typedef union { - struct - { - int i0, i1, f; - }; - int array[3]; -} SEdge; - -static void BuildNeighborsFast(STriInfo pTriInfos[], SEdge * pEdges, const int piTriListIn[], const int iNrTrianglesIn); -static void BuildNeighborsSlow(STriInfo pTriInfos[], const int piTriListIn[], const int iNrTrianglesIn); - -// returns the texture area times 2 -static float CalcTexArea(const SMikkTSpaceContext * pContext, const int indices[]) -{ - const SVec3 t1 = GetTexCoord(pContext, indices[0]); - const SVec3 t2 = GetTexCoord(pContext, indices[1]); - const SVec3 t3 = GetTexCoord(pContext, indices[2]); - - const float t21x = t2.x-t1.x; - const float t21y = t2.y-t1.y; - const float t31x = t3.x-t1.x; - const float t31y = t3.y-t1.y; - - const float fSignedAreaSTx2 = t21x*t31y - t21y*t31x; - - return fSignedAreaSTx2<0 ? (-fSignedAreaSTx2) : fSignedAreaSTx2; -} - -static void InitTriInfo(STriInfo pTriInfos[], const int piTriListIn[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn) -{ - int f=0, i=0, t=0; - // pTriInfos[f].iFlag is cleared in GenerateInitialVerticesIndexList() which is called before this function. - - // generate neighbor info list - for (f=0; f0 ? ORIENT_PRESERVING : 0); - - if ( NotZero(fSignedAreaSTx2) ) - { - const float fAbsArea = fabsf(fSignedAreaSTx2); - const float fLenOs = Length(vOs); - const float fLenOt = Length(vOt); - const float fS = (pTriInfos[f].iFlag&ORIENT_PRESERVING)==0 ? (-1.0f) : 1.0f; - if ( NotZero(fLenOs) ) pTriInfos[f].vOs = vscale(fS/fLenOs, vOs); - if ( NotZero(fLenOt) ) pTriInfos[f].vOt = vscale(fS/fLenOt, vOt); - - // evaluate magnitudes prior to normalization of vOs and vOt - pTriInfos[f].fMagS = fLenOs / fAbsArea; - pTriInfos[f].fMagT = fLenOt / fAbsArea; - - // if this is a good triangle - if ( NotZero(pTriInfos[f].fMagS) && NotZero(pTriInfos[f].fMagT)) - pTriInfos[f].iFlag &= (~GROUP_WITH_ANY); - } - } - - // force otherwise healthy quads to a fixed orientation - while (t<(iNrTrianglesIn-1)) - { - const int iFO_a = pTriInfos[t].iOrgFaceNumber; - const int iFO_b = pTriInfos[t+1].iOrgFaceNumber; - if (iFO_a==iFO_b) // this is a quad - { - const tbool bIsDeg_a = (pTriInfos[t].iFlag&MARK_DEGENERATE)!=0 ? TTRUE : TFALSE; - const tbool bIsDeg_b = (pTriInfos[t+1].iFlag&MARK_DEGENERATE)!=0 ? TTRUE : TFALSE; - - // bad triangles should already have been removed by - // DegenPrologue(), but just in case check bIsDeg_a and bIsDeg_a are false - if ((bIsDeg_a||bIsDeg_b)==TFALSE) - { - const tbool bOrientA = (pTriInfos[t].iFlag&ORIENT_PRESERVING)!=0 ? TTRUE : TFALSE; - const tbool bOrientB = (pTriInfos[t+1].iFlag&ORIENT_PRESERVING)!=0 ? TTRUE : TFALSE; - // if this happens the quad has extremely bad mapping!! - if (bOrientA!=bOrientB) - { - //printf("found quad with bad mapping\n"); - tbool bChooseOrientFirstTri = TFALSE; - if ((pTriInfos[t+1].iFlag&GROUP_WITH_ANY)!=0) bChooseOrientFirstTri = TTRUE; - else if ( CalcTexArea(pContext, &piTriListIn[t*3+0]) >= CalcTexArea(pContext, &piTriListIn[(t+1)*3+0]) ) - bChooseOrientFirstTri = TTRUE; - - // force match - { - const int t0 = bChooseOrientFirstTri ? t : (t+1); - const int t1 = bChooseOrientFirstTri ? (t+1) : t; - pTriInfos[t1].iFlag &= (~ORIENT_PRESERVING); // clear first - pTriInfos[t1].iFlag |= (pTriInfos[t0].iFlag&ORIENT_PRESERVING); // copy bit - } - } - } - t += 2; - } - else - ++t; - } - - // match up edge pairs - { - SEdge * pEdges = (SEdge *) malloc(sizeof(SEdge)*iNrTrianglesIn*3); - if (pEdges==NULL) - BuildNeighborsSlow(pTriInfos, piTriListIn, iNrTrianglesIn); - else - { - BuildNeighborsFast(pTriInfos, pEdges, piTriListIn, iNrTrianglesIn); - - free(pEdges); - } - } -} - -///////////////////////////////////////////////////////////////////////////////////////////////////// -///////////////////////////////////////////////////////////////////////////////////////////////////// - -static tbool AssignRecur(const int piTriListIn[], STriInfo psTriInfos[], const int iMyTriIndex, SGroup * pGroup); -static void AddTriToGroup(SGroup * pGroup, const int iTriIndex); - -static int Build4RuleGroups(STriInfo pTriInfos[], SGroup pGroups[], int piGroupTrianglesBuffer[], const int piTriListIn[], const int iNrTrianglesIn) -{ - const int iNrMaxGroups = iNrTrianglesIn*3; - int iNrActiveGroups = 0; - int iOffset = 0, f=0, i=0; - (void)iNrMaxGroups; /* quiet warnings in non debug mode */ - for (f=0; fiVertexRepresentitive = vert_index; - pTriInfos[f].AssignedGroup[i]->bOrientPreservering = (pTriInfos[f].iFlag&ORIENT_PRESERVING)!=0; - pTriInfos[f].AssignedGroup[i]->iNrFaces = 0; - pTriInfos[f].AssignedGroup[i]->pFaceIndices = &piGroupTrianglesBuffer[iOffset]; - ++iNrActiveGroups; - - AddTriToGroup(pTriInfos[f].AssignedGroup[i], f); - bOrPre = (pTriInfos[f].iFlag&ORIENT_PRESERVING)!=0 ? TTRUE : TFALSE; - neigh_indexL = pTriInfos[f].FaceNeighbors[i]; - neigh_indexR = pTriInfos[f].FaceNeighbors[i>0?(i-1):2]; - if (neigh_indexL>=0) // neighbor - { - const tbool bAnswer = - AssignRecur(piTriListIn, pTriInfos, neigh_indexL, - pTriInfos[f].AssignedGroup[i] ); - - const tbool bOrPre2 = (pTriInfos[neigh_indexL].iFlag&ORIENT_PRESERVING)!=0 ? TTRUE : TFALSE; - const tbool bDiff = bOrPre!=bOrPre2 ? TTRUE : TFALSE; - assert(bAnswer || bDiff); - (void)bAnswer, (void)bDiff; /* quiet warnings in non debug mode */ - } - if (neigh_indexR>=0) // neighbor - { - const tbool bAnswer = - AssignRecur(piTriListIn, pTriInfos, neigh_indexR, - pTriInfos[f].AssignedGroup[i] ); - - const tbool bOrPre2 = (pTriInfos[neigh_indexR].iFlag&ORIENT_PRESERVING)!=0 ? TTRUE : TFALSE; - const tbool bDiff = bOrPre!=bOrPre2 ? TTRUE : TFALSE; - assert(bAnswer || bDiff); - (void)bAnswer, (void)bDiff; /* quiet warnings in non debug mode */ - } - - // update offset - iOffset += pTriInfos[f].AssignedGroup[i]->iNrFaces; - // since the groups are disjoint a triangle can never - // belong to more than 3 groups. Subsequently something - // is completely screwed if this assertion ever hits. - assert(iOffset <= iNrMaxGroups); - } - } - } - - return iNrActiveGroups; -} - -static void AddTriToGroup(SGroup * pGroup, const int iTriIndex) -{ - pGroup->pFaceIndices[pGroup->iNrFaces] = iTriIndex; - ++pGroup->iNrFaces; -} - -static tbool AssignRecur(const int piTriListIn[], STriInfo psTriInfos[], - const int iMyTriIndex, SGroup * pGroup) -{ - STriInfo * pMyTriInfo = &psTriInfos[iMyTriIndex]; - - // track down vertex - const int iVertRep = pGroup->iVertexRepresentitive; - const int * pVerts = &piTriListIn[3*iMyTriIndex+0]; - int i=-1; - if (pVerts[0]==iVertRep) i=0; - else if (pVerts[1]==iVertRep) i=1; - else if (pVerts[2]==iVertRep) i=2; - assert(i>=0 && i<3); - - // early out - if (pMyTriInfo->AssignedGroup[i] == pGroup) return TTRUE; - else if (pMyTriInfo->AssignedGroup[i]!=NULL) return TFALSE; - if ((pMyTriInfo->iFlag&GROUP_WITH_ANY)!=0) - { - // first to group with a group-with-anything triangle - // determines it's orientation. - // This is the only existing order dependency in the code!! - if ( pMyTriInfo->AssignedGroup[0] == NULL && - pMyTriInfo->AssignedGroup[1] == NULL && - pMyTriInfo->AssignedGroup[2] == NULL ) - { - pMyTriInfo->iFlag &= (~ORIENT_PRESERVING); - pMyTriInfo->iFlag |= (pGroup->bOrientPreservering ? ORIENT_PRESERVING : 0); - } - } - { - const tbool bOrient = (pMyTriInfo->iFlag&ORIENT_PRESERVING)!=0 ? TTRUE : TFALSE; - if (bOrient != pGroup->bOrientPreservering) return TFALSE; - } - - AddTriToGroup(pGroup, iMyTriIndex); - pMyTriInfo->AssignedGroup[i] = pGroup; - - { - const int neigh_indexL = pMyTriInfo->FaceNeighbors[i]; - const int neigh_indexR = pMyTriInfo->FaceNeighbors[i>0?(i-1):2]; - if (neigh_indexL>=0) - AssignRecur(piTriListIn, psTriInfos, neigh_indexL, pGroup); - if (neigh_indexR>=0) - AssignRecur(piTriListIn, psTriInfos, neigh_indexR, pGroup); - } - - - - return TTRUE; -} - -///////////////////////////////////////////////////////////////////////////////////////////////////// -///////////////////////////////////////////////////////////////////////////////////////////////////// - -static tbool CompareSubGroups(const SSubGroup * pg1, const SSubGroup * pg2); -static void QuickSort(int* pSortBuffer, int iLeft, int iRight, unsigned int uSeed); -static STSpace EvalTspace(int face_indices[], const int iFaces, const int piTriListIn[], const STriInfo pTriInfos[], const SMikkTSpaceContext * pContext, const int iVertexRepresentitive); - -static tbool GenerateTSpaces(STSpace psTspace[], const STriInfo pTriInfos[], const SGroup pGroups[], - const int iNrActiveGroups, const int piTriListIn[], const float fThresCos, - const SMikkTSpaceContext * pContext) -{ - STSpace * pSubGroupTspace = NULL; - SSubGroup * pUniSubGroups = NULL; - int * pTmpMembers = NULL; - int iMaxNrFaces=0, iUniqueTspaces=0, g=0, i=0; - for (g=0; giNrFaces; i++) // triangles - { - const int f = pGroup->pFaceIndices[i]; // triangle number - int index=-1, iVertIndex=-1, iOF_1=-1, iMembers=0, j=0, l=0; - SSubGroup tmp_group; - tbool bFound; - SVec3 n, vOs, vOt; - if (pTriInfos[f].AssignedGroup[0]==pGroup) index=0; - else if (pTriInfos[f].AssignedGroup[1]==pGroup) index=1; - else if (pTriInfos[f].AssignedGroup[2]==pGroup) index=2; - assert(index>=0 && index<3); - - iVertIndex = piTriListIn[f*3+index]; - assert(iVertIndex==pGroup->iVertexRepresentitive); - - // is normalized already - n = GetNormal(pContext, iVertIndex); - - // project - vOs = vsub(pTriInfos[f].vOs, vscale(vdot(n,pTriInfos[f].vOs), n)); - vOt = vsub(pTriInfos[f].vOt, vscale(vdot(n,pTriInfos[f].vOt), n)); - if ( VNotZero(vOs) ) vOs = Normalize(vOs); - if ( VNotZero(vOt) ) vOt = Normalize(vOt); - - // original face number - iOF_1 = pTriInfos[f].iOrgFaceNumber; - - iMembers = 0; - for (j=0; jiNrFaces; j++) - { - const int t = pGroup->pFaceIndices[j]; // triangle number - const int iOF_2 = pTriInfos[t].iOrgFaceNumber; - - // project - SVec3 vOs2 = vsub(pTriInfos[t].vOs, vscale(vdot(n,pTriInfos[t].vOs), n)); - SVec3 vOt2 = vsub(pTriInfos[t].vOt, vscale(vdot(n,pTriInfos[t].vOt), n)); - if ( VNotZero(vOs2) ) vOs2 = Normalize(vOs2); - if ( VNotZero(vOt2) ) vOt2 = Normalize(vOt2); - - { - const tbool bAny = ( (pTriInfos[f].iFlag | pTriInfos[t].iFlag) & GROUP_WITH_ANY )!=0 ? TTRUE : TFALSE; - // make sure triangles which belong to the same quad are joined. - const tbool bSameOrgFace = iOF_1==iOF_2 ? TTRUE : TFALSE; - - const float fCosS = vdot(vOs,vOs2); - const float fCosT = vdot(vOt,vOt2); - - assert(f!=t || bSameOrgFace); // sanity check - if (bAny || bSameOrgFace || (fCosS>fThresCos && fCosT>fThresCos)) - pTmpMembers[iMembers++] = t; - } - } - - // sort pTmpMembers - tmp_group.iNrFaces = iMembers; - tmp_group.pTriMembers = pTmpMembers; - if (iMembers>1) - { - unsigned int uSeed = INTERNAL_RND_SORT_SEED; // could replace with a random seed? - QuickSort(pTmpMembers, 0, iMembers-1, uSeed); - } - - // look for an existing match - bFound = TFALSE; - l=0; - while (liVertexRepresentitive); - ++iUniqueSubGroups; - } - - // output tspace - { - const int iOffs = pTriInfos[f].iTSpacesOffs; - const int iVert = pTriInfos[f].vert_num[index]; - STSpace * pTS_out = &psTspace[iOffs+iVert]; - assert(pTS_out->iCounter<2); - assert(((pTriInfos[f].iFlag&ORIENT_PRESERVING)!=0) == pGroup->bOrientPreservering); - if (pTS_out->iCounter==1) - { - *pTS_out = AvgTSpace(pTS_out, &pSubGroupTspace[l]); - pTS_out->iCounter = 2; // update counter - pTS_out->bOrient = pGroup->bOrientPreservering; - } - else - { - assert(pTS_out->iCounter==0); - *pTS_out = pSubGroupTspace[l]; - pTS_out->iCounter = 1; // update counter - pTS_out->bOrient = pGroup->bOrientPreservering; - } - } - } - - // clean up and offset iUniqueTspaces - for (s=0; s=0 && i<3); - - // project - index = piTriListIn[3*f+i]; - n = GetNormal(pContext, index); - vOs = vsub(pTriInfos[f].vOs, vscale(vdot(n,pTriInfos[f].vOs), n)); - vOt = vsub(pTriInfos[f].vOt, vscale(vdot(n,pTriInfos[f].vOt), n)); - if ( VNotZero(vOs) ) vOs = Normalize(vOs); - if ( VNotZero(vOt) ) vOt = Normalize(vOt); - - i2 = piTriListIn[3*f + (i<2?(i+1):0)]; - i1 = piTriListIn[3*f + i]; - i0 = piTriListIn[3*f + (i>0?(i-1):2)]; - - p0 = GetPosition(pContext, i0); - p1 = GetPosition(pContext, i1); - p2 = GetPosition(pContext, i2); - v1 = vsub(p0,p1); - v2 = vsub(p2,p1); - - // project - v1 = vsub(v1, vscale(vdot(n,v1),n)); if ( VNotZero(v1) ) v1 = Normalize(v1); - v2 = vsub(v2, vscale(vdot(n,v2),n)); if ( VNotZero(v2) ) v2 = Normalize(v2); - - // weight contribution by the angle - // between the two edge vectors - fCos = vdot(v1,v2); fCos=fCos>1?1:(fCos<(-1) ? (-1) : fCos); - fAngle = (float) acos(fCos); - fMagS = pTriInfos[f].fMagS; - fMagT = pTriInfos[f].fMagT; - - res.vOs=vadd(res.vOs, vscale(fAngle,vOs)); - res.vOt=vadd(res.vOt,vscale(fAngle,vOt)); - res.fMagS+=(fAngle*fMagS); - res.fMagT+=(fAngle*fMagT); - fAngleSum += fAngle; - } - } - - // normalize - if ( VNotZero(res.vOs) ) res.vOs = Normalize(res.vOs); - if ( VNotZero(res.vOt) ) res.vOt = Normalize(res.vOt); - if (fAngleSum>0) - { - res.fMagS /= fAngleSum; - res.fMagT /= fAngleSum; - } - - return res; -} - -static tbool CompareSubGroups(const SSubGroup * pg1, const SSubGroup * pg2) -{ - tbool bStillSame=TTRUE; - int i=0; - if (pg1->iNrFaces!=pg2->iNrFaces) return TFALSE; - while (iiNrFaces && bStillSame) - { - bStillSame = pg1->pTriMembers[i]==pg2->pTriMembers[i] ? TTRUE : TFALSE; - if (bStillSame) ++i; - } - return bStillSame; -} - -static void QuickSort(int* pSortBuffer, int iLeft, int iRight, unsigned int uSeed) -{ - int iL, iR, n, index, iMid, iTmp; - - // Random - unsigned int t=uSeed&31; - t=(uSeed<>(32-t)); - uSeed=uSeed+t+3; - // Random end - - iL=iLeft; iR=iRight; - n = (iR-iL)+1; - assert(n>=0); - index = (int) (uSeed%n); - - iMid=pSortBuffer[index + iL]; - - - do - { - while (pSortBuffer[iL] < iMid) - ++iL; - while (pSortBuffer[iR] > iMid) - --iR; - - if (iL <= iR) - { - iTmp = pSortBuffer[iL]; - pSortBuffer[iL] = pSortBuffer[iR]; - pSortBuffer[iR] = iTmp; - ++iL; --iR; - } - } - while (iL <= iR); - - if (iLeft < iR) - QuickSort(pSortBuffer, iLeft, iR, uSeed); - if (iL < iRight) - QuickSort(pSortBuffer, iL, iRight, uSeed); -} - -///////////////////////////////////////////////////////////////////////////////////////////// -///////////////////////////////////////////////////////////////////////////////////////////// - -static void QuickSortEdges(SEdge * pSortBuffer, int iLeft, int iRight, const int channel, unsigned int uSeed); -static void GetEdge(int * i0_out, int * i1_out, int * edgenum_out, const int indices[], const int i0_in, const int i1_in); - -static void BuildNeighborsFast(STriInfo pTriInfos[], SEdge * pEdges, const int piTriListIn[], const int iNrTrianglesIn) -{ - // build array of edges - unsigned int uSeed = INTERNAL_RND_SORT_SEED; // could replace with a random seed? - int iEntries=0, iCurStartIndex=-1, f=0, i=0; - for (f=0; f pSortBuffer[iRight].array[channel]) - { - sTmp = pSortBuffer[iLeft]; - pSortBuffer[iLeft] = pSortBuffer[iRight]; - pSortBuffer[iRight] = sTmp; - } - return; - } - - // Random - t=uSeed&31; - t=(uSeed<>(32-t)); - uSeed=uSeed+t+3; - // Random end - - iL=iLeft, iR=iRight; - n = (iR-iL)+1; - assert(n>=0); - index = (int) (uSeed%n); - - iMid=pSortBuffer[index + iL].array[channel]; - - do - { - while (pSortBuffer[iL].array[channel] < iMid) - ++iL; - while (pSortBuffer[iR].array[channel] > iMid) - --iR; - - if (iL <= iR) - { - sTmp = pSortBuffer[iL]; - pSortBuffer[iL] = pSortBuffer[iR]; - pSortBuffer[iR] = sTmp; - ++iL; --iR; - } - } - while (iL <= iR); - - if (iLeft < iR) - QuickSortEdges(pSortBuffer, iLeft, iR, channel, uSeed); - if (iL < iRight) - QuickSortEdges(pSortBuffer, iL, iRight, channel, uSeed); -} - -// resolve ordering and edge number -static void GetEdge(int * i0_out, int * i1_out, int * edgenum_out, const int indices[], const int i0_in, const int i1_in) -{ - *edgenum_out = -1; - - // test if first index is on the edge - if (indices[0]==i0_in || indices[0]==i1_in) - { - // test if second index is on the edge - if (indices[1]==i0_in || indices[1]==i1_in) - { - edgenum_out[0]=0; // first edge - i0_out[0]=indices[0]; - i1_out[0]=indices[1]; - } - else - { - edgenum_out[0]=2; // third edge - i0_out[0]=indices[2]; - i1_out[0]=indices[0]; - } - } - else - { - // only second and third index is on the edge - edgenum_out[0]=1; // second edge - i0_out[0]=indices[1]; - i1_out[0]=indices[2]; - } -} - - -///////////////////////////////////////////////////////////////////////////////////////////// -/////////////////////////////////// Degenerate triangles //////////////////////////////////// - -static void DegenPrologue(STriInfo pTriInfos[], int piTriList_out[], const int iNrTrianglesIn, const int iTotTris) -{ - int iNextGoodTriangleSearchIndex=-1; - tbool bStillFindingGoodOnes; - - // locate quads with only one good triangle - int t=0; - while (t<(iTotTris-1)) - { - const int iFO_a = pTriInfos[t].iOrgFaceNumber; - const int iFO_b = pTriInfos[t+1].iOrgFaceNumber; - if (iFO_a==iFO_b) // this is a quad - { - const tbool bIsDeg_a = (pTriInfos[t].iFlag&MARK_DEGENERATE)!=0 ? TTRUE : TFALSE; - const tbool bIsDeg_b = (pTriInfos[t+1].iFlag&MARK_DEGENERATE)!=0 ? TTRUE : TFALSE; - if ((bIsDeg_a^bIsDeg_b)!=0) - { - pTriInfos[t].iFlag |= QUAD_ONE_DEGEN_TRI; - pTriInfos[t+1].iFlag |= QUAD_ONE_DEGEN_TRI; - } - t += 2; - } - else - ++t; - } - - // reorder list so all degen triangles are moved to the back - // without reordering the good triangles - iNextGoodTriangleSearchIndex = 1; - t=0; - bStillFindingGoodOnes = TTRUE; - while (t (t+1)); - - // swap triangle t0 and t1 - if (!bJustADegenerate) - { - int i=0; - for (i=0; i<3; i++) - { - const int index = piTriList_out[t0*3+i]; - piTriList_out[t0*3+i] = piTriList_out[t1*3+i]; - piTriList_out[t1*3+i] = index; - } - { - const STriInfo tri_info = pTriInfos[t0]; - pTriInfos[t0] = pTriInfos[t1]; - pTriInfos[t1] = tri_info; - } - } - else - bStillFindingGoodOnes = TFALSE; // this is not supposed to happen - } - - if (bStillFindingGoodOnes) ++t; - } - - assert(bStillFindingGoodOnes); // code will still work. - assert(iNrTrianglesIn == t); -} - -static void DegenEpilogue(STSpace psTspace[], STriInfo pTriInfos[], int piTriListIn[], const SMikkTSpaceContext * pContext, const int iNrTrianglesIn, const int iTotTris) -{ - int t=0, i=0; - // deal with degenerate triangles - // punishment for degenerate triangles is O(N^2) - for (t=iNrTrianglesIn; t http://image.diku.dk/projects/media/morten.mikkelsen.08.pdf - * Note that though the tangent spaces at the vertices are generated in an order-independent way, - * by this implementation, the interpolated tangent space is still affected by which diagonal is - * chosen to split each quad. A sensible solution is to have your tools pipeline always - * split quads by the shortest diagonal. This choice is order-independent and works with mirroring. - * If these have the same length then compare the diagonals defined by the texture coordinates. - * XNormal which is a tool for baking normal maps allows you to write your own tangent space plugin - * and also quad triangulator plugin. - */ - - -typedef int tbool; -typedef struct SMikkTSpaceContext SMikkTSpaceContext; - -typedef struct { - // Returns the number of faces (triangles/quads) on the mesh to be processed. - int (*m_getNumFaces)(const SMikkTSpaceContext * pContext); - - // Returns the number of vertices on face number iFace - // iFace is a number in the range {0, 1, ..., getNumFaces()-1} - int (*m_getNumVerticesOfFace)(const SMikkTSpaceContext * pContext, const int iFace); - - // returns the position/normal/texcoord of the referenced face of vertex number iVert. - // iVert is in the range {0,1,2} for triangles and {0,1,2,3} for quads. - void (*m_getPosition)(const SMikkTSpaceContext * pContext, float fvPosOut[], const int iFace, const int iVert); - void (*m_getNormal)(const SMikkTSpaceContext * pContext, float fvNormOut[], const int iFace, const int iVert); - void (*m_getTexCoord)(const SMikkTSpaceContext * pContext, float fvTexcOut[], const int iFace, const int iVert); - - // either (or both) of the two setTSpace callbacks can be set. - // The call-back m_setTSpaceBasic() is sufficient for basic normal mapping. - - // This function is used to return the tangent and fSign to the application. - // fvTangent is a unit length vector. - // For normal maps it is sufficient to use the following simplified version of the bitangent which is generated at pixel/vertex level. - // bitangent = fSign * cross(vN, tangent); - // Note that the results are returned unindexed. It is possible to generate a new index list - // But averaging/overwriting tangent spaces by using an already existing index list WILL produce INCRORRECT results. - // DO NOT! use an already existing index list. - void (*m_setTSpaceBasic)(const SMikkTSpaceContext * pContext, const float fvTangent[], const float fSign, const int iFace, const int iVert); - - // This function is used to return tangent space results to the application. - // fvTangent and fvBiTangent are unit length vectors and fMagS and fMagT are their - // true magnitudes which can be used for relief mapping effects. - // fvBiTangent is the "real" bitangent and thus may not be perpendicular to fvTangent. - // However, both are perpendicular to the vertex normal. - // For normal maps it is sufficient to use the following simplified version of the bitangent which is generated at pixel/vertex level. - // fSign = bIsOrientationPreserving ? 1.0f : (-1.0f); - // bitangent = fSign * cross(vN, tangent); - // Note that the results are returned unindexed. It is possible to generate a new index list - // But averaging/overwriting tangent spaces by using an already existing index list WILL produce INCRORRECT results. - // DO NOT! use an already existing index list. - void (*m_setTSpace)(const SMikkTSpaceContext * pContext, const float fvTangent[], const float fvBiTangent[], const float fMagS, const float fMagT, - const tbool bIsOrientationPreserving, const int iFace, const int iVert); -} SMikkTSpaceInterface; - -struct SMikkTSpaceContext -{ - SMikkTSpaceInterface * m_pInterface; // initialized with callback functions - void * m_pUserData; // pointer to client side mesh data etc. (passed as the first parameter with every interface call) -}; - -// these are both thread safe! -tbool genTangSpaceDefault(const SMikkTSpaceContext * pContext); // Default (recommended) fAngularThreshold is 180 degrees (which means threshold disabled) -tbool genTangSpace(const SMikkTSpaceContext * pContext, const float fAngularThreshold); - - -// To avoid visual errors (distortions/unwanted hard edges in lighting), when using sampled normal maps, the -// normal map sampler must use the exact inverse of the pixel shader transformation. -// The most efficient transformation we can possibly do in the pixel shader is -// achieved by using, directly, the "unnormalized" interpolated tangent, bitangent and vertex normal: vT, vB and vN. -// pixel shader (fast transform out) -// vNout = normalize( vNt.x * vT + vNt.y * vB + vNt.z * vN ); -// where vNt is the tangent space normal. The normal map sampler must likewise use the -// interpolated and "unnormalized" tangent, bitangent and vertex normal to be compliant with the pixel shader. -// sampler does (exact inverse of pixel shader): -// float3 row0 = cross(vB, vN); -// float3 row1 = cross(vN, vT); -// float3 row2 = cross(vT, vB); -// float fSign = dot(vT, row0)<0 ? -1 : 1; -// vNt = normalize( fSign * float3(dot(vNout,row0), dot(vNout,row1), dot(vNout,row2)) ); -// where vNout is the sampled normal in some chosen 3D space. -// -// Should you choose to reconstruct the bitangent in the pixel shader instead -// of the vertex shader, as explained earlier, then be sure to do this in the normal map sampler also. -// Finally, beware of quad triangulations. If the normal map sampler doesn't use the same triangulation of -// quads as your renderer then problems will occur since the interpolated tangent spaces will differ -// eventhough the vertex level tangent spaces match. This can be solved either by triangulating before -// sampling/exporting or by using the order-independent choice of diagonal for splitting quads suggested earlier. -// However, this must be used both by the sampler and your tools/rendering pipeline. - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/Source/Falcor/Core/API/Buffer.cpp b/Source/Falcor/Core/API/Buffer.cpp index 0d896420ad..88a83e3873 100644 --- a/Source/Falcor/Core/API/Buffer.cpp +++ b/Source/Falcor/Core/API/Buffer.cpp @@ -60,6 +60,12 @@ namespace Falcor : Resource(Type::Buffer, bindFlags, size) , mCpuAccess(cpuAccess) { + // Check that buffer size is within 4GB limit. Larger buffers are currently not well supported in D3D12. + // TODO: Revisit this check in the future. + if (size > (1ull << 32)) + { + logWarning("Creating GPU buffer of size " + std::to_string(size) + " bytes. Buffers above 4GB are not currently well supported."); + } } Buffer::SharedPtr Buffer::create(size_t size, BindFlags bindFlags, CpuAccess cpuAccess, const void* pInitData) @@ -67,12 +73,13 @@ namespace Falcor Buffer::SharedPtr pBuffer = SharedPtr(new Buffer(size, bindFlags, cpuAccess)); pBuffer->apiInit(pInitData != nullptr); if (pInitData) pBuffer->setBlob(pInitData, 0, size); + pBuffer->mElementCount = uint32_t(size); return pBuffer; } Buffer::SharedPtr Buffer::createTyped(ResourceFormat format, uint32_t elementCount, BindFlags bindFlags, CpuAccess cpuAccess, const void* pInitData) { - size_t size = elementCount * getFormatBytesPerBlock(format); + size_t size = (size_t)elementCount * getFormatBytesPerBlock(format); SharedPtr pBuffer = create(size, bindFlags, cpuAccess, pInitData); assert(pBuffer); @@ -89,7 +96,7 @@ namespace Falcor const void* pInitData, bool createCounter) { - size_t size = structSize * elementCount; + size_t size = (size_t)structSize * elementCount; Buffer::SharedPtr pBuffer = create(size, bindFlags, cpuAccess, pInitData); assert(pBuffer); @@ -322,6 +329,14 @@ namespace Falcor return mpCBV; } + uint32_t Buffer::getElementSize() const + { + if (mStructSize != 0) return mStructSize; + if (mFormat == ResourceFormat::Unknown) return 1; + + throw std::exception("Buffer::getElementSize() - inferring element size from resourec format is unimplemented"); + } + SCRIPT_BINDING(Buffer) { pybind11::class_(m, "Buffer"); diff --git a/Source/Falcor/Core/API/Buffer.h b/Source/Falcor/Core/API/Buffer.h index 63ac8ab65b..71f69964b4 100644 --- a/Source/Falcor/Core/API/Buffer.h +++ b/Source/Falcor/Core/API/Buffer.h @@ -192,6 +192,26 @@ namespace Falcor */ virtual UnorderedAccessView::SharedPtr getUAV() override; +#if _ENABLE_CUDA + /** Get the CUDA device address for this resource. + \return CUDA device address. + Throws an exception if the buffer is not shared. + */ + virtual void* getCUDADeviceAddress() const override; + + /** Get the CUDA device address for a view of this resource. + */ + virtual void* getCUDADeviceAddress(ResourceViewInfo const& viewInfo) const override; +#endif + + /** Get the size of each element in this buffer. + + For a typed buffer, this will be the size of the format. + For a structured buffer, this will be the same value as `getStructSize()`. + For a raw buffer, this will be the number of bytes. + */ + uint32_t getElementSize() const; + /** Get a constant buffer view */ ConstantBufferView::SharedPtr getCBV(); @@ -216,7 +236,7 @@ namespace Falcor */ size_t getSize() const { return mSize; } - /** Get the element count. For structured-buffers, this is the number of structs. For typed-buffers, this is the number of elements. For other buffer, will return 0 + /** Get the element count. For structured-buffers, this is the number of structs. For typed-buffers, this is the number of elements. For other buffer, will return the size in bytes. */ uint32_t getElementCount() const { return mElementCount; } @@ -296,6 +316,9 @@ namespace Falcor ConstantBufferView::SharedPtr mpCBV; // For constant-buffers Buffer::SharedPtr mpUAVCounter; // For structured-buffers + mutable void* mCUDAExternalMemory = nullptr; + mutable void* mCUDADeviceAddress = nullptr; + /** Helper for converting host type to resource format for typed buffers. See list of supported formats for typed UAV loads: https://docs.microsoft.com/en-us/windows/win32/direct3d12/typed-unordered-access-view-loads diff --git a/Source/Falcor/Core/API/D3D12/D3D12Buffer.cpp b/Source/Falcor/Core/API/D3D12/D3D12Buffer.cpp index 3199089233..ff6ac934f7 100644 --- a/Source/Falcor/Core/API/D3D12/D3D12Buffer.cpp +++ b/Source/Falcor/Core/API/D3D12/D3D12Buffer.cpp @@ -50,13 +50,14 @@ namespace Falcor bufDesc.SampleDesc.Count = 1; bufDesc.SampleDesc.Quality = 0; bufDesc.Width = size; + assert(bufDesc.Width > 0); D3D12_RESOURCE_STATES d3dState = getD3D12ResourceState(initState); ID3D12ResourcePtr pApiHandle; D3D12_HEAP_FLAGS heapFlags = is_set(bindFlags, ResourceBindFlags::Shared) ? D3D12_HEAP_FLAG_SHARED : D3D12_HEAP_FLAG_NONE; d3d_call(pDevice->CreateCommittedResource(&heapProps, heapFlags, &bufDesc, d3dState, nullptr, IID_PPV_ARGS(&pApiHandle))); - - // Map and upload data if needed + assert(pApiHandle); + return pApiHandle; } diff --git a/Source/Falcor/Core/API/D3D12/D3D12CopyContext.cpp b/Source/Falcor/Core/API/D3D12/D3D12CopyContext.cpp index b43af9be51..bd6a4e4bb6 100644 --- a/Source/Falcor/Core/API/D3D12/D3D12CopyContext.cpp +++ b/Source/Falcor/Core/API/D3D12/D3D12CopyContext.cpp @@ -177,9 +177,12 @@ namespace Falcor mpFence->syncCpu(); D3D12_PLACED_SUBRESOURCE_FOOTPRINT& footprint = mFootprint; - //Get buffer data + // Calculate row size. GPU pitch can be different because it is aligned to D3D12_TEXTURE_DATA_PITCH_ALIGNMENT + assert(footprint.Footprint.Width % getFormatWidthCompressionRatio(mTextureFormat) == 0); // Should divide evenly + uint32_t actualRowSize = (footprint.Footprint.Width / getFormatWidthCompressionRatio(mTextureFormat)) * getFormatBytesPerBlock(mTextureFormat); + + // Get buffer data std::vector result; - uint32_t actualRowSize = footprint.Footprint.Width * getFormatBytesPerBlock(mTextureFormat); result.resize(mRowCount * actualRowSize); uint8_t* pData = reinterpret_cast(mpBuffer->map(Buffer::MapType::Read)); diff --git a/Source/Falcor/Core/API/D3D12/D3D12DescriptorPool.cpp b/Source/Falcor/Core/API/D3D12/D3D12DescriptorPool.cpp index 37f4966a6e..96b849cecd 100644 --- a/Source/Falcor/Core/API/D3D12/D3D12DescriptorPool.cpp +++ b/Source/Falcor/Core/API/D3D12/D3D12DescriptorPool.cpp @@ -43,6 +43,7 @@ namespace Falcor case DescriptorPool::Type::TypedBufferUav: case DescriptorPool::Type::StructuredBufferSrv: case DescriptorPool::Type::StructuredBufferUav: + case DescriptorPool::Type::AccelerationStructureSrv: case DescriptorPool::Type::Cbv: return D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV; case DescriptorPool::Type::Dsv: @@ -60,7 +61,7 @@ namespace Falcor void DescriptorPool::apiInit() { // Find out how many heaps we need - static_assert(DescriptorPool::kTypeCount == 12, "Unexpected desc count, make sure all desc types are supported"); + static_assert(DescriptorPool::kTypeCount == 13, "Unexpected desc count, make sure all desc types are supported"); uint32_t descCount[D3D12_DESCRIPTOR_HEAP_TYPE_NUM_TYPES] = { 0 }; descCount[D3D12_DESCRIPTOR_HEAP_TYPE_RTV] = mDesc.mDescCount[(uint32_t)Type::Rtv]; @@ -69,11 +70,12 @@ namespace Falcor descCount[D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV] = mDesc.mDescCount[(uint32_t)Type::Cbv]; descCount[D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV] += mDesc.mDescCount[(uint32_t)Type::TextureSrv] + mDesc.mDescCount[(uint32_t)Type::RawBufferSrv] + mDesc.mDescCount[(uint32_t)Type::TypedBufferSrv] + mDesc.mDescCount[(uint32_t)Type::StructuredBufferSrv]; descCount[D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV] += mDesc.mDescCount[(uint32_t)Type::TextureUav] + mDesc.mDescCount[(uint32_t)Type::RawBufferUav] + mDesc.mDescCount[(uint32_t)Type::TypedBufferUav] + mDesc.mDescCount[(uint32_t)Type::StructuredBufferUav]; + descCount[D3D12_DESCRIPTOR_HEAP_TYPE_CBV_SRV_UAV] += mDesc.mDescCount[(uint32_t)Type::AccelerationStructureSrv]; mpApiData = std::make_shared(); for (uint32_t i = 0; i < arraysize(mpApiData->pHeaps); i++) { - if (descCount[i]) + if (descCount[i] > 0) { mpApiData->pHeaps[i] = D3D12DescriptorHeap::create(D3D12_DESCRIPTOR_HEAP_TYPE(i), descCount[i], mDesc.mShaderVisible); } diff --git a/Source/Falcor/Core/API/D3D12/D3D12Device.cpp b/Source/Falcor/Core/API/D3D12/D3D12Device.cpp index 3a898a25c9..9fdf1e33f0 100644 --- a/Source/Falcor/Core/API/D3D12/D3D12Device.cpp +++ b/Source/Falcor/Core/API/D3D12/D3D12Device.cpp @@ -250,6 +250,17 @@ namespace Falcor else if (features2.ProgrammableSamplePositionsTier == D3D12_PROGRAMMABLE_SAMPLE_POSITIONS_TIER_2) supported |= Device::SupportedFeatures::ProgrammableSamplePositionsFull; } + D3D12_FEATURE_DATA_D3D12_OPTIONS3 features3; + hr = pDevice->CheckFeatureSupport(D3D12_FEATURE_D3D12_OPTIONS3, &features3, sizeof(D3D12_FEATURE_DATA_D3D12_OPTIONS3)); + if (FAILED(hr) || !features3.BarycentricsSupported) + { + logInfo("Barycentrics are not supported on this device."); + } + else + { + supported |= Device::SupportedFeatures::Barycentrics; + } + D3D12_FEATURE_DATA_D3D12_OPTIONS5 features5; hr = pDevice->CheckFeatureSupport(D3D12_FEATURE_D3D12_OPTIONS5, &features5, sizeof(D3D12_FEATURE_DATA_D3D12_OPTIONS5)); if (FAILED(hr) || features5.RaytracingTier == D3D12_RAYTRACING_TIER_NOT_SUPPORTED) @@ -259,6 +270,7 @@ namespace Falcor else { supported |= Device::SupportedFeatures::Raytracing; + if (features5.RaytracingTier == D3D12_RAYTRACING_TIER_1_1) supported |= Device::SupportedFeatures::RaytracingTier1_1; } return supported; diff --git a/Source/Falcor/Core/API/D3D12/D3D12Fbo.cpp b/Source/Falcor/Core/API/D3D12/D3D12Fbo.cpp index bd997e6bc2..5575f7504f 100644 --- a/Source/Falcor/Core/API/D3D12/D3D12Fbo.cpp +++ b/Source/Falcor/Core/API/D3D12/D3D12Fbo.cpp @@ -30,50 +30,6 @@ namespace Falcor { - template - ViewType getViewDimension(Resource::Type type, bool isArray); - - template<> - D3D12_RTV_DIMENSION getViewDimension(Resource::Type type, bool isTextureArray) - { - switch(type) - { - case Resource::Type::Texture1D: - return (isTextureArray) ? D3D12_RTV_DIMENSION_TEXTURE1DARRAY : D3D12_RTV_DIMENSION_TEXTURE1D; - case Resource::Type::Texture2D: - return (isTextureArray) ? D3D12_RTV_DIMENSION_TEXTURE2DARRAY : D3D12_RTV_DIMENSION_TEXTURE2D; - case Resource::Type::Texture3D: - assert(isTextureArray == false); - return D3D12_RTV_DIMENSION_TEXTURE3D; - case Resource::Type::Texture2DMultisample: - return (isTextureArray) ? D3D12_RTV_DIMENSION_TEXTURE2DMSARRAY : D3D12_RTV_DIMENSION_TEXTURE2DMS; - case Resource::Type::TextureCube: - return D3D12_RTV_DIMENSION_TEXTURE2DARRAY; - default: - should_not_get_here(); - return D3D12_RTV_DIMENSION_UNKNOWN; - } - } - - template<> - D3D12_DSV_DIMENSION getViewDimension(Resource::Type type, bool isTextureArray) - { - switch(type) - { - case Resource::Type::Texture1D: - return (isTextureArray) ? D3D12_DSV_DIMENSION_TEXTURE1DARRAY : D3D12_DSV_DIMENSION_TEXTURE1D; - case Resource::Type::Texture2D: - return (isTextureArray) ? D3D12_DSV_DIMENSION_TEXTURE2DARRAY : D3D12_DSV_DIMENSION_TEXTURE2D; - case Resource::Type::Texture2DMultisample: - return (isTextureArray) ? D3D12_DSV_DIMENSION_TEXTURE2DMSARRAY : D3D12_DSV_DIMENSION_TEXTURE2DMS; - case Resource::Type::TextureCube: - return D3D12_DSV_DIMENSION_TEXTURE2DARRAY; - default: - should_not_get_here(); - return D3D12_DSV_DIMENSION_UNKNOWN; - } - } - Fbo::Fbo() { mColorAttachments.resize(getMaxColorTargetCount()); @@ -111,7 +67,9 @@ namespace Falcor } else { - return RenderTargetView::getNullView(); + // TODO: mColorAttachments doesn't contain enough information to fully determine the view dimension. Assume 2D for now. + auto dimension = rt.arraySize > 1 ? RenderTargetView::Dimension::Texture2DArray : RenderTargetView::Dimension::Texture2D; + return RenderTargetView::getNullView(dimension); } } @@ -123,7 +81,9 @@ namespace Falcor } else { - return DepthStencilView::getNullView(); + // TODO: mDepthStencil doesn't contain enough information to fully determine the view dimension. Assume 2D for now. + auto dimension = mDepthStencil.arraySize > 1 ? DepthStencilView::Dimension::Texture2DArray : DepthStencilView::Dimension::Texture2D; + return DepthStencilView::getNullView(dimension); } } } diff --git a/Source/Falcor/Core/API/D3D12/D3D12RenderContext.cpp b/Source/Falcor/Core/API/D3D12/D3D12RenderContext.cpp index a940b678be..7bced58af4 100644 --- a/Source/Falcor/Core/API/D3D12/D3D12RenderContext.cpp +++ b/Source/Falcor/Core/API/D3D12/D3D12RenderContext.cpp @@ -190,11 +190,12 @@ namespace Falcor static void D3D12SetFbo(RenderContext* pCtx, const Fbo* pFbo) { - // We are setting the entire RTV array to make sure everything that was previously bound is detached + // We are setting the entire RTV array to make sure everything that was previously bound is detached. + // We're using 2D null views for any unused slots. uint32_t colorTargets = Fbo::getMaxColorTargetCount(); - auto pNullRtv = RenderTargetView::getNullView(); + auto pNullRtv = RenderTargetView::getNullView(RenderTargetView::Dimension::Texture2D); std::vector pRTV(colorTargets, pNullRtv->getApiHandle()->getCpuHandle(0)); - HeapCpuHandle pDSV = DepthStencilView::getNullView()->getApiHandle()->getCpuHandle(0); + HeapCpuHandle pDSV = DepthStencilView::getNullView(DepthStencilView::Dimension::Texture2D)->getApiHandle()->getCpuHandle(0); if (pFbo) { diff --git a/Source/Falcor/Core/API/D3D12/D3D12Resource.cpp b/Source/Falcor/Core/API/D3D12/D3D12Resource.cpp index e3fbd192b0..5b8a38d95a 100644 --- a/Source/Falcor/Core/API/D3D12/D3D12Resource.cpp +++ b/Source/Falcor/Core/API/D3D12/D3D12Resource.cpp @@ -143,14 +143,24 @@ namespace Falcor } - SharedResourceApiHandle Resource::createSharedApiHandle() + SharedResourceApiHandle Resource::getSharedApiHandle() const { - ID3D12DevicePtr pDevicePtr = gpDevice->getApiHandle(); - auto s = string_2_wstring(mName); - SharedResourceApiHandle pHandle; + if (!mSharedApiHandle) + { + ID3D12DevicePtr pDevicePtr = gpDevice->getApiHandle(); + auto s = string_2_wstring(mName); + SharedResourceApiHandle pHandle; - HRESULT res = pDevicePtr->CreateSharedHandle(mApiHandle, 0, GENERIC_ALL, s.c_str(), &pHandle); - if (res == S_OK) return pHandle; - else return nullptr; + HRESULT res = pDevicePtr->CreateSharedHandle(mApiHandle, 0, GENERIC_ALL, s.c_str(), &pHandle); + if (res == S_OK) + { + mSharedApiHandle = pHandle; + } + else + { + throw std::exception("Resource::getSharedApiHandle(): failed to create shared handle"); + } + } + return mSharedApiHandle; } } diff --git a/Source/Falcor/Core/API/D3D12/D3D12ResourceViews.cpp b/Source/Falcor/Core/API/D3D12/D3D12ResourceViews.cpp index b6ce417d84..eb9d972e0b 100644 --- a/Source/Falcor/Core/API/D3D12/D3D12ResourceViews.cpp +++ b/Source/Falcor/Core/API/D3D12/D3D12ResourceViews.cpp @@ -33,37 +33,147 @@ namespace Falcor { - template - ResourceView::~ResourceView() = default; - - template - ViewType getViewDimension(Resource::Type type, bool isArray); - - Texture::SharedPtr getEmptyTexture() + namespace { - return Texture::SharedPtr(); + /** Translate resource type enum to Falcor view dimension. + */ + ReflectionResourceType::Dimensions getDimension(Resource::Type type, bool isTextureArray) + { + switch (type) + { + case Resource::Type::Buffer: + assert(isTextureArray == false); + return ReflectionResourceType::Dimensions::Buffer; + case Resource::Type::Texture1D: + return (isTextureArray) ? ReflectionResourceType::Dimensions::Texture1DArray : ReflectionResourceType::Dimensions::Texture1D; + case Resource::Type::Texture2D: + return (isTextureArray) ? ReflectionResourceType::Dimensions::Texture2DArray : ReflectionResourceType::Dimensions::Texture2D; + case Resource::Type::Texture2DMultisample: + return (isTextureArray) ? ReflectionResourceType::Dimensions::Texture2DMSArray : ReflectionResourceType::Dimensions::Texture2DMS; + case Resource::Type::Texture3D: + assert(isTextureArray == false); + return ReflectionResourceType::Dimensions::Texture3D; + case Resource::Type::TextureCube: + return (isTextureArray) ? ReflectionResourceType::Dimensions::TextureCubeArray : ReflectionResourceType::Dimensions::TextureCube; + default: + should_not_get_here(); + return ReflectionResourceType::Dimensions::Unknown; + } + } + + /** Translate Falcor view dimension to D3D12 view dimension. + */ + template + ViewType getViewDimension(ReflectionResourceType::Dimensions dimension); + + template<> + D3D12_SRV_DIMENSION getViewDimension(ReflectionResourceType::Dimensions dimension) + { + switch (dimension) + { + case ReflectionResourceType::Dimensions::Buffer: return D3D12_SRV_DIMENSION_BUFFER; + case ReflectionResourceType::Dimensions::Texture1D: return D3D12_SRV_DIMENSION_TEXTURE1D; + case ReflectionResourceType::Dimensions::Texture1DArray: return D3D12_SRV_DIMENSION_TEXTURE1DARRAY; + case ReflectionResourceType::Dimensions::Texture2D: return D3D12_SRV_DIMENSION_TEXTURE2D; + case ReflectionResourceType::Dimensions::Texture2DArray: return D3D12_SRV_DIMENSION_TEXTURE2DARRAY; + case ReflectionResourceType::Dimensions::Texture2DMS: return D3D12_SRV_DIMENSION_TEXTURE2DMS; + case ReflectionResourceType::Dimensions::Texture2DMSArray: return D3D12_SRV_DIMENSION_TEXTURE2DMSARRAY; + case ReflectionResourceType::Dimensions::Texture3D: return D3D12_SRV_DIMENSION_TEXTURE3D; + case ReflectionResourceType::Dimensions::TextureCube: return D3D12_SRV_DIMENSION_TEXTURECUBE; + case ReflectionResourceType::Dimensions::TextureCubeArray: return D3D12_SRV_DIMENSION_TEXTURECUBEARRAY; + case ReflectionResourceType::Dimensions::AccelerationStructure: return D3D12_SRV_DIMENSION_RAYTRACING_ACCELERATION_STRUCTURE; + default: + should_not_get_here(); + return D3D12_SRV_DIMENSION_UNKNOWN; + } + } + + template<> + D3D12_UAV_DIMENSION getViewDimension(ReflectionResourceType::Dimensions dimension) + { + switch (dimension) + { + case ReflectionResourceType::Dimensions::Buffer: return D3D12_UAV_DIMENSION_BUFFER; + case ReflectionResourceType::Dimensions::Texture1D: return D3D12_UAV_DIMENSION_TEXTURE1D; + case ReflectionResourceType::Dimensions::Texture1DArray: return D3D12_UAV_DIMENSION_TEXTURE1DARRAY; + case ReflectionResourceType::Dimensions::Texture2D: return D3D12_UAV_DIMENSION_TEXTURE2D; + case ReflectionResourceType::Dimensions::Texture2DArray: return D3D12_UAV_DIMENSION_TEXTURE2DARRAY; + case ReflectionResourceType::Dimensions::Texture3D: return D3D12_UAV_DIMENSION_TEXTURE3D; + default: + should_not_get_here(); + return D3D12_UAV_DIMENSION_UNKNOWN; + } + } + + template<> + D3D12_DSV_DIMENSION getViewDimension(ReflectionResourceType::Dimensions dimension) + { + switch (dimension) + { + case ReflectionResourceType::Dimensions::Texture1D: return D3D12_DSV_DIMENSION_TEXTURE1D; + case ReflectionResourceType::Dimensions::Texture1DArray: return D3D12_DSV_DIMENSION_TEXTURE1DARRAY; + case ReflectionResourceType::Dimensions::Texture2D: return D3D12_DSV_DIMENSION_TEXTURE2D; + case ReflectionResourceType::Dimensions::Texture2DArray: return D3D12_DSV_DIMENSION_TEXTURE2DARRAY; + case ReflectionResourceType::Dimensions::Texture2DMS: return D3D12_DSV_DIMENSION_TEXTURE2DMS; + case ReflectionResourceType::Dimensions::Texture2DMSArray: return D3D12_DSV_DIMENSION_TEXTURE2DMSARRAY; + // TODO: Falcor previously mapped cube to 2D array. Not sure if needed anymore. + //case ReflectionResourceType::Dimensions::TextureCube: return D3D12_DSV_DIMENSION_TEXTURE2DARRAY; + default: + should_not_get_here(); + return D3D12_DSV_DIMENSION_UNKNOWN; + } + } + + template<> + D3D12_RTV_DIMENSION getViewDimension(ReflectionResourceType::Dimensions dimension) + { + switch (dimension) + { + case ReflectionResourceType::Dimensions::Buffer: return D3D12_RTV_DIMENSION_BUFFER; + case ReflectionResourceType::Dimensions::Texture1D: return D3D12_RTV_DIMENSION_TEXTURE1D; + case ReflectionResourceType::Dimensions::Texture1DArray: return D3D12_RTV_DIMENSION_TEXTURE1DARRAY; + case ReflectionResourceType::Dimensions::Texture2D: return D3D12_RTV_DIMENSION_TEXTURE2D; + case ReflectionResourceType::Dimensions::Texture2DArray: return D3D12_RTV_DIMENSION_TEXTURE2DARRAY; + case ReflectionResourceType::Dimensions::Texture2DMS: return D3D12_RTV_DIMENSION_TEXTURE2DMS; + case ReflectionResourceType::Dimensions::Texture2DMSArray: return D3D12_RTV_DIMENSION_TEXTURE2DMSARRAY; + case ReflectionResourceType::Dimensions::Texture3D: return D3D12_RTV_DIMENSION_TEXTURE3D; + // TODO: Falcor previously mapped cube to 2D array. Not sure if needed anymore. + //case ReflectionResourceType::Dimensions::TextureCube: return D3D12_RTV_DIMENSION_TEXTURE2DARRAY; + default: + should_not_get_here(); + return D3D12_RTV_DIMENSION_UNKNOWN; + } + } } + template + ResourceView::~ResourceView() = default; + D3D12_SHADER_RESOURCE_VIEW_DESC createBufferSrvDesc(const Buffer* pBuffer, uint32_t firstElement, uint32_t elementCount) { assert(pBuffer); D3D12_SHADER_RESOURCE_VIEW_DESC desc = {}; - uint32_t bufferElementCount = ShaderResourceView::kMaxPossible; + uint32_t bufferElementSize = 0; + uint32_t bufferElementCount = 0; if (pBuffer->isTyped()) { + assert(getFormatPixelsPerBlock(pBuffer->getFormat()) == 1); + bufferElementSize = getFormatBytesPerBlock(pBuffer->getFormat()); bufferElementCount = pBuffer->getElementCount(); desc.Format = getDxgiFormat(pBuffer->getFormat()); } else if (pBuffer->isStructured()) { + bufferElementSize = pBuffer->getStructSize(); bufferElementCount = pBuffer->getElementCount(); desc.Format = DXGI_FORMAT_UNKNOWN; desc.Buffer.StructureByteStride = pBuffer->getStructSize(); } else { - bufferElementCount = (uint32_t)pBuffer->getSize() / sizeof(float); + bufferElementSize = sizeof(uint32_t); + bufferElementCount = (uint32_t)(pBuffer->getSize() / sizeof(uint32_t)); desc.Format = DXGI_FORMAT_R32_TYPELESS; desc.Buffer.Flags = D3D12_BUFFER_SRV_FLAG_RAW; } @@ -76,6 +186,14 @@ namespace Falcor desc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; desc.ViewDimension = D3D12_SRV_DIMENSION_BUFFER; + // D3D12 doesn't currently handle views that extend to close to 4GB or beyond the base address. + // TODO: Revisit this check in the future. + assert(bufferElementSize > 0); + if (desc.Buffer.FirstElement + desc.Buffer.NumElements > ((1ull << 32) / bufferElementSize - 8)) + { + throw std::exception("Buffer SRV exceeds the maximum supported size"); + } + return desc; } @@ -89,7 +207,7 @@ namespace Falcor desc.Format = getDxgiFormat(colorFormat); bool isTextureArray = pTexture->getArraySize() > 1; - desc.ViewDimension = getViewDimension(pTexture->getType(), isTextureArray); + desc.ViewDimension = getViewDimension(getDimension(pTexture->getType(), isTextureArray)); switch (pTexture->getType()) { @@ -155,14 +273,12 @@ namespace Falcor return desc; } - template + template DescType createDsvRtvUavDescCommon(const Resource* pResource, uint32_t mipLevel, uint32_t firstArraySlice, uint32_t arraySize) { - DescType desc = {}; const Texture* pTexture = dynamic_cast(pResource); assert(pTexture); // Buffers should not get here - desc = {}; uint32_t arrayMultiplier = (pResource->getType() == Resource::Type::TextureCube) ? 6 : 1; if (arraySize == Resource::kMaxPossible) @@ -170,7 +286,9 @@ namespace Falcor arraySize = pTexture->getArraySize() - firstArraySlice; } - desc.ViewDimension = getViewDimension(pTexture->getType(), pTexture->getArraySize() > 1); + DescType desc = {}; + desc.Format = getDxgiFormat(pTexture->getFormat()); + desc.ViewDimension = getViewDimension(getDimension(pTexture->getType(), pTexture->getArraySize() > 1)); switch (pResource->getType()) { @@ -199,27 +317,35 @@ namespace Falcor desc.Texture2D.MipSlice = mipLevel; } break; - default: - if (finalCall) should_not_get_here(); - } - desc.Format = getDxgiFormat(pTexture->getFormat()); - - return desc; - } - - template - DescType createDsvRtvDesc(const Resource* pResource, uint32_t mipLevel, uint32_t firstArraySlice, uint32_t arraySize) - { - DescType desc = createDsvRtvUavDescCommon(pResource, mipLevel, firstArraySlice, arraySize); - - if (pResource->getType() == Resource::Type::Texture2DMultisample) - { - const Texture* pTexture = dynamic_cast(pResource); - if (pTexture->getArraySize() > 1) + case Resource::Type::Texture2DMultisample: + if constexpr (std::is_same_v || std::is_same_v) { - desc.Texture2DMSArray.ArraySize = arraySize; - desc.Texture2DMSArray.FirstArraySlice = firstArraySlice; + if (pTexture->getArraySize() > 1) + { + desc.Texture2DMSArray.ArraySize = arraySize; + desc.Texture2DMSArray.FirstArraySlice = firstArraySlice; + } + } + else + { + throw std::exception("Texture2DMultisample does not support UAV views"); + } + break; + case Resource::Type::Texture3D: + if constexpr (std::is_same_v || std::is_same_v) + { + assert(pTexture->getArraySize() == 1); + desc.Texture3D.MipSlice = mipLevel; + desc.Texture3D.FirstWSlice = 0; + desc.Texture3D.WSize = pTexture->getDepth(mipLevel); + } + else + { + throw std::exception("Texture3D does not support DSV views"); } + break; + default: + should_not_get_here(); } return desc; @@ -227,12 +353,12 @@ namespace Falcor D3D12_DEPTH_STENCIL_VIEW_DESC createDsvDesc(const Resource* pResource, uint32_t mipLevel, uint32_t firstArraySlice, uint32_t arraySize) { - return createDsvRtvDesc(pResource, mipLevel, firstArraySlice, arraySize); + return createDsvRtvUavDescCommon(pResource, mipLevel, firstArraySlice, arraySize); } D3D12_RENDER_TARGET_VIEW_DESC createRtvDesc(const Resource* pResource, uint32_t mipLevel, uint32_t firstArraySlice, uint32_t arraySize) { - return createDsvRtvDesc(pResource, mipLevel, firstArraySlice, arraySize); + return createDsvRtvUavDescCommon(pResource, mipLevel, firstArraySlice, arraySize); } D3D12_UNORDERED_ACCESS_VIEW_DESC createBufferUavDesc(const Buffer* pBuffer, uint32_t firstElement, uint32_t elementCount) @@ -240,22 +366,26 @@ namespace Falcor assert(pBuffer); D3D12_UNORDERED_ACCESS_VIEW_DESC desc = {}; - desc = {}; - uint32_t bufferElementCount = UnorderedAccessView::kMaxPossible; + uint32_t bufferElementSize = 0; + uint32_t bufferElementCount = 0; if (pBuffer->isTyped()) { + assert(getFormatPixelsPerBlock(pBuffer->getFormat()) == 1); + bufferElementSize = getFormatBytesPerBlock(pBuffer->getFormat()); bufferElementCount = pBuffer->getElementCount(); desc.Format = getDxgiFormat(pBuffer->getFormat()); } else if (pBuffer->isStructured()) { + bufferElementSize = pBuffer->getStructSize(); bufferElementCount = pBuffer->getElementCount(); desc.Format = DXGI_FORMAT_UNKNOWN; desc.Buffer.StructureByteStride = pBuffer->getStructSize(); } else { - bufferElementCount = ((uint32_t)pBuffer->getSize() / sizeof(float)); + bufferElementSize = sizeof(uint32_t); + bufferElementCount = (uint32_t)(pBuffer->getSize() / sizeof(uint32_t)); desc.Format = DXGI_FORMAT_R32_TYPELESS; desc.Buffer.Flags = D3D12_BUFFER_UAV_FLAG_RAW; } @@ -267,6 +397,14 @@ namespace Falcor desc.ViewDimension = D3D12_UAV_DIMENSION_BUFFER; + // D3D12 doesn't currently handle views that extend to close to 4GB or beyond the base address. + // TODO: Revisit this check in the future. + assert(bufferElementSize > 0); + if (desc.Buffer.FirstElement + desc.Buffer.NumElements > ((1ull << 32) / bufferElementSize - 8)) + { + throw std::exception("Buffer UAV exceeds the maximum supported size"); + } + return desc; } @@ -282,74 +420,78 @@ namespace Falcor ShaderResourceView::SharedPtr ShaderResourceView::create(ConstTextureSharedPtrRef pTexture, uint32_t mostDetailedMip, uint32_t mipCount, uint32_t firstArraySlice, uint32_t arraySize) { - if (!pTexture && getNullView()) return getNullView(); - - D3D12_SHADER_RESOURCE_VIEW_DESC desc; - Resource::ApiHandle resHandle = nullptr; - if(pTexture) - { - desc = createTextureSrvDesc(pTexture.get(), firstArraySlice, arraySize, mostDetailedMip, mipCount); - resHandle = pTexture->getApiHandle(); - } - else - { - desc = {}; - desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; - desc.ViewDimension = D3D12_SRV_DIMENSION_TEXTURE2D; - desc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; - } + assert(pTexture); + D3D12_SHADER_RESOURCE_VIEW_DESC desc = createTextureSrvDesc(pTexture.get(), firstArraySlice, arraySize, mostDetailedMip, mipCount); + Resource::ApiHandle resHandle = pTexture->getApiHandle(); - SharedPtr pObj = SharedPtr(new ShaderResourceView(pTexture, createSrvDescriptor(desc, resHandle), mostDetailedMip, mipCount, firstArraySlice, arraySize)); - return pObj; + return SharedPtr(new ShaderResourceView(pTexture, createSrvDescriptor(desc, resHandle), mostDetailedMip, mipCount, firstArraySlice, arraySize)); } ShaderResourceView::SharedPtr ShaderResourceView::create(ConstBufferSharedPtrRef pBuffer, uint32_t firstElement, uint32_t elementCount) { - if (!pBuffer && getNullView()) return getNullView(); + assert(pBuffer); + D3D12_SHADER_RESOURCE_VIEW_DESC desc = createBufferSrvDesc(pBuffer.get(), firstElement, elementCount); + Resource::ApiHandle resHandle = pBuffer->getApiHandle(); - D3D12_SHADER_RESOURCE_VIEW_DESC desc; - Resource::ApiHandle resHandle = nullptr; - if (pBuffer) - { - desc = createBufferSrvDesc(pBuffer.get(), firstElement, elementCount); - resHandle = pBuffer->getApiHandle(); - } - else - { - desc = {}; - desc.Format = DXGI_FORMAT_UNKNOWN; - desc.ViewDimension = D3D12_SRV_DIMENSION_BUFFER; - desc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; - } + return SharedPtr(new ShaderResourceView(pBuffer, createSrvDescriptor(desc, resHandle), firstElement, elementCount)); + } - SharedPtr pObj = SharedPtr(new ShaderResourceView(pBuffer, createSrvDescriptor(desc, resHandle), firstElement, elementCount)); - return pObj; + ShaderResourceView::SharedPtr ShaderResourceView::create(Dimension dimension) + { + // Create a null view of the specified dimension. + D3D12_SHADER_RESOURCE_VIEW_DESC desc = {}; + desc.Format = (dimension == Dimension::AccelerationStructure ? DXGI_FORMAT_UNKNOWN : DXGI_FORMAT_R32_UINT); + desc.ViewDimension = getViewDimension(dimension); + desc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; + + return SharedPtr(new ShaderResourceView(std::weak_ptr(), createSrvDescriptor(desc, nullptr), 0, 0)); } - DepthStencilView::SharedPtr DepthStencilView::create(ConstTextureSharedPtrRef pTexture, uint32_t mipLevel, uint32_t firstArraySlice, uint32_t arraySize) + ShaderResourceView::SharedPtr ShaderResourceView::createViewForAccelerationStructure(ConstBufferSharedPtrRef pBuffer) { - if (!pTexture && getNullView()) return getNullView(); + // Views for acceleration structures pass the GPU VA as part of the view desc. + // Note that in the call to CreateShaderResourceView() the resource ptr should be nullptr. + assert(pBuffer); + D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {}; + srvDesc.ViewDimension = D3D12_SRV_DIMENSION_RAYTRACING_ACCELERATION_STRUCTURE; + srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; + srvDesc.RaytracingAccelerationStructure.Location = pBuffer->getGpuAddress(); - D3D12_DEPTH_STENCIL_VIEW_DESC desc; - Resource::ApiHandle resHandle = nullptr; - if(pTexture) - { - desc = createDsvDesc(pTexture.get(), mipLevel, firstArraySlice, arraySize); - resHandle = pTexture->getApiHandle(); - } - else - { - desc = {}; - desc.Format = DXGI_FORMAT_D16_UNORM; - desc.ViewDimension = D3D12_DSV_DIMENSION_TEXTURE2D; - } + DescriptorSet::Layout layout; + layout.addRange(DescriptorSet::Type::AccelerationStructureSrv, 0, 1); + ShaderResourceView::ApiHandle handle = DescriptorSet::create(gpDevice->getCpuDescriptorPool(), layout); + gpDevice->getApiHandle()->CreateShaderResourceView(nullptr, &srvDesc, handle->getCpuHandle(0)); + return SharedPtr(new ShaderResourceView(pBuffer, handle)); + } + + DepthStencilView::ApiHandle createDsvDescriptor(const D3D12_DEPTH_STENCIL_VIEW_DESC& desc, Resource::ApiHandle resHandle) + { DescriptorSet::Layout layout; layout.addRange(DescriptorSet::Type::Dsv, 0, 1); - ApiHandle handle = DescriptorSet::create(gpDevice->getCpuDescriptorPool(), layout); + DepthStencilView::ApiHandle handle = DescriptorSet::create(gpDevice->getCpuDescriptorPool(), layout); gpDevice->getApiHandle()->CreateDepthStencilView(resHandle, &desc, handle->getCpuHandle(0)); - return SharedPtr(new DepthStencilView(pTexture, handle, mipLevel, firstArraySlice, arraySize)); + return handle; + } + + DepthStencilView::SharedPtr DepthStencilView::create(ConstTextureSharedPtrRef pTexture, uint32_t mipLevel, uint32_t firstArraySlice, uint32_t arraySize) + { + assert(pTexture); + D3D12_DEPTH_STENCIL_VIEW_DESC desc = createDsvDesc(pTexture.get(), mipLevel, firstArraySlice, arraySize); + Resource::ApiHandle resHandle = pTexture->getApiHandle(); + + return SharedPtr(new DepthStencilView(pTexture, createDsvDescriptor(desc, resHandle), mipLevel, firstArraySlice, arraySize)); + } + + DepthStencilView::SharedPtr DepthStencilView::create(Dimension dimension) + { + // Create a null view of the specified dimension. + D3D12_DEPTH_STENCIL_VIEW_DESC desc = {}; + desc.Format = DXGI_FORMAT_D32_FLOAT; + desc.ViewDimension = getViewDimension(dimension); + + return SharedPtr(new DepthStencilView(std::weak_ptr(), createDsvDescriptor(desc, nullptr), 0, 0, 1)); } UnorderedAccessView::ApiHandle createUavDescriptor(const D3D12_UNORDERED_ACCESS_VIEW_DESC& desc, Resource::ApiHandle resHandle, Resource::ApiHandle counterHandle) @@ -358,111 +500,100 @@ namespace Falcor layout.addRange(DescriptorSet::Type::TextureUav, 0, 1); UnorderedAccessView::ApiHandle handle = DescriptorSet::create(gpDevice->getCpuDescriptorPool(), layout); gpDevice->getApiHandle()->CreateUnorderedAccessView(resHandle, counterHandle, &desc, handle->getCpuHandle(0)); + return handle; } UnorderedAccessView::SharedPtr UnorderedAccessView::create(ConstTextureSharedPtrRef pTexture, uint32_t mipLevel, uint32_t firstArraySlice, uint32_t arraySize) { - if (!pTexture && getNullView()) return getNullView(); - - D3D12_UNORDERED_ACCESS_VIEW_DESC desc; - Resource::ApiHandle resHandle = nullptr; - - if(pTexture != nullptr) - { - desc = createDsvRtvUavDescCommon(pTexture.get(), mipLevel, firstArraySlice, arraySize); - resHandle = pTexture->getApiHandle(); - } - else - { - desc = {}; - desc.Format = DXGI_FORMAT_R32_UINT; - desc.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE2D; - } + assert(pTexture); + D3D12_UNORDERED_ACCESS_VIEW_DESC desc = createDsvRtvUavDescCommon(pTexture.get(), mipLevel, firstArraySlice, arraySize); + Resource::ApiHandle resHandle = pTexture->getApiHandle(); return SharedPtr(new UnorderedAccessView(pTexture, createUavDescriptor(desc, resHandle, nullptr), mipLevel, firstArraySlice, arraySize)); } UnorderedAccessView::SharedPtr UnorderedAccessView::create(ConstBufferSharedPtrRef pBuffer, uint32_t firstElement, uint32_t elementCount) { - if (!pBuffer && getNullView()) return getNullView(); - - D3D12_UNORDERED_ACCESS_VIEW_DESC desc; - Resource::ApiHandle resHandle = nullptr; + assert(pBuffer); + D3D12_UNORDERED_ACCESS_VIEW_DESC desc = createBufferUavDesc(pBuffer.get(), firstElement, elementCount); + Resource::ApiHandle resHandle = pBuffer->getApiHandle(); Resource::ApiHandle counterHandle = nullptr; - - if (pBuffer != nullptr) - { - desc = createBufferUavDesc(pBuffer.get(), firstElement, elementCount); - resHandle = pBuffer->getApiHandle(); - - if (pBuffer->getUAVCounter()) - { - counterHandle = pBuffer->getUAVCounter()->getApiHandle(); - } - } - else + if (pBuffer->getUAVCounter()) { - desc = {}; - desc.Format = DXGI_FORMAT_R32_UINT; - desc.ViewDimension = D3D12_UAV_DIMENSION_TEXTURE2D; + counterHandle = pBuffer->getUAVCounter()->getApiHandle(); } return SharedPtr(new UnorderedAccessView(pBuffer, createUavDescriptor(desc, resHandle, counterHandle), firstElement, elementCount)); } - RenderTargetView::~RenderTargetView() = default; - - RenderTargetView::SharedPtr RenderTargetView::create(ConstTextureSharedPtrRef pTexture, uint32_t mipLevel, uint32_t firstArraySlice, uint32_t arraySize) + UnorderedAccessView::SharedPtr UnorderedAccessView::create(Dimension dimension) { - if (!pTexture && getNullView()) return getNullView(); + // Create a null view of the specified dimension. + D3D12_UNORDERED_ACCESS_VIEW_DESC desc = {}; + desc.Format = DXGI_FORMAT_R32_UINT; + desc.ViewDimension = getViewDimension(dimension); - D3D12_RENDER_TARGET_VIEW_DESC desc; - Resource::ApiHandle resHandle = nullptr; - if(pTexture) - { - desc = createRtvDesc(pTexture.get(), mipLevel, firstArraySlice, arraySize); - resHandle = pTexture->getApiHandle(); - } - else - { - desc = {}; - desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;; - desc.ViewDimension = D3D12_RTV_DIMENSION_TEXTURE2D; - } + return SharedPtr(new UnorderedAccessView(std::weak_ptr(), createUavDescriptor(desc, nullptr, nullptr), 0, 0)); + } + RenderTargetView::~RenderTargetView() = default; + + RenderTargetView::ApiHandle createRtvDescriptor(const D3D12_RENDER_TARGET_VIEW_DESC& desc, Resource::ApiHandle resHandle) + { DescriptorSet::Layout layout; layout.addRange(DescriptorSet::Type::Rtv, 0, 1); - ApiHandle handle = DescriptorSet::create(gpDevice->getCpuDescriptorPool(), layout); + RenderTargetView::ApiHandle handle = DescriptorSet::create(gpDevice->getCpuDescriptorPool(), layout); gpDevice->getApiHandle()->CreateRenderTargetView(resHandle, &desc, handle->getCpuHandle(0)); - SharedPtr pObj = SharedPtr(new RenderTargetView(pTexture, handle, mipLevel, firstArraySlice, arraySize)); - return pObj; + return handle; } - ConstantBufferView::SharedPtr ConstantBufferView::create(ConstBufferSharedPtrRef pBuffer) + RenderTargetView::SharedPtr RenderTargetView::create(ConstTextureSharedPtrRef pTexture, uint32_t mipLevel, uint32_t firstArraySlice, uint32_t arraySize) { - if (!pBuffer && getNullView()) return getNullView(); + assert(pTexture); + D3D12_RENDER_TARGET_VIEW_DESC desc = createRtvDesc(pTexture.get(), mipLevel, firstArraySlice, arraySize); + Resource::ApiHandle resHandle = pTexture->getApiHandle(); - D3D12_CONSTANT_BUFFER_VIEW_DESC desc; - Resource::ApiHandle resHandle = nullptr; - if (pBuffer) - { - desc.BufferLocation = pBuffer->getGpuAddress(); - desc.SizeInBytes = (uint32_t)pBuffer->getSize(); - resHandle = pBuffer->getApiHandle(); - } - else - { - desc = {}; - } + return SharedPtr(new RenderTargetView(pTexture, createRtvDescriptor(desc, resHandle), mipLevel, firstArraySlice, arraySize)); + } + RenderTargetView::SharedPtr RenderTargetView::create(Dimension dimension) + { + // Create a null view of the specified dimension. + D3D12_RENDER_TARGET_VIEW_DESC desc = {}; + desc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + desc.ViewDimension = getViewDimension(dimension); + + return SharedPtr(new RenderTargetView(std::weak_ptr(), createRtvDescriptor(desc, nullptr), 0, 0, 1)); + } + + ConstantBufferView::ApiHandle createCbvDescriptor(const D3D12_CONSTANT_BUFFER_VIEW_DESC& desc, Resource::ApiHandle resHandle) + { DescriptorSet::Layout layout; layout.addRange(DescriptorSet::Type::Cbv, 0, 1); - ApiHandle handle = DescriptorSet::create(gpDevice->getCpuDescriptorPool(), layout); + ConstantBufferView::ApiHandle handle = DescriptorSet::create(gpDevice->getCpuDescriptorPool(), layout); gpDevice->getApiHandle()->CreateConstantBufferView(&desc, handle->getCpuHandle(0)); - SharedPtr pObj = SharedPtr(new ConstantBufferView(pBuffer, handle)); - return pObj; + return handle; + } + + ConstantBufferView::SharedPtr ConstantBufferView::create(ConstBufferSharedPtrRef pBuffer) + { + assert(pBuffer); + D3D12_CONSTANT_BUFFER_VIEW_DESC desc = {}; + desc.BufferLocation = pBuffer->getGpuAddress(); + desc.SizeInBytes = (uint32_t)pBuffer->getSize(); + Resource::ApiHandle resHandle = pBuffer->getApiHandle(); + + return SharedPtr(new ConstantBufferView(pBuffer, createCbvDescriptor(desc, resHandle))); + } + + ConstantBufferView::SharedPtr ConstantBufferView::create() + { + // Create a null view. + D3D12_CONSTANT_BUFFER_VIEW_DESC desc = {}; + + return SharedPtr(new ConstantBufferView(std::weak_ptr(), createCbvDescriptor(desc, nullptr))); } } diff --git a/Source/Falcor/Core/API/D3D12/D3D12State.cpp b/Source/Falcor/Core/API/D3D12/D3D12State.cpp index e57d96bab8..dd15b41499 100644 --- a/Source/Falcor/Core/API/D3D12/D3D12State.cpp +++ b/Source/Falcor/Core/API/D3D12/D3D12State.cpp @@ -406,6 +406,7 @@ namespace Falcor case RootSignature::DescType::RawBufferSrv: case RootSignature::DescType::TypedBufferSrv: case RootSignature::DescType::StructuredBufferSrv: + case RootSignature::DescType::AccelerationStructureSrv: return D3D12_DESCRIPTOR_RANGE_TYPE_SRV; case RootSignature::DescType::TextureUav: case RootSignature::DescType::RawBufferUav: @@ -464,6 +465,7 @@ namespace Falcor case RootSignature::DescType::RawBufferSrv: case RootSignature::DescType::TypedBufferSrv: case RootSignature::DescType::StructuredBufferSrv: + case RootSignature::DescType::AccelerationStructureSrv: desc.ParameterType = D3D12_ROOT_PARAMETER_TYPE_SRV; break; case RootSignature::DescType::RawBufferUav: diff --git a/Source/Falcor/Core/API/D3D12/D3D12Texture.cpp b/Source/Falcor/Core/API/D3D12/D3D12Texture.cpp index e9bb3237ce..33bc269756 100644 --- a/Source/Falcor/Core/API/D3D12/D3D12Texture.cpp +++ b/Source/Falcor/Core/API/D3D12/D3D12Texture.cpp @@ -32,48 +32,6 @@ namespace Falcor { - template - ViewType getViewDimension(Resource::Type type, bool isArray); - - template<> - D3D12_UAV_DIMENSION getViewDimension(Texture::Type type, bool isTextureArray) - { - switch (type) - { - case Texture::Type::Texture1D: - return (isTextureArray) ? D3D12_UAV_DIMENSION_TEXTURE1DARRAY : D3D12_UAV_DIMENSION_TEXTURE1D; - case Texture::Type::Texture2D: - return (isTextureArray) ? D3D12_UAV_DIMENSION_TEXTURE2DARRAY : D3D12_UAV_DIMENSION_TEXTURE2D; - case Texture::Type::TextureCube: - return D3D12_UAV_DIMENSION_TEXTURE2DARRAY; - default: - should_not_get_here(); - return D3D12_UAV_DIMENSION_UNKNOWN; - } - } - - template<> - D3D12_SRV_DIMENSION getViewDimension(Texture::Type type, bool isTextureArray) - { - switch(type) - { - case Texture::Type::Texture1D: - return (isTextureArray) ? D3D12_SRV_DIMENSION_TEXTURE1DARRAY : D3D12_SRV_DIMENSION_TEXTURE1D; - case Texture::Type::Texture2D: - return (isTextureArray) ? D3D12_SRV_DIMENSION_TEXTURE2DARRAY : D3D12_SRV_DIMENSION_TEXTURE2D; - case Texture::Type::Texture3D: - assert(isTextureArray == false); - return D3D12_SRV_DIMENSION_TEXTURE3D; - case Texture::Type::Texture2DMultisample: - return (isTextureArray) ? D3D12_SRV_DIMENSION_TEXTURE2DMSARRAY : D3D12_SRV_DIMENSION_TEXTURE2DMS; - case Texture::Type::TextureCube: - return (isTextureArray) ? D3D12_SRV_DIMENSION_TEXTURECUBEARRAY : D3D12_SRV_DIMENSION_TEXTURECUBE; - default: - should_not_get_here(); - return D3D12_SRV_DIMENSION_UNKNOWN; - } - } - D3D12_RESOURCE_DIMENSION getResourceDimension(Texture::Type type) { switch (type) @@ -121,6 +79,8 @@ namespace Falcor { desc.DepthOrArraySize = mArraySize; } + assert(desc.Width > 0 && desc.Height > 0); + assert(desc.MipLevels > 0 && desc.DepthOrArraySize > 0 && desc.SampleDesc.Count > 0); D3D12_CLEAR_VALUE clearValue = {}; D3D12_CLEAR_VALUE* pClearVal = nullptr; @@ -143,6 +103,7 @@ namespace Falcor D3D12_HEAP_FLAGS heapFlags = is_set(mBindFlags, ResourceBindFlags::Shared) ? D3D12_HEAP_FLAG_SHARED : D3D12_HEAP_FLAG_NONE; d3d_call(gpDevice->getApiHandle()->CreateCommittedResource(&kDefaultHeapProps, heapFlags, &desc, D3D12_RESOURCE_STATE_COMMON, pClearVal, IID_PPV_ARGS(&mApiHandle))); + assert(mApiHandle); if (pData) { diff --git a/Source/Falcor/Core/API/DescriptorPool.h b/Source/Falcor/Core/API/DescriptorPool.h index 0bd1e96cf3..2a4b27db9d 100644 --- a/Source/Falcor/Core/API/DescriptorPool.h +++ b/Source/Falcor/Core/API/DescriptorPool.h @@ -56,6 +56,7 @@ namespace Falcor Cbv, StructuredBufferUav, StructuredBufferSrv, + AccelerationStructureSrv, Dsv, Rtv, Sampler, diff --git a/Source/Falcor/Core/API/DescriptorSet.cpp b/Source/Falcor/Core/API/DescriptorSet.cpp index b945ed2d86..91c3ba10e0 100644 --- a/Source/Falcor/Core/API/DescriptorSet.cpp +++ b/Source/Falcor/Core/API/DescriptorSet.cpp @@ -58,5 +58,4 @@ namespace Falcor mRanges.push_back(r); return *this; } - } diff --git a/Source/Falcor/Core/API/Device.h b/Source/Falcor/Core/API/Device.h index 60b3905ca5..f90a82e30b 100644 --- a/Source/Falcor/Core/API/Device.h +++ b/Source/Falcor/Core/API/Device.h @@ -77,7 +77,9 @@ namespace Falcor None = 0x0, ProgrammableSamplePositionsPartialOnly = 0x1, // On D3D12, this means tier 1 support. Allows one sample position to be set. ProgrammableSamplePositionsFull = 0x2, // On D3D12, this means tier 2 support. Allows up to 4 sample positions to be set. - Raytracing = 0x4 // On D3D12, DirectX Raytracing is supported. It is up to the user to not use raytracing functions when not supported. + Barycentrics = 0x4, // On D3D12, pixel shader barycentrics are supported. + Raytracing = 0x8, // On D3D12, DirectX Raytracing is supported. It is up to the user to not use raytracing functions when not supported. + RaytracingTier1_1 = 0x10, // On D3D12, DirectX Raytracing Tier 1.1 is supported. }; /** Create a new device. diff --git a/Source/Falcor/Core/API/Formats.h b/Source/Falcor/Core/API/Formats.h index 49019feb03..b56c93f4c1 100644 --- a/Source/Falcor/Core/API/Formats.h +++ b/Source/Falcor/Core/API/Formats.h @@ -35,7 +35,7 @@ namespace Falcor */ /** These flags are hints the driver to what pipeline stages the resource will be bound to. -*/ + */ enum class ResourceBindFlags : uint32_t { None = 0x0, ///< The resource will not be bound the pipeline. Use this to create a staging resource @@ -268,6 +268,14 @@ namespace Falcor return kFormatDesc[(uint32_t)format].numChannelBits[channel]; } + /** Get the number of bytes per row. If format is compressed, width should be evenly divisible by the compression ratio. + */ + inline uint32_t getFormatRowPitch(ResourceFormat format, uint32_t width) + { + assert(width % getFormatWidthCompressionRatio(format) == 0); + return (width / getFormatWidthCompressionRatio(format)) * getFormatBytesPerBlock(format); + } + /** Check if a format represents sRGB color space */ inline bool isSrgbFormat(ResourceFormat format) diff --git a/Source/Falcor/Core/API/Resource.h b/Source/Falcor/Core/API/Resource.h index eb7fd468c4..051e64af78 100644 --- a/Source/Falcor/Core/API/Resource.h +++ b/Source/Falcor/Core/API/Resource.h @@ -34,6 +34,7 @@ namespace Falcor class Texture; class Buffer; class ParameterBlock; + struct ResourceViewInfo; class dlldecl Resource : public std::enable_shared_from_this { @@ -114,9 +115,13 @@ namespace Falcor */ const ApiHandle& getApiHandle() const { return mApiHandle; } - /** Creates a shared resource API handle. + /** Get a shared resource API handle. + + The handle will be created on-demand if it does not already exist. + Throws if a shared handle cannot be created for this resource. */ - SharedResourceApiHandle createSharedApiHandle(); + SharedResourceApiHandle getSharedApiHandle() const; + struct ViewInfoHashFunc { @@ -157,6 +162,18 @@ namespace Falcor std::shared_ptr asTexture() { return this ? std::dynamic_pointer_cast(shared_from_this()) : nullptr; } std::shared_ptr asBuffer() { return this ? std::dynamic_pointer_cast(shared_from_this()) : nullptr; } +#if _ENABLE_CUDA + /** Get the CUDA device address for this resource. + \return CUDA device address. + Throws an exception if the resource is not shared. + */ + virtual void* getCUDADeviceAddress() const = 0; + + /** Get the CUDA device address for a view of this resource. + */ + virtual void* getCUDADeviceAddress(ResourceViewInfo const& viewInfo) const = 0; +#endif + protected: friend class CopyContext; @@ -179,6 +196,7 @@ namespace Falcor size_t mSize = 0; GpuAddress mGpuVaOffset = 0; std::string mName; + mutable SharedResourceApiHandle mSharedApiHandle = 0; mutable std::unordered_map mSrvs; mutable std::unordered_map mRtvs; diff --git a/Source/Falcor/Core/API/ResourceViews.cpp b/Source/Falcor/Core/API/ResourceViews.cpp index e6be5bcacf..f98f1a2783 100644 --- a/Source/Falcor/Core/API/ResourceViews.cpp +++ b/Source/Falcor/Core/API/ResourceViews.cpp @@ -30,16 +30,58 @@ namespace Falcor { - static NullResourceViews gNullViews; - Texture::SharedPtr getEmptyTexture(); + namespace + { + struct NullResourceViews + { + std::array srv; + std::array uav; + std::array dsv; + std::array rtv; + ConstantBufferView::SharedPtr cbv; + }; + + NullResourceViews gNullViews; + } void createNullViews() { - gNullViews.srv = ShaderResourceView::create(getEmptyTexture(), 0, 1, 0, 1); - gNullViews.dsv = DepthStencilView::create(getEmptyTexture(), 0, 0, 1); - gNullViews.uav = UnorderedAccessView::create(getEmptyTexture(), 0, 0, 1); - gNullViews.rtv = RenderTargetView::create(getEmptyTexture(), 0, 0, 1); - gNullViews.cbv = ConstantBufferView::create(Buffer::SharedPtr()); + gNullViews.srv[(size_t)ShaderResourceView::Dimension::Buffer] = ShaderResourceView::create(ShaderResourceView::Dimension::Buffer); + gNullViews.srv[(size_t)ShaderResourceView::Dimension::Texture1D] = ShaderResourceView::create(ShaderResourceView::Dimension::Texture1D); + gNullViews.srv[(size_t)ShaderResourceView::Dimension::Texture1DArray] = ShaderResourceView::create(ShaderResourceView::Dimension::Texture1DArray); + gNullViews.srv[(size_t)ShaderResourceView::Dimension::Texture2D] = ShaderResourceView::create(ShaderResourceView::Dimension::Texture2D); + gNullViews.srv[(size_t)ShaderResourceView::Dimension::Texture2DArray] = ShaderResourceView::create(ShaderResourceView::Dimension::Texture2DArray); + gNullViews.srv[(size_t)ShaderResourceView::Dimension::Texture2DMS] = ShaderResourceView::create(ShaderResourceView::Dimension::Texture2DMS); + gNullViews.srv[(size_t)ShaderResourceView::Dimension::Texture2DMSArray] = ShaderResourceView::create(ShaderResourceView::Dimension::Texture2DMSArray); + gNullViews.srv[(size_t)ShaderResourceView::Dimension::Texture3D] = ShaderResourceView::create(ShaderResourceView::Dimension::Texture3D); + gNullViews.srv[(size_t)ShaderResourceView::Dimension::TextureCube] = ShaderResourceView::create(ShaderResourceView::Dimension::TextureCube); + gNullViews.srv[(size_t)ShaderResourceView::Dimension::TextureCubeArray] = ShaderResourceView::create(ShaderResourceView::Dimension::TextureCubeArray); + gNullViews.srv[(size_t)ShaderResourceView::Dimension::AccelerationStructure] = ShaderResourceView::create(ShaderResourceView::Dimension::AccelerationStructure); + + gNullViews.uav[(size_t)UnorderedAccessView::Dimension::Buffer] = UnorderedAccessView::create(UnorderedAccessView::Dimension::Buffer); + gNullViews.uav[(size_t)UnorderedAccessView::Dimension::Texture1D] = UnorderedAccessView::create(UnorderedAccessView::Dimension::Texture1D); + gNullViews.uav[(size_t)UnorderedAccessView::Dimension::Texture1DArray] = UnorderedAccessView::create(UnorderedAccessView::Dimension::Texture1DArray); + gNullViews.uav[(size_t)UnorderedAccessView::Dimension::Texture2D] = UnorderedAccessView::create(UnorderedAccessView::Dimension::Texture2D); + gNullViews.uav[(size_t)UnorderedAccessView::Dimension::Texture2DArray] = UnorderedAccessView::create(UnorderedAccessView::Dimension::Texture2DArray); + gNullViews.uav[(size_t)UnorderedAccessView::Dimension::Texture3D] = UnorderedAccessView::create(UnorderedAccessView::Dimension::Texture3D); + + gNullViews.dsv[(size_t)DepthStencilView::Dimension::Texture1D] = DepthStencilView::create(DepthStencilView::Dimension::Texture1D); + gNullViews.dsv[(size_t)DepthStencilView::Dimension::Texture1DArray] = DepthStencilView::create(DepthStencilView::Dimension::Texture1DArray); + gNullViews.dsv[(size_t)DepthStencilView::Dimension::Texture2D] = DepthStencilView::create(DepthStencilView::Dimension::Texture2D); + gNullViews.dsv[(size_t)DepthStencilView::Dimension::Texture2DArray] = DepthStencilView::create(DepthStencilView::Dimension::Texture2DArray); + gNullViews.dsv[(size_t)DepthStencilView::Dimension::Texture2DMS] = DepthStencilView::create(DepthStencilView::Dimension::Texture2DMS); + gNullViews.dsv[(size_t)DepthStencilView::Dimension::Texture2DMSArray] = DepthStencilView::create(DepthStencilView::Dimension::Texture2DMSArray); + + gNullViews.rtv[(size_t)RenderTargetView::Dimension::Buffer] = RenderTargetView::create(RenderTargetView::Dimension::Buffer); + gNullViews.rtv[(size_t)RenderTargetView::Dimension::Texture1D] = RenderTargetView::create(RenderTargetView::Dimension::Texture1D); + gNullViews.rtv[(size_t)RenderTargetView::Dimension::Texture1DArray] = RenderTargetView::create(RenderTargetView::Dimension::Texture1DArray); + gNullViews.rtv[(size_t)RenderTargetView::Dimension::Texture2D] = RenderTargetView::create(RenderTargetView::Dimension::Texture2D); + gNullViews.rtv[(size_t)RenderTargetView::Dimension::Texture2DArray] = RenderTargetView::create(RenderTargetView::Dimension::Texture2DArray); + gNullViews.rtv[(size_t)RenderTargetView::Dimension::Texture2DMS] = RenderTargetView::create(RenderTargetView::Dimension::Texture2DMS); + gNullViews.rtv[(size_t)RenderTargetView::Dimension::Texture2DMSArray] = RenderTargetView::create(RenderTargetView::Dimension::Texture2DMSArray); + gNullViews.rtv[(size_t)RenderTargetView::Dimension::Texture3D] = RenderTargetView::create(RenderTargetView::Dimension::Texture3D); + + gNullViews.cbv = ConstantBufferView::create(); } void releaseNullViews() @@ -47,11 +89,34 @@ namespace Falcor gNullViews = {}; } - ShaderResourceView::SharedPtr ShaderResourceView::getNullView() { return gNullViews.srv; } - DepthStencilView::SharedPtr DepthStencilView::getNullView() { return gNullViews.dsv; } - UnorderedAccessView::SharedPtr UnorderedAccessView::getNullView() { return gNullViews.uav; } - RenderTargetView::SharedPtr RenderTargetView::getNullView() { return gNullViews.rtv;} - ConstantBufferView::SharedPtr ConstantBufferView::getNullView() { return gNullViews.cbv;} + ShaderResourceView::SharedPtr ShaderResourceView::getNullView(ShaderResourceView::Dimension dimension) + { + assert((size_t)dimension < gNullViews.srv.size() && gNullViews.srv[(size_t)dimension]); + return gNullViews.srv[(size_t)dimension]; + } + + UnorderedAccessView::SharedPtr UnorderedAccessView::getNullView(UnorderedAccessView::Dimension dimension) + { + assert((size_t)dimension < gNullViews.uav.size() && gNullViews.uav[(size_t)dimension]); + return gNullViews.uav[(size_t)dimension]; + } + + DepthStencilView::SharedPtr DepthStencilView::getNullView(DepthStencilView::Dimension dimension) + { + assert((size_t)dimension < gNullViews.dsv.size() && gNullViews.dsv[(size_t)dimension]); + return gNullViews.dsv[(size_t)dimension]; + } + + RenderTargetView::SharedPtr RenderTargetView::getNullView(RenderTargetView::Dimension dimension) + { + assert((size_t)dimension < gNullViews.rtv.size() && gNullViews.rtv[(size_t)dimension]); + return gNullViews.rtv[(size_t)dimension]; + } + + ConstantBufferView::SharedPtr ConstantBufferView::getNullView() + { + return gNullViews.cbv; + } SCRIPT_BINDING(ResourceView) { diff --git a/Source/Falcor/Core/API/ResourceViews.h b/Source/Falcor/Core/API/ResourceViews.h index 7b15d386f5..75454ee202 100644 --- a/Source/Falcor/Core/API/ResourceViews.h +++ b/Source/Falcor/Core/API/ResourceViews.h @@ -26,6 +26,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **************************************************************************/ #pragma once +#include "Core/Program/ProgramReflection.h" #include namespace Falcor @@ -76,6 +77,7 @@ namespace Falcor { public: using ApiHandle = ApiHandleType; + using Dimension = ReflectionResourceType::Dimensions; static const uint32_t kMaxPossible = -1; virtual ~ResourceView(); @@ -85,6 +87,9 @@ namespace Falcor ResourceView(ResourceWeakPtr& pResource, ApiHandle handle, uint32_t firstElement, uint32_t elementCount) : mApiHandle(handle), mpResource(pResource), mViewInfo(firstElement, elementCount) {} + ResourceView(ResourceWeakPtr& pResource, ApiHandle handle) + : mApiHandle(handle), mpResource(pResource) {} + /** Get the raw API handle. */ const ApiHandle& getApiHandle() const { return mApiHandle; } @@ -96,6 +101,16 @@ namespace Falcor /** Get the resource referenced by the view. */ Resource* getResource() const { return mpResource.lock().get(); } + +#if _ENABLE_CUDA + /** Get the CUDA device address for this view. + */ + void* getCUDADeviceAddress() const + { + return mpResource.lock()->getCUDADeviceAddress(mViewInfo); + } +#endif + protected: ApiHandle mApiHandle; ResourceViewInfo mViewInfo; @@ -110,15 +125,18 @@ namespace Falcor static SharedPtr create(ConstTextureSharedPtrRef pTexture, uint32_t mostDetailedMip, uint32_t mipCount, uint32_t firstArraySlice, uint32_t arraySize); static SharedPtr create(ConstBufferSharedPtrRef pBuffer, uint32_t firstElement, uint32_t elementCount); - static SharedPtr getNullView(); + static SharedPtr create(Dimension dimension); + static SharedPtr createViewForAccelerationStructure(ConstBufferSharedPtrRef pBuffer); - // This is currently used by RtScene to create an SRV for the TLAS, since the create() functions above assume texture or buffer types. + static SharedPtr getNullView(Dimension dimension); + + private: ShaderResourceView(ResourceWeakPtr pResource, ApiHandle handle, uint32_t mostDetailedMip, uint32_t mipCount, uint32_t firstArraySlice, uint32_t arraySize) : ResourceView(pResource, handle, mostDetailedMip, mipCount, firstArraySlice, arraySize) {} - private: - ShaderResourceView(ResourceWeakPtr pResource, ApiHandle handle, uint32_t firstElement, uint32_t elementCount) : ResourceView(pResource, handle, firstElement, elementCount) {} + ShaderResourceView(ResourceWeakPtr pResource, ApiHandle handle) + : ResourceView(pResource, handle) {} }; class dlldecl DepthStencilView : public ResourceView @@ -128,7 +146,10 @@ namespace Falcor using SharedConstPtr = std::shared_ptr; static SharedPtr create(ConstTextureSharedPtrRef pTexture, uint32_t mipLevel, uint32_t firstArraySlice, uint32_t arraySize); - static SharedPtr getNullView(); + static SharedPtr create(Dimension dimension); + + static SharedPtr getNullView(Dimension dimension); + private: DepthStencilView(ResourceWeakPtr pResource, ApiHandle handle, uint32_t mipLevel, uint32_t firstArraySlice, uint32_t arraySize) : ResourceView(pResource, handle, mipLevel, 1, firstArraySlice, arraySize) {} @@ -142,7 +163,10 @@ namespace Falcor static SharedPtr create(ConstTextureSharedPtrRef pTexture, uint32_t mipLevel, uint32_t firstArraySlice, uint32_t arraySize); static SharedPtr create(ConstBufferSharedPtrRef pBuffer, uint32_t firstElement, uint32_t elementCount); - static SharedPtr getNullView(); + static SharedPtr create(Dimension dimension); + + static SharedPtr getNullView(Dimension dimension); + private: UnorderedAccessView(ResourceWeakPtr pResource, ApiHandle handle, uint32_t mipLevel, uint32_t firstArraySlice, uint32_t arraySize) : ResourceView(pResource, handle, mipLevel, 1, firstArraySlice, arraySize) {} @@ -157,8 +181,12 @@ namespace Falcor using SharedPtr = std::shared_ptr; using SharedConstPtr = std::shared_ptr; static SharedPtr create(ConstTextureSharedPtrRef pTexture, uint32_t mipLevel, uint32_t firstArraySlice, uint32_t arraySize); - static SharedPtr getNullView(); + static SharedPtr create(Dimension dimension); + + static SharedPtr getNullView(Dimension dimension); + ~RenderTargetView(); + private: RenderTargetView(ResourceWeakPtr pResource, ApiHandle handle, uint32_t mipLevel, uint32_t firstArraySlice, uint32_t arraySize) : ResourceView(pResource, handle, mipLevel, 1, firstArraySlice, arraySize) {} @@ -170,19 +198,12 @@ namespace Falcor using SharedPtr = std::shared_ptr; using SharedConstPtr = std::shared_ptr; static SharedPtr create(ConstBufferSharedPtrRef pBuffer); + static SharedPtr create(); + static SharedPtr getNullView(); private: ConstantBufferView(ResourceWeakPtr pResource, ApiHandle handle) : - ResourceView(pResource, handle, 0, 1, 0, 1) {} - }; - - struct NullResourceViews - { - ShaderResourceView::SharedPtr srv; - ConstantBufferView::SharedPtr cbv; - RenderTargetView::SharedPtr rtv; - UnorderedAccessView::SharedPtr uav; - DepthStencilView::SharedPtr dsv; + ResourceView(pResource, handle, 0, 1, 0, 1) {} }; } diff --git a/Source/Falcor/Core/API/RootSignature.cpp b/Source/Falcor/Core/API/RootSignature.cpp index a0ea8dbc44..e6dc9f99df 100644 --- a/Source/Falcor/Core/API/RootSignature.cpp +++ b/Source/Falcor/Core/API/RootSignature.cpp @@ -105,6 +105,7 @@ namespace Falcor case RootSignature::DescType::RawBufferSrv: case RootSignature::DescType::TypedBufferSrv: case RootSignature::DescType::StructuredBufferSrv: + case RootSignature::DescType::AccelerationStructureSrv: case RootSignature::DescType::Cbv: case RootSignature::DescType::Sampler: return ReflectionResourceType::ShaderAccess::Read; diff --git a/Source/Falcor/Core/API/Texture.cpp b/Source/Falcor/Core/API/Texture.cpp index 08f7eb9218..5b981aba49 100644 --- a/Source/Falcor/Core/API/Texture.cpp +++ b/Source/Falcor/Core/API/Texture.cpp @@ -31,6 +31,8 @@ #include "RenderContext.h" #include "Utils/Threading.h" +#include + namespace Falcor { namespace @@ -226,6 +228,18 @@ namespace Falcor return getUAV(0); } +#if _ENABLE_CUDA + void* Texture::getCUDADeviceAddress() const + { + throw std::exception("Texture::getCUDADeviceAddress() - unimplemented"); + } + + void* Texture::getCUDADeviceAddress(ResourceViewInfo const& viewInfo) const + { + throw std::exception("Texture::getCUDADeviceAddress() - unimplemented"); + } +#endif + RenderTargetView::SharedPtr Texture::getRTV(uint32_t mipLevel, uint32_t firstArraySlice, uint32_t arraySize) { auto createFunc = [](Texture* pTexture, uint32_t mostDetailedMip, uint32_t mipCount, uint32_t firstArraySlice, uint32_t arraySize) @@ -248,6 +262,11 @@ namespace Falcor void Texture::captureToFile(uint32_t mipLevel, uint32_t arraySlice, const std::string& filename, Bitmap::FileFormat format, Bitmap::ExportFlags exportFlags) { + if (format == Bitmap::FileFormat::DdsFile) + { + throw std::exception("Texture::captureToFile does not yet support saving to DDS."); + } + assert(mType == Type::Texture2D); RenderContext* pContext = gpDevice->getRenderContext(); // Handle the special case where we have an HDR texture with less then 3 channels @@ -281,6 +300,11 @@ namespace Falcor void Texture::uploadInitData(const void* pData, bool autoGenMips) { + // TODO: This is a hack to allow multi-threaded texture loading using AsyncTextureLoader. + // Replace with something better. + static std::mutex mutex; + std::lock_guard lock(mutex); + assert(gpDevice); auto pRenderContext = gpDevice->getRenderContext(); if (autoGenMips) @@ -335,7 +359,21 @@ namespace Falcor } } - uint32_t Texture::getTextureSizeInBytes() + uint64_t Texture::getTexelCount() const + { + uint64_t count = 0; + for (uint32_t i = 0; i < getMipCount(); i++) + { + uint64_t texelsInMip = (uint64_t)getWidth(i) * getHeight(i) * getDepth(i); + assert(texelsInMip > 0); + count += texelsInMip; + } + count *= getArraySize(); + assert(count > 0); + return count; + } + + uint64_t Texture::getTextureSizeInBytes() const { ID3D12DevicePtr pDevicePtr = gpDevice->getApiHandle(); ID3D12ResourcePtr pTexResource = this->getApiHandle(); @@ -344,11 +382,10 @@ namespace Falcor D3D12_RESOURCE_DESC desc = pTexResource->GetDesc(); assert(desc.Dimension == D3D12_RESOURCE_DIMENSION_TEXTURE2D); - assert(desc.Width == mWidth); - assert(desc.Height == mHeight); d3d12ResourceAllocationInfo = pDevicePtr->GetResourceAllocationInfo(0, 1, &desc); - return (uint32_t)d3d12ResourceAllocationInfo.SizeInBytes; + assert(d3d12ResourceAllocationInfo.SizeInBytes > 0); + return d3d12ResourceAllocationInfo.SizeInBytes; } SCRIPT_BINDING(Texture) diff --git a/Source/Falcor/Core/API/Texture.h b/Source/Falcor/Core/API/Texture.h index a42766b51a..02b67784cf 100644 --- a/Source/Falcor/Core/API/Texture.h +++ b/Source/Falcor/Core/API/Texture.h @@ -180,6 +180,19 @@ namespace Falcor */ virtual UnorderedAccessView::SharedPtr getUAV() override; +#if _ENABLE_CUDA + /** Get the CUDA device address for this resource. + \return CUDA device address. + Throws an exception if the resource is not (or cannot be) shared with CUDA. + */ + virtual void* getCUDADeviceAddress() const override; + + /** Get the CUDA device address for a view of this resource. + Throws an exception if the resource is not (or cannot be) shared with CUDA. + */ + virtual void* getCUDADeviceAddress(ResourceViewInfo const& viewInfo) const override; +#endif + /** Get a shader-resource view. \param[in] mostDetailedMip The most detailed mip level of the view \param[in] mipCount The number of mip-levels to bind. If this is equal to Texture#kMaxPossible, will create a view ranging from mostDetailedMip to the texture's mip levels count @@ -230,9 +243,13 @@ namespace Falcor */ const std::string& getSourceFilename() const { return mSourceFilename; } + /** Returns the total number of texels across all mip levels and array slices. + */ + uint64_t getTexelCount() const; + /** Returns the size of the texture in bytes as allocated in GPU memory. */ - uint32_t getTextureSizeInBytes(); + uint64_t getTextureSizeInBytes() const; protected: Texture(uint32_t width, uint32_t height, uint32_t depth, uint32_t arraySize, uint32_t mipLevels, uint32_t sampleCount, ResourceFormat format, Type Type, BindFlags bindFlags); diff --git a/Source/Falcor/Core/API/TextureLoader.cpp b/Source/Falcor/Core/API/TextureLoader.cpp index 745e3041aa..446acd3fde 100644 --- a/Source/Falcor/Core/API/TextureLoader.cpp +++ b/Source/Falcor/Core/API/TextureLoader.cpp @@ -27,682 +27,28 @@ **************************************************************************/ #include "stdafx.h" #include "Core/API/Texture.h" -#include "Utils/Image/DDSHeader.h" #include "Utils/BinaryFileStream.h" #include "Utils/StringUtils.h" #include +#include "Utils/Image/ImageIO.h" static const bool kTopDown = true; namespace Falcor { - using namespace DdsHelper; - - static const uint32_t kDdsMagicNumber = 0x20534444; - - bool checkDdsChannelMask(const DdsHeader::PixelFormat& format, uint32_t r, uint32_t g, uint32_t b, uint32_t a) - { - return (format.rMask == r && format.gMask == g && format.bMask == b && format.aMask == a); - } - - uint32_t makeFourCC(const char name[4]) - { - uint32_t fourCC = 0; - for(uint32_t i = 0; i < 4; i++) - { - uint32_t shift = i * 8; - fourCC |= ((uint32_t)name[i]) << shift; - } - return fourCC; - } - - ResourceFormat falcorFormatFromDXGIFormat(DXFormat fmt) - { - switch (fmt) - { - case FORMAT_R32G8X24_TYPELESS: - case FORMAT_R16G16B16A16_TYPELESS: - case FORMAT_R32G32B32_TYPELESS: - case FORMAT_R32G32B32A32_TYPELESS: - case FORMAT_R16G16B16A16_SNORM: - case FORMAT_R32G32_TYPELESS: - case FORMAT_R32_FLOAT_X8X24_TYPELESS: - case FORMAT_X32_TYPELESS_G8X24_UINT: - case FORMAT_R10G10B10A2_TYPELESS: - case FORMAT_Y416: - case FORMAT_Y210: - case FORMAT_Y216: - case FORMAT_R8G8B8A8_TYPELESS: - case FORMAT_R16G16_TYPELESS: - case FORMAT_R32_TYPELESS: - case FORMAT_R24G8_TYPELESS: - case FORMAT_R24_UNORM_X8_TYPELESS: - case FORMAT_X24_TYPELESS_G8_UINT: - case FORMAT_R8G8_B8G8_UNORM: - case FORMAT_G8R8_G8B8_UNORM: - case FORMAT_R10G10B10_XR_BIAS_A2_UNORM: - case FORMAT_B8G8R8A8_TYPELESS: - case FORMAT_B8G8R8X8_TYPELESS: - case FORMAT_AYUV: - case FORMAT_Y410: - case FORMAT_YUY2: - case FORMAT_P010: - case FORMAT_P016: - case FORMAT_R8G8_TYPELESS: - case FORMAT_R16_TYPELESS: - case FORMAT_A8P8: - case FORMAT_B4G4R4A4_UNORM: - case FORMAT_NV12: - case FORMAT_420_OPAQUE: - case FORMAT_NV11: - case FORMAT_R8_TYPELESS: - case FORMAT_AI44: - case FORMAT_IA44: - case FORMAT_P8: - case FORMAT_R1_UNORM: - case FORMAT_BC1_TYPELESS: - case FORMAT_BC4_TYPELESS: - case FORMAT_BC2_TYPELESS: - case FORMAT_BC3_TYPELESS: - case FORMAT_BC5_TYPELESS: - case FORMAT_BC6H_TYPELESS: - case FORMAT_BC7_TYPELESS: - return ResourceFormat::Unknown; - case FORMAT_R32G32B32A32_FLOAT: - return ResourceFormat::RGBA32Float; - case FORMAT_R32G32B32A32_UINT: - return ResourceFormat::RGBA32Uint; - case FORMAT_R32G32B32A32_SINT: - return ResourceFormat::RGBA32Int; - case FORMAT_R32G32B32_FLOAT: - return ResourceFormat::RGB32Float; - case FORMAT_R32G32B32_UINT: - return ResourceFormat::RGB32Uint; - case FORMAT_R32G32B32_SINT: - return ResourceFormat::RGB32Int; - case FORMAT_R16G16B16A16_FLOAT: - return ResourceFormat::RGBA16Float; - case FORMAT_R16G16B16A16_UNORM: - return ResourceFormat::RGBA16Unorm; - case FORMAT_R16G16B16A16_UINT: - return ResourceFormat::RGBA16Uint; - case FORMAT_R16G16B16A16_SINT: - return ResourceFormat::RGBA16Int; - case FORMAT_R32G32_FLOAT: - return ResourceFormat::RG32Float; - case FORMAT_R32G32_UINT: - return ResourceFormat::RG32Uint; - case FORMAT_R32G32_SINT: - return ResourceFormat::RG32Int; - case FORMAT_D32_FLOAT_S8X24_UINT: - return ResourceFormat::D32FloatS8X24; - case FORMAT_R10G10B10A2_UNORM: - return ResourceFormat::RGB10A2Unorm; - case FORMAT_R10G10B10A2_UINT: - return ResourceFormat::RGB10A2Uint; - case FORMAT_R11G11B10_FLOAT: - return ResourceFormat::R11G11B10Float; - case FORMAT_R8G8B8A8_UNORM: - return ResourceFormat::RGBA8Unorm; - case FORMAT_R8G8B8A8_UNORM_SRGB: - return ResourceFormat::RGBA8UnormSrgb; - case FORMAT_R8G8B8A8_UINT: - return ResourceFormat::RGBA8Uint; - case FORMAT_R8G8B8A8_SNORM: - return ResourceFormat::RGBA8Snorm; - case FORMAT_R8G8B8A8_SINT: - return ResourceFormat::RGBA8Int; - case FORMAT_R16G16_FLOAT: - return ResourceFormat::RG16Float; - case FORMAT_R16G16_UNORM: - return ResourceFormat::RG16Unorm; - case FORMAT_R16G16_UINT: - return ResourceFormat::RG16Uint; - case FORMAT_R16G16_SNORM: - return ResourceFormat::RG16Snorm; - case FORMAT_R16G16_SINT: - return ResourceFormat::RG16Int; - case FORMAT_D32_FLOAT: - return ResourceFormat::D32Float; - case FORMAT_R32_FLOAT: - return ResourceFormat::R32Float; - case FORMAT_R32_UINT: - return ResourceFormat::R32Uint; - case FORMAT_R32_SINT: - return ResourceFormat::R32Int; - case FORMAT_D24_UNORM_S8_UINT: - return ResourceFormat::D24UnormS8; - case FORMAT_R9G9B9E5_SHAREDEXP: - return ResourceFormat::RGB9E5Float; - case FORMAT_B8G8R8A8_UNORM: - return ResourceFormat::BGRA8Unorm; - case FORMAT_B8G8R8X8_UNORM: - return ResourceFormat::BGRX8Unorm; - case FORMAT_B8G8R8A8_UNORM_SRGB: - return ResourceFormat::BGRA8UnormSrgb; - case FORMAT_B8G8R8X8_UNORM_SRGB: - return ResourceFormat::BGRX8UnormSrgb; - case FORMAT_R8G8_UNORM: - return ResourceFormat::RG8Unorm; - case FORMAT_R8G8_UINT: - return ResourceFormat::RG8Uint; - case FORMAT_R8G8_SNORM: - return ResourceFormat::RG8Snorm; - case FORMAT_R8G8_SINT: - return ResourceFormat::RG8Int; - case FORMAT_R16_FLOAT: - return ResourceFormat::R16Float; - case FORMAT_D16_UNORM: - return ResourceFormat::D16Unorm; - case FORMAT_R16_UNORM: - return ResourceFormat::R16Unorm; - case FORMAT_R16_UINT: - return ResourceFormat::R16Uint; - case FORMAT_R16_SNORM: - return ResourceFormat::R16Snorm; - case FORMAT_R16_SINT: - return ResourceFormat::R16Int; - case FORMAT_B5G6R5_UNORM: - return ResourceFormat::R5G6B5Unorm; - case FORMAT_B5G5R5A1_UNORM: - return ResourceFormat::RGB5A1Unorm; - case FORMAT_R8_UNORM: - return ResourceFormat::R8Unorm; - case FORMAT_R8_UINT: - return ResourceFormat::R8Uint; - case FORMAT_R8_SNORM: - return ResourceFormat::R8Snorm; - case FORMAT_R8_SINT: - return ResourceFormat::R8Int; - case FORMAT_A8_UNORM: - return ResourceFormat::Alpha8Unorm; - case FORMAT_BC1_UNORM: - return ResourceFormat::BC1Unorm; - case FORMAT_BC1_UNORM_SRGB: - return ResourceFormat::BC1UnormSrgb; - case FORMAT_BC4_UNORM: - return ResourceFormat::BC4Unorm; - case FORMAT_BC4_SNORM: - return ResourceFormat::BC4Snorm; - case FORMAT_BC2_UNORM: - return ResourceFormat::BC2Unorm; - case FORMAT_BC2_UNORM_SRGB: - return ResourceFormat::BC2UnormSrgb; - case FORMAT_BC3_UNORM: - return ResourceFormat::BC3Unorm; - case FORMAT_BC3_UNORM_SRGB: - return ResourceFormat::BC3UnormSrgb; - case FORMAT_BC5_UNORM: - return ResourceFormat::BC5Unorm; - case FORMAT_BC5_SNORM: - return ResourceFormat::BC5Snorm; - case FORMAT_BC6H_SF16: - return ResourceFormat::BC6HS16; - case FORMAT_BC6H_UF16: - return ResourceFormat::BC6HU16; - case FORMAT_BC7_UNORM: - return ResourceFormat::BC7Unorm; - case FORMAT_BC7_UNORM_SRGB: - return ResourceFormat::BC7UnormSrgb; - default: - return ResourceFormat::Unknown; - } - } - - DXFormat getRgbDxgiFormat(const DdsHeader::PixelFormat& format) - { - switch(format.bitcount) - { - case 32: - if(checkDdsChannelMask(format, 0x000000ff, 0x0000ff00, 0x00ff0000, 0xff000000)) - { - return FORMAT_R8G8B8A8_UNORM; - } - - if(checkDdsChannelMask(format, 0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000)) - { - return FORMAT_B8G8R8A8_UNORM; - } - - if(checkDdsChannelMask(format, 0x00ff0000, 0x0000ff00, 0x000000ff, 0x00000000)) - { - return FORMAT_B8G8R8X8_UNORM; - } - - if(checkDdsChannelMask(format, 0x3ff00000, 0x000ffc00, 0x000003ff, 0xc0000000)) - { - return FORMAT_R10G10B10A2_UNORM; - } - - if(checkDdsChannelMask(format, 0x0000ffff, 0xffff0000, 0x00000000, 0x00000000)) - { - return FORMAT_R16G16_UNORM; - } - - if(checkDdsChannelMask(format, 0xffffffff, 0x00000000, 0x00000000, 0x00000000)) - { - return FORMAT_R32_FLOAT; - } - break; - - case 16: - if(checkDdsChannelMask(format, 0x7c00, 0x03e0, 0x001f, 0x8000)) - { - return FORMAT_B5G5R5A1_UNORM; - } - if(checkDdsChannelMask(format, 0xf800, 0x07e0, 0x001f, 0x0000)) - { - return FORMAT_B5G6R5_UNORM; - } - - if(checkDdsChannelMask(format, 0x0f00, 0x00f0, 0x000f, 0xf000)) - { - return FORMAT_B4G4R4A4_UNORM; - } - break; - } - should_not_get_here(); - return FORMAT_UNKNOWN; - } - - DXFormat getLuminanceDxgiFormat(const DdsHeader::PixelFormat& format) - { - switch(format.bitcount) - { - case 16: - if(checkDdsChannelMask(format, 0x0000ffff, 0x00000000, 0x00000000, 0x00000000)) - { - return FORMAT_R16_UNORM; - } - if(checkDdsChannelMask(format, 0x000000ff, 0x00000000, 0x00000000, 0x0000ff00)) - { - return FORMAT_R8G8_UNORM; - } - break; - case 8: - if(checkDdsChannelMask(format, 0x000000ff, 0x00000000, 0x00000000, 0x00000000)) - { - return FORMAT_R8_UNORM; - } - break; - } - should_not_get_here(); - return FORMAT_UNKNOWN; - } - - DXFormat getDxgiAlphaFormat(const DdsHeader::PixelFormat& format) - { - switch(format.bitcount) - { - case 8: - return FORMAT_A8_UNORM; - default: - should_not_get_here(); - return FORMAT_UNKNOWN; - } - } - - DXFormat getDxgiBumpFormat(const DdsHeader::PixelFormat& format) - { - switch(format.bitcount) - { - case 16: - if(checkDdsChannelMask(format, 0x00ff, 0xff00, 0x0000, 0x0000)) - { - return FORMAT_R8G8_SNORM; - } - break; - case 32: - if(checkDdsChannelMask(format, 0x000000ff, 0x0000ff00, 0x00ff0000, 0xff000000)) - { - return FORMAT_R8G8B8A8_SNORM; - } - if(checkDdsChannelMask(format, 0x0000ffff, 0xffff0000, 0x00000000, 0x00000000)) - { - return FORMAT_R16G16_SNORM; - } - break; - } - should_not_get_here(); - return FORMAT_UNKNOWN; - } - - DXFormat getDxgiFormatFrom4CC(uint32_t fourCC) - { - if(fourCC == makeFourCC("DXT1")) - { - return FORMAT_BC1_UNORM; - } - if(fourCC == makeFourCC("DXT2")) - { - return FORMAT_BC2_UNORM; - } - if(fourCC == makeFourCC("DXT3")) - { - return FORMAT_BC2_UNORM; - } - if(fourCC == makeFourCC("DXT4")) - { - return FORMAT_BC3_UNORM; - } - if(fourCC == makeFourCC("DXT5")) - { - return FORMAT_BC3_UNORM; - } - - if(fourCC == makeFourCC("ATI1")) - { - return FORMAT_BC4_UNORM; - } - if(fourCC == makeFourCC("BC4U")) - { - return FORMAT_BC4_UNORM; - } - if(fourCC == makeFourCC("BC4S")) - { - return FORMAT_BC4_SNORM; - } - - if(fourCC == makeFourCC("ATI2")) - { - return FORMAT_BC5_UNORM; - } - if(fourCC == makeFourCC("BC5U")) - { - return FORMAT_BC5_UNORM; - } - if(fourCC == makeFourCC("BC5S")) - { - return FORMAT_BC5_SNORM; - } - - if(fourCC == makeFourCC("RGBG")) - { - return FORMAT_R8G8_B8G8_UNORM; - } - if(fourCC == makeFourCC("GRGB")) - { - return FORMAT_G8R8_G8B8_UNORM; - } - - if(fourCC == makeFourCC("YUY2")) - { - return FORMAT_YUY2; - } - - switch(fourCC) - { - case 36: - return FORMAT_R16G16B16A16_UNORM; - case 110: - return FORMAT_R16G16B16A16_SNORM; - case 111: - return FORMAT_R16_FLOAT; - case 112: - return FORMAT_R16G16_FLOAT; - case 113: - return FORMAT_R16G16B16A16_FLOAT; - case 114: - return FORMAT_R32_FLOAT; - case 115: - return FORMAT_R32G32_FLOAT; - case 116: - return FORMAT_R32G32B32A32_FLOAT; - } - - should_not_get_here(); - return FORMAT_UNKNOWN; - } - - DXFormat getDxgiFormatFromPixelFormat(const DdsHeader::PixelFormat& format) - { - if(format.flags & DdsHeader::PixelFormat::kRgbMask) - { - return getRgbDxgiFormat(format); - } - else if (format.flags & DdsHeader::PixelFormat::kLuminanceMask) - { - return getLuminanceDxgiFormat(format); - } - else if(format.flags & DdsHeader::PixelFormat::kAlphaMask) - { - return getDxgiAlphaFormat(format); - } - else if (format.flags & DdsHeader::PixelFormat::kBumpMask) - { - return getDxgiBumpFormat(format); - } - else if(format.flags & DdsHeader::PixelFormat::kFourCCFlag) - { - return getDxgiFormatFrom4CC(format.fourCC); - } - - return FORMAT_UNKNOWN; - } - - ResourceFormat getDdsResourceFormat(const DdsData& data) - { - if(data.hasDX10Header) - { - return falcorFormatFromDXGIFormat(data.dx10Header.dxgiFormat); - } - else - { - return falcorFormatFromDXGIFormat(getDxgiFormatFromPixelFormat(data.header.pixelFormat)); - } - } - - //Flip the data so it follows opengl conventions - void flipData(DdsData& ddsData, ResourceFormat format, uint32_t width, uint32_t height, uint32_t depth, uint32_t mipDepth, bool isCubemap = false) - { - if (!isCompressedFormat(format) && !kTopDown) - { - std::vector oldData(ddsData.data.size()); - oldData.swap(ddsData.data); - const uint8_t* currentTexture = oldData.data(); - const uint8_t* currentDepth = oldData.data(); - uint8_t* currentPos = ddsData.data.data(); - - for (uint32_t mipCounter = 0; mipCounter < mipDepth; ++mipCounter) - { - uint32_t heightPitch = std::max(width >> mipCounter, 1U) * getFormatBytesPerBlock(format); - uint32_t currentMipHeight = std::max(height >> mipCounter, 1U); - uint32_t depthPitch = currentMipHeight * heightPitch; - - for (uint32_t depthCounter = 0; depthCounter < depth; ++depthCounter) - { - currentTexture = currentDepth + depthPitch * depthCounter; - - if (isCubemap) - { - if (depthCounter % 6 == 2) - { - currentTexture += depthPitch; - } - else if (depthCounter % 6 == 3) - { - currentTexture -= depthPitch; - } - } - - for (uint32_t heightCounter = 1; heightCounter <= currentMipHeight; ++heightCounter) - { - std::memcpy(currentPos, currentTexture + (currentMipHeight - heightCounter) * heightPitch, heightPitch); - currentPos += heightPitch; - } - - } - - currentDepth += depthPitch * depth; - } - } - } - - bool loadDDSDataFromFile(const std::string filename, DdsData& ddsData) - { - BinaryFileStream stream(filename, BinaryFileStream::Mode::Read); - - // Check the dds identifier - uint32_t ddsIdentifier; - stream >> ddsIdentifier; - if (ddsIdentifier != kDdsMagicNumber) - { - logError("The dds file " + filename + " is not a valid dds file"); - return false; - } - - stream >> ddsData.header; - - if ((ddsData.header.pixelFormat.flags & DdsHeader::PixelFormat::kFourCCFlag) && (makeFourCC("DX10") == ddsData.header.pixelFormat.fourCC)) - { - ddsData.hasDX10Header = true; - stream >> ddsData.dx10Header; - } - else - { - ddsData.hasDX10Header = false; - } - - uint32_t dataSize = stream.getRemainingStreamSize(); - ddsData.data.resize(dataSize); - stream.read(ddsData.data.data(), dataSize); - return true; - } - - static ResourceFormat convertBgrxFormatToBgra(DdsData& ddsData, ResourceFormat format) - { -#ifdef FALCOR_VK - switch (format) - { - case ResourceFormat::BGRX8Unorm: - format = ResourceFormat::BGRA8Unorm; - break; - case ResourceFormat::BGRX8UnormSrgb: - format = ResourceFormat::BGRA8UnormSrgb; - break; - default: - return format; - } - - for (size_t i = 3; i < ddsData.data.size(); i+=4) - { - ddsData.data[i] = 0xFF; - } -#endif - return format; - } - - Texture::SharedPtr createTextureFromDx10Dds(DdsData& ddsData, const std::string& filename, ResourceFormat format, uint32_t mipLevels, Texture::BindFlags bindFlags) - { - format = convertBgrxFormatToBgra(ddsData, format); - - uint32_t arraySize = ddsData.dx10Header.arraySize; - assert(arraySize > 0); - - switch(ddsData.dx10Header.resourceDimension) - { - case DXResourceDimension::RESOURCE_DIMENSION_TEXTURE1D: - return Texture::create1D(ddsData.header.width, format, arraySize, mipLevels, ddsData.data.data(), bindFlags); - case DXResourceDimension::RESOURCE_DIMENSION_TEXTURE2D: - if(ddsData.dx10Header.miscFlag & DdsHeaderDX10::kCubeMapMask) - { - flipData(ddsData, format, ddsData.header.width, ddsData.header.height, 6 * arraySize, mipLevels == Texture::kMaxPossible ? 1 : mipLevels, true); - return Texture::createCube(ddsData.header.width, ddsData.header.height, format, arraySize, mipLevels, ddsData.data.data(), bindFlags); - } - else - { - flipData(ddsData, format, ddsData.header.width, ddsData.header.height, arraySize, mipLevels == Texture::kMaxPossible ? 1 : mipLevels); - return Texture::create2D(ddsData.header.width, ddsData.header.height, format, arraySize, mipLevels, ddsData.data.data(), bindFlags); - } - case DXResourceDimension::RESOURCE_DIMENSION_TEXTURE3D: - flipData(ddsData, format, ddsData.header.width, ddsData.header.height, ddsData.header.depth, mipLevels == Texture::kMaxPossible ? 1 : mipLevels); - return Texture::create3D(ddsData.header.width, ddsData.header.height, ddsData.header.depth, format, mipLevels, ddsData.data.data(), bindFlags); - case DXResourceDimension::RESOURCE_DIMENSION_BUFFER: - case DXResourceDimension::RESOURCE_DIMENSION_UNKNOWN: - logError("The resource dimension specified in " + filename + " is not supported by Falcor"); - return nullptr; - default: - logError("Unknown resource dimension specified in " + filename); - return nullptr; - } - } - - Texture::SharedPtr createTextureFromLegacyDds(DdsData& ddsData, const std::string& filename, ResourceFormat format, uint32_t mipLevels, Texture::BindFlags bindFlags) - { - format = convertBgrxFormatToBgra(ddsData, format); - - // Load the volume or 3D texture - if(ddsData.header.flags & DdsHeader::kDepthMask) - { - flipData(ddsData, format, ddsData.header.width, ddsData.header.height, ddsData.header.depth, mipLevels == Texture::kMaxPossible ? 1 : mipLevels); - return Texture::create3D(ddsData.header.width, ddsData.header.height, ddsData.header.depth, format, mipLevels, ddsData.data.data(), bindFlags); - } - // Load the cubemap texture - else if(ddsData.header.caps[1] & DdsHeader::kCaps2CubeMapMask) - { - return Texture::createCube(ddsData.header.width, ddsData.header.height, format, 1, mipLevels, ddsData.data.data(), bindFlags); - } - // This is a 2D Texture - else - { - flipData(ddsData, format, ddsData.header.width, ddsData.header.height, 1, mipLevels == Texture::kMaxPossible ? 1 : mipLevels); - return Texture::create2D(ddsData.header.width, ddsData.header.height, format, 1, mipLevels, ddsData.data.data(), bindFlags); - } - - should_not_get_here(); - return nullptr; - } - - Texture::SharedPtr createTextureFromDDSFile(const std::string filename, bool generateMips, bool loadAsSrgb, Texture::BindFlags bindFlags) - { - DdsData ddsData; - if (!loadDDSDataFromFile(filename, ddsData)) return nullptr; - - ResourceFormat format = getDdsResourceFormat(ddsData); - if (format == ResourceFormat::Unknown) - { - logError("Unknown resource format in DDS file " + filename); - return nullptr; - } - - if (loadAsSrgb) - { - format = linearToSrgbFormat(format); - } - - uint32_t mipLevels; - if (generateMips == false || isCompressedFormat(format)) - { - mipLevels = (ddsData.header.flags & DdsHeader::kMipCountMask) ? std::max(ddsData.header.mipCount, 1U) : 1; - } - else - { - mipLevels = Texture::kMaxPossible; - } - - if (ddsData.hasDX10Header) - { - return createTextureFromDx10Dds(ddsData, filename, format, mipLevels, bindFlags); - } - else - { - return createTextureFromLegacyDds(ddsData, filename, format, mipLevels, bindFlags); - } - } - Texture::SharedPtr Texture::createFromFile(const std::string& filename, bool generateMipLevels, bool loadAsSrgb, Texture::BindFlags bindFlags) { std::string fullpath; if (findFileInDataDirectories(filename, fullpath) == false) { - logError("Error when loading image file. Can't find image file " + filename); + logWarning("Error when loading image file. Can't find image file '" + filename + "'"); return nullptr; } Texture::SharedPtr pTex; if (hasSuffix(filename, ".dds")) { - pTex = createTextureFromDDSFile(fullpath, generateMipLevels, loadAsSrgb, bindFlags); + pTex = ImageIO::loadTextureFromDDS(filename, loadAsSrgb); } else { diff --git a/Source/Falcor/Core/API/Vulkan/VKResourceViews.cpp b/Source/Falcor/Core/API/Vulkan/VKResourceViews.cpp index d111507e91..40216b31ef 100644 --- a/Source/Falcor/Core/API/Vulkan/VKResourceViews.cpp +++ b/Source/Falcor/Core/API/Vulkan/VKResourceViews.cpp @@ -46,12 +46,6 @@ namespace Falcor return Texture::create2D(1, 1, ResourceFormat::RGBA8Unorm, 1, 1, blackPixel, Resource::BindFlags::ShaderResource | Resource::BindFlags::RenderTarget | Resource::BindFlags::UnorderedAccess); } - ResourceWeakPtr getEmptyTexture() - { - static Texture::SharedPtr sBlackTexture = createBlackTexture(); - return sBlackTexture; - } - VkImageViewType getViewType(Resource::Type type, bool isArray) { switch (type) diff --git a/Source/Falcor/Core/BufferTypes/ParameterBlock.cpp b/Source/Falcor/Core/BufferTypes/ParameterBlock.cpp index 519b050e91..19670069cb 100644 --- a/Source/Falcor/Core/BufferTypes/ParameterBlock.cpp +++ b/Source/Falcor/Core/BufferTypes/ParameterBlock.cpp @@ -158,11 +158,12 @@ namespace Falcor return pResource->shared_from_this(); } - const std::array kRootSrvDescriptorTypes = + const std::array kRootSrvDescriptorTypes = { DescriptorSet::Type::RawBufferSrv, DescriptorSet::Type::TypedBufferSrv, DescriptorSet::Type::StructuredBufferSrv, + DescriptorSet::Type::AccelerationStructureSrv, }; const std::array kRootUavDescriptorTypes = @@ -172,12 +173,13 @@ namespace Falcor DescriptorSet::Type::StructuredBufferUav, }; - const std::array kSrvDescriptorTypes = + const std::array kSrvDescriptorTypes = { DescriptorSet::Type::TextureSrv, DescriptorSet::Type::RawBufferSrv, DescriptorSet::Type::TypedBufferSrv, DescriptorSet::Type::StructuredBufferSrv, + DescriptorSet::Type::AccelerationStructureSrv, }; const std::array kUavDescriptorTypes = @@ -247,7 +249,18 @@ namespace Falcor } } - ParameterBlock::~ParameterBlock() = default; + ParameterBlock::~ParameterBlock() + { +#if _ENABLE_CUDA + if (mUnderlyingCUDABuffer.kind == CUDABufferKind::Host) + { + if (auto pData = mUnderlyingCUDABuffer.pData) + { + free(pData); + } + } +#endif + } ParameterBlock::SharedPtr ParameterBlock::create(const std::shared_ptr& pProgramVersion, const ReflectionType::SharedConstPtr& pElementType) { @@ -292,6 +305,7 @@ namespace Falcor case DescriptorSet::Type::RawBufferSrv: case DescriptorSet::Type::TypedBufferSrv: case DescriptorSet::Type::StructuredBufferSrv: + case DescriptorSet::Type::AccelerationStructureSrv: state.srvCount += range.count; break; case DescriptorSet::Type::TextureUav: @@ -433,28 +447,26 @@ namespace Falcor bool ParameterBlock::checkDescriptorSrvUavCommon( const BindLocation& bindLocation, + const Resource::SharedPtr& pResource, const std::variant& pView, const char* funcName) const { #if _LOG_ENABLED if (!checkResourceIndices(bindLocation, funcName)) return false; - auto& bindingInfo = mpReflector->getResourceRangeBindingInfo(bindLocation.getResourceRangeIndex()); + const auto& bindingInfo = mpReflector->getResourceRangeBindingInfo(bindLocation.getResourceRangeIndex()); bool isUav = std::holds_alternative(pView); if (bindingInfo.isDescriptorSet()) { - if (!checkDescriptorType(bindLocation, isUav ? kUavDescriptorTypes : kSrvDescriptorTypes, funcName)) return false; + if (!(isUav ? checkDescriptorType(bindLocation, kUavDescriptorTypes, funcName) : checkDescriptorType(bindLocation, kSrvDescriptorTypes, funcName))) return false; // TODO: Check that resource type/dimension matches the descriptor type. } else if (bindingInfo.isRootDescriptor()) { - if (!checkDescriptorType(bindLocation, isUav ? kRootUavDescriptorTypes : kRootSrvDescriptorTypes, funcName)) return false; + if (!(isUav ? checkDescriptorType(bindLocation, kRootUavDescriptorTypes, funcName) : checkDescriptorType(bindLocation, kRootSrvDescriptorTypes, funcName))) return false; // For root descriptors, also check that the resource is compatible. - auto pResource = isUav - ? getResourceFromView(std::get(pView).get()) - : getResourceFromView(std::get(pView).get()); if (!checkRootDescriptorResourceCompatibility(pResource, funcName)) return false; // TODO: Check that view points to the start of the buffer. @@ -609,23 +621,30 @@ namespace Falcor bool ParameterBlock::setResourceSrvUavCommon(const BindLocation& bindLoc, const Resource::SharedPtr& pResource, const char* funcName) { - size_t flatIndex = getFlatIndex(bindLoc); + // Check if the bind location is a root descriptor or a descriptor set. + // If binding to a descriptor set, we'll create a default view for the resource. For root descriptors, we don't need a view. + const auto& bindingInfo = mpReflector->getResourceRangeBindingInfo(bindLoc.getResourceRangeIndex()); + const bool isRoot = bindingInfo.isRootDescriptor(); + const size_t flatIndex = getFlatIndex(bindLoc); + + const ReflectionResourceType* pResouceReflection = bindLoc.getType()->asResourceType(); + assert(pResouceReflection && pResouceReflection->getDimensions() == bindingInfo.dimension); if (isUavType(bindLoc.getType())) { - auto pUAV = pResource ? pResource->getUAV() : UnorderedAccessView::getNullView(); - if (!checkDescriptorSrvUavCommon(bindLoc, pUAV, funcName)) return false; + auto pUAV = (pResource && !isRoot) ? pResource->getUAV() : UnorderedAccessView::getNullView(bindingInfo.dimension); + if (!checkDescriptorSrvUavCommon(bindLoc, pResource, pUAV, funcName)) return false; auto& assignedUAV = mUAVs[flatIndex]; - if (assignedUAV.pView == pUAV) return true; + if (assignedUAV.pResource == pResource && assignedUAV.pView == pUAV) return true; assignedUAV.pView = pUAV; assignedUAV.pResource = pResource; } else if (isSrvType(bindLoc.getType())) { - auto pSRV = pResource ? pResource->getSRV() : ShaderResourceView::getNullView(); - if (!checkDescriptorSrvUavCommon(bindLoc, pSRV, funcName)) return false; + auto pSRV = (pResource && !isRoot) ? pResource->getSRV() : ShaderResourceView::getNullView(bindingInfo.dimension); + if (!checkDescriptorSrvUavCommon(bindLoc, pResource, pSRV, funcName)) return false; auto& assignedSRV = mSRVs[flatIndex]; - if (assignedSRV.pView == pSRV) return true; + if (assignedSRV.pResource == pResource && assignedSRV.pView == pSRV) return true; assignedSRV.pView = pSRV; assignedSRV.pResource = pResource; } @@ -801,16 +820,21 @@ namespace Falcor bool ParameterBlock::setSrv(const BindLocation& bindLocation, const ShaderResourceView::SharedPtr& pSrv) { - if (!checkDescriptorSrvUavCommon(bindLocation, pSrv, "setSrv()")) return false; + auto pResource = getResourceFromView(pSrv.get()); + if (!checkDescriptorSrvUavCommon(bindLocation, pResource, pSrv, "setSrv()")) return false; size_t flatIndex = getFlatIndex(bindLocation); auto& assignedSRV = mSRVs[flatIndex]; - const ShaderResourceView::SharedPtr pView = pSrv ? pSrv : ShaderResourceView::getNullView(); - if(assignedSRV.pView == pView) return true; + const auto& bindingInfo = mpReflector->getResourceRangeBindingInfo(bindLocation.getResourceRangeIndex()); + const ReflectionResourceType* pResouceReflection = bindLocation.getType()->asResourceType(); + assert(pResouceReflection && pResouceReflection->getDimensions() == bindingInfo.dimension); + + const ShaderResourceView::SharedPtr pView = pSrv ? pSrv : ShaderResourceView::getNullView(bindingInfo.dimension); + if (assignedSRV.pResource == pResource && assignedSRV.pView == pView) return true; assignedSRV.pView = pView; - assignedSRV.pResource = getResourceFromView(pView.get()); + assignedSRV.pResource = pResource; markDescriptorSetDirty(bindLocation); return true; @@ -818,14 +842,18 @@ namespace Falcor bool ParameterBlock::setUav(const BindLocation& bindLocation, const UnorderedAccessView::SharedPtr& pUav) { - if (!checkDescriptorSrvUavCommon(bindLocation, pUav, "setUav()")) return false; + auto pResource = getResourceFromView(pUav.get()); + if (!checkDescriptorSrvUavCommon(bindLocation, pResource, pUav, "setUav()")) return false; size_t flatIndex = getFlatIndex(bindLocation); auto& assignedUAV = mUAVs[flatIndex]; - UnorderedAccessView::SharedPtr pView = pUav ? pUav : UnorderedAccessView::getNullView(); + const auto& bindingInfo = mpReflector->getResourceRangeBindingInfo(bindLocation.getResourceRangeIndex()); + const ReflectionResourceType* pResouceReflection = bindLocation.getType()->asResourceType(); + assert(pResouceReflection && pResouceReflection->getDimensions() == bindingInfo.dimension); - if (assignedUAV.pView == pView) return true; + UnorderedAccessView::SharedPtr pView = pUav ? pUav : UnorderedAccessView::getNullView(bindingInfo.dimension); + if (assignedUAV.pResource == pResource && assignedUAV.pView == pView) return true; assignedUAV.pView = pView; assignedUAV.pResource = getResourceFromView(pView.get()); @@ -1334,7 +1362,9 @@ namespace Falcor for(auto resourceRangeIndex : setInfo.resourceRangeIndices) { + // TODO: Should this use the specialized reflector's element type instead? auto resourceRange = getElementType()->getResourceRange(resourceRangeIndex); + const auto& bindingInfo = pReflector->getResourceRangeBindingInfo(resourceRangeIndex); DescriptorSet::Type descriptorType = resourceRange.descriptorType; size_t descriptorCount = resourceRange.count; @@ -1373,9 +1403,11 @@ namespace Falcor case DescriptorSet::Type::RawBufferSrv: case DescriptorSet::Type::TypedBufferSrv: case DescriptorSet::Type::StructuredBufferSrv: + case DescriptorSet::Type::AccelerationStructureSrv: { auto pView = mSRVs[flatIndex].pView; - if(!pView) pView = ShaderResourceView::getNullView(); + assert(bindingInfo.dimension != ReflectionResourceType::Dimensions::Unknown); + if(!pView) pView = ShaderResourceView::getNullView(bindingInfo.dimension); pDescSet->setSrv(destRangeIndex, descriptorIndex, pView.get()); } break; @@ -1385,7 +1417,8 @@ namespace Falcor case DescriptorSet::Type::StructuredBufferUav: { auto pView = mUAVs[flatIndex].pView; - if(!pView) pView = UnorderedAccessView::getNullView(); + assert(bindingInfo.dimension != ReflectionResourceType::Dimensions::Unknown); + if(!pView) pView = UnorderedAccessView::getNullView(bindingInfo.dimension); pDescSet->setUav(destRangeIndex, descriptorIndex, pView.get()); } break; @@ -1397,6 +1430,7 @@ namespace Falcor } } + // Recursively bind resources in sub-objects. for( auto subObjectRange : setInfo.subObjects ) { auto resourceRangeIndex = subObjectRange.resourceRangeIndexOfSubObject; diff --git a/Source/Falcor/Core/BufferTypes/ParameterBlock.h b/Source/Falcor/Core/BufferTypes/ParameterBlock.h index 6c55ea8e04..6043e3cd62 100644 --- a/Source/Falcor/Core/BufferTypes/ParameterBlock.h +++ b/Source/Falcor/Core/BufferTypes/ParameterBlock.h @@ -338,6 +338,46 @@ namespace Falcor */ const Buffer::SharedPtr& getUnderlyingConstantBuffer() const; +#if _ENABLE_CUDA + /** Get a host-memory pointer that represents the contents of this shader object + as a CUDA-compatible buffer. + + In the case where this parameter block represents a `ProgramVars`, the resulting + buffer can be passed as argument data for a kernel launch. + + \return Host-memory pointer to a copy of the block, or throws on error + (e.g., parameter types that are unsupported on CUDA). + + The lifetime of the returned pointer is tied to the `ParameterBlock`, + and does not need to be explicitly deleted by the caller. The pointer + may become invalid if: + + * The parameter block is deleted + * A call to `getCUDADeviceBuffer()` is made on the same parameter block + * Another call it made to `getCUDAHostBuffer()` after changes have been made to parameters in the block + */ + void* getCUDAHostBuffer(size_t& outSize); + + /** Get a device-memory pointer that represents the contents of this shader object + as a CUDA-compatible buffer. + + The resulting buffer can be used to represent this shader object when it + is used as a constant buffer or parameter block. + + \return Device-memory pointer to a copy of the block, or throws on error + (e.g., parameter types that are unsupported on CUDA). + + The lifetime of the returned pointer is tied to the `ParameterBlock`, + and does not need to be explicitly deleted by the caller. The pointer + may become invalid if: + + * The parameter block is deleted + * A call to `getCUDAHostBuffer()` is made on the same parameter block + * Another call it made to `getCUDADeviceBuffer()` after changes have been made to parameters in the block + */ + void* getCUDADeviceBuffer(size_t& outSize); +#endif + typedef uint64_t ChangeEpoch; protected: @@ -391,13 +431,13 @@ namespace Falcor struct AssignedSRV { - ShaderResourceView::SharedPtr pView; + ShaderResourceView::SharedPtr pView; // Can be a null view even when a valid resource is assigned, if the bind location is a root descriptor. Resource::SharedPtr pResource; }; struct AssignedUAV { - UnorderedAccessView::SharedPtr pView; + UnorderedAccessView::SharedPtr pView; // Can be a null view even when a valid resource is assigned, if the bind location is a root descriptor. Resource::SharedPtr pResource; }; @@ -421,6 +461,7 @@ namespace Falcor bool checkDescriptorType(const BindLocation& bindLocation, const std::array& allowedTypes, const char* funcName) const; bool checkDescriptorSrvUavCommon( const BindLocation& bindLocation, + const Resource::SharedPtr& pResource, const std::variant& pView, const char* funcName) const; bool checkRootDescriptorResourceCompatibility(const Resource::SharedPtr& pResource, const std::string& funcName) const; @@ -457,6 +498,53 @@ namespace Falcor ChangeEpoch epochOfLastChange; }; mutable std::vector mSets; + +#if _ENABLE_CUDA + + // The following members pertain to the issue of exposing the + // current state/contents of a shader object to CUDA kernels. + + /** A kind of data buffer used for communicating with CUDA. + */ + enum class CUDABufferKind + { + Host, ///< A buffer in host memory + Device, ///< A buffer in device memory + }; + + /** Get a CUDA-compatible buffer that represents the contents of this shader object. + */ + void* getCUDABuffer( + CUDABufferKind bufferKind, + size_t& outSize); + + /** Get a CUDA-compatible buffer that represents the contents of this shader object. + */ + void* getCUDABuffer( + const ParameterBlockReflection* pReflector, + CUDABufferKind bufferKind, + size_t& outSize); + + /** Update the CUDA-compatible buffer stored on this parameter block to reflect + the current state of the shader object. + */ + void updateCUDABuffer( + const ParameterBlockReflection* pReflector, + CUDABufferKind bufferKind); + + /** Information about the CUDA buffer (if any) used to represnet the state of + this shader object + */ + struct UnderlyingCUDABuffer + { + Buffer::SharedPtr pBuffer; + void* pData = nullptr; + ChangeEpoch epochOfLastObservedChange = 0; + size_t size = 0; + CUDABufferKind kind = CUDABufferKind::Host; + }; + UnderlyingCUDABuffer mUnderlyingCUDABuffer; +#endif }; template bool ShaderVar::setImpl(const T& val) const diff --git a/Source/Falcor/Core/BufferTypes/VariablesBufferUI.cpp b/Source/Falcor/Core/BufferTypes/VariablesBufferUI.cpp index ef3d23402e..0ce1053569 100644 --- a/Source/Falcor/Core/BufferTypes/VariablesBufferUI.cpp +++ b/Source/Falcor/Core/BufferTypes/VariablesBufferUI.cpp @@ -163,7 +163,7 @@ namespace Falcor } toolTipString.append("\nType: " + memberTypeString); - widget.tooltip(toolTipString.c_str(), true); + widget.tooltip(toolTipString, true); } void VariablesBufferUI::renderUIVarInternal(Gui::Widgets& widget, const std::string& memberName, const ShaderVar& var) diff --git a/Source/Falcor/Core/FalcorConfig.h b/Source/Falcor/Core/FalcorConfig.h index 2377c5cfce..9189b8539d 100644 --- a/Source/Falcor/Core/FalcorConfig.h +++ b/Source/Falcor/Core/FalcorConfig.h @@ -34,3 +34,4 @@ #define _PROFILING_LOG_BATCH_SIZE 1024 * 1 // This can be used to control how many samples are accumulated before they are dumped to file. #define _ENABLE_NVAPI 0 // Set this to 1 to enable NVIDIA specific DX extensions. Make sure you have the NVAPI package in your 'Externals' directory. View the readme for more information. +#define _ENABLE_CUDA 0 // Set this to 1 to enable CUDA use and CUDA/DX interoperation. Make sure you have the CUDA SDK package in your 'Externals' directory. View the readme for more information. diff --git a/Source/Falcor/Core/Platform/OS.cpp b/Source/Falcor/Core/Platform/OS.cpp index 56f27b89a5..6ddf5c15ee 100644 --- a/Source/Falcor/Core/Platform/OS.cpp +++ b/Source/Falcor/Core/Platform/OS.cpp @@ -109,11 +109,18 @@ namespace Falcor return gDataDirectories; } - void addDataDirectory(const std::string& dir) + void addDataDirectory(const std::string& dir, bool addToFront) { if (std::find(gDataDirectories.begin(), gDataDirectories.end(), dir) == gDataDirectories.end()) { - gDataDirectories.push_back(dir); + if (addToFront) + { + gDataDirectories.insert(gDataDirectories.begin(), dir); + } + else + { + gDataDirectories.push_back(dir); + } } } @@ -158,7 +165,7 @@ namespace Falcor for (const auto& dir : gDataDirectories) { - fullPath = canonicalizeFilename(dir + '/' + filename); + fullPath = canonicalizeFilename((fs::path(dir) / filename).string()); if (doesFileExist(fullPath)) { return true; @@ -177,7 +184,7 @@ namespace Falcor { for (const auto& dir : gShaderDirectories) { - fullPath = canonicalizeFilename(dir + '/' + filename); + fullPath = canonicalizeFilename((fs::path(dir) / filename).string()); if (doesFileExist(fullPath)) { return true; @@ -192,7 +199,7 @@ namespace Falcor for (uint32_t i = 0; i < (uint32_t)-1; i++) { std::string newPrefix = prefix + '.' + std::to_string(i); - filename = directory + '/' + newPrefix + "." + extension; + filename = (fs::path(directory) / newPrefix).string() + "." + extension; if (doesFileExist(filename) == false) { diff --git a/Source/Falcor/Core/Platform/OS.h b/Source/Falcor/Core/Platform/OS.h index 1c9e7dc07f..29fc8ba28e 100644 --- a/Source/Falcor/Core/Platform/OS.h +++ b/Source/Falcor/Core/Platform/OS.h @@ -136,7 +136,7 @@ namespace Falcor */ dlldecl void msgBoxTitle(const std::string& title); - /** Finds a file in one of the media directories. The arguments must not alias. + /** Finds a file in one of the data search directories. The arguments must not alias. \param[in] filename The file to look for \param[in] fullPath If the file was found, the full path to the file. If the file wasn't found, this is invalid. \return true if the file was found, otherwise false @@ -192,7 +192,7 @@ namespace Falcor */ dlldecl bool chooseFolderDialog(std::string& folder); - /** Checks if a file exists in the file system. This function doesn't look in the common directories. + /** Checks if a file exists in the file system. This function doesn't look in the search directories. \param[in] filename The file to look for \return true if the file was found, otherwise false */ @@ -265,13 +265,14 @@ namespace Falcor */ dlldecl const std::vector& getDataDirectoriesList(); - /** Adds a folder into the search directory. Once added, calls to FindFileInCommonDirs() will search that directory as well - \param[in] dir The new directory to add to the common directories. + /** Adds a folder to data search directories. Once added, calls to findFileInDataDirectories() will search that directory as well. + \param[in] dir The new directory to add to the data search directories. + \param[in] addToFront Add the new directory to the front of the list, making it the highest priority. */ - dlldecl void addDataDirectory(const std::string& dir); + dlldecl void addDataDirectory(const std::string& dir, bool addToFront = false); - /** Removes a folder from the search directories - \param[in] dir The directory name to remove from the common directories. + /** Removes a folder from the data search directories + \param[in] dir The directory name to remove from the data search directories. */ dlldecl void removeDataDirectory(const std::string& dir); diff --git a/Source/Falcor/Core/Platform/Windows/Windows.cpp b/Source/Falcor/Core/Platform/Windows/Windows.cpp index 81cc733e76..a76eeb7247 100644 --- a/Source/Falcor/Core/Platform/Windows/Windows.cpp +++ b/Source/Falcor/Core/Platform/Windows/Windows.cpp @@ -695,15 +695,15 @@ namespace Falcor uint32_t bitScanReverse(uint32_t a) { unsigned long index; - _BitScanReverse(&index, a); - return (uint32_t)index; + auto ret = _BitScanReverse(&index, a); + return ret ? (uint32_t)index : 0; } uint32_t bitScanForward(uint32_t a) { unsigned long index; - _BitScanForward(&index, a); - return (uint32_t)index; + auto ret = _BitScanForward(&index, a); + return ret ? (uint32_t)index : 0; } uint32_t popcount(uint32_t a) diff --git a/Source/Falcor/Core/Program/CUDAProgram.cpp b/Source/Falcor/Core/Program/CUDAProgram.cpp new file mode 100644 index 0000000000..fade258195 --- /dev/null +++ b/Source/Falcor/Core/Program/CUDAProgram.cpp @@ -0,0 +1,1086 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#include "stdafx.h" +#if _ENABLE_CUDA +#include "CUDAProgram.h" + +// This file implements the CUDA-specific logic that allows the `Program`, +// `ProgramVars`, etc. types to support execution via CUDA. +// +// The approach taken here relies on details of how Slang compiles compute +// shader entry points for execution on CUDA, and we will call out those +// details where we make use of them. If Slang makes breaking changes to +// its CUDA code generation approach, this file will need to be updated +// accordingly. + +// We avoid including CUDA headers in other files because of conflicts +// they can apparently create with the vector types that Falcor uses. +// +#include ".packman/Cuda/include/cuda.h" +#include ".packman/Cuda/include/cuda_runtime_api.h" +#pragma comment(lib, "cuda") +#pragma comment(lib, "cudart") + +namespace Falcor +{ + // For those unfamiliar with the way that the Falcor `Program` hierarchy + // is implemented behind the scenes: + // + // * A `Program` (subclasses include `ComputeProgram`, `GraphicsProgram`, etc.) + // represents a collection of Slang source files/strings containing zero + // or more entry points. It presents a mutable set of `#define`s and allows + // creation of `ProgramVersion`s based on the current `#define`s. + // + // * A `ProgramVersion` represents a program that has been parsed/checked by + // Slang, using a particular set of `#define`s. It allows reflection to be + // performed, which then allows `ProgramVars` to be allocated. + // + // * A `ProgramVars` represents a mapping of the parameters of a `ProgramVersion` + // to the value to use for each parameter. Some parameters might have + // interface types, or otherwise interact with Slang langauge features for + // specialization. + // + // * A `ProgramKernels` represents a compiled "variant" of a `ProgramVersion`, + // based on taking the program version and specializing it to zero or more + // types that were bound to its parameters in a particular `ProgramVars`. + // + // Our goal in integrating CUDA support is to leverage as much of the existing + // work done on these types as possible. In practice, we only introduce two + // CUDA-specific types: `CUDAProgram` and `CUDAProgramKernels`. + // + // The `CUDAProgram` type was declared in `CUDAProgram.h`, and is responsible + // for overriding a few key `virtual` methods from `Program` so that it can + // facilitate CUDA-specific behavior. + // + // The creation logic for `CUDAProgram` is straightforward, and nearly identical + // to that for `ComputeProgram`. + + CUDAProgram::SharedPtr CUDAProgram::createFromFile( + const std::string& filename, + const std::string& csEntry, + const DefineList& programDefines, + Shader::CompilerFlags flags, + const std::string& shaderModel) + { + Desc d(filename); + if (!shaderModel.empty()) d.setShaderModel(shaderModel); + d.setCompilerFlags(flags); + d.csEntry(csEntry); + return create(d, programDefines); + } + + CUDAProgram::SharedPtr CUDAProgram::create( + const Program::Desc& desc, + const DefineList& programDefines) + { + SharedPtr pProg = SharedPtr(new CUDAProgram); + pProg->init(desc, programDefines); + return pProg; + } + + // The first place where an interesting difference arises is when + // it is time to invoke the Slang compiler front-end to parse and + // check the code of a program. + // + void CUDAProgram::setUpSlangCompilationTarget( + slang::TargetDesc& ioTargetDesc, + char const*& ioTargetMacroName) const + { + // When compiling Slang source code for execution via CUDA, we need to + // customize the compilation environment in two ways that differ from + // the existing D3D and VK paths. + // + // First, we need to make sure to generate PTX code instead of DXBC, + // DXIL, SPIR-V, etc. + // + ioTargetDesc.format = SLANG_PTX; + + // Second, we set a global define of `FALCOR_CUDA` to allow source code + // to customize its behavior based on knowledge of the target. + // + // Note: Shader code should try to avoid using this macro if at all possible, + // so that as much code as possible can remain portable. + // + ioTargetMacroName = "FALCOR_CUDA"; + } + + // The next customization point comes when it is time to load compiled + // code into a `ProgramKernels`, because the `CUDAProgram` needs to + // ensure that a `CUDAProgramKernels` gets created instead. + // + // The `CUDAProgramKernels` type stores and interacts with CUDA-specific + // types, so it is necessary to declare it here in a source file instead + // of a header. + // + class dlldecl CUDAProgramKernels : public ProgramKernels + { + public: + using SharedPtr = std::shared_ptr; + using SharedConstPtr = std::shared_ptr; + + /** Create a CUDA program kernels. + \param[in] pVersion The program version the kernels represent. + \param[in] pReflector The reflection information for the compiled/specialized kernels. + \param[in] uniqueEntryPointGroups The (deduplicated) entry-point groups that the program includes + \param[in,out] log Log of error messages for compilation/linking failures + \param[in] name Name to use for debugging purposes. + \return New object, or throws an exception if creation failed. + */ + static SharedPtr create( + const ProgramVersion* pVersion, + const ProgramReflection::SharedPtr& pReflector, + const UniqueEntryPointGroups& uniqueEntryPointGroups, + std::string& log, + const std::string& name = "") + { + return SharedPtr(new CUDAProgramKernels(pVersion, pReflector, uniqueEntryPointGroups, name)); + } + + virtual ~CUDAProgramKernels(); + + /** Dispatch a CUDA grid using the compiled kernels. + \param[in] pVars Parameter data to use (must have been created from the same base `Program` + \param[in] gridShape The shape of the dispatch grid, in units of thread blocks. + */ + void dispatchCompute(ComputeVars* pVars, uint3 const& gridShape) const; + + protected: + CUDAProgramKernels( + const ProgramVersion* pVersion, + const ProgramReflection::SharedPtr& pReflector, + const UniqueEntryPointGroups& uniqueEntryPointGroups, + const std::string& name = "") + : ProgramKernels(pVersion, pReflector, uniqueEntryPointGroups, name) + { + init(); + } + + void init(); + + /// The CUDA module that contains the compiled code of the entry-point kernel(s). + CUmodule mCudaModule = 0; + + // We currently only support CUDA for compute programs, which have + // a single entry point kernel function. If/when we generalize to support + // OptiX for ray-tracing programs, we would need to have a variant of + // `CUDAProgramKernels` that instead stores a full OptiX PSO including + // all the relevant kernels. + + /// The CUDA function for the entry point kernel. + CUfunction mCudaEntryPoint = 0; + + /// The device address of the `SLANG_globalParams` variable (if any) for the CUDA module. + CUdeviceptr mpGlobalParamsSymbol = 0; + }; + + // With the declaration of `CUDAProgramKernels` out of the way, the logic + // required for a `CUDAProgram` to actually load one is unremarkable. + // + // Note that `createProgramKernels` is an internal routine used by `Program` + // to allow its subclasses to customize the way that compiled kernels are + // loaded and represented. It is not part of the user-facing API of `Program`. + // + ProgramKernels::SharedPtr CUDAProgram::createProgramKernels( + const ProgramVersion* pVersion, + const ProgramReflection::SharedPtr& pReflector, + const ProgramKernels::UniqueEntryPointGroups& uniqueEntryPointGroups, + std::string& log, + const std::string& name) const + { + return CUDAProgramKernels::create( + pVersion, + pReflector, + uniqueEntryPointGroups, + log, + name); + } + + // The more interesting work occurs inside of `CUDAProgramKernels::init`, + // but before we get to that we will define a few utilities for interacting + // with the CUDA API. + // + // We want to detect errors when interacting with the CUDA and + // turn them into exceptions to match the default error-handling + // policy used in the rest of the code. + // + // CUDA functions can return either a `CUresult` or a `cudaError_t`, + // so we add helper functions for each of these. + // + // TODO: OptiX uses yet another type for errors, so a third overload + // will be needed if/when OptiX support is added. + + static void checkCUDAResult(CUresult result, char const* what) + { + if (result != 0) + { + throw std::exception(what); + } + } + + static void checkCUDAResult(cudaError_t result, char const* what) + { + if (result != 0) + { + throw std::exception(what); + } + } + + // To make it easier to use the error-checking facilities, we + // add a helper macro that invokes a CUDA function and immediately + // passes it to `checkCUDAResult` for translation to an exception + // (if needed). + // +#define FALCOR_CUDA_THROW_ON_FAIL(EXPR) \ + checkCUDAResult(EXPR, "CUDA failure in '" #EXPR "'") + + // With the utilities in place, we can now work through the steps + // of loading Slang-compiled PTX code via the CUDA API. + + void CUDAProgramKernels::init() + { + // First, we ensure that the CUDA API has been initialized. + // + FALCOR_CUDA_THROW_ON_FAIL(cuInit(0)); + + // Next we get the device to use for CUDA operations. + // + // Note: It might eventually be important to allow a `CUDAProgram` + // and/or its kernels to be loaded on a specific device under + // application control. Such a feature would need to be part of + // an overhauled approach to exposing CUDA as a "first-class" + // API in Falcor. + // + CUdevice cudaDevice; + FALCOR_CUDA_THROW_ON_FAIL(cuDeviceGet(&cudaDevice, 0)); + + // We also create a CUDA context to manage CUDA-related + // state on the current thread. + // + // TODO: This need to be something the application can + // own/control. + // + unsigned int flags = 0; + CUcontext cudaContext; + FALCOR_CUDA_THROW_ON_FAIL(cuCtxCreate(&cudaContext, flags, cudaDevice)); + + // For now expect that every `CUDAProgram` represents a compute + // program with a single entry point. + // + auto pComputeShader = getShader(ShaderType::Compute); + if(!pComputeShader) + { + throw std::exception("expected CUDA program to have a single compute entry point"); + } + + // The binary "blob" stored on the compute entry point is the PTX + // code for the kernel, and we load it into the CUDA API as a module. + // + FALCOR_CUDA_THROW_ON_FAIL(cuModuleLoadData(&mCudaModule, pComputeShader->getD3DBlob()->GetBufferPointer())); + + // The CUDA module could in principle contain zero or more entry points + // or other global symbols, so that we need to query for the global + // function symbol that represents our desired entry point. + // + auto entryPointName = pComputeShader->getEntryPoint(); + FALCOR_CUDA_THROW_ON_FAIL(cuModuleGetFunction(&mCudaEntryPoint, mCudaModule, entryPointName.c_str())); + + // TODO: Eventually it would be better to support having a single CUDA module that + // might contain multiple entry points (which could even be composed into + // multiple distinct programs, in the OptiX case). + // + // The big catch here is that much of the Falcor code has been written with the assumption + // that the binary code is always associated with the individual kernels/entry-points and + // not with a program/module. + // + // The second catch is that the Falcor API does not make a distinction between a `Program` + // as a unit of shader code loading vs. a `Program` as a unit of assembling shader code + // to make an executable entity (like a PSO). + + // Shader parameters in the input Slang code could appear as either entry-point `uniform` parameters + // or global parameters (where the latter is what most existing HLSL code defaults to). + // + // Shader parameters get translated from Slang to CUDA in a way that depends on how they + // were declared: + // + // * Entry-point `uniform` parameters in Slang translate directly to entry-point parameters + // in the generated CUDA code (in order to match the programmer's intuition). + // + // * Global-scope parameters in Slang get aggregated into a `struct` type and then are + // used to declare a single global `__constant__` parameter, named `SLANG_globalParams`. + // + // In order to handle any global-scope parameters, we will query the module for a symbol + // named `SLANG_globalParams` and save its address (if it is present). + // + // Note: We do *not* throw if this call to `cuModuleGetGlobal` returns an error, because + // it is valid for the loaded module to not define a symbol of this name (e.g., if the + // module had no global-scope parameters). + // + // Note: This is an important place where we rely on the details of how Slang generates + // code for CUDA. If the name of the global symbols changes from `SLANG_globalParams` or + // the code generation strategy changes in other ways (e.g., by declaring distinct global + // symbols for each global parameter), then we will need to update this logic to match. + // + size_t globalParamsSymbolSize = 0; + cuModuleGetGlobal(&mpGlobalParamsSymbol, &globalParamsSymbolSize, mCudaModule, "SLANG_globalParams"); + } + + CUDAProgramKernels::~CUDAProgramKernels() + { + // When destroying a CUDA program kernels object, we need to + // unload the CUDA module it loaded. + // + if (mCudaModule) + { + cuModuleUnload(mCudaModule); + } + } + + // Because the allocation of `ParameterBlock`s in Falcor is driven by Slang-generated + // reflection information, most of the existing reflection and parameter-binding + // APIs continue to Just Work for a `CUDAProgram`. Note that we do *not* define + // a custom `CUDAProgramVars` or `CUDAParameterBlock` type to represent the parameter + // bindings for CUDA, and instead rely on the existing types. + // + // We will revisit the question of how shader parameter binding for CUDA needs to + // be handled once we see the requirements that arise when we actually try to + // execute a CUDA kernel. + + void CUDAProgram::dispatchCompute( + ComputeContext* pContext, + ComputeVars* pVars, + uint3 const& gridShape) + { + // When a CUDA program is told to dispatch itself, it queries the + // active version and the kernels for that active version, + // and then delegates to the kernels for the details. + // + auto pProgramVersion = getActiveVersion(); + auto pKernels = std::dynamic_pointer_cast(pProgramVersion->getKernels(pVars)); + pKernels->dispatchCompute(pVars, gridShape); + } + + void CUDAProgramKernels::dispatchCompute( + ComputeVars* pGlobalVars, + uint3 const& gridShape) const + { + // Our primary goal here is to issue a single call to `cuLaunchKernel`, + // and all the other work we do is toward the goal of determining its + // parameters: + // + // CUresult CUDAAPI cuLaunchKernel( + // CUfunction kernelFunc, + // unsigned int gridShapeX, unsigned int gridShapeY, unsigned int gridShapeZ, + // unsigned int blockShapeX, unsigned int blockShapeY, unsigned int blockShapeZ, + // unsigned int dynamicSharedMemorySizeInBytes, + // CUstream stream, + // void** kernelParams, + // void** extra); + // + // We will work through the the paramete list in order to figure + // out what we need to pass in. + // + // The kernel function is easy, since it is already stored on + // the `CUDAProgramKernels`. + // + CUfunction kernelFunc = mCudaEntryPoint; + + // The grid shape is also easy, since it was passed in directly. + + // The block shape can be determined dynamically for CUDA, but we know + // that the original Slang/HLSL shader had a `[numthreads(...)]` attribute + // that should tell us what to do, so we can query that. + // + uint3 blockShape = pGlobalVars->getReflection()->getThreadGroupSize(); + + // CUDA will automatically allocate the fixed/static shared memory + // requirements for a kernel, and the dynamic size is only needed + // when declaring a shared-memory arrays of statically unknown + // size (which is not allowed in Slang/HLSL). + // + unsigned int dynamicSharedMemorySizeInBytes = 0; + + // For now we will always execute CUDA kernels on the default stream. + // + // TODO: This system could be extended to support multiple streams + // by having a CUDA-specific subclass of `ComputeContext` that represents + // a stream. + // + CUstream stream = 0; + + // Now we come to the tricky part of things: actually providing the + // parameter data that the kernel requires. + // + // The typical way to drive `cuLaunchKernel` is with an array of + // pointers to argument values for the parameters, but this requires + // building up that array by knowing the number and type of each + // parameter. + // + // We technically have access to information on the number and type + // of the parameters (via reflection), but actually using reflection + // data on the critical path here seems slow. We are going to use + // a less well known part of the CUDA API instead of the ordinary + // kernel parameters. + // + void** kernelParams = nullptr; + + // The `extra` parameter to `cuLaunchKernel` is structured as a kind + // of key/value list, and it supports passing in the argument data + // for *all* of the kernel parameters as a single buffer. + // + // We start by assuming that we can get the entry-point arguments + // and encode them as a CUDA-friendly host-memory buffer. + // (The details are implemented later in this file) + // + auto pEntryPointArgs = pGlobalVars->getEntryPointGroupVars(0); + assert(pEntryPointArgs); + size_t entryPointArgsSize = 0; + void* entryPointArgsData = pEntryPointArgs->getCUDAHostBuffer(entryPointArgsSize); + + // Now that we have a contiguous buffer that represents the entry-point + // arguments, we can set up the `extra` argument so that it passes + // in the pointer and size. + // + // When these extra options are specified, CUDA will use the provided + // buffer instead of the explicit `kernelParams`. + // + void* extra[] = { + CU_LAUNCH_PARAM_BUFFER_POINTER, (void*)entryPointArgsData, + CU_LAUNCH_PARAM_BUFFER_SIZE, &entryPointArgsSize, + CU_LAUNCH_PARAM_END, + }; + + // At this point we've determined the values to pass for + // all the arguments to `cuLaunchKernel`, and we are *almost* + // ready to just go ahead and make the call. + // + // The last remaining wrinkle is that because of how typical + // Slang/HLSL shaders are written, there might also be shader + // parameters at global scope (not just entry-point parameters). + // + // As discussed earlier in this file, the Slang compiler bundles + // all global-scope shader parameters into a single `__constant__` + // variable named `SLANG_globalParams`. If there are any such + // parameters, then the symbol will exist and we will have its + // (device) address. + // + if (mpGlobalParamsSymbol) + { + // The argument values to use for global parameters were + // already passed in as the `ProgramVars* pGlobalVars` argument + // to this function. + // + // We start by querying for a device-memory buffer that can + // represents those parameters in a CUDA-compatible layout. + // + size_t globalParamsDataSize = 0; + void* pGlobalParamsDeviceData = pGlobalVars->getCUDADeviceBuffer(globalParamsDataSize); + + // Next we kick of an async copy from that device-memory buffer + // over to the global `__constant__` symbol. + // + // Note: this is an important place where the design choice + // in Slang to bundle all the global-scope parameters together + // pays off; we can issue a single `cudaMemcpyAsync` to set + // all the parameters, instead of having to use reflection + // and emit one copy per parameter. + // + // Note: it might seem wasteful to first reify the argument + // values into an allocated device-memory buffer and *then* + // copy over to the address of the global `__constant__`. Why + // can't we just have `pGlobalVars` write its state directly + // to the global variable? + // + // The key reason why we cannot write directly to the global + // `__constant__` in host code here is that there could still + // be prior kernel launches in flight that are reading from + // that data. + // + // In this case, we are relying on the fact that a `cudaMemcpyAsync` + // in the `cudaMemcpyDeviceToDevice` mode is guaranteed not + // to overlap with GPU kernel execution. Thus, our strategy + // of first collecting the arguments in a device-memory buffer + // and then copying it over avoids having to synchronize + // execution between CPU and GPU. + // + cudaMemcpyAsync( + (void*)mpGlobalParamsSymbol, + (void*)pGlobalParamsDeviceData, + globalParamsDataSize, + cudaMemcpyDeviceToDevice, + stream); + } + + // Now that we've figured out all the argument values to use, + // the actual call to `cuLaunchKernel` is straightforward. + // + FALCOR_CUDA_THROW_ON_FAIL(cuLaunchKernel( + kernelFunc, + gridShape.x, gridShape.y, gridShape.z, + blockShape.x, blockShape.y, blockShape.z, + dynamicSharedMemorySizeInBytes, + stream, + kernelParams, + extra)); + + // For bringup/debugging, we also immediately synchronize with + // the CUDA context to ensure that our kernel has completed + // execution. Any errors encountered during execution should + // show up as an exception here. + // + // TODO: Once we are confident that the CUDA path is working + // reasonably well, we can move this synchronization out and + // give users another way to wait on CUDA. + // + FALCOR_CUDA_THROW_ON_FAIL(cuCtxSynchronize()); + } + + // The main missing detail that came up in dispatching CUDA + // compute was the problem of getting host- or device-memory + // buffers from a `ParameterBlock` (reminder: `ProgramVars` + // is a subclass of `ParameterBlock`). + // + // We will bottleneck both the host-memory and device-memory + // cases through a single routine because they share so much + // of their logic. + + void* ParameterBlock::getCUDAHostBuffer(size_t& outSize) + { + return getCUDABuffer(CUDABufferKind::Host, outSize); + } + + void* ParameterBlock::getCUDADeviceBuffer(size_t& outSize) + { + return getCUDABuffer(CUDABufferKind::Device, outSize); + } + + void* ParameterBlock::getCUDABuffer( + CUDABufferKind bufferKind, + size_t& outSize) + { + // A parameter block might need to look at Slang specialization + // information (e.g., the way that interface-type parameters + // have been bound) before making decisions about how parameter + // data should be laid out. + // + // We start by checking for any changes that might alter the + // way the block/program is being specialized, and perform + // subsequent steps using the refleciton information for + // the specialized variant. + // + updateSpecialization(); + auto pReflector = mpSpecializedReflector.get(); + + return getCUDABuffer(pReflector, bufferKind, outSize); + } + + void* ParameterBlock::getCUDABuffer( + const ParameterBlockReflection* pReflector, + CUDABufferKind bufferKind, + size_t& outSize) + { + // Because parameter blocks in Falcor are mutable rather than + // write-once, it is possible that a change made to a byte in + // a nested constant buffer or parameter block could have caused + // it to be reallocated, getting a new device address, and thus + // requiring a new version of *this* block to be generated. + // + // We start by checking for "indirect" changes that need to + // be accounted for in the change epoch of this block. + // + checkForIndirectChanges(pReflector); + auto epochOfLastChange = mEpochOfLastChange; + + // We cache a CUDA buffer on this parameter block, and will + // try to use it if no parameter values have changed. + // + // If there have been changes made to this block (or the blocks + // it transitively points to), then we need to reallocate + // and fill in the buffer. + // + if (mUnderlyingCUDABuffer.epochOfLastObservedChange != epochOfLastChange) + { + updateCUDABuffer(pReflector, bufferKind); + mUnderlyingCUDABuffer.epochOfLastObservedChange = epochOfLastChange; + } + + // Note: The above logic did *not* check that any cached buffer + // has the required `bufferKind`. The reason for that is because + // there is expected to be a clear split: an `EntryPointGroupVars` + // will always be queried for a host-memory buffer, and all other + // cases will always be queried for a device-memory buffer. + // + // We defensively check and then error out if the assumption didn't hold. + // + if (mUnderlyingCUDABuffer.kind != bufferKind) + { + throw std::exception("Inconsistent CUDA buffer kind requested"); + } + + // Whether or not we were able to use the cached buffer, + // we return the buffer pointer and size once we are done. + // + outSize = mUnderlyingCUDABuffer.size; + return mUnderlyingCUDABuffer.pData; + } + + void ParameterBlock::updateCUDABuffer( + const ParameterBlockReflection* pReflector, + CUDABufferKind bufferKind) + { + // If there is an existing buffer already allocated, we need to free it. + // + if (mUnderlyingCUDABuffer.pData) + { + auto pBufferData = mUnderlyingCUDABuffer.pData; + if (mUnderlyingCUDABuffer.kind == CUDABufferKind::Host) + { + // In the case of a host-memory buffer, we can free + // it without worrying about any in-flight GPU accesses. + // + free(pBufferData); + } + else + { + // In the case of a device-memory buffer, we rely on + // the existing logic for freeing `Buffer`s, rather + // than try to use `cudaFree()` and have to worry + // about CPU/GPU synchornization. + // + mUnderlyingCUDABuffer.pBuffer = nullptr; + } + } + + // The refleciton information can tell us how big the type + // being stored in the block is, and that tells us how big + // of a buffer to allocate. + // + auto bufferSize = pReflector->getElementType()->getByteSize(); + + // We need to allocate a new buffer either in device or + // host memory, as determined by the requested `bufferKind`. + // + Buffer::SharedPtr pBuffer; + void* pBufferData = nullptr; + // + // The kind of buffer to allocate also determined the kind + // of `cudaMemcpy` operation(s) we need to perform when + // filling it in. + // + cudaMemcpyKind memcpyKind; + + if (bufferKind == CUDABufferKind::Device) + { + // Note: the device-memory buffer we allocate will only + // be used in CUDA API calls, so we could in principle + // just allocate it with `cudaMalloc()`. The problem + // in that case would be handling deallocation of the + // buffer at the right time. + // + // Instead, we allocate an ordinary `Buffer` and then + // share it over to CUDA, so that we can allow the + // existing Falcor memory management to apply. + // + ResourceBindFlags flags = ResourceBindFlags::Constant | ResourceBindFlags::UnorderedAccess | ResourceBindFlags::ShaderResource | ResourceBindFlags::Shared; + pBuffer = Buffer::create(bufferSize, flags); + pBufferData = pBuffer->getCUDADeviceAddress(); + memcpyKind = cudaMemcpyHostToDevice; + } + else + { + pBufferData = malloc(bufferSize); + memcpyKind = cudaMemcpyHostToHost; + } + + // Once the allocation is done, we can fill in the storage + // on this parameter block that tracks the underlying CUDA + // buffer. + // + mUnderlyingCUDABuffer.pBuffer = pBuffer; + mUnderlyingCUDABuffer.pData = pBufferData; + mUnderlyingCUDABuffer.size = bufferSize; + mUnderlyingCUDABuffer.kind = bufferKind; + + // Now we need tostart in on the task of actually filling in + // the buffer with a representation of the values bound + // into this `ParameterBlock`. + // + // For fields of "ordinary" types (scalars, vectors, matrices, + // and arrays/structures of those), the values are simply stored + // in the `mData` array of the parameter block, and we can + // simply copy that data over to the CUDA buffer. + // + auto dataSize = mData.size(); + assert(dataSize <= bufferSize); + FALCOR_CUDA_THROW_ON_FAIL(cudaMemcpy(pBufferData, mData.data(), dataSize, memcpyKind)); + + // For fields with "extraordinary" types (buffers, textures, etc.) + // the values are stored in the `ParameterBlock` as arrays associated + // with the different "resource ranges" in its type layout. + // + // For CUDA, however, even these buffer/texture/etc. types are in + // effect "ordinary" data, and they will be represented as plain + // bytes in the buffer we are filling in. + // + // Thus, we need to walk through all of the resource ranges indicated + // by the reflection information for this block, and assign appropriate + // data for each one over to the CUDA buffer. + // + auto resourceRangeCount = pReflector->getResourceRangeCount(); + for (uint32_t resourceRangeIndex = 0; resourceRangeIndex < resourceRangeCount; ++resourceRangeIndex) + { + // We need both the information on how the resource range was allocated + // into the storage of the `ParameterBlock`, and also on how it is to + // be bound into the API-specific storage. + // + auto resourceRange = pReflector->getResourceRange(resourceRangeIndex); + auto resourceRangeBindingInfo = pReflector->getResourceRangeBindingInfo(resourceRangeIndex); + + // Each resource range represents one or more values with the + // same descriptor type. + // + DescriptorSet::Type descriptorType = resourceRange.descriptorType; + size_t descriptorCount = resourceRange.count; + + // We will go ahead and loop through all of the descriptors, and set each + // one individually. + // + // TODO: A more efficient approach might seek to hoist the `switch` statement + // that follows outside of the loop, and instead perform the loop inside of + // each `case`. The current appraoch has been taken to favor simple verification + // of correctness over maximum efficiency. + // + for (uint32_t descriptorIndex = 0; descriptorIndex < descriptorCount; descriptorIndex++) + { + // Each buffer/texture/whatever value in the resource range is bound + // at some "flat" index in one of the arrays stored on the `ParameterBlock`. + // We can compute that flat index based on the information stored on the + // resource range and the index of the descriptor (in the case of a range + // that represents an array). + // + size_t flatIndex = resourceRange.baseIndex + descriptorIndex; + + // For CUDA, every parameter in a block is stored at some byte offset within + // the data of that block, and its "register" index as stored in the Falcor + // reflection data is actually its byte offset. + // + // We can thus compute the destination bytes within the buffer by offsetting + // from the start of the buffer by the reflected register index. + // + auto pDest = (char*)pBufferData + resourceRangeBindingInfo.regIndex; + + // TODO: The above logic is not taking `descriptorIndex` into account. We have + // a problem here that the Falcor encoding of resource ranges does not include + // any information about the "stride" of each range (the increment to add to + // get from one array element to the next). Without that information, we can + // only handle ranges with a single element for now. + // + if (descriptorIndex != 0) + { + throw std::exception("unsupported: resource/object arrays in CUDA parameter block"); + } + + // The remaining work to be done depends entirely on the kind of + // buffers/sampler/texture/whatever binding we are dealing with. + // + switch (resourceRangeBindingInfo.flavor) + { + case ParameterBlockReflection::ResourceRangeBindingInfo::Flavor::ConstantBuffer: + case ParameterBlockReflection::ResourceRangeBindingInfo::Flavor::ParameterBlock: + { + // A Slang `ParameterBlock` or `ConstantBuffer` will translate + // to a simple `X*` in the output CUDA code, and as such they are + // among the simplest cases to handle here. + // + // We start by asking the "sub-object" represent the block/buffer to + // produce a CUDA-compatible device memory buffer. + // + auto pSubObject = mParameterBlocks[flatIndex].pBlock; + auto pSubObjectReflector = resourceRangeBindingInfo.pSubObjectReflector.get(); + size_t subObjectSize = 0; + CUdeviceptr pSubObjectDevicePtr = (CUdeviceptr) pSubObject->getCUDABuffer(pSubObjectReflector, CUDABufferKind::Device, subObjectSize); + + // Once we have the device-memory pointer that represents the sub-object, + // we simply write its bytes (the bytes of the *pointer* and not those + // being pointed to) into the buffer we are building. + // + FALCOR_CUDA_THROW_ON_FAIL(cudaMemcpy(pDest, &pSubObjectDevicePtr, sizeof(pSubObjectDevicePtr), memcpyKind)); + } + break; + + case ParameterBlockReflection::ResourceRangeBindingInfo::Flavor::Simple: + case ParameterBlockReflection::ResourceRangeBindingInfo::Flavor::RootDescriptor: + default: + // The common case for resource ranges is that they represent single + // descriptor-bound resources/samplers, and in these cases we need + // to consider the descriptor type to know what should be bound. + // + switch (descriptorType) + { + default: + should_not_get_here(); + return; + + case DescriptorSet::Type::RawBufferSrv: + case DescriptorSet::Type::StructuredBufferSrv: + { + // A Slang `StructuredBuffer` translates to CUDA as a + // structure with two fields: + // + // 1. An `X*` device pointer for the data. + // 2. A `size_t` for the element count. + // + // A `ByteAddressBuffer` translates in a way that is + // equivalent to a `StructuredBuffer`. + + // We start by computing the view that should be used + // (filling in a default view if one is not bound). + // + auto pView = mSRVs[flatIndex].pView; + if (!pView) pView = ShaderResourceView::getNullView(resourceRangeBindingInfo.dimension); + + // Next, we rely on a utility function to give us + // a CUDA device pointer that is equivalent to the + // given view. + // + CUdeviceptr pViewDevicePtr = (CUdeviceptr) pView->getCUDADeviceAddress(); + + // The view itself should know how many elements it covers. + // + // TODO: We need to confirm that byte-address buffer views + // have `elementCount` set to the number of bytes. + // + size_t viewElementCount = pView->getViewInfo().elementCount; + + // Once we've computed the values for fields (1) and (2) + // described above, we can fill them in. + // + FALCOR_CUDA_THROW_ON_FAIL(cudaMemcpy(pDest, &pViewDevicePtr, sizeof(pViewDevicePtr), memcpyKind)); + FALCOR_CUDA_THROW_ON_FAIL(cudaMemcpy(pDest + sizeof(pViewDevicePtr), &viewElementCount, sizeof(viewElementCount), memcpyKind)); + + // TODO: If Slang ever adds support for the implicit atomic + // counter on a structured buffer, then the layout may need + // to change to include a third pointer. + } + break; + + case DescriptorSet::Type::RawBufferUav: + case DescriptorSet::Type::StructuredBufferUav: + { + // The logic for seting a `RW(StructuredBuffer|ByteAddressBuffer)` + // is identical to that above for read-only buffers, with the + // exception of using the `UnorderedAccessView` type instead. + + auto pView = mUAVs[flatIndex].pView; + if (!pView) pView = UnorderedAccessView::getNullView(resourceRangeBindingInfo.dimension); + + CUdeviceptr pViewDevicePtr = (CUdeviceptr) pView->getCUDADeviceAddress(); + size_t viewElementCount = pView->getViewInfo().elementCount; + + FALCOR_CUDA_THROW_ON_FAIL(cudaMemcpy(pDest, &pViewDevicePtr, sizeof(pViewDevicePtr), memcpyKind)); + FALCOR_CUDA_THROW_ON_FAIL(cudaMemcpy(pDest + sizeof(pViewDevicePtr), &viewElementCount, sizeof(viewElementCount), memcpyKind)); + } + break; + + case DescriptorSet::Type::Sampler: + // CUDA does not support separate samplers, so there is no + // meaningful translation of a Slang `SamplerState` over to CUDA. + // We can simply skip over these ranges. + // + // TODO: There is a risk of error if the user's code relies on separate + // samplers, and the binding logic here silently ignoring samplers + // seems to contribute to the problem. As it stands, there isn't a much + // better option that we can implement, since issuing an error here + // would make a lot of existing Falcor shader code incompatible with CUDA. + break; + + case DescriptorSet::Type::TextureSrv: + case DescriptorSet::Type::TextureUav: + case DescriptorSet::Type::TypedBufferSrv: + case DescriptorSet::Type::TypedBufferUav: + // Slang translates any read-only texture/buffer that involves format + // conversion/interpretation into a `CUtexObject`. + // + // Similarly, any read-write or write-only texture/buffer that involves + // format conversion/interpretation translates as a `CUsurfObject`. + // + // TODO: Support for these cases needs to be added. They require adding + // support to resources/views for querying the CUDA texture/surface object + // handle. + // + throw std::exception("unexpected: interface-type field in CUDA parameter block"); + break; + } + break; + + case ParameterBlockReflection::ResourceRangeBindingInfo::Flavor::Interface: + { + // A Slang parameter of interface type (`IThing`) translates to + // a structured representation with the following pieces: + // + // 1. A pointer to the run-time type information for the concrete + // type `C` being stored. + // + // 2. A pointer to a "witness table," which can be understood as + // an array of function pointers that show how `C` implements + // the chosen interface (`IThing`). + // + // 3. The actual bytes of a `C` value, if it fits within the storage + // constraints imposed by `IThing`, or a device pointer to a `C`, + // in the cse where `C` is tood big. + // + // TODO: Actually handling all these details requires further integration + // of Falcor with the Slang reflection information to allow the required + // information to be looked up. + // + // For now we simply fail if there are any interface-type fields in + // the parameter block. + // + throw std::exception("unexpected: interface-type field in CUDA parameter block"); + } + break; + } + } + } + + // Once all of the "extraordinary" parameters represented as resource + // ranges have been copied over to the buffer, its contents should reflect + // the current state of this block. + } + + // In order to bind buffers/resources into a CUDA parameter block, we + // need to be able to produce a CUDA-compatible device memory address + // for those buffers/resources. E.g., the above code calls `getCUDADeviceAddress()` + // on SRVs/UAVs and expects it to Just Work. + // + // We now turn our attention to implementing the parts of the `Buffer` + // API that allow sharing with CUDA to work. + + void* Buffer::getCUDADeviceAddress() const + { + // Our goal is to get the device memory address at which this + // buffer resides, so that we can share it with CUDA code. + // + // Because a `Buffer` directly represents a GPU allocation + // (no implicit versioning), we can cache and re-use the + // device address each time is is queried. + // + void* deviceAddress = mCUDADeviceAddress; + if (!deviceAddress) + { + // If the device address has not been created/cached, + // then we need to go about setting it up using the + // CUDA API. + // + // No matter what, we need to know the size of the + // buffer/memory that we plan to share. + // + auto sizeInBytes = getSize(); + + // CUDA manages sharing of buffers through the idea of an + // "external memory" object, which represents the relationship + // with another API's objects. + // + // Just as with the device address, we cache and re-use the + // external memory relationship if one already exists. + // + cudaExternalMemory_t externalMemory = (cudaExternalMemory_t)mCUDAExternalMemory; + if (!externalMemory) + { + // If no external memory relationship exists, we will + // try to set one up, which requires working with + // the shared handle for this resource (which will + // only exist if the resource was created with + // sharing enabled). + // + HANDLE sharedHandle = getSharedApiHandle(); + if (sharedHandle == NULL) return nullptr; + + // In order to create the external memory association + // for CUDA, we need to fill in a descriptor struct. + // + // Note: This logic is D3D12-specific, so in order for + // Falcor to support other graphics APIs this code would + // need to be moved into a D3D-specific location. + // + cudaExternalMemoryHandleDesc externalMemoryHandleDesc; + memset(&externalMemoryHandleDesc, 0, sizeof(externalMemoryHandleDesc)); + externalMemoryHandleDesc.type = cudaExternalMemoryHandleTypeD3D12Resource; + externalMemoryHandleDesc.handle.win32.handle = sharedHandle; + externalMemoryHandleDesc.size = sizeInBytes; + externalMemoryHandleDesc.flags = cudaExternalMemoryDedicated; + + // Once we have filled in the descriptor, we can request + // that CUDA create the required association between the + // D3D buffer and a its own memory. + // + FALCOR_CUDA_THROW_ON_FAIL(cudaImportExternalMemory(&externalMemory, &externalMemoryHandleDesc)); + mCUDAExternalMemory = externalMemory; + } + + // The CUDA "external memory" handle is not itself a device + // pointer, so we need to query for a suitable device address + // for the buffer with another call. + // + // Just as for the external memory, we fill in a descriptor + // structure (although in this case we only need to specify + // the size). + // + cudaExternalMemoryBufferDesc bufferDesc; + memset(&bufferDesc, 0, sizeof(bufferDesc)); + bufferDesc.size = sizeInBytes; + + // Finally, we can "map" the buffer to get a device address. + // + FALCOR_CUDA_THROW_ON_FAIL(cudaExternalMemoryGetMappedBuffer(&deviceAddress, externalMemory, &bufferDesc)); + mCUDADeviceAddress = deviceAddress; + } + return deviceAddress; + } + + void* Buffer::getCUDADeviceAddress(ResourceViewInfo const& viewInfo) const + { + // Getting the CUDA device address for a view of a buffer starts + // with determining the device address of the buffer itself. + // + auto bufferAddress = getCUDADeviceAddress(); + + // Next, we need to determine the offset from the start of the buffer + // that we intend to use, which is determined by the index of the + // first element in the view, and the size of each element. + // + // Note: in the case of a "raw" buffer (unformatted, non-structured) + // the `firstElement` is assumed to be in units of bytes, and the + // `Buffer::getElementSize()` function returns 1. + // + size_t offset = viewInfo.firstElement * getElementSize(); + + return (char*)bufferAddress + offset; + } + +} +#endif diff --git a/Source/Falcor/Core/Program/CUDAProgram.h b/Source/Falcor/Core/Program/CUDAProgram.h new file mode 100644 index 0000000000..7dc7b57ab9 --- /dev/null +++ b/Source/Falcor/Core/Program/CUDAProgram.h @@ -0,0 +1,84 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#pragma once +#if _ENABLE_CUDA +#include "ComputeProgram.h" + +namespace Falcor +{ + class dlldecl CUDAProgram : public ComputeProgram + { + public: + using SharedPtr = std::shared_ptr; + using SharedConstPtr = std::shared_ptr; + + ~CUDAProgram() = default; + + /** Create a new compute program from file. + Note that this call merely creates a program object. The actual compilation and link happens at a later time. + \param[in] filename Compute program filename. + \param[in] csEntry Name of the entry point in the program. + \param[in] programDefines Optional list of macro definitions to set into the program. + \param[in] flags Optional program compilation flags. + \param[in] shaderModel Optional string describing which shader model to use. + \return A new object, or an exception is thrown if creation failed. + */ + static SharedPtr createFromFile(const std::string& filename, const std::string& csEntry, const DefineList& programDefines = DefineList(), Shader::CompilerFlags flags = Shader::CompilerFlags::None, const std::string& shaderModel = ""); + + /** Create a new compute program. + Note that this call merely creates a program object. The actual compilation and link happens at a later time. + \param[in] desc The program description. + \param[in] programDefines Optional list of macro definitions to set into the program. + \return A new object, or an exception is thrown if creation failed. + */ + static SharedPtr create(const Program::Desc& desc, const DefineList& programDefines = DefineList()); + + /** Dispatch the program using the argument values set in `pVars`. + */ + virtual void dispatchCompute( + ComputeContext* pContext, + ComputeVars* pVars, + uint3 const& threadGroupCount) override; + + protected: + virtual void setUpSlangCompilationTarget( + slang::TargetDesc& ioTargetDesc, + char const*& ioTargetMacroName) const override; + + virtual ProgramKernels::SharedPtr createProgramKernels( + const ProgramVersion* pVersion, + const ProgramReflection::SharedPtr& pReflector, + const ProgramKernels::UniqueEntryPointGroups& uniqueEntryPointGroups, + std::string& log, + const std::string& name = "") const override; + + private: + CUDAProgram() = default; + }; +} +#endif diff --git a/Source/Falcor/Core/Program/ComputeProgram.cpp b/Source/Falcor/Core/Program/ComputeProgram.cpp index ea8e986cdf..a11604f20c 100644 --- a/Source/Falcor/Core/Program/ComputeProgram.cpp +++ b/Source/Falcor/Core/Program/ComputeProgram.cpp @@ -46,6 +46,16 @@ namespace Falcor return pProg; } + void ComputeProgram::dispatchCompute( + ComputeContext* pContext, + ComputeVars* pVars, + uint3 const& threadGroupCount) + { + auto pState = ComputeState::create(); + pState->setProgram(std::static_pointer_cast(shared_from_this())); + pContext->dispatch(pState.get(), pVars, threadGroupCount); + } + SCRIPT_BINDING(ComputeProgram) { pybind11::class_(m, "ComputeProgram"); diff --git a/Source/Falcor/Core/Program/ComputeProgram.h b/Source/Falcor/Core/Program/ComputeProgram.h index 5088083e5d..1ce4ee52d6 100644 --- a/Source/Falcor/Core/Program/ComputeProgram.h +++ b/Source/Falcor/Core/Program/ComputeProgram.h @@ -59,7 +59,14 @@ namespace Falcor */ static SharedPtr create(const Program::Desc& desc, const DefineList& programDefines = DefineList()); - private: + /** Dispatch the program using the argument values set in `pVars`. + */ + virtual void dispatchCompute( + ComputeContext* pContext, + ComputeVars* pVars, + uint3 const& threadGroupCount); + + protected: ComputeProgram() = default; }; } diff --git a/Source/Falcor/Core/Program/Program.cpp b/Source/Falcor/Core/Program/Program.cpp index af8ebb0e57..f0e50d938c 100644 --- a/Source/Falcor/Core/Program/Program.cpp +++ b/Source/Falcor/Core/Program/Program.cpp @@ -35,7 +35,7 @@ namespace Falcor #ifdef FALCOR_VK const std::string kSupportedShaderModels[] = { "400", "410", "420", "430", "440", "450" }; #elif defined FALCOR_D3D12 - const std::string kSupportedShaderModels[] = { "4_0", "4_1", "5_0", "5_1", "6_0", "6_1", "6_2", "6_3" }; + const std::string kSupportedShaderModels[] = { "4_0", "4_1", "5_0", "5_1", "6_0", "6_1", "6_2", "6_3", "6_4", "6_5" }; #endif static Program::DefineList sGlobalDefineList; @@ -57,16 +57,15 @@ namespace Falcor Program::Desc& Program::Desc::addShaderLibrary(std::string const& path) { Source source(ShaderLibrary::create(path)); - source.firstEntryPoint = uint32_t(mEntryPoints.size()); - mActiveSource = (int32_t) mSources.size(); + mActiveSource = (int32_t)mSources.size(); mSources.emplace_back(std::move(source)); return *this; } Program::Desc& Program::Desc::addShaderString(const std::string& shader) { - mActiveSource = (int32_t) mSources.size(); + mActiveSource = (int32_t)mSources.size(); mSources.emplace_back(shader); return *this; @@ -74,56 +73,58 @@ namespace Falcor Program::Desc& Program::Desc::beginEntryPointGroup() { - EntryPointGroup group; - group.firstEntryPoint = uint32_t(mEntryPoints.size()); - group.entryPointCount = 0; - - mActiveGroup = (int32_t) mGroups.size(); - mGroups.push_back(group); + mActiveGroup = (int32_t)mGroups.size(); + mGroups.push_back(EntryPointGroup()); return *this; } Program::Desc& Program::Desc::entryPoint(ShaderType shaderType, std::string const& name) { - if(name.size() == 0) + if (name.size() == 0) return *this; - if(mActiveSource < 0) - { - throw std::exception("Cannot add an entry point without first adding a source file/library"); - } - - if(mActiveGroup < 0) + if (mActiveGroup < 0) { beginEntryPointGroup(); } - EntryPoint entryPoint; - entryPoint.stage = shaderType; - entryPoint.name = name; - - entryPoint.sourceIndex = mActiveSource; - entryPoint.groupIndex = mActiveGroup; - - mGroups[mActiveGroup].entryPointCount++; - mSources[mActiveSource].entryPointCount++; - - mEntryPoints.push_back(entryPoint); - + uint32_t entryPointIndex = declareEntryPoint(shaderType, name); + mGroups[mActiveGroup].entryPoints.push_back(entryPointIndex); return *this; } Program::Desc& Program::Desc::addDefaultVertexShaderIfNeeded() { // Don't set default vertex shader if one was set already. - if(hasEntryPoint(ShaderType::Vertex)) + if (hasEntryPoint(ShaderType::Vertex)) { return *this; } return addShaderLibrary("Scene/Raster.slang").entryPoint(ShaderType::Vertex, "defaultVS"); } + uint32_t Program::Desc::declareEntryPoint(ShaderType type, const std::string& name) + { + assert(!name.empty()); + + if (mActiveSource < 0) + { + throw std::exception("Cannot declare an entry point without first adding a source file/library"); + } + + EntryPoint entryPoint; + entryPoint.stage = type; + entryPoint.name = name; + entryPoint.sourceIndex = mActiveSource; + + uint32_t index = (uint32_t)mEntryPoints.size(); + mEntryPoints.push_back(entryPoint); + mSources[mActiveSource].entryPoints.push_back(index); + + return index; + } + Program::Desc& Program::Desc::setShaderModel(const std::string& sm) { // Check that the model is supported @@ -155,9 +156,9 @@ namespace Falcor bool Program::Desc::hasEntryPoint(ShaderType stage) const { - for(auto& entryPoint : mEntryPoints) + for (auto& entryPoint : mEntryPoints) { - if(entryPoint.stage == stage) + if (entryPoint.stage == stage) { return true; } @@ -184,9 +185,9 @@ namespace Falcor { std::string desc; - int32_t groupCount = (int32_t) mDesc.mGroups.size(); + int32_t groupCount = (int32_t)mDesc.mGroups.size(); - for(auto& src : mDesc.mSources) + for (auto& src : mDesc.mSources) { switch (src.type) { @@ -200,13 +201,12 @@ namespace Falcor should_not_get_here(); } - uint32_t entryPointCount = src.entryPointCount; desc += "("; - for( uint32_t ee = 0; ee < entryPointCount; ++ee ) + for (size_t ee = 0; ee < src.entryPoints.size(); ++ee) { - auto& entryPoint = mDesc.mEntryPoints[src.firstEntryPoint + ee]; + auto& entryPoint = mDesc.mEntryPoints[src.entryPoints[ee]]; - if(ee != 0) desc += ", "; + if (ee != 0) desc += ", "; desc += entryPoint.name; } desc += ")"; @@ -218,9 +218,9 @@ namespace Falcor bool Program::addDefine(const std::string& name, const std::string& value) { // Make sure that it doesn't exist already - if(mDefineList.find(name) != mDefineList.end()) + if (mDefineList.find(name) != mDefineList.end()) { - if(mDefineList[name] == value) + if (mDefineList[name] == value) { // Same define return false; @@ -246,7 +246,7 @@ namespace Falcor bool Program::removeDefine(const std::string& name) { - if(mDefineList.find(name) != mDefineList.end()) + if (mDefineList.find(name) != mDefineList.end()) { markDirty(); mDefineList.erase(name); @@ -300,19 +300,19 @@ namespace Falcor bool Program::checkIfFilesChanged() { - if(mpActiveVersion == nullptr) + if (mpActiveVersion == nullptr) { // We never linked, so nothing really changed return false; } // Have any of the files we depend on changed? - for(auto& entry : mFileTimeMap) + for (auto& entry : mFileTimeMap) { auto& path = entry.first; auto& modifiedTime = entry.second; - if( modifiedTime != getFileModifiedTime(path) ) + if (modifiedTime != getFileModifiedTime(path)) { return true; } @@ -364,7 +364,7 @@ namespace Falcor // Translation a Falcor `ShaderType` to the corresponding `SlangStage` SlangStage getSlangStage(ShaderType type) { - switch(type) + switch (type) { case ShaderType::Vertex: return SLANG_STAGE_VERTEX; case ShaderType::Pixel: return SLANG_STAGE_PIXEL; @@ -397,8 +397,26 @@ namespace Falcor #endif } + void Program::setUpSlangCompilationTarget( + slang::TargetDesc& ioTargetDesc, + char const*& ioTargetMacroName) const + { +#ifdef FALCOR_VK + ioTargetMacroName = "FALCOR_VK"; + ioTargetDesc.format = SLANG_SPIRV; +#elif defined FALCOR_D3D12 + ioTargetMacroName = "FALCOR_D3D"; + + // If the profile string starts with a `4_` or a `5_`, use DXBC. Otherwise, use DXIL + if (hasPrefix(mDesc.mShaderModel, "4_") || hasPrefix(mDesc.mShaderModel, "5_")) ioTargetDesc.format = SLANG_DXBC; + else ioTargetDesc.format = SLANG_DXIL; +#else +#error unknown shader compilation target +#endif + } + SlangCompileRequest* Program::createSlangCompileRequest( - const DefineList& defineList) const + const DefineList& defineList) const { slang::IGlobalSession* pSlangGlobalSession = getSlangGlobalSession(); assert(pSlangGlobalSession); @@ -417,7 +435,7 @@ namespace Falcor slangSearchPaths.push_back(path.c_str()); } sessionDesc.searchPaths = slangSearchPaths.data(); - sessionDesc.searchPathCount = (SlangInt) slangSearchPaths.size(); + sessionDesc.searchPathCount = (SlangInt)slangSearchPaths.size(); slang::TargetDesc targetDesc; targetDesc.format = SLANG_TARGET_UNKNOWN; @@ -447,25 +465,13 @@ namespace Falcor const char* targetMacroName; // Pick the right target based on the current graphics API -#ifdef FALCOR_VK - targetMacroName = "FALCOR_VK"; - targetDesc.format = SLANG_SPIRV; -#elif defined FALCOR_D3D12 - targetMacroName = "FALCOR_D3D"; - - // If the profile string starts with a `4_` or a `5_`, use DXBC. Otherwise, use DXIL - if (hasPrefix(mDesc.mShaderModel, "4_") || hasPrefix(mDesc.mShaderModel, "5_")) targetDesc.format = SLANG_DXBC; - else targetDesc.format = SLANG_DXIL; -#else -#error unknown shader compilation target -#endif - + setUpSlangCompilationTarget(targetDesc, targetMacroName); // Pass any `#define` flags along to Slang, since we aren't doing our // own preprocessing any more. // std::vector slangDefines; - const auto addSlangDefine = [&slangDefines] (const char* name, const char* value) + const auto addSlangDefine = [&slangDefines](const char* name, const char* value) { slangDefines.push_back({ name, value }); }; @@ -489,7 +495,7 @@ namespace Falcor addSlangDefine(sm.c_str(), "1"); sessionDesc.preprocessorMacros = slangDefines.data(); - sessionDesc.preprocessorMacroCount = (SlangInt) slangDefines.size(); + sessionDesc.preprocessorMacroCount = (SlangInt)slangDefines.size(); sessionDesc.targets = &targetDesc; sessionDesc.targetCount = 1; @@ -541,7 +547,7 @@ namespace Falcor // translation unit for Slang (so that they can see and resolve // definitions). // - for(auto src : mDesc.mSources) + for (auto src : mDesc.mSources) { // Register the translation unit with Slang int translationUnitIndex = spAddTranslationUnit(pSlangRequest, SLANG_SOURCE_LANGUAGE_SLANG, nullptr); @@ -576,10 +582,8 @@ namespace Falcor // Each entry point references the index of the source // it uses, and luckily, the Slang API can use these // indices directly. - for(auto& entryPoint : mDesc.mEntryPoints) + for (auto& entryPoint : mDesc.mEntryPoints) { - auto& group = mDesc.mGroups[entryPoint.groupIndex]; - spAddEntryPoint( pSlangRequest, entryPoint.sourceIndex, @@ -633,7 +637,7 @@ namespace Falcor if (pSlangDiagnostics && pSlangDiagnostics->getBufferSize() > 0) { - log += (char const*) pSlangDiagnostics->getBufferPointer(); + log += (char const*)pSlangDiagnostics->getBufferPointer(); } return failed ? nullptr : pSpecializedSlangProgram; @@ -669,11 +673,11 @@ namespace Falcor uint32_t allEntryPointCount = uint32_t(mDesc.mEntryPoints.size()); std::vector> pLinkedEntryPoints; - for( uint32_t ee = 0; ee < allEntryPointCount; ++ee ) + for (uint32_t ee = 0; ee < allEntryPointCount; ++ee) { auto pSlangEntryPoint = pVersion->getSlangEntryPoint(ee); - slang::IComponentType* componentTypes[] = {pSpecializedSlangGlobalScope, pSlangEntryPoint}; + slang::IComponentType* componentTypes[] = { pSpecializedSlangGlobalScope, pSlangEntryPoint }; ComPtr pLinkedSlangEntryPoint; ComPtr pSlangDiagnostics; @@ -731,7 +735,7 @@ namespace Falcor // std::vector componentTypesForProgram; componentTypesForProgram.push_back(pSpecializedSlangGlobalScope); - for( uint32_t ee = 0; ee < allEntryPointCount; ++ee ) + for (uint32_t ee = 0; ee < allEntryPointCount; ++ee) { // TODO: Eventually this would need to use the specialized // (but not linked) version of each entry point. @@ -748,6 +752,34 @@ namespace Falcor ProgramReflection::SharedPtr pReflector; doSlangReflection(pVersion, pSpecializedSlangProgram, pLinkedEntryPoints, pReflector, log); + // Create Shader objects for each entry point and cache them here + std::vector allShaders; + for (uint32_t i = 0; i < allEntryPointCount; i++) + { + auto pLinkedEntryPoint = pLinkedEntryPoints[i]; + auto entryPointDesc = mDesc.mEntryPoints[i]; + + Shader::Blob blob; + ComPtr pSlangDiagnostics; + bool failed = SLANG_FAILED(pLinkedEntryPoint->getEntryPointCode( + /* entryPointIndex: */ 0, + /* targetIndex: */ 0, + blob.writeRef(), + pSlangDiagnostics.writeRef())); + + if (pSlangDiagnostics && pSlangDiagnostics->getBufferSize() > 0) + { + log += (char const*)pSlangDiagnostics->getBufferPointer(); + } + + if (failed) return nullptr; + + Shader::SharedPtr shader = createShaderFromBlob(blob, entryPointDesc.stage, entryPointDesc.name, mDesc.getCompilerFlags(), log); + if (!shader) return nullptr; + + allShaders.push_back(std::move(shader)); + } + // In order to construct the `ProgramKernels` we need to extract // the kernels for each entry-point group. // @@ -759,7 +791,7 @@ namespace Falcor // one-to-one with the entries in `pLinkedEntryPointGroups`. // uint32_t entryPointGroupCount = uint32_t(mDesc.mGroups.size()); - for( uint32_t gg = 0; gg < entryPointGroupCount; ++gg ) + for (uint32_t gg = 0; gg < entryPointGroupCount; ++gg) { auto entryPointGroupDesc = mDesc.mGroups[gg]; @@ -767,48 +799,38 @@ namespace Falcor // code for its constituent entry points, using the "linked" // version of the entry-point group. // - auto groupEntryPointCount = entryPointGroupDesc.entryPointCount; std::vector shaders; - for(uint32_t ee = 0; ee < groupEntryPointCount; ++ee) + for (auto entryPointIndex : entryPointGroupDesc.entryPoints) { - auto entryPointIndex = entryPointGroupDesc.firstEntryPoint + ee; - - auto pLinkedEntryPoint = pLinkedEntryPoints[entryPointIndex]; - auto entryPointDesc = mDesc.mEntryPoints[entryPointIndex]; - - Shader::Blob blob; - ComPtr pSlangDiagnostics; - bool failed = SLANG_FAILED(pLinkedEntryPoint->getEntryPointCode( - /* entryPointIndex: */ 0, - /* targetIndex: */ 0, - blob.writeRef(), - pSlangDiagnostics.writeRef())); - - if (pSlangDiagnostics && pSlangDiagnostics->getBufferSize() > 0) - { - log += (char const*) pSlangDiagnostics->getBufferPointer(); - } - - if (failed) return nullptr; - - Shader::SharedPtr shader = createShaderFromBlob(blob, entryPointDesc.stage, entryPointDesc.name, mDesc.getCompilerFlags(), log); - if (!shader) return nullptr; - - shaders.emplace_back(std::move(shader)); + shaders.push_back(allShaders[entryPointIndex]); } auto pGroupReflector = pReflector->getEntryPointGroup(gg); - auto pEntryPointGroupKernels = createEntryPointGroupKernels(shaders, pGroupReflector); entryPointGroups.push_back(pEntryPointGroupKernels); } + return createProgramKernels( + pVersion, + pReflector, + entryPointGroups, + log, + getProgramDescString()); + } + + ProgramKernels::SharedPtr Program::createProgramKernels( + const ProgramVersion* pVersion, + const ProgramReflection::SharedPtr& pReflector, + const ProgramKernels::UniqueEntryPointGroups& uniqueEntryPointGroups, + std::string& log, + const std::string& name) const + { return ProgramKernels::create( - pVersion, - pReflector, - entryPointGroups, - log, - getProgramDescString()); + pVersion, + pReflector, + uniqueEntryPointGroups, + log, + name); } ProgramVersion::SharedPtr Program::preprocessAndCreateProgramVersion( @@ -819,7 +841,7 @@ namespace Falcor SlangResult slangResult = spCompile(pSlangRequest); log += spGetDiagnosticOutput(pSlangRequest); - if(SLANG_FAILED(slangResult)) + if (SLANG_FAILED(slangResult)) { spDestroyCompileRequest(pSlangRequest); return nullptr; @@ -833,8 +855,8 @@ namespace Falcor ComPtr pSlangSession(pSlangGlobalScope->getSession()); std::vector> pSlangEntryPoints; - uint32_t entryPointCount = (uint32_t) mDesc.mEntryPoints.size(); - for( uint32_t ee = 0; ee < entryPointCount; ++ee ) + uint32_t entryPointCount = (uint32_t)mDesc.mEntryPoints.size(); + for (uint32_t ee = 0; ee < entryPointCount; ++ee) { auto entryPointDesc = mDesc.mEntryPoints[ee]; @@ -849,7 +871,7 @@ namespace Falcor // Extract list of files referenced, for dependency-tracking purposes int depFileCount = spGetDependencyFileCount(pSlangRequest); - for(int ii = 0; ii < depFileCount; ++ii) + for (int ii = 0; ii < depFileCount; ++ii) { std::string depFilePath = spGetDependencyFilePath(pSlangRequest, ii); mFileTimeMap[depFilePath] = getFileModifiedTime(depFilePath); @@ -883,7 +905,7 @@ namespace Falcor pSlangProgram.writeRef()); ProgramReflection::SharedPtr pReflector; - if( !doSlangReflection(pVersion.get(), pSlangGlobalScope, pSlangEntryPoints, pReflector, log) ) + if (!doSlangReflection(pVersion.get(), pSlangGlobalScope, pSlangEntryPoints, pReflector, log)) { return nullptr; } @@ -906,7 +928,7 @@ namespace Falcor bool Program::link() const { - while(1) + while (1) { // Create the program std::string log; @@ -960,14 +982,14 @@ namespace Falcor // The read iterator will be implicit in our loop over the // entire array of programs: // - for(auto& pWeakProgram : sPrograms) + for (auto& pWeakProgram : sPrograms) { // We will skip any programs where the weak pointer // has changed to `nullptr` because the object was // already deleted. // auto pProgram = pWeakProgram.lock(); - if(!pProgram) + if (!pProgram) continue; // Now we know that we have a valid (non-null) `Program`, @@ -980,7 +1002,7 @@ namespace Falcor // we can skip further processing of this program // (unless forceReload flag is set). // - if(!(pProgram->checkIfFilesChanged() || forceReload)) + if (!(pProgram->checkIfFilesChanged() || forceReload)) continue; // If any files have changed, then we need to reset diff --git a/Source/Falcor/Core/Program/Program.h b/Source/Falcor/Core/Program/Program.h index 96d4e9b60f..a076469e64 100644 --- a/Source/Falcor/Core/Program/Program.h +++ b/Source/Falcor/Core/Program/Program.h @@ -88,7 +88,7 @@ namespace Falcor Desc& dumpIntermediates(bool enable) { enable ? mShaderFlags |= Shader::CompilerFlags::DumpIntermediates : mShaderFlags &= ~(Shader::CompilerFlags::DumpIntermediates); return *this; } /** Set the shader model string. This depends on the API you are using. - For DirectX it should be `4_0`, `4_1`, `5_0`, `5_1`, `6_0`, `6_1`, `6_2`, or `6_3`. The default is `6_0`. Shader model `6.x` will use dxcompiler, previous shader models use fxc. + For DirectX it should be `4_0`, `4_1`, `5_0`, `5_1`, `6_0`, `6_1`, `6_2`, `6_3`, `6_4`, or `6_5`. The default is `6_2`. Shader model `6.x` will use dxcompiler, previous shader models use fxc. For Vulkan, it should be `400`, `410`, `420`, `430`, `440` or `450`. The default is `450` */ Desc& setShaderModel(const std::string& sm); @@ -111,6 +111,7 @@ namespace Falcor Desc& beginEntryPointGroup(); Desc& addDefaultVertexShaderIfNeeded(); + uint32_t declareEntryPoint(ShaderType type, const std::string& name); struct Source { @@ -127,24 +128,19 @@ namespace Falcor ShaderLibrary::SharedPtr pLibrary; std::string str; - uint32_t firstEntryPoint = 0; - uint32_t entryPointCount = 0; + std::vector entryPoints; }; - struct EntryPointGroup { - uint32_t firstEntryPoint; - uint32_t entryPointCount; + std::vector entryPoints; }; struct EntryPoint { std::string name; ShaderType stage; - int32_t sourceIndex; - int32_t groupIndex; }; std::vector mSources; @@ -233,10 +229,10 @@ namespace Falcor const ProgramReflection::SharedPtr& getReflector() const { return getActiveVersion()->getReflector(); } uint32_t getEntryPointGroupCount() const { return uint32_t(mDesc.mGroups.size()); } - uint32_t getGroupEntryPointCount(uint32_t groupIndex) const { return mDesc.mGroups[groupIndex].entryPointCount; } + uint32_t getGroupEntryPointCount(uint32_t groupIndex) const { return (uint32_t)mDesc.mGroups[groupIndex].entryPoints.size(); } uint32_t getGroupEntryPointIndex(uint32_t groupIndex, uint32_t entryPointIndexInGroup) const { - return mDesc.mGroups[groupIndex].firstEntryPoint + entryPointIndexInGroup; + return mDesc.mGroups[groupIndex].entryPoints[entryPointIndexInGroup]; } protected: @@ -251,6 +247,10 @@ namespace Falcor SlangCompileRequest* createSlangCompileRequest( DefineList const& defineList) const; + virtual void setUpSlangCompilationTarget( + slang::TargetDesc& ioTargetDesc, + char const*& ioTargetMacroName) const; + bool doSlangReflection( ProgramVersion const* pVersion, slang::IComponentType* pSlangGlobalScope, @@ -269,6 +269,13 @@ namespace Falcor const std::vector& shaders, EntryPointGroupReflection::SharedPtr const& pReflector) const; + virtual ProgramKernels::SharedPtr createProgramKernels( + const ProgramVersion* pVersion, + const ProgramReflection::SharedPtr& pReflector, + const ProgramKernels::UniqueEntryPointGroups& uniqueEntryPointGroups, + std::string& log, + const std::string& name = "") const; + // The description used to create this program Desc mDesc; diff --git a/Source/Falcor/Core/Program/ProgramReflection.cpp b/Source/Falcor/Core/Program/ProgramReflection.cpp index af9dc8415e..fc6350037d 100644 --- a/Source/Falcor/Core/Program/ProgramReflection.cpp +++ b/Source/Falcor/Core/Program/ProgramReflection.cpp @@ -211,20 +211,28 @@ namespace Falcor return ReflectionResourceType::Type::Sampler; case TypeReflection::Kind::ShaderStorageBuffer: return ReflectionResourceType::Type::StructuredBuffer; + case TypeReflection::Kind::TextureBuffer: + return ReflectionResourceType::Type::TypedBuffer; case TypeReflection::Kind::Resource: switch (pSlangType->getResourceShape() & SLANG_RESOURCE_BASE_SHAPE_MASK) { case SLANG_STRUCTURED_BUFFER: return ReflectionResourceType::Type::StructuredBuffer; - case SLANG_BYTE_ADDRESS_BUFFER: return ReflectionResourceType::Type::RawBuffer; case SLANG_TEXTURE_BUFFER: return ReflectionResourceType::Type::TypedBuffer; - default: + case SLANG_ACCELERATION_STRUCTURE: + return ReflectionResourceType::Type::AccelerationStructure; + case SLANG_TEXTURE_1D: + case SLANG_TEXTURE_2D: + case SLANG_TEXTURE_3D: + case SLANG_TEXTURE_CUBE: return ReflectionResourceType::Type::Texture; + default: + should_not_get_here(); + return ReflectionResourceType::Type(-1); } - break; default: should_not_get_here(); return ReflectionResourceType::Type(-1); @@ -311,6 +319,8 @@ namespace Falcor return ReflectionResourceType::Dimensions::TextureCube; case SLANG_TEXTURE_CUBE_ARRAY: return ReflectionResourceType::Dimensions::TextureCubeArray; + case SLANG_ACCELERATION_STRUCTURE: + return ReflectionResourceType::Dimensions::AccelerationStructure; case SLANG_TEXTURE_BUFFER: case SLANG_STRUCTURED_BUFFER: @@ -638,9 +648,11 @@ namespace Falcor // Check that the root descriptor type is supported. if (isRootDescriptor) { - if (type != ReflectionResourceType::Type::RawBuffer && type != ReflectionResourceType::Type::StructuredBuffer) + // Check the resource type and shader access. + if (type != ReflectionResourceType::Type::RawBuffer && type != ReflectionResourceType::Type::StructuredBuffer && + type != ReflectionResourceType::Type::AccelerationStructure) { - logError("Resource '" + name + "' cannot be bound as root descriptor. Only raw and structured buffers are supported."); + logError("Resource '" + name + "' cannot be bound as root descriptor. Only raw buffers, structured buffers, and acceleration structures are supported."); return nullptr; } if (shaderAccess != ReflectionResourceType::ShaderAccess::Read && @@ -649,6 +661,8 @@ namespace Falcor logError("Buffer '" + name + "' cannot be bound as root descriptor. Only SRV/UAVs are supported."); return nullptr; } + assert(type != ReflectionResourceType::Type::AccelerationStructure || shaderAccess == ReflectionResourceType::ShaderAccess::Read); + // Check that it's not an append/consume structured buffer, which is unsupported for root descriptors. // RWStructuredBuffer with counter is also not supported, but we cannot see that on the type declaration. // At bind time, we'll validate that the buffer has not been created with a UAV counter. @@ -661,7 +675,7 @@ namespace Falcor return nullptr; } } - assert(dims == ReflectionResourceType::Dimensions::Buffer); // We shouldn't get here otherwise + assert(dims == ReflectionResourceType::Dimensions::Buffer || dims == ReflectionResourceType::Dimensions::AccelerationStructure); // We shouldn't get here otherwise } ReflectionResourceType::SharedPtr pType = ReflectionResourceType::create(type, dims, structuredType, retType, shaderAccess, pSlangType); @@ -670,6 +684,7 @@ namespace Falcor ParameterBlockReflection::ResourceRangeBindingInfo bindingInfo; bindingInfo.regIndex = (uint32_t)getRegisterIndexFromPath(pPath->pPrimary, category); bindingInfo.regSpace = getRegisterSpaceFromPath(pPath->pPrimary, category); + bindingInfo.dimension = dims; if (isRootDescriptor) bindingInfo.flavor = ParameterBlockReflection::ResourceRangeBindingInfo::Flavor::RootDescriptor; @@ -707,12 +722,27 @@ namespace Falcor pProgramVersion); pSubBlock->setElementType(pElementType); - if (pElementType->getByteSize() != 0) + auto pContainerLayout = pSlangType->getContainerVarLayout(); + ExtendedReflectionPath containerPath(pPath, pContainerLayout); + int32_t containerCategoryCount = pContainerLayout->getCategoryCount(); + for (int32_t containerCategoryIndex = 0; containerCategoryIndex < containerCategoryCount; ++containerCategoryIndex) { - ParameterBlockReflection::DefaultConstantBufferBindingInfo defaultConstantBufferInfo; - defaultConstantBufferInfo.regIndex = bindingInfo.regIndex; - defaultConstantBufferInfo.regSpace = bindingInfo.regSpace; - pSubBlock->setDefaultConstantBufferBindingInfo(defaultConstantBufferInfo); + auto containerCategory = pContainerLayout->getCategoryByIndex(containerCategoryIndex); + switch (containerCategory) + { + case slang::ParameterCategory::DescriptorTableSlot: + case slang::ParameterCategory::ConstantBuffer: + { + ParameterBlockReflection::DefaultConstantBufferBindingInfo defaultConstantBufferInfo; + defaultConstantBufferInfo.regIndex = (uint32_t)getRegisterIndexFromPath(containerPath.pPrimary, containerCategory); + defaultConstantBufferInfo.regSpace = getRegisterSpaceFromPath(containerPath.pPrimary, containerCategory); + pSubBlock->setDefaultConstantBufferBindingInfo(defaultConstantBufferInfo); + } + break; + + default: + break; + } } pSubBlock->finalize(); @@ -1106,22 +1136,57 @@ namespace Falcor static bool isVaryingParameter(slang::VariableLayoutReflection* pSlangParam) { + // TODO: It is unfortunate that Falcor has to maintain this logic, + // since there is nearly identical logic already in Slang. + // + // The basic problem is that we want to know whether a parameter + // is logically "uniform" or logically "varying." + // + // In the common cases, we can tell by looking at the kind(s) + // of resources the parameter consumes; if it uses any + // kinds of resources that only make sense for varying + // parameters, then it is varying. + // unsigned int categoryCount = pSlangParam->getCategoryCount(); for( unsigned int ii = 0; ii < categoryCount; ++ii ) { switch(pSlangParam->getCategoryByIndex(ii)) { + // Varying cross-stage input/output obviously marks + // a varying parameter, as do the special categories + // of input used for ray-tracing shaders. + // case slang::ParameterCategory::VaryingInput: case slang::ParameterCategory::VaryingOutput: case slang::ParameterCategory::RayPayload: case slang::ParameterCategory::HitAttributes: return true; + // Everything else indicates a uniform parameter. + // default: return false; } } - return false; + + // If we get to the end of the loop above, then it + // means that there must have been *zero* categories + // of resources consumed by the parameter. + // + // There are two cases where that could have happened: + // + // 1. A parameter of a zero-size type (an empty `struct` + // or a `void` parameter). In this case uniform-vs-varying + // is a meaningless distinction. + // + // 2. A varying "system value" parameter, which doesn't + // consume any application-bindable resources. + // + // Because case (1) is unimportant, we choose the default + // behavior based on (2). If a parameter doesn't appear + // to consume any resources, we assume it is varying. + // + return true; } static uint32_t getUniformParameterCount( @@ -1198,7 +1263,7 @@ namespace Falcor auto pParam = reflectVariable( pSlangParam, - 0, + pElementType->getResourceRangeCount(), pGroup.get(), &path, pProgramVersion); @@ -1265,8 +1330,40 @@ namespace Falcor : mpProgramVersion(pProgramVersion) , mpSlangReflector(pSlangReflector) { - // For the most part the program scope needs to be reflected like a struct type - ReflectionStructType::SharedPtr pGlobalStruct = ReflectionStructType::create(0, "", nullptr); + // For Falcor's purposes, the global scope of a program can be treated + // much like a user-defined `struct` type, where the fields are the + // global shader parameters. + // + // Slang provides two ways to iterate over the parameters of a program: + // + // 1. We can directly query `getParameterCount()` and then `getParameterByIndex()`, + // to enumerate all the global shader parameters. + // + // 2. We can query `getGlobalParamsTypeLayout()` which returns a type layout + // that represents all of the global-scope parameters bundled together. + // + // Our code will mostly use option (1), but we will do a little bit of + // option (2) to be able to get the total size of the global parameters, + // for cases where a default constant buffer is needed for the global + // scope. + // + auto pSlangGlobalParamsTypeLayout = pSlangReflector->getGlobalParamsTypeLayout(); + + // The Slang type layout for the global scope either directly represents the + // parameters as a struct type `G`, or it represents those parameters wrapped + // up into a constant buffer like `ConstantBuffer`. If we are in the latter + // case, then we want to get the element type (`G`) from the constant buffer + // type layout. + // + if (auto pElementTypeLayout = pSlangGlobalParamsTypeLayout->getElementTypeLayout()) + pSlangGlobalParamsTypeLayout = pElementTypeLayout; + + // Once we have the Slang type layout for the `struct` of global parameters, + // we can easily query its size in bytes. + // + size_t slangGlobalParamsSize = pSlangGlobalParamsTypeLayout->getSize(slang::ParameterCategory::Uniform); + + ReflectionStructType::SharedPtr pGlobalStruct = ReflectionStructType::create(slangGlobalParamsSize, "", nullptr); ParameterBlockReflection::SharedPtr pDefaultBlock = ParameterBlockReflection::createEmpty(pProgramVersion); pDefaultBlock->setElementType(pGlobalStruct); @@ -1359,6 +1456,7 @@ namespace Falcor case DescriptorSet::Type::RawBufferSrv: case DescriptorSet::Type::TypedBufferSrv: case DescriptorSet::Type::StructuredBufferSrv: + case DescriptorSet::Type::AccelerationStructureSrv: fieldRange.baseIndex = ioBuildState.srvCount; ioBuildState.srvCount += fieldRange.count; break; @@ -1488,6 +1586,7 @@ namespace Falcor case DescriptorSet::Type::RawBufferSrv: case DescriptorSet::Type::TypedBufferSrv: case DescriptorSet::Type::StructuredBufferSrv: + case DescriptorSet::Type::AccelerationStructureSrv: regIndex += counters.srvCount; counters.srvCount += rangeInfo.count; break; @@ -1580,6 +1679,10 @@ namespace Falcor ? DescriptorSet::Type::TypedBufferSrv : DescriptorSet::Type::TypedBufferUav; break; + case ReflectionResourceType::Type::AccelerationStructure: + assert(shaderAccess == ReflectionResourceType::ShaderAccess::Read); + return DescriptorSet::Type::AccelerationStructureSrv; + break; case ReflectionResourceType::Type::Sampler: return DescriptorSet::Type::Sampler; break; diff --git a/Source/Falcor/Core/Program/ProgramReflection.h b/Source/Falcor/Core/Program/ProgramReflection.h index 9527705476..09dd1c8b30 100644 --- a/Source/Falcor/Core/Program/ProgramReflection.h +++ b/Source/Falcor/Core/Program/ProgramReflection.h @@ -27,8 +27,8 @@ **************************************************************************/ #pragma once #include "Core/API/DescriptorSet.h" - #include +#include namespace Falcor { @@ -1036,7 +1036,10 @@ namespace Falcor Texture2DMS, Texture2DMSArray, TextureCubeArray, + AccelerationStructure, Buffer, + + Count }; /** For structured-buffers, describes the type of the buffer @@ -1059,7 +1062,8 @@ namespace Falcor RawBuffer, TypedBuffer, Sampler, - ConstantBuffer + ConstantBuffer, + AccelerationStructure, }; /** Create a new object @@ -1305,6 +1309,7 @@ namespace Falcor }; Flavor flavor = Flavor::Simple; + ReflectionResourceType::Dimensions dimension = ReflectionResourceType::Dimensions::Unknown; uint32_t regIndex = 0; ///< The register index uint32_t regSpace = 0; ///< The register space diff --git a/Source/Falcor/Core/Program/ProgramVars.cpp b/Source/Falcor/Core/Program/ProgramVars.cpp index 857846ebc1..a2086d3ee5 100644 --- a/Source/Falcor/Core/Program/ProgramVars.cpp +++ b/Source/Falcor/Core/Program/ProgramVars.cpp @@ -328,4 +328,12 @@ namespace Falcor { return applyProgramVarsCommon(this, pContext, bindRootSig, pRootSignature); } + + void ComputeVars::dispatchCompute(ComputeContext* pContext, uint3 const& threadGroupCount) + { + auto pProgram = std::dynamic_pointer_cast(getReflection()->getProgramVersion()->getProgram()); + assert(pProgram); + pProgram->dispatchCompute(pContext, this, threadGroupCount); + } + } diff --git a/Source/Falcor/Core/Program/ProgramVars.h b/Source/Falcor/Core/Program/ProgramVars.h index 463b38d64c..a986a0308f 100644 --- a/Source/Falcor/Core/Program/ProgramVars.h +++ b/Source/Falcor/Core/Program/ProgramVars.h @@ -144,6 +144,10 @@ namespace Falcor virtual bool apply(ComputeContext* pContext, bool bindRootSig, RootSignature* pRootSignature); + /** Dispatch the program using the argument values set in this object. + */ + void dispatchCompute(ComputeContext* pContext, uint3 const& threadGroupCount); + protected: ComputeVars(const ProgramReflection::SharedConstPtr& pReflector); }; diff --git a/Source/Falcor/Core/Renderer.h b/Source/Falcor/Core/Renderer.h index 1edd568de8..a28b1382e7 100644 --- a/Source/Falcor/Core/Renderer.h +++ b/Source/Falcor/Core/Renderer.h @@ -49,68 +49,79 @@ namespace Falcor class IFramework { public: - /** Get the render-context for the current frame. This might change each frame*/ + /** Get the render-context for the current frame. This might change each frame. + */ virtual RenderContext* getRenderContext() = 0; - /** Get the current FBO*/ + /** Get the current FBO. + */ virtual std::shared_ptr getTargetFbo() = 0; - /** Get the window*/ + /** Get the window. + */ virtual Window* getWindow() = 0; - /** Get the global Clock object + /** Get the global Clock object. */ virtual Clock& getGlobalClock() = 0; - /** Get the global FrameRate object + /** Get the global FrameRate object. */ virtual FrameRate& getFrameRate() = 0; - /** Resize the swap-chain buffers*/ + /** Resize the swap-chain buffers. + */ virtual void resizeSwapChain(uint32_t width, uint32_t height) = 0; - /** Check if a key is pressed*/ + /** Render a frame. + */ + virtual void renderFrame() = 0; + + /** Check if a key is pressed. + */ virtual bool isKeyPressed(const KeyboardEvent::Key& key) = 0; - /** Show/hide the UI */ + /** Show/hide the UI. + */ virtual void toggleUI(bool showUI) = 0; - /** Show/hide the UI */ + /** Show/hide the UI. + */ virtual bool isUiEnabled() = 0; /** Takes and outputs a screenshot. */ virtual std::string captureScreen(const std::string explicitFilename = "", const std::string explicitOutputDirectory = "") = 0; - /* Shutdown the app + /** Shutdown the app. */ virtual void shutdown() = 0; - /** Pause/resume the renderer. The GUI will still be rendered + /** Pause/resume the renderer. The GUI will still be rendered. */ virtual void pauseRenderer(bool pause) = 0; - /** Check if the renderer running + /** Check if the renderer running. */ virtual bool isRendererPaused() = 0; - /** Get the current configuration + /** Get the current configuration. */ virtual SampleConfig getConfig() = 0; - /** Render the global UI. You'll can open a GUI window yourself before calling it + /** Render the global UI. You'll can open a GUI window yourself before calling it. */ virtual void renderGlobalUI(Gui* pGui) = 0; - /** Get the global shortcuts message + /** Get the global shortcuts message. */ virtual std::string getKeyboardShortcutsStr() = 0; - /** Set VSYNC + /** Set VSYNC. */ virtual void toggleVsync(bool on) = 0; - /** Get the VSYNC state + /** Get the VSYNC state. */ virtual bool isVsyncEnabled() = 0; }; diff --git a/Source/Falcor/Core/Sample.cpp b/Source/Falcor/Core/Sample.cpp index 0578062414..7a958320a2 100644 --- a/Source/Falcor/Core/Sample.cpp +++ b/Source/Falcor/Core/Sample.cpp @@ -67,6 +67,11 @@ namespace Falcor if(mpRenderer) mpRenderer->onResizeSwapChain(width, height); } + void Sample::handleRenderFrame() + { + renderFrame(); + } + void Sample::handleKeyboardEvent(const KeyboardEvent& keyEvent) { if (mSuppressInput) @@ -112,7 +117,7 @@ namespace Falcor break; #if _PROFILING_ENABLED case KeyboardEvent::Key::P: - gProfileEnabled = !gProfileEnabled; + Profiler::instance().setEnabled(!Profiler::instance().isEnabled()); break; #endif case KeyboardEvent::Key::V: @@ -193,7 +198,7 @@ namespace Falcor } catch (const std::exception & e) { - logError("Error:\n" + std::string(e.what())); + logError("Caught exception:\n\n" + std::string(e.what()) + "\n\nEnable breaking on exceptions in the debugger to get a full stack trace."); } Logger::shutdown(); } @@ -230,7 +235,7 @@ namespace Falcor } catch (const std::exception & e) { - logError("Error:\n" + std::string(e.what())); + logError("Caught exception:\n\n" + std::string(e.what()) + "\n\nEnable breaking on exceptions in the debugger to get a full stack trace."); } Logger::shutdown(); } @@ -407,7 +412,9 @@ namespace Falcor { PROFILE("renderUI"); - if (mShowUI || gProfileEnabled) + auto& profiler = Profiler::instance(); + + if (mShowUI || profiler.isEnabled()) { mpGui->beginFrame(); @@ -418,22 +425,26 @@ namespace Falcor mVideoCapture.pUI->render(w); } - if (gProfileEnabled) + if (profiler.isEnabled()) { uint32_t y = gpDevice->getSwapChainFbo()->getHeight() - 360; - mpGui->setActiveFont(kMonospaceFont); - - Gui::Window profilerWindow(mpGui.get(), "Profiler", gProfileEnabled, { 800, 350 }, { 10, y }); - Profiler::endEvent("renderUI"); // Stop the timer + bool open = profiler.isEnabled(); + Gui::Window profilerWindow(mpGui.get(), "Profiler", open, { 800, 350 }, { 10, y }); + profiler.endEvent("renderUI"); // Stop the timer - if(gProfileEnabled) + if (open) { - profilerWindow.text(Profiler::getEventsString().c_str()); - Profiler::startEvent("renderUI"); + std::string text = profiler.getEventsString(); + if (profilerWindow.button("Print to log")) logInfo("\n" + text); + ImGui::PushFont(mpGui->getFont(kMonospaceFont)); + profilerWindow.text(text); + ImGui::PopFont(); + profiler.startEvent("renderUI"); profilerWindow.release(); } - mpGui->setActiveFont(""); + + profiler.setEnabled(open); } mpGui->render(getRenderContext(), gpDevice->getSwapChainFbo(), (float)mFrameRate.getLastFrameTime()); @@ -478,7 +489,7 @@ namespace Falcor if (mpPixelZoom) mpPixelZoom->render(pRenderContext, pSwapChainFbo.get()); #if _PROFILING_ENABLED - Profiler::endFrame(); + Profiler::instance().endFrame(); #endif // Capture video frame after UI is rendered if (captureVideoUI) captureVideoFrame(); diff --git a/Source/Falcor/Core/Sample.h b/Source/Falcor/Core/Sample.h index 373cc35013..040456c0a4 100644 --- a/Source/Falcor/Core/Sample.h +++ b/Source/Falcor/Core/Sample.h @@ -75,6 +75,7 @@ namespace Falcor Clock& getGlobalClock() override { return mClock; } FrameRate& getFrameRate() override { return mFrameRate; } void resizeSwapChain(uint32_t width, uint32_t height) override; + void renderFrame() override; bool isKeyPressed(const KeyboardEvent::Key& key) override; void toggleUI(bool showUI) override { mShowUI = showUI; } bool isUiEnabled() override { return mShowUI; } @@ -95,8 +96,8 @@ namespace Falcor bool mRendererPaused = false; ///< Freezes the renderer Window::SharedPtr mpWindow; ///< The application's window - void renderFrame() override; void handleWindowSizeChange() override; + void handleRenderFrame() override; void handleKeyboardEvent(const KeyboardEvent& keyEvent) override; void handleMouseEvent(const MouseEvent& mouseEvent) override; void handleDroppedFile(const std::string& filename) override; diff --git a/Source/Falcor/Core/Window.cpp b/Source/Falcor/Core/Window.cpp index ed15f68c17..abf54acd61 100644 --- a/Source/Falcor/Core/Window.cpp +++ b/Source/Falcor/Core/Window.cpp @@ -34,8 +34,8 @@ #ifdef _WIN32 #define GLFW_EXPOSE_NATIVE_WIN32 -#include "glfw3.h" -#include "glfw3native.h" +#include "GLFW/glfw3.h" +#include "GLFW/glfw3native.h" #else // LINUX // Replace the defines we undef'd in FalcorVK.h, because glfw will need them when it includes Xlib @@ -490,7 +490,7 @@ namespace Falcor while (glfwWindowShouldClose(mpGLFWWindow) == false) { glfwPollEvents(); - mpCallbacks->renderFrame(); + mpCallbacks->handleRenderFrame(); } } diff --git a/Source/Falcor/Core/Window.h b/Source/Falcor/Core/Window.h index 719f0c339a..4398be3726 100644 --- a/Source/Falcor/Core/Window.h +++ b/Source/Falcor/Core/Window.h @@ -66,7 +66,7 @@ namespace Falcor { public: virtual void handleWindowSizeChange() = 0; - virtual void renderFrame() = 0; + virtual void handleRenderFrame() = 0; virtual void handleKeyboardEvent(const KeyboardEvent& keyEvent) = 0; virtual void handleMouseEvent(const MouseEvent& mouseEvent) = 0; virtual void handleDroppedFile(const std::string& filename) = 0; diff --git a/Source/Falcor/Experimental/Scene/Lights/EmissiveLightSampler.cpp b/Source/Falcor/Experimental/Scene/Lights/EmissiveLightSampler.cpp index d8197c4c3f..b19be75be4 100644 --- a/Source/Falcor/Experimental/Scene/Lights/EmissiveLightSampler.cpp +++ b/Source/Falcor/Experimental/Scene/Lights/EmissiveLightSampler.cpp @@ -30,9 +30,9 @@ namespace Falcor { - bool EmissiveLightSampler::prepareProgram(Program* pProgram) const + Program::DefineList EmissiveLightSampler::getDefines() const { - return pProgram->addDefine("_EMISSIVE_LIGHT_SAMPLER_TYPE", std::to_string((uint32_t)mType)); + return {{ "_EMISSIVE_LIGHT_SAMPLER_TYPE", std::to_string((uint32_t)mType) }}; } SCRIPT_BINDING(EmissiveLightSampler) @@ -40,5 +40,7 @@ namespace Falcor pybind11::enum_ type(m, "EmissiveLightSamplerType"); type.value("Uniform", EmissiveLightSamplerType::Uniform); type.value("LightBVH", EmissiveLightSamplerType::LightBVH); + type.value("Power", EmissiveLightSamplerType::Power); + type.value("Null", EmissiveLightSamplerType::Null); } } diff --git a/Source/Falcor/Experimental/Scene/Lights/EmissiveLightSampler.h b/Source/Falcor/Experimental/Scene/Lights/EmissiveLightSampler.h index bdcd5784a7..0318d113a5 100644 --- a/Source/Falcor/Experimental/Scene/Lights/EmissiveLightSampler.h +++ b/Source/Falcor/Experimental/Scene/Lights/EmissiveLightSampler.h @@ -47,13 +47,10 @@ namespace Falcor */ virtual bool update(RenderContext* pRenderContext) { return false; } - /** Add compile-time specialization to program to use this light sampler. - This function must be called every frame before the sampler is bound. - Note that ProgramVars may need to be re-created after this call, check the return value. - \param[in] pProgram The Program to add compile-time specialization to. - \return True if the ProgramVars needs to be re-created. + /** Return a list of shader defines to use this light sampler. + * \return Returns a list of shader defines. */ - virtual bool prepareProgram(Program* pProgram) const; + virtual Program::DefineList getDefines() const; /** Bind the light sampler data to a given shader var */ diff --git a/Source/Falcor/Experimental/Scene/Lights/EmissiveLightSampler.slang b/Source/Falcor/Experimental/Scene/Lights/EmissiveLightSampler.slang index 78d22ca98f..09dc86d5df 100644 --- a/Source/Falcor/Experimental/Scene/Lights/EmissiveLightSampler.slang +++ b/Source/Falcor/Experimental/Scene/Lights/EmissiveLightSampler.slang @@ -27,6 +27,7 @@ **************************************************************************/ #include "EmissiveLightSamplerType.slangh" __exported import Experimental.Scene.Lights.EmissiveLightSamplerInterface; +__exported import Utils.Sampling.SampleGeneratorInterface; /** The host sets the _EMISSIVE_LIGHT_SAMPLER_TYPE define to select sampler. @@ -35,29 +36,44 @@ __exported import Experimental.Scene.Lights.EmissiveLightSamplerInterface; size of the 'EmissiveLightSampler' type may vary depending on the type. */ -#if defined(_EMISSIVE_LIGHT_SAMPLER_TYPE) && _EMISSIVE_LIGHT_SAMPLER_TYPE == EMISSIVE_LIGHT_SAMPLER_UNIFORM +// If _EMISSIVE_LIGHT_SAMPLER_TYPE is not defined, define it to the null sampler. +// This case happens if the user imports EmissiveLightSampler.slang but doesn't use it. +// This is a workaround to avoid compiler warnings since Slang warns on undefined +// preprocessor defines even if they are inside #ifdef .. #endif blocks. +#ifndef _EMISSIVE_LIGHT_SAMPLER_TYPE +#define _EMISSIVE_LIGHT_SAMPLER_TYPE EMISSIVE_LIGHT_SAMPLER_NULL +#endif + +#if _EMISSIVE_LIGHT_SAMPLER_TYPE == EMISSIVE_LIGHT_SAMPLER_UNIFORM import Experimental.Scene.Lights.EmissiveUniformSampler; typedef EmissiveUniformSampler EmissiveLightSampler; -#elif defined(_EMISSIVE_LIGHT_SAMPLER_TYPE) && _EMISSIVE_LIGHT_SAMPLER_TYPE == EMISSIVE_LIGHT_SAMPLER_LIGHT_BVH +#elif _EMISSIVE_LIGHT_SAMPLER_TYPE == EMISSIVE_LIGHT_SAMPLER_LIGHT_BVH import Experimental.Scene.Lights.LightBVHSampler; typedef LightBVHSampler EmissiveLightSampler; -#elif defined(_EMISSIVE_LIGHT_SAMPLER_TYPE) - // Compile-time error if _EMISSIVE_LIGHT_SAMPLER_TYPE is an invalid type. - #error _EMISSIVE_LIGHT_SAMPLER_TYPE is not set to a supported type. See EmissiveLightSamplerType.slangh. +#elif _EMISSIVE_LIGHT_SAMPLER_TYPE == EMISSIVE_LIGHT_SAMPLER_POWER + import Experimental.Scene.Lights.EmissivePowerSampler; + typedef EmissivePowerSampler EmissiveLightSampler; -#else - // If _EMISSIVE_LIGHT_SAMPLER_TYPE is not defined, declare a null sampler. - // This case happens if the user imports EmissiveLightSampler.slang but doesn't use it. +#elif _EMISSIVE_LIGHT_SAMPLER_TYPE == EMISSIVE_LIGHT_SAMPLER_NULL + + /** Null sampler. + This is provided so that implementations always have a valid sampler struct. + */ struct NullEmissiveSampler : IEmissiveLightSampler { - bool sampleLight(const float3 posW, const float3 normalW, inout SampleGenerator sg, out TriangleLightSample ls) + bool sampleLight(const float3 posW, const float3 normalW, const bool onSurface, inout S sg, out TriangleLightSample ls) { return false; } - float evalPdf(float3 posW, float3 normalW, const TriangleHit hit) + float evalTriangleSelectionPdf(const float3 posW, const float3 normalW, const bool onSurface, const uint triangleIndex) + { + return 0.f; + } + + float evalPdf(const float3 posW, const float3 normalW, const bool onSurface, const TriangleHit hit) { return 0.f; } @@ -66,4 +82,8 @@ __exported import Experimental.Scene.Lights.EmissiveLightSamplerInterface; }; typedef NullEmissiveSampler EmissiveLightSampler; +#else + // Compile-time error if _EMISSIVE_LIGHT_SAMPLER_TYPE is an invalid type. + #error _EMISSIVE_LIGHT_SAMPLER_TYPE is not set to a supported type. See EmissiveLightSamplerType.slangh. + #endif diff --git a/Source/Falcor/Experimental/Scene/Lights/EmissiveLightSamplerHelpers.slang b/Source/Falcor/Experimental/Scene/Lights/EmissiveLightSamplerHelpers.slang index 3372572218..995c477e35 100644 --- a/Source/Falcor/Experimental/Scene/Lights/EmissiveLightSamplerHelpers.slang +++ b/Source/Falcor/Experimental/Scene/Lights/EmissiveLightSamplerHelpers.slang @@ -56,7 +56,6 @@ bool sampleTriangle(const float3 posW, const uint triangleIndex, const float2 u, { ls = {}; ls.triangleIndex = triangleIndex; - const EmissiveTriangle tri = gScene.lightCollection.getTriangle(triangleIndex); // Sample the triangle uniformly. @@ -86,6 +85,9 @@ bool sampleTriangle(const float3 posW, const uint triangleIndex, const float2 u, float denom = max(FLT_MIN, cosTheta * tri.area); ls.pdf = distSqr / denom; + // Save light sample triangle barycentric coordinates + ls.uv = u; + // TODO: We can simplify the expressions by using the unnormalized quantities for computing the pdf. //ls.pdf = -2.f * distSqr * ls.distance / (dot(N, L); // Optimized (except N would have to be properly flipped above, before normalW is computed). return true; diff --git a/Source/Falcor/Experimental/Scene/Lights/EmissiveLightSamplerInterface.slang b/Source/Falcor/Experimental/Scene/Lights/EmissiveLightSamplerInterface.slang index 621d3203dd..2a7b0f3d7e 100644 --- a/Source/Falcor/Experimental/Scene/Lights/EmissiveLightSamplerInterface.slang +++ b/Source/Falcor/Experimental/Scene/Lights/EmissiveLightSamplerInterface.slang @@ -25,7 +25,7 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **************************************************************************/ -__exported import Utils.Sampling.SampleGenerator; +__exported import Utils.Sampling.SampleGeneratorInterface; /** Slang interface and structs used by emissive light samplers. */ @@ -41,7 +41,8 @@ struct TriangleLightSample float3 dir; ///< Normalized direction from the shading point to the sampled point on the light source in world space. float distance; ///< Distance from the shading point to the sampled point. float3 Le; ///< Emitted radiance. This is zero if the light is back-facing or sample is invalid. - float pdf; ///< Probability density with respect to solid angle from the shading point. The range is [0,inf] (inclusive), where pdf == 0.0 indicates an invalid sample. + float pdf; ///< Probability density with respect to solid angle from the shading point. The range is [0,inf] (inclusive), where pdf == 0.0 indicated an invalid sample. + float2 uv; ///< Light sample barycentric coords over the triangle }; /** Describes a light sample at a hit point on an emissive triangle. @@ -61,17 +62,28 @@ interface IEmissiveLightSampler /** Draw a single light sample. \param[in] posW Shading point in world space. \param[in] normalW Normal at the shading point in world space. + \param[in] onSurface True if only upper hemisphere should be considered. \param[in,out] sg Sample generator. \param[out] ls Light sample. Only valid if true is returned. \return True if a sample was generated, false otherwise. */ - bool sampleLight(const float3 posW, const float3 normalW, inout SampleGenerator sg, out TriangleLightSample ls); + bool sampleLight(const float3 posW, const float3 normalW, const bool onSurface, inout S sg, out TriangleLightSample ls); + + /** Evaluate the PDF associated with selecting an emissive triangle. + \param[in] posW Shading point in world space. + \param[in] normalW Normal at the shading point in world space. + \param[in] onSurface True if only upper hemisphere should be considered. + \param[in] triangleIndex index of selected triangle + \return Probability of selecting the input triangle + */ + float evalTriangleSelectionPdf(const float3 posW, const float3 normalW, const bool onSurface, const uint triangleIndex); /** Evaluate the PDF at a shading point given a hit point on an emissive triangle. \param[in] posW Shading point in world space. \param[in] normalW Normal at the shading point in world space. + \param[in] onSurface True if only upper hemisphere should be considered. \param[in] hit Triangle hit data. \return Probability density with respect to solid angle at the shading point. */ - float evalPdf(float3 posW, float3 normalW, const TriangleHit hit); + float evalPdf(const float3 posW, const float3 normalW, const bool onSurface, const TriangleHit hit); }; diff --git a/Source/Falcor/Experimental/Scene/Lights/EmissiveLightSamplerType.slangh b/Source/Falcor/Experimental/Scene/Lights/EmissiveLightSamplerType.slangh index da3c4f5ea0..b7c6b1f584 100644 --- a/Source/Falcor/Experimental/Scene/Lights/EmissiveLightSamplerType.slangh +++ b/Source/Falcor/Experimental/Scene/Lights/EmissiveLightSamplerType.slangh @@ -41,16 +41,23 @@ enum class EmissiveLightSamplerType { Uniform = 0, LightBVH = 1, + Power = 2, + + Null = 0xff, }; // For shader specialization in EmissiveLightSampler.slang we can't use the enums. // TODO: Find a way to remove this workaround. #define EMISSIVE_LIGHT_SAMPLER_UNIFORM 0 #define EMISSIVE_LIGHT_SAMPLER_LIGHT_BVH 1 +#define EMISSIVE_LIGHT_SAMPLER_POWER 2 +#define EMISSIVE_LIGHT_SAMPLER_NULL 0xff #ifdef HOST_CODE static_assert((uint32_t)EmissiveLightSamplerType::Uniform == EMISSIVE_LIGHT_SAMPLER_UNIFORM); static_assert((uint32_t)EmissiveLightSamplerType::LightBVH == EMISSIVE_LIGHT_SAMPLER_LIGHT_BVH); +static_assert((uint32_t)EmissiveLightSamplerType::Power == EMISSIVE_LIGHT_SAMPLER_POWER); +static_assert((uint32_t)EmissiveLightSamplerType::Null == EMISSIVE_LIGHT_SAMPLER_NULL); #endif END_NAMESPACE_FALCOR diff --git a/Source/Falcor/Experimental/Scene/Lights/EmissivePowerSampler.cpp b/Source/Falcor/Experimental/Scene/Lights/EmissivePowerSampler.cpp new file mode 100644 index 0000000000..73085106c7 --- /dev/null +++ b/Source/Falcor/Experimental/Scene/Lights/EmissivePowerSampler.cpp @@ -0,0 +1,182 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#include "stdafx.h" +#include "EmissivePowerSampler.h" +#include +#include +#include +#include + +namespace Falcor +{ + EmissivePowerSampler::SharedPtr EmissivePowerSampler::create(RenderContext* pRenderContext, Scene::SharedPtr pScene) + { + return SharedPtr(new EmissivePowerSampler(pRenderContext, pScene)); + } + + bool EmissivePowerSampler::update(RenderContext* pRenderContext) + { + PROFILE("EmissivePowerSampler::update"); + + bool samplerChanged = false;; + + // Check if light collection has changed. + if (is_set(mpScene->getUpdates(), Scene::UpdateFlags::LightCollectionChanged)) + { + mNeedsRebuild = true; + } + + // Rebuild if necessary + if (mNeedsRebuild) + { + // Get global list of emissive triangles. + assert(mpLightCollection); + const auto& triangles = mpLightCollection->getMeshLightTriangles(); + + const size_t numTris = triangles.size(); + std::vector weights(numTris); + for (size_t i = 0; i < numTris; i++) weights[i] = triangles[i].flux; + + mTriangleTable = generateAliasTable(std::move(weights)); + + mNeedsRebuild = false; + samplerChanged = true; + } + + return samplerChanged; + } + + bool EmissivePowerSampler::setShaderData(const ShaderVar& var) const + { + assert(var.isValid()); + + var["_emissivePower"]["invWeightsSum"] = 1.0f / mTriangleTable.weightSum; + var["_emissivePower"]["triangleAliasTable"] = mTriangleTable.fullTable; + + return true; + } + + EmissivePowerSampler::EmissivePowerSampler(RenderContext* pRenderContext, Scene::SharedPtr pScene) + : EmissiveLightSampler(EmissiveLightSamplerType::Power, pScene) + { + // Make sure the light collection is created. + mpLightCollection = pScene->getLightCollection(pRenderContext); + } + + EmissivePowerSampler::AliasTable EmissivePowerSampler::generateAliasTable(std::vector weights) + { + uint32_t N = uint32_t(weights.size()); + std::uniform_int_distribution rngDist; + + double sum = 0.0f; + for (float f : weights) + { + sum += f; + } + for (float& f : weights) + { + f *= N / float(sum); + } + + std::vector permutation(N); + for (uint32_t i = 0; i < N; ++i) + { + permutation[i] = i; + } + std::sort(permutation.begin(), permutation.end(), [&](uint32_t a, uint32_t b) { return weights[a] < weights[b]; }); + + std::vector thresholds(N); + std::vector redirect(N); + std::vector merged(N); + std::vector fullTable(N); + + uint32_t head = 0; + uint32_t tail = N - 1; + + while (head != tail) + { + int i = permutation[head]; + int j = permutation[tail]; + + thresholds[i] = weights[i]; + redirect[i] = j; + weights[j] -= 1.0f - weights[i]; + + if (head == tail - 1) + { + thresholds[j] = 1.0f; + redirect[j] = j; + break; + } + else if (weights[j] < 1.0f) + { + std::swap(permutation[head], permutation[tail]); + tail--; + } + else + { + head++; + } + } + + for (uint32_t i = 0; i < N; ++i) + { + permutation[i] = i; + } + + for (uint32_t i = 0; i < N; ++i) + { + uint32_t dst = i + (rngDist(mAliasTableRng) % (N - i)); + std::swap(thresholds[i], thresholds[dst]); + std::swap(redirect[i], redirect[dst]); + std::swap(permutation[i], permutation[dst]); + } + + for (uint32_t i = 0; i < N; ++i) + { + merged[i] = uint2(redirect[i], permutation[i]); + + // Pack 16-bit threshold (i.e., a half float) plus 2x 24-bit table entries + uint32_t prob = (uint32_t(f32tof16(thresholds[i])) << 16u); + uint2 lowPrec = uint2(redirect[i] & 0xFFFFFFu, permutation[i] & 0xFFFFFFu); + uint2 mergedEntry = uint2(prob | ((lowPrec.x >> 8u) & 0xFFFFu), ((lowPrec.x & 0xFFu) << 24u) | lowPrec.y); + fullTable[i] = mergedEntry; + } + + AliasTable result + { + float(sum), + N, + Buffer::createTyped(N), + }; + + result.fullTable->setBlob(&fullTable[0], 0, N * sizeof(uint2)); + + return result; + } +} diff --git a/Source/Falcor/Experimental/Scene/Lights/EmissivePowerSampler.h b/Source/Falcor/Experimental/Scene/Lights/EmissivePowerSampler.h new file mode 100644 index 0000000000..7594573199 --- /dev/null +++ b/Source/Falcor/Experimental/Scene/Lights/EmissivePowerSampler.h @@ -0,0 +1,87 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#pragma once +#include "EmissiveLightSampler.h" +#include "LightCollection.h" + +namespace Falcor +{ + /** Sample geometry proportionally to its emissive power. + */ + class dlldecl EmissivePowerSampler : public EmissiveLightSampler + { + public: + using SharedPtr = std::shared_ptr; + using SharedConstPtr = std::shared_ptr; + + struct AliasTable + { + float weightSum; ///< Total weight of all elements used to create the alias table + uint32_t N; ///< Number of entries in the alias table (and # elements in the buffers) + Buffer::SharedPtr fullTable; ///< A compressed/packed merged table. Max 2^24 (16 million) entries per table. + }; + + virtual ~EmissivePowerSampler() = default; + + /** Creates a EmissivePowerSampler for a given scene. + \param[in] pRenderContext The render context. + \param[in] pScene The scene. + \param[in] options The options to override the default behavior. + */ + static SharedPtr create(RenderContext* pRenderContext, Scene::SharedPtr pScene); + + /** Updates the sampler to the current frame. + \param[in] pRenderContext The render context. + \return True if the sampler was updated. + */ + virtual bool update(RenderContext* pRenderContext) override; + + /** Bind the light sampler data to a given shader variable. + \param[in] var Shader variable. + \return True if successful, false otherwise. + */ + virtual bool setShaderData(const ShaderVar& var) const override; + + protected: + EmissivePowerSampler(RenderContext* pRenderContext, Scene::SharedPtr pScene); + + /** Generate an alias table + \param[in] weights The weights we'd like to sample each entry proportional to + \returns The alias table + */ + AliasTable generateAliasTable(std::vector weights); + + // Internal state + bool mNeedsRebuild = true; ///< Trigger rebuild on the next call to update(). We should always build on the first call, so the initial value is true. + + LightCollection::SharedConstPtr mpLightCollection; + + std::mt19937 mAliasTableRng; + AliasTable mTriangleTable; + }; +} diff --git a/Source/Falcor/Experimental/Scene/Lights/EmissivePowerSampler.slang b/Source/Falcor/Experimental/Scene/Lights/EmissivePowerSampler.slang new file mode 100644 index 0000000000..47b2174d56 --- /dev/null +++ b/Source/Falcor/Experimental/Scene/Lights/EmissivePowerSampler.slang @@ -0,0 +1,122 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#include "Utils/Math/MathConstants.slangh" + +import Scene.Scene; +import Utils.Sampling.SampleGeneratorInterface; +import Experimental.Scene.Lights.EmissiveLightSamplerHelpers; +import Experimental.Scene.Lights.EmissiveLightSamplerInterface; + +struct EmissivePower +{ + float invWeightsSum; + Buffer triangleAliasTable; +}; + +/** Emissive light sampler that samples proportionally to emissive power. + + The sampler implements the IEmissiveLightSampler interface (see + EmissiveLightSamplerInterface.slang for usage information). + + The struct wraps a LightCollection that stores the pre-processed lights. + The program should instantiate the struct below. See EmissiveLightSampler.slang. +*/ +struct EmissivePowerSampler : IEmissiveLightSampler +{ + EmissivePower _emissivePower; + + /** Draw a single light sample. + \param[in] posW Shading point in world space. + \param[in] normalW Normal at the shading point in world space. + \param[in,out] sg Sample generator. + \param[out] ls Light sample. Only valid if true is returned. + \return True if a sample was generated, false otherwise. + */ + bool sampleLight(const float3 posW, const float3 normalW, const bool onSurface, inout S sg, out TriangleLightSample ls) + { + if (gScene.lightCollection.isEmpty()) return false; + + // Randomly pick a triangle out of the global list with uniform probability. + float uLight = sampleNext1D(sg); + uint triangleCount = gScene.lightCollection.triangleCount; + // Safety precaution as the result of the multiplication may be rounded to triangleCount even if uLight < 1.0 when triangleCount is large. + uint triangleIndex = min((uint)(uLight * triangleCount), triangleCount - 1); + + uint2 packed = _emissivePower.triangleAliasTable[triangleIndex]; + float threshold = f16tof32(packed.x >> 16u); + uint selectAbove = ((packed.x & 0xFFFFu) << 8u) | ((packed.y >> 24u) & 0xFFu); + uint selectBelow = packed.y & 0xFFFFFFu; + + // Test the threshold in the current table entry; pick one of the two options + triangleIndex = (sampleNext1D(sg) >= threshold) ? selectAbove : selectBelow; + + float triangleSelectionPdf = gScene.lightCollection.fluxData[triangleIndex].flux * _emissivePower.invWeightsSum; + + // Sample the triangle uniformly. + float2 u = sampleNext2D(sg); + + if (!sampleTriangle(posW, triangleIndex, u, ls)) return false; + + // The final probability density is the product of the sampling probabilities. + ls.pdf *= triangleSelectionPdf; + return true; + } + + /** Evaluate the PDF associated with selecting an emissive triangle. + \param[in] posW Shading point in world space. + \param[in] normalW Normal at the shading point in world space. + \param[in] onSurface True if only upper hemisphere should be considered. + \param[in] triangleIndex Index of selected triangle. + \return Probability of drawing the input triangle proportionally to its emissive power + */ + float evalTriangleSelectionPdf(const float3 posW, const float3 normalW, const bool onSurface, const uint triangleIndex) + { + if (triangleIndex == LightCollection::kInvalidIndex) return 0.f; + + return gScene.lightCollection.fluxData[triangleIndex].flux * _emissivePower.invWeightsSum; + } + + /** Evaluate the PDF at a shading point given a hit point on an emissive triangle. + \param[in] posW Shading point in world space. + \param[in] normalW Normal at the shading point in world space. + \param[in] hit Triangle hit data. + \return Probability density with respect to solid angle at the shading point. + */ + float evalPdf(const float3 posW, const float3 normalW, const bool onSurface, const TriangleHit hit) + { + if (hit.triangleIndex == LightCollection::kInvalidIndex) return 0.f; + + float triangleSelectionPdf = evalTriangleSelectionPdf(posW, normalW, onSurface, hit.triangleIndex); + + // Compute triangle sampling probability with respect to solid angle from the shading point. + float trianglePdf = evalTrianglePdf(posW, hit); + + // The final probability density is the product of the sampling probabilities. + return triangleSelectionPdf * trianglePdf; + } +}; diff --git a/Source/Falcor/Experimental/Scene/Lights/EmissiveUniformSampler.slang b/Source/Falcor/Experimental/Scene/Lights/EmissiveUniformSampler.slang index faeeecddf2..34573b768c 100644 --- a/Source/Falcor/Experimental/Scene/Lights/EmissiveUniformSampler.slang +++ b/Source/Falcor/Experimental/Scene/Lights/EmissiveUniformSampler.slang @@ -28,7 +28,7 @@ #include "Utils/Math/MathConstants.slangh" import Scene.Scene; -import Utils.Sampling.SampleGenerator; +import Utils.Sampling.SampleGeneratorInterface; import Experimental.Scene.Lights.EmissiveLightSamplerHelpers; import Experimental.Scene.Lights.EmissiveLightSamplerInterface; @@ -47,11 +47,12 @@ struct EmissiveUniformSampler : IEmissiveLightSampler /** Draw a single light sample. \param[in] posW Shading point in world space. \param[in] normalW Normal at the shading point in world space. + \param[in] onSurface True if only upper hemisphere should be considered. \param[in,out] sg Sample generator. \param[out] ls Light sample. Only valid if true is returned. \return True if a sample was generated, false otherwise. */ - bool sampleLight(const float3 posW, const float3 normalW, inout SampleGenerator sg, out TriangleLightSample ls) + bool sampleLight(const float3 posW, const float3 normalW, const bool onSurface, inout S sg, out TriangleLightSample ls) { if (gScene.lightCollection.getActiveTriangleCount() == 0) return false; @@ -71,18 +72,33 @@ struct EmissiveUniformSampler : IEmissiveLightSampler return true; } + /** Evaluate the PDF associated with selecting an emissive triangle. + \param[in] posW Shading point in world space. + \param[in] normalW Normal at the shading point in world space. + \param[in] onSurface True if only upper hemisphere should be considered. + \param[in] triangleIndex Index of selected triangle. + \return Probability of drawing the input triangle from an uniform distribution. + */ + float evalTriangleSelectionPdf(const float3 posW, const float3 normalW, const bool onSurface, const uint triangleIndex) + { + if (triangleIndex == LightCollection::kInvalidIndex) return 0.f; + + // Lights are chosen uniformly so the selection probability is just one over the number of lights. + return gScene.lightCollection.getActiveTriangleCount() > 0 ? 1.f / (float)gScene.lightCollection.getActiveTriangleCount() : 0.f; + } + /** Evaluate the PDF at a shading point given a hit point on an emissive triangle. \param[in] posW Shading point in world space. \param[in] normalW Normal at the shading point in world space. + \param[in] onSurface True if only upper hemisphere should be considered. \param[in] hit Triangle hit data. \return Probability density with respect to solid angle at the shading point. */ - float evalPdf(float3 posW, float3 normalW, const TriangleHit hit) + float evalPdf(const float3 posW, const float3 normalW, const bool onSurface, const TriangleHit hit) { - if (hit.triangleIndex == LightCollection::kInvalidIndex) return 0; + if (hit.triangleIndex == LightCollection::kInvalidIndex) return 0.f; - // Lights are chosen uniformly so the selection probability is just one over the number of lights. - float triangleSelectionPdf = gScene.lightCollection.getActiveTriangleCount() > 0 ? 1.f / (float)gScene.lightCollection.getActiveTriangleCount() : 0.f; + float triangleSelectionPdf = evalTriangleSelectionPdf(posW, normalW, onSurface, hit.triangleIndex); // Compute triangle sampling probability with respect to solid angle from the shading point. float trianglePdf = evalTrianglePdf(posW, hit); diff --git a/Source/Falcor/Experimental/Scene/Lights/EnvMap.cpp b/Source/Falcor/Experimental/Scene/Lights/EnvMap.cpp index 5e4ed2fc5f..564f4c1d6c 100644 --- a/Source/Falcor/Experimental/Scene/Lights/EnvMap.cpp +++ b/Source/Falcor/Experimental/Scene/Lights/EnvMap.cpp @@ -43,6 +43,7 @@ namespace Falcor if (widgets.var("Rotation XYZ", rotation, -360.f, 360.f, 0.5f)) setRotation(rotation); widgets.var("Intensity", mData.intensity, 0.f, 1000000.f); widgets.var("Color tint", mData.tint, 0.f, 1.f); + widgets.text("EnvMap: " + mpEnvMap->getSourceFilename()); } void EnvMap::setRotation(float3 degreesXYZ) @@ -51,11 +52,7 @@ namespace Falcor { mRotation = degreesXYZ; - auto rotX = glm::eulerAngleX(glm::radians(mRotation.x)); - auto rotY = glm::eulerAngleY(glm::radians(mRotation.y)); - auto rotZ = glm::eulerAngleZ(glm::radians(mRotation.z)); - - auto transform = rotZ * rotY * rotX; + auto transform = glm::eulerAngleXYZ(glm::radians(mRotation.x), glm::radians(mRotation.y), glm::radians(mRotation.z)); mData.transform = static_cast(transform); mData.invTransform = static_cast(glm::inverse(transform)); @@ -97,6 +94,11 @@ namespace Falcor return getChanges(); } + uint64_t EnvMap::getMemoryUsageInBytes() const + { + return mpEnvMap ? mpEnvMap->getTextureSizeInBytes() : 0; + } + EnvMap::EnvMap(const std::string& filename) { // Load environment map from file. Set it to generate mips and use linear color. @@ -114,6 +116,7 @@ namespace Falcor SCRIPT_BINDING(EnvMap) { pybind11::class_ envMap(m, "EnvMap"); + envMap.def(pybind11::init(&EnvMap::create), "filename"_a); envMap.def_property_readonly("filename", &EnvMap::getFilename); envMap.def_property("rotation", &EnvMap::getRotation, &EnvMap::setRotation); envMap.def_property("intensity", &EnvMap::getIntensity, &EnvMap::setIntensity); diff --git a/Source/Falcor/Experimental/Scene/Lights/EnvMap.h b/Source/Falcor/Experimental/Scene/Lights/EnvMap.h index c618eb968b..a50065f397 100644 --- a/Source/Falcor/Experimental/Scene/Lights/EnvMap.h +++ b/Source/Falcor/Experimental/Scene/Lights/EnvMap.h @@ -52,7 +52,9 @@ namespace Falcor void renderUI(Gui::Widgets& widgets); /** Set rotation angles. - Rotation is applied as rotation around X axis, followed by rotation around Y and Z axes. + Rotation is applied as rotation around Z, Y and X axes, in that order. + Note that glm::extractEulerAngleXYZ() may be used to extract these angles from + a transformation matrix. \param[in] degreesXYZ Rotation angles in degrees for XYZ. */ void setRotation(float3 degreesXYZ); @@ -104,6 +106,10 @@ namespace Falcor */ Changes getChanges() const { return mChanges; } + /** Get the total GPU memory usage in bytes. + */ + uint64_t getMemoryUsageInBytes() const; + protected: EnvMap(const std::string& filename); diff --git a/Source/Falcor/Experimental/Scene/Lights/EnvMap.slang b/Source/Falcor/Experimental/Scene/Lights/EnvMap.slang index 8e07d64cbb..b39c467524 100644 --- a/Source/Falcor/Experimental/Scene/Lights/EnvMap.slang +++ b/Source/Falcor/Experimental/Scene/Lights/EnvMap.slang @@ -37,13 +37,22 @@ struct EnvMap EnvMapData data; ///< Environment map data. + /** Returns the dimensions of the env map texture. + */ + uint2 getDimensions() + { + uint2 dim; + envMap.GetDimensions(dim.x, dim.y); + return dim; + } + /** Evaluates the radiance coming from world space direction 'dir'. */ float3 eval(float3 dir, float lod = 0.f) { // Get (u,v) coord in latitude-longitude map format. float2 uv = world_to_latlong_map(toLocal(dir)); - return data.intensity * data.tint * envMap.SampleLevel(envSampler, uv, lod).rgb; + return envMap.SampleLevel(envSampler, uv, lod).rgb * getIntensity(); } /** Transform direction from local to world space. @@ -61,4 +70,11 @@ struct EnvMap // TODO: For identity transform we might want to skip this statically. return mul(dir, (float3x3)data.invTransform); } + + /** Get the intensity scaling factor (including tint). + */ + float3 getIntensity() + { + return data.intensity * data.tint; + } }; diff --git a/Source/Falcor/Scene/Lights/LightProbeIntegration.ps.slang b/Source/Falcor/Experimental/Scene/Lights/EnvMapIntegration.ps.slang similarity index 100% rename from Source/Falcor/Scene/Lights/LightProbeIntegration.ps.slang rename to Source/Falcor/Experimental/Scene/Lights/EnvMapIntegration.ps.slang diff --git a/Source/Falcor/Experimental/Scene/Lights/EnvMapLighting.cpp b/Source/Falcor/Experimental/Scene/Lights/EnvMapLighting.cpp new file mode 100644 index 0000000000..8310d58ee8 --- /dev/null +++ b/Source/Falcor/Experimental/Scene/Lights/EnvMapLighting.cpp @@ -0,0 +1,137 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#include "stdafx.h" +#include "EnvMapLighting.h" +#include "RenderGraph/BasePasses/FullScreenPass.h" +#include "Core/API/RenderContext.h" +#include "Core/API/Device.h" + +namespace Falcor +{ + namespace + { + const char* kShader = "Experimental/Scene/Lights/EnvMapIntegration.ps.slang"; + + Texture::SharedPtr executeSingleMip(RenderContext* pContext, const FullScreenPass::SharedPtr& pPass, const Texture::SharedPtr& pTexture, const Sampler::SharedPtr& pSampler, uint32_t size, ResourceFormat format, uint32_t sampleCount) + { + pPass["gInputTex"] = pTexture; + pPass["gSampler"] = pSampler; + pPass["DataCB"]["gSampleCount"] = sampleCount; + + // Output texture + Fbo::SharedPtr pFbo = Fbo::create2D(size, size, Fbo::Desc().setColorTarget(0, format)); + + // Execute + pPass->execute(pContext, pFbo); + return pFbo->getColorTexture(0); + } + + Texture::SharedPtr integrateDFG(RenderContext* pContext, uint32_t size, ResourceFormat format, uint32_t sampleCount) + { + auto pPass = FullScreenPass::create(std::string(kShader), Program::DefineList().add("_INTEGRATE_DFG")); + return executeSingleMip(pContext, pPass, nullptr, nullptr, size, format, sampleCount); + } + + Texture::SharedPtr integrateDiffuseLD(RenderContext* pContext, const Texture::SharedPtr& pTexture, const Sampler::SharedPtr& pSampler, uint32_t size, ResourceFormat format, uint32_t sampleCount) + { + auto pPass = FullScreenPass::create(std::string(kShader), Program::DefineList().add("_INTEGRATE_DIFFUSE_LD")); + return executeSingleMip(pContext, pPass, pTexture, pSampler, size, format, sampleCount); + } + + Texture::SharedPtr integrateSpecularLD(RenderContext* pContext, const Texture::SharedPtr& pTexture, const Sampler::SharedPtr& pSampler, uint32_t size, ResourceFormat format, uint32_t sampleCount) + { + auto pPass = FullScreenPass::create(std::string(kShader), Program::DefineList().add("_INTEGRATE_SPECULAR_LD")); + pPass["gInputTex"] = pTexture; + pPass["gSampler"] = pSampler; + pPass["DataCB"]["gSampleCount"] = sampleCount; + + Texture::SharedPtr pOutput = Texture::create2D(size, size, format, 1, Texture::kMaxPossible, nullptr, Resource::BindFlags::ShaderResource | Resource::BindFlags::RenderTarget); + + // Execute on each mip level + uint32_t mipCount = pOutput->getMipCount(); + for (uint32_t i = 0; i < mipCount; i++) + { + Fbo::SharedPtr pFbo = Fbo::create(); + pFbo->attachColorTarget(pOutput, 0, i); + + // Roughness to integrate for on current mip level + pPass["DataCB"]["gRoughness"] = float(i) / float(mipCount - 1); + pPass->execute(pContext, pFbo); + } + + return pOutput; + } + } + + EnvMapLighting::EnvMapLighting(RenderContext* pContext, const EnvMap::SharedPtr& pEnvMap, uint32_t diffSamples, uint32_t specSamples, uint32_t diffSize, uint32_t specSize, ResourceFormat preFilteredFormat) + : mpEnvMap(pEnvMap) + , mDiffSampleCount(diffSamples) + , mSpecSampleCount(specSamples) + { + mpDFGSampler = Sampler::create(Sampler::Desc().setFilterMode(Sampler::Filter::Point, Sampler::Filter::Point, Sampler::Filter::Point).setAddressingMode(Sampler::AddressMode::Clamp, Sampler::AddressMode::Clamp, Sampler::AddressMode::Clamp)); + mpDFGTexture = integrateDFG(pContext, 128, ResourceFormat::RGBA16Float, 128); + + auto pEnvTexture = pEnvMap->getEnvMap(); + auto pEnvSampler = pEnvMap->getEnvSampler(); + + mpSampler = Sampler::create(Sampler::Desc().setFilterMode(Sampler::Filter::Linear, Sampler::Filter::Linear, Sampler::Filter::Linear).setAddressingMode(Sampler::AddressMode::Wrap, Sampler::AddressMode::Clamp, Sampler::AddressMode::Clamp)); + mpDiffuseTexture = integrateDiffuseLD(pContext, pEnvTexture, pEnvSampler, diffSize, preFilteredFormat, diffSamples); + mpSpecularTexture = integrateSpecularLD(pContext, pEnvTexture, pEnvSampler, specSize, preFilteredFormat, specSamples); + } + + EnvMapLighting::SharedPtr EnvMapLighting::create(RenderContext* pContext, const EnvMap::SharedPtr& pEnvMap, uint32_t diffSampleCount, uint32_t specSampleCount, uint32_t diffSize, uint32_t specSize, ResourceFormat preFilteredFormat) + { + if (pEnvMap->getEnvMap()->getMipCount() == 1) + { + logWarning("Environment map texture sould have a valid mip chain."); + } + + return SharedPtr(new EnvMapLighting(pContext, pEnvMap, diffSampleCount, specSampleCount, diffSize, specSize, preFilteredFormat)); + } + + void EnvMapLighting::setShaderData(const ShaderVar& var) + { + if(!var.isValid()) return; + + var["dfgTexture"] = mpDFGTexture; + var["dfgSampler"] = mpDFGSampler; + + var["diffuseTexture"] = mpDiffuseTexture; + var["specularTexture"] = mpSpecularTexture; + var["sampler"] = mpSampler; + } + + uint64_t EnvMapLighting::getMemoryUsageInBytes() const + { + uint64_t m = 0; + m += mpDFGTexture->getTextureSizeInBytes(); + m += mpDiffuseTexture->getTextureSizeInBytes(); + m += mpSpecularTexture->getTextureSizeInBytes(); + return m; + } +} diff --git a/Source/Falcor/Experimental/Scene/Lights/EnvMapLighting.h b/Source/Falcor/Experimental/Scene/Lights/EnvMapLighting.h new file mode 100644 index 0000000000..1c2c3ca2fa --- /dev/null +++ b/Source/Falcor/Experimental/Scene/Lights/EnvMapLighting.h @@ -0,0 +1,87 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#pragma once +#include "EnvMap.h" + +namespace Falcor +{ + class RenderContext; + class Gui; + class ProgramVars; + class ParameterBlock; + + /** Helper for image based lighting using an environment map radiance probe. + */ + class dlldecl EnvMapLighting + { + public: + using SharedPtr = std::shared_ptr; + using SharedConstPtr = std::shared_ptr; + + static const uint32_t kDefaultDiffSamples = 4096; + static const uint32_t kDefaultSpecSamples = 1024; + static const uint32_t kDefaultDiffSize = 128; + static const uint32_t kDefaultSpecSize = 1024; + + /** Create a environment map lighting helper. + \param[in] pContext The current render context to be used for pre-integration. + \param[in] pEnvMap Environment map. + \param[in] diffSampleCount How many times to sample when generating diffuse texture. + \param[in] specSampleCount How many times to sample when generating specular texture. + \param[in] diffSize The width and height of the pre-filtered diffuse texture. We always create a square texture. + \param[in] specSize The width and height of the pre-filtered specular texture. We always create a square texture. + \param[in] preFilteredFormat The format of the pre-filtered texture + */ + static SharedPtr create(RenderContext* pContext, const EnvMap::SharedPtr& pEnvMap, uint32_t diffSampleCount = kDefaultDiffSamples, uint32_t specSampleCount = kDefaultSpecSamples, uint32_t diffSize = kDefaultDiffSize, uint32_t specSize = kDefaultSpecSize, ResourceFormat preFilteredFormat = ResourceFormat::RGBA16Float); + + /** Get the associated environment map. + */ + const EnvMap::SharedPtr& getEnvMap() const { return mpEnvMap; } + + /** Bind the environment map lighting helper into a shader var. + */ + void setShaderData(const ShaderVar& var); + + /** Get the total GPU memory usage in bytes. + */ + uint64_t getMemoryUsageInBytes() const; + + private: + EnvMapLighting(RenderContext* pContext, const EnvMap::SharedPtr& pEnvMap, uint32_t diffSamples, uint32_t specSamples, uint32_t diffSize, uint32_t specSize, ResourceFormat preFilteredFormat); + + EnvMap::SharedPtr mpEnvMap; + uint32_t mDiffSampleCount; + uint32_t mSpecSampleCount; + + Texture::SharedPtr mpDFGTexture; + Sampler::SharedPtr mpDFGSampler; + Texture::SharedPtr mpDiffuseTexture; + Texture::SharedPtr mpSpecularTexture; + Sampler::SharedPtr mpSampler; + }; +} diff --git a/Source/Falcor/Experimental/Scene/Lights/EnvMapLighting.slang b/Source/Falcor/Experimental/Scene/Lights/EnvMapLighting.slang new file mode 100644 index 0000000000..a2343be6c7 --- /dev/null +++ b/Source/Falcor/Experimental/Scene/Lights/EnvMapLighting.slang @@ -0,0 +1,96 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +import Scene.Scene; +import Scene.ShadingData; +import Utils.Helpers; +import Utils.Math.MathHelpers; + +struct EnvMapLighting +{ + Texture2D dfgTexture; ///< Texture containing shared pre-integrated (DFG) term + SamplerState dfgSampler; + + Texture2D diffuseTexture; ///< Texture containing pre-integrated diffuse (LD) term + Texture2D specularTexture; ///< Texture containing pre-integrated specular (LD) term + SamplerState sampler; + + float linearRoughnessToLod(float linearRoughness, float mipCount) + { + return sqrt(linearRoughness) * (mipCount - 1); + } + + float3 getDiffuseDominantDir(float3 N, float3 V, float ggxAlpha) + { + float a = 1.02341 * ggxAlpha - 1.51174; + float b = -0.511705 * ggxAlpha + 0.755868; + float factor = saturate((saturate(dot(N, V)) * a + b) * ggxAlpha); + return normalize(lerp(N, V, factor)); + } + + float3 getSpecularDominantDir(float3 N, float3 R, float ggxAlpha) + { + float smoothness = 1 - ggxAlpha; + float factor = smoothness * (sqrt(smoothness) + ggxAlpha); + return normalize(lerp(N, R, factor)); + } + + float3 evalDiffuse(ShadingData sd) + { + float3 dominantDir = getDiffuseDominantDir(sd.N, sd.V, sd.ggxAlpha); + float2 uv = world_to_latlong_map(gScene.envMap.toLocal(dominantDir)); + + float width, height, mipCount; + diffuseTexture.GetDimensions(0, width, height, mipCount); + + float3 diffuseLighting = diffuseTexture.SampleLevel(sampler, uv, 0).rgb; + float preintegratedDisneyBRDF = dfgTexture.SampleLevel(dfgSampler, float2(sd.NdotV, sd.ggxAlpha), 0).z; + + return diffuseLighting * preintegratedDisneyBRDF * sd.diffuse.rgb * gScene.envMap.getIntensity(); + } + + float3 evalSpecular(ShadingData sd, float3 L) + { + float dfgWidth, dfgHeight; + dfgTexture.GetDimensions(dfgWidth, dfgHeight); + + float width, height, mipCount; + specularTexture.GetDimensions(0, width, height, mipCount); + + float3 dominantDir = getSpecularDominantDir(sd.N, L, sd.ggxAlpha); + float2 uv = world_to_latlong_map(gScene.envMap.toLocal(dominantDir)); + + float mipLevel = linearRoughnessToLod(sd.ggxAlpha, mipCount); + + float3 ld = specularTexture.SampleLevel(sampler, uv, mipLevel).rgb; + + float2 dfg = dfgTexture.SampleLevel(dfgSampler, float2(sd.NdotV, sd.ggxAlpha), 0).xy; + + // ld * (f0 * Gv * (1 - Fc)) + (f90 * Gv * Fc) + return ld * (sd.specular * dfg.x + dfg.y) * gScene.envMap.getIntensity(); + } +} diff --git a/Source/Falcor/Experimental/Scene/Lights/EnvMapSampler.slang b/Source/Falcor/Experimental/Scene/Lights/EnvMapSampler.slang index fb3f9af50e..9378a88fd6 100644 --- a/Source/Falcor/Experimental/Scene/Lights/EnvMapSampler.slang +++ b/Source/Falcor/Experimental/Scene/Lights/EnvMapSampler.slang @@ -59,7 +59,7 @@ struct EnvMapSampler /** Importance sampling of the environment map. */ - bool sample(const float2 rnd, inout EnvMapSample result) + bool sample(const float2 rnd, out EnvMapSample result) { float2 p = rnd; // Random sample in [0,1)^2. uint2 pos = 0; // Top-left texel pos of current 2x2 region. diff --git a/Source/Falcor/Experimental/Scene/Lights/LightBVH.cpp b/Source/Falcor/Experimental/Scene/Lights/LightBVH.cpp index 51002c2243..50e8535839 100644 --- a/Source/Falcor/Experimental/Scene/Lights/LightBVH.cpp +++ b/Source/Falcor/Experimental/Scene/Lights/LightBVH.cpp @@ -99,10 +99,9 @@ namespace Falcor " Internal node count: " + std::to_string(stats.internalNodeCount) + "\n" + " Leaf node count: " + std::to_string(stats.leafNodeCount) + "\n" + " Triangle count: " + std::to_string(stats.triangleCount) + "\n"; - widget.text(statsStr.c_str()); + widget.text(statsStr); - Gui::Group nodeGroup(widget.gui(), "Node count per level"); - if (nodeGroup.open()) + if (auto nodeGroup = widget.group("Node count per level")) { std::string countStr; for (uint32_t level = 0; level < stats.nodeCountPerLevel.size(); ++level) @@ -110,13 +109,10 @@ namespace Falcor countStr += " Node count at level " + std::to_string(level) + ": " + std::to_string(stats.nodeCountPerLevel[level]) + "\n"; } if (!countStr.empty()) countStr.pop_back(); - nodeGroup.text(countStr.c_str()); - - nodeGroup.release(); + nodeGroup.text(countStr); } - Gui::Group leafGroup(widget.gui(), "Leaf node count histogram for triangle counts"); - if (leafGroup.open()) + if (auto leafGroup = widget.group("Leaf node count histogram for triangle counts")) { std::string countStr; for (uint32_t triangleCount = 0; triangleCount < stats.leafCountPerTriangleCount.size(); ++triangleCount) @@ -124,9 +120,7 @@ namespace Falcor countStr += " Leaf nodes with " + std::to_string(triangleCount) + " triangles: " + std::to_string(stats.leafCountPerTriangleCount[triangleCount]) + "\n"; } if (!countStr.empty()) countStr.pop_back(); - leafGroup.text(countStr.c_str()); - - leafGroup.release(); + leafGroup.text(countStr); } } diff --git a/Source/Falcor/Experimental/Scene/Lights/LightBVH.h b/Source/Falcor/Experimental/Scene/Lights/LightBVH.h index 9ecc0a7e8f..46ce5765e4 100644 --- a/Source/Falcor/Experimental/Scene/Lights/LightBVH.h +++ b/Source/Falcor/Experimental/Scene/Lights/LightBVH.h @@ -28,7 +28,7 @@ #pragma once #include "LightCollection.h" #include "LightBVHTypes.slang" -#include "Utils/Math/BBox.h" +#include "Utils/Math/AABB.h" #include "Utils/Math/Vector.h" #include "Utils/UI/Gui.h" #include diff --git a/Source/Falcor/Experimental/Scene/Lights/LightBVHBuilder.cpp b/Source/Falcor/Experimental/Scene/Lights/LightBVHBuilder.cpp index 552874f541..dbc8efc6e7 100644 --- a/Source/Falcor/Experimental/Scene/Lights/LightBVHBuilder.cpp +++ b/Source/Falcor/Experimental/Scene/Lights/LightBVHBuilder.cpp @@ -201,6 +201,20 @@ namespace return dir; } + /** Returns the volume of a bounding box. + \param[in] epsilon Replace dimensions that are zero by this value. + \return the volume of the bounding box if it is valid, -inf otherwise. + */ + float aabbVolume(const AABB& bb, float epsilon) + { + if (bb.valid() == false) + { + return -std::numeric_limits::infinity(); + } + const float3 dims = glm::max(float3(epsilon), bb.extent()); + return dims.x * dims.y * dims.z; + } + const Gui::DropdownList kSplitHeuristicList = { { (uint32_t)LightBVHBuilder::SplitHeuristic::Equal, "Equal" }, @@ -315,8 +329,7 @@ namespace Falcor optionsChanged |= widget.var("Max triangle count per leaf", options.maxTriangleCountPerLeaf, 1u, kMaxLeafTriangleCount); optionsChanged |= widget.dropdown("Split heuristic", kSplitHeuristicList, (uint32_t&)options.splitHeuristicSelection); - Gui::Group splitGroup(widget, "Split Options", true); - if (splitGroup.open()) + if (auto splitGroup = widget.group("Split Options", true)) { optionsChanged |= splitGroup.var("Bin count", options.binCount); optionsChanged |= splitGroup.checkbox("Create leaves ASAP", options.createLeavesASAP); @@ -338,8 +351,6 @@ namespace Falcor optionsChanged |= splitGroup.checkbox("Use pre-integration", options.usePreintegration); optionsChanged |= splitGroup.checkbox("Use lighting cones", options.useLightingCones); } - - splitGroup.release(); } return optionsChanged; @@ -355,7 +366,7 @@ namespace Falcor // Compute the AABB and total flux of the node. float nodeFlux = 0.f; - BBox nodeBounds; + AABB nodeBounds; for (uint32_t dataIndex = triangleRange.begin; dataIndex < triangleRange.end; ++dataIndex) { nodeBounds |= data.trianglesData[dataIndex].bounds; @@ -374,7 +385,7 @@ namespace Falcor assert(triangleRange.begin < splitResult.triangleIndex && splitResult.triangleIndex < triangleRange.end); // Sort the centroids and update the lists accordingly. - auto comp = [dim = splitResult.axis](const TriangleSortData& d1, const TriangleSortData& d2) { return d1.bounds.centroid()[dim] < d2.bounds.centroid()[dim]; }; + auto comp = [dim = splitResult.axis](const TriangleSortData& d1, const TriangleSortData& d2) { return d1.bounds.center()[dim] < d2.bounds.center()[dim]; }; std::nth_element(std::begin(data.trianglesData) + triangleRange.begin, std::begin(data.trianglesData) + splitResult.triangleIndex, std::begin(data.trianglesData) + triangleRange.end, comp); // Allocate internal node. @@ -497,10 +508,10 @@ namespace Falcor return coneDirection; } - LightBVHBuilder::SplitResult LightBVHBuilder::computeSplitWithEqual(const BuildingData& /*data*/, const Range& triangleRange, const BBox& nodeBounds, const Options& /*parameters*/) + LightBVHBuilder::SplitResult LightBVHBuilder::computeSplitWithEqual(const BuildingData& /*data*/, const Range& triangleRange, const AABB& nodeBounds, const Options& /*parameters*/) { // Find the largest dimension. - float3 dimensions = nodeBounds.dimensions(); + float3 dimensions = nodeBounds.extent(); uint32_t dimension = dimensions[2] >= dimensions[0] && dimensions[2] >= dimensions[1] ? 2 : (dimensions[1] >= dimensions[0] ? 1 : 0); @@ -516,22 +527,22 @@ namespace Falcor If the node is empty (invalid bounds), the cost evaluates to zero. See Eqn 15 in Moreau and Clarberg, "Importance Sampling of Many Lights on the GPU", Ray Tracing Gems, Ch. 18, 2019. */ - static float evalSAH(const BBox& bounds, const uint32_t triangleCount, const LightBVHBuilder::Options& parameters) + static float evalSAH(const AABB& bounds, const uint32_t triangleCount, const LightBVHBuilder::Options& parameters) { - float aabbCost = bounds.valid() ? (parameters.useVolumeOverSA ? bounds.volume(parameters.volumeEpsilon) : bounds.surfaceArea()) : 0.f; + float aabbCost = bounds.valid() ? (parameters.useVolumeOverSA ? aabbVolume(bounds, parameters.volumeEpsilon) : bounds.area()) : 0.f; float cost = aabbCost * (float)triangleCount; assert(cost >= 0.f && !std::isnan(cost) && !std::isinf(cost)); return cost; } - LightBVHBuilder::SplitResult LightBVHBuilder::computeSplitWithBinnedSAH(const BuildingData& data, const Range& triangleRange, const BBox& nodeBounds, const Options& parameters) + LightBVHBuilder::SplitResult LightBVHBuilder::computeSplitWithBinnedSAH(const BuildingData& data, const Range& triangleRange, const AABB& nodeBounds, const Options& parameters) { std::pair overallBestSplit = std::make_pair(std::numeric_limits::infinity(), SplitResult()); assert(!overallBestSplit.second.isValid()); struct Bin { - BBox bounds = BBox(); + AABB bounds; uint32_t triangleCount = 0; Bin() = default; @@ -560,7 +571,7 @@ namespace Falcor float bmin = nodeBounds.minPoint[dimension], bmax = nodeBounds.maxPoint[dimension]; assert(bmin < bmax); float scale = (float)parameters.binCount / (bmax - bmin); - float p = td.bounds.centroid()[dimension]; + float p = td.bounds.center()[dimension]; assert(bmin <= p && p <= bmax); return std::min((uint32_t)((p - bmin) * scale), parameters.binCount - 1); }; @@ -618,7 +629,7 @@ namespace Falcor if (parameters.splitAlongLargest) { // Find the largest dimension. - float3 dimensions = nodeBounds.dimensions(); + float3 dimensions = nodeBounds.extent(); uint32_t largestDimension = dimensions[2] >= dimensions[0] && dimensions[2] >= dimensions[1] ? 2 : (dimensions[1] >= dimensions[0] && dimensions[1] >= dimensions[2] ? 1 : 0); @@ -668,10 +679,10 @@ namespace Falcor If the node is empty (invalid bounds), the cost evaluates to zero. See Eqn 16 in Moreau and Clarberg, "Importance Sampling of Many Lights on the GPU", Ray Tracing Gems, Ch. 18, 2019. */ - static float evalSAOH(const BBox& bounds, const float flux, const float cosTheta, const LightBVHBuilder::Options& parameters) + static float evalSAOH(const AABB& bounds, const float flux, const float cosTheta, const LightBVHBuilder::Options& parameters) { float fluxCost = parameters.usePreintegration ? flux : 1.0f; - float aabbCost = bounds.valid() ? (parameters.useVolumeOverSA ? bounds.volume(parameters.volumeEpsilon) : bounds.surfaceArea()) : 0.f; + float aabbCost = bounds.valid() ? (parameters.useVolumeOverSA ? aabbVolume(bounds, parameters.volumeEpsilon) : bounds.area()) : 0.f; float theta = cosTheta != kInvalidCosConeAngle ? safeACos(cosTheta) : glm::pi(); float orientationCost = parameters.useLightingCones ? computeOrientationCost(theta) : 1.0f; float cost = fluxCost * aabbCost * orientationCost; @@ -679,19 +690,19 @@ namespace Falcor return cost; } - LightBVHBuilder::SplitResult LightBVHBuilder::computeSplitWithBinnedSAOH(const BuildingData& data, const Range& triangleRange, const BBox& nodeBounds, const Options& parameters) + LightBVHBuilder::SplitResult LightBVHBuilder::computeSplitWithBinnedSAOH(const BuildingData& data, const Range& triangleRange, const AABB& nodeBounds, const Options& parameters) { std::pair overallBestSplit = std::make_pair(std::numeric_limits::infinity(), SplitResult()); assert(!overallBestSplit.second.isValid()); // Find the largest dimension. - float3 dimensions = nodeBounds.dimensions(); + float3 dimensions = nodeBounds.extent(); uint32_t largestDimension = dimensions[2] >= dimensions[0] && dimensions[2] >= dimensions[1] ? 2 : (dimensions[1] >= dimensions[0] && dimensions[1] >= dimensions[2] ? 1 : 0); struct Bin { - BBox bounds = BBox(); + AABB bounds; uint32_t triangleCount = 0; float flux = 0.0f; float3 coneDirection = float3(0.0f); @@ -730,7 +741,7 @@ namespace Falcor float w = bmax - bmin; assert(w >= 0.f); // The node bounds can be zero if all primitives are axis-aligned and coplanar float scale = w > FLT_MIN ? (float)parameters.binCount / w : 0.f; - float p = td.bounds.centroid()[dimension]; + float p = td.bounds.center()[dimension]; assert(bmin <= p && p <= bmax); return std::min((uint32_t)((p - bmin) * scale), parameters.binCount - 1); }; diff --git a/Source/Falcor/Experimental/Scene/Lights/LightBVHBuilder.h b/Source/Falcor/Experimental/Scene/Lights/LightBVHBuilder.h index 57e71155be..b0da189a5c 100644 --- a/Source/Falcor/Experimental/Scene/Lights/LightBVHBuilder.h +++ b/Source/Falcor/Experimental/Scene/Lights/LightBVHBuilder.h @@ -27,7 +27,7 @@ **************************************************************************/ #pragma once #include "LightBVH.h" -#include "Utils/Math/BBox.h" +#include "Utils/Math/AABB.h" #include "Utils/Math/Vector.h" #include "Utils/UI/Gui.h" #include @@ -112,7 +112,7 @@ namespace Falcor struct TriangleSortData { - BBox bounds; ///< World-space bounding box for the light source(s). + AABB bounds; ///< World-space bounding box for the light source(s). float3 center = {}; ///< Center point. float3 coneDirection = {}; ///< Light emission normal direction. float cosConeAngle = 1.f; ///< Cosine normal bounding cone (half) angle. @@ -137,7 +137,7 @@ namespace Falcor \param[in] nodeBounds Bounds for the node to be splitted. \param[in] parameters Various parameters defining how the building should occur. */ - using SplitHeuristicFunction = std::function; + using SplitHeuristicFunction = std::function; LightBVHBuilder(const Options& options); @@ -172,9 +172,9 @@ namespace Falcor static float3 computeLightingCone(const Range& triangleRange, const BuildingData& data, float& cosTheta); // See the documentation of SplitHeuristicFunction. - static SplitResult computeSplitWithEqual(const BuildingData& /*data*/, const Range& triangleRange, const BBox& nodeBounds, const Options& /*parameters*/); - static SplitResult computeSplitWithBinnedSAH(const BuildingData& data, const Range& triangleRange, const BBox& nodeBounds, const Options& parameters); - static SplitResult computeSplitWithBinnedSAOH(const BuildingData& data, const Range& triangleRange, const BBox& nodeBounds, const Options& parameters); + static SplitResult computeSplitWithEqual(const BuildingData& /*data*/, const Range& triangleRange, const AABB& nodeBounds, const Options& /*parameters*/); + static SplitResult computeSplitWithBinnedSAH(const BuildingData& data, const Range& triangleRange, const AABB& nodeBounds, const Options& parameters); + static SplitResult computeSplitWithBinnedSAOH(const BuildingData& data, const Range& triangleRange, const AABB& nodeBounds, const Options& parameters); static SplitHeuristicFunction getSplitFunction(SplitHeuristic heuristic); diff --git a/Source/Falcor/Experimental/Scene/Lights/LightBVHSampler.cpp b/Source/Falcor/Experimental/Scene/Lights/LightBVHSampler.cpp index e33a58ddd6..e3d71b1cef 100644 --- a/Source/Falcor/Experimental/Scene/Lights/LightBVHSampler.cpp +++ b/Source/Falcor/Experimental/Scene/Lights/LightBVHSampler.cpp @@ -79,20 +79,20 @@ namespace Falcor return samplerChanged; } - bool LightBVHSampler::prepareProgram(Program* pProgram) const + Program::DefineList LightBVHSampler::getDefines() const { // Call the base class first. - bool varsChanged = EmissiveLightSampler::prepareProgram(pProgram); + auto defines = EmissiveLightSampler::getDefines(); // Add our defines. None of these change the program vars. - pProgram->addDefine("_USE_BOUNDING_CONE", mOptions.useBoundingCone ? "1" : "0"); - pProgram->addDefine("_USE_LIGHTING_CONE", mOptions.useLightingCone ? "1" : "0"); - pProgram->addDefine("_DISABLE_NODE_FLUX", mOptions.disableNodeFlux ? "1" : "0"); - pProgram->addDefine("_USE_UNIFORM_TRIANGLE_SAMPLING", mOptions.useUniformTriangleSampling ? "1" : "0"); - pProgram->addDefine("_ACTUAL_MAX_TRIANGLES_PER_NODE", std::to_string(mOptions.buildOptions.maxTriangleCountPerLeaf)); - pProgram->addDefine("_SOLID_ANGLE_BOUND_METHOD", std::to_string((uint32_t)mOptions.solidAngleBoundMethod)); - - return varsChanged; + defines.add("_USE_BOUNDING_CONE", mOptions.useBoundingCone ? "1" : "0"); + defines.add("_USE_LIGHTING_CONE", mOptions.useLightingCone ? "1" : "0"); + defines.add("_DISABLE_NODE_FLUX", mOptions.disableNodeFlux ? "1" : "0"); + defines.add("_USE_UNIFORM_TRIANGLE_SAMPLING", mOptions.useUniformTriangleSampling ? "1" : "0"); + defines.add("_ACTUAL_MAX_TRIANGLES_PER_NODE", std::to_string(mOptions.buildOptions.maxTriangleCountPerLeaf)); + defines.add("_SOLID_ANGLE_BOUND_METHOD", std::to_string((uint32_t)mOptions.solidAngleBoundMethod)); + + return defines; } bool LightBVHSampler::setShaderData(const ShaderVar& var) const diff --git a/Source/Falcor/Experimental/Scene/Lights/LightBVHSampler.h b/Source/Falcor/Experimental/Scene/Lights/LightBVHSampler.h index 6146bcc01f..b6a085c1a9 100644 --- a/Source/Falcor/Experimental/Scene/Lights/LightBVHSampler.h +++ b/Source/Falcor/Experimental/Scene/Lights/LightBVHSampler.h @@ -26,7 +26,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **************************************************************************/ #pragma once -#include "Utils/Math/BBox.h" +#include "Utils/Math/AABB.h" #include "EmissiveLightSampler.h" #include "LightBVH.h" #include "LightBVHBuilder.h" @@ -84,13 +84,10 @@ namespace Falcor */ virtual bool update(RenderContext* pRenderContext) override; - /** Add compile-time specialization to program to use this light sampler. - This function must be called every frame before the sampler is bound. - Note that ProgramVars may need to be re-created after this call, check the return value. - \param[in] pProgram The Program to add compile-time specialization to. - \return True if the ProgramVars needs to be re-created. + /** Return a list of shader defines to use this light sampler. + * \return Returns a list of shader defines. */ - virtual bool prepareProgram(Program* pProgram) const override; + virtual Program::DefineList getDefines() const override; /** Bind the light sampler data to a given shader variable. \param[in] var Shader variable. diff --git a/Source/Falcor/Experimental/Scene/Lights/LightBVHSampler.slang b/Source/Falcor/Experimental/Scene/Lights/LightBVHSampler.slang index c8dda41824..73f41e650d 100644 --- a/Source/Falcor/Experimental/Scene/Lights/LightBVHSampler.slang +++ b/Source/Falcor/Experimental/Scene/Lights/LightBVHSampler.slang @@ -30,7 +30,7 @@ import Scene.Scene; import Scene.ShadingData; import Utils.Math.MathHelpers; -import Utils.Sampling.SampleGenerator; +import Utils.Sampling.SampleGeneratorInterface; import Experimental.Scene.Lights.LightBVH; import Experimental.Scene.Lights.LightBVHSamplerSharedDefinitions; import Experimental.Scene.Lights.LightCollection; @@ -56,6 +56,11 @@ import Experimental.Scene.Lights.EmissiveLightSamplerInterface; struct LightBVHSampler : IEmissiveLightSampler { // Compile-time constants + static const bool kUseBoundingCone = _USE_BOUNDING_CONE; + static const bool kUseLightingCone = _USE_LIGHTING_CONE; + static const bool kDisableNodeFlux = _DISABLE_NODE_FLUX; + static const bool kUseUniformTriangleSampling = _USE_UNIFORM_TRIANGLE_SAMPLING; + static const uint kActualMaxTrianglesPerNode = _ACTUAL_MAX_TRIANGLES_PER_NODE; static const SolidAngleBoundMethod kSolidAngleBoundMethod = (SolidAngleBoundMethod)(_SOLID_ANGLE_BOUND_METHOD); LightBVH _lightBVH; ///< The BVH around the light sources. @@ -63,11 +68,12 @@ struct LightBVHSampler : IEmissiveLightSampler /** Draw a single light sample. \param[in] posW Shading point in world space. \param[in] normalW Normal at the shading point in world space. + \param[in] onSurface True if only upper hemisphere should be considered. \param[in,out] sg Sample generator. \param[out] ls Light sample. Only valid if true is returned. \return True if a sample was generated, false otherwise. */ - bool sampleLight(const float3 posW, const float3 normalW, inout SampleGenerator sg, out TriangleLightSample ls) + bool sampleLight(const float3 posW, const float3 normalW, const bool onSurface, inout S sg, out TriangleLightSample ls) { if (gScene.lightCollection.isEmpty()) return false; @@ -75,7 +81,7 @@ struct LightBVHSampler : IEmissiveLightSampler uint triangleIndex; float trianglePdf; float uLight = sampleNext1D(sg); - if (!sampleLightViaBVH(posW, normalW, uLight, trianglePdf, triangleIndex)) return false; + if (!sampleLightViaBVH(posW, normalW, onSurface, uLight, trianglePdf, triangleIndex)) return false; // Sample a point on the triangle uniformly. float2 u = sampleNext2D(sg); @@ -86,15 +92,16 @@ struct LightBVHSampler : IEmissiveLightSampler return true; } - /** Evaluate the PDF at a shading point given a hit point on an emissive triangle. + /** Evaluate the PDF associated with selecting an emissive triangle. \param[in] posW Shading point in world space. \param[in] normalW Normal at the shading point in world space. - \param[in] hit Triangle hit data. - \return Probability density with respect to solid angle at the shading point. + \param[in] onSurface True if only upper hemisphere should be considered. + \param[in] triangleIndex Index of selected triangle. + \return Probability of drawing the input triangle */ - float evalPdf(float3 posW, float3 normalW, const TriangleHit hit) + float evalTriangleSelectionPdf(const float3 posW, const float3 normalW, const bool onSurface, const uint triangleIndex) { - if (hit.triangleIndex == LightCollection::kInvalidIndex) return 0; + if (triangleIndex == LightCollection::kInvalidIndex) return 0.f; float traversalPdf = 1.0f; float triangleSelectionPdf = 1.0f; @@ -103,21 +110,37 @@ struct LightBVHSampler : IEmissiveLightSampler // Load the triangle bitmask as 2x32 bits instead of uint64_t due to driver bug. // TODO: Change buffer to uint64_t format and remove this workaround when http://nvbugs/2817745 is fixed. //uint64_t bitmask = _lightBVH.triangleBitmasks[hit.triangleIndex]; - uint2 tmp = _lightBVH.triangleBitmasks[hit.triangleIndex]; + uint2 tmp = _lightBVH.triangleBitmasks[triangleIndex]; uint64_t bitmask = ((uint64_t)tmp.y << 32) | tmp.x; uint leafNodeIndex; - traversalPdf = evalBVHTraversalPdf(posW, normalW, bitmask, leafNodeIndex); + traversalPdf = evalBVHTraversalPdf(posW, normalW, onSurface, bitmask, leafNodeIndex); if (traversalPdf == 0.0f) return 0.0f; - triangleSelectionPdf = evalNodeSamplingPdf(posW, normalW, leafNodeIndex, hit.triangleIndex); + triangleSelectionPdf = evalNodeSamplingPdf(posW, normalW, onSurface, leafNodeIndex, triangleIndex); if (triangleSelectionPdf == 0.0f) return 0.0f; + return traversalPdf * triangleSelectionPdf; + } + + /** Evaluate the PDF at a shading point given a hit point on an emissive triangle. + \param[in] posW Shading point in world space. + \param[in] normalW Normal at the shading point in world space. + \param[in] onSurface True if only upper hemisphere should be considered. + \param[in] hit Triangle hit data. + \return Probability density with respect to solid angle at the shading point. + */ + float evalPdf(const float3 posW, const float3 normalW, const bool onSurface, const TriangleHit hit) + { + if (hit.triangleIndex == LightCollection::kInvalidIndex) return 0.f; + + float triangleSelectionPdf = evalTriangleSelectionPdf(posW, normalW, onSurface, hit.triangleIndex); + // Compute triangle sampling probability with respect to solid angle from the shading point. float trianglePdf = evalTrianglePdf(posW, hit); // The final probability density is the product of the sampling probabilities. - return traversalPdf * triangleSelectionPdf * trianglePdf; + return triangleSelectionPdf * trianglePdf; } @@ -186,60 +209,57 @@ struct LightBVHSampler : IEmissiveLightSampler /** Computes node importance from a given shading point. \param[in] posW Shading point in world space. \param[in] normalW Normal at the shading point in world space. + \param[in] onSurface True if only upper hemisphere should be considered. \param[in] nodeIndex Node index in BVH. \return Relative importance of this node. */ - float computeImportance(const float3 posW, const float3 normalW, const uint nodeIndex) + float computeImportance(const float3 posW, const float3 normalW, const bool onSurface, const uint nodeIndex) { const SharedNodeAttributes nodeAttribs = _lightBVH.getNodeAttributes(nodeIndex); - #if _DISABLE_NODE_FLUX > 0 float flux = 1.f; - #else - float flux = nodeAttribs.flux; - #endif + if (!kDisableNodeFlux) flux = nodeAttribs.flux; // TODO: Optimize by returning squared distance. float distance = length(nodeAttribs.origin - posW); float NdotL = 1.f; - #if _USE_BOUNDING_CONE > 0 || _USE_LIGHTING_CONE > 0 - float cosThetaBoundingCone = 0.0f; - NdotL = boundCosineTerm(posW, normalW, nodeAttribs.origin, nodeAttribs.extent, cosThetaBoundingCone); - #endif - - #if !(_USE_BOUNDING_CONE > 0) - NdotL = 1.0f; - #endif - - float orientationWeight = 1.0f; - #if _USE_LIGHTING_CONE > 0 - // Note: coneAngle is the angle of the bounding cone for the node's dominant light directions. - // It is _not_ the angle of the bounding cone within which light is emitted. - // The current assumption is that all emitters are diffuse, so the actual light cone angle is PI/2 larger than coneAngle. - // Conty Estevez and Kulla's 2018 paper uses a second cone angle (theta_e) to bound the emitted light. - // We might want to add that to support non-diffuse emitters or switch to another representation altogether. - float cosConeAngle = nodeAttribs.cosConeAngle; - float3 dirToAabb = (nodeAttribs.origin - posW) / distance; // TODO: dirToAabb won't be normalized for very short distances, as the computation of distance has a clamp. - if (cosConeAngle != kInvalidCosConeAngle && cosConeAngle > 0.f) // theta_o + theta_e < pi. (Note: assumes theta_e = pi/2!) + float cosThetaBoundingCone = 0.f; + if (kUseLightingCone || (kUseBoundingCone && onSurface)) { - float sinConeAngle = sqrt(max(0.f, 1.f - cosConeAngle * cosConeAngle)); + NdotL = boundCosineTerm(posW, normalW, nodeAttribs.origin, nodeAttribs.extent, cosThetaBoundingCone); + if (!(kUseBoundingCone && onSurface)) NdotL = 1.f; // Do not use NdotL bound in volumes or if disabled + } - float cosTheta = dot(nodeAttribs.coneDirection, -dirToAabb); // theta = Angle between dominant light dir - float sinTheta = sqrt(max(0.f, 1.f - cosTheta * cosTheta)); + float orientationWeight = 1.f; + if (kUseLightingCone) + { + // Note: coneAngle is the angle of the bounding cone for the node's dominant light directions. + // It is _not_ the angle of the bounding cone within which light is emitted. + // The current assumption is that all emitters are diffuse, so the actual light cone angle is PI/2 larger than coneAngle. + // Conty Estevez and Kulla's 2018 paper uses a second cone angle (theta_e) to bound the emitted light. + // We might want to add that to support non-diffuse emitters or switch to another representation altogether. + float cosConeAngle = nodeAttribs.cosConeAngle; + float3 dirToAabb = (nodeAttribs.origin - posW) / distance; // TODO: dirToAabb won't be normalized for very short distances, as the computation of distance has a clamp. + if (cosConeAngle != kInvalidCosConeAngle && cosConeAngle > 0.f) // theta_o + theta_e < pi. (Note: assumes theta_e = pi/2!) + { + float sinConeAngle = sqrt(max(0.f, 1.f - cosConeAngle * cosConeAngle)); - float sinThetaBoundingCone = sqrt(max(0.f, 1 - cosThetaBoundingCone * cosThetaBoundingCone)); + float cosTheta = dot(nodeAttribs.coneDirection, -dirToAabb); // theta = Angle between dominant light dir + float sinTheta = sqrt(max(0.f, 1.f - cosTheta * cosTheta)); - // thetaPrime = max(0.0f, theta - coneAngle - thetaBoundingCone); - // First compute the sine and cosine of max(0, theta - coneAngle). - float cosTheta0 = cosSubClamped(sinTheta, cosTheta, sinConeAngle, cosConeAngle); - float sinTheta0 = sinSubClamped(sinTheta, cosTheta, sinConeAngle, cosConeAngle); - // Now we can find the cosine of max(0, (theta - coneAngle) - thetaBoundingCone): - float cosThetaPrime = cosSubClamped(sinTheta0, cosTheta0, sinThetaBoundingCone, cosThetaBoundingCone); + float sinThetaBoundingCone = sqrt(max(0.f, 1 - cosThetaBoundingCone * cosThetaBoundingCone)); - orientationWeight = max(0.f, cosThetaPrime); + // thetaPrime = max(0.0f, theta - coneAngle - thetaBoundingCone); + // First compute the sine and cosine of max(0, theta - coneAngle). + float cosTheta0 = cosSubClamped(sinTheta, cosTheta, sinConeAngle, cosConeAngle); + float sinTheta0 = sinSubClamped(sinTheta, cosTheta, sinConeAngle, cosConeAngle); + // Now we can find the cosine of max(0, (theta - coneAngle) - thetaBoundingCone): + float cosThetaPrime = cosSubClamped(sinTheta0, cosTheta0, sinThetaBoundingCone, cosThetaBoundingCone); + + orientationWeight = max(0.f, cosThetaPrime); + } } - #endif // _USE_LIGHTING_CONE // We clamp the distance to the AABB by half its radius, as // "[t]he center of the cluster is not representative of the emitter positions over short distances." -- Conty Estévez and Kulla, Importance Sampling of Many Lights with Adaptive Tree Splitting. @@ -252,12 +272,13 @@ struct LightBVHSampler : IEmissiveLightSampler /** Traverses the light BVH to select a leaf node (range of lights) to sample. \param[in] posW Shading point in world space. \param[in] normalW Normal at the shading point in world space. + \param[in] onSurface True if only upper hemisphere should be considered. \param[in,out] u Uniform random number. Upon return, u is still uniform and can be used for sampling among the triangles in the leaf node. \param[out] pdf Probabiliy of the sampled leaf node, only valid if true is returned. \param[out] nodeIndex The index of the sampled BVH leaf node, only valid if true is returned. \return True if a leaf node was sampled, false otherwise. */ - bool traverseTree(const float3 posW, const float3 normalW, inout float u, out float pdf, out uint nodeIndex) + bool traverseTree(const float3 posW, const float3 normalW, const bool onSurface, inout float u, out float pdf, out uint nodeIndex) { pdf = 1.0f; nodeIndex = 0; @@ -268,8 +289,8 @@ struct LightBVHSampler : IEmissiveLightSampler uint leftNodeIndex = nodeIndex + 1; uint rightNodeIndex = _lightBVH.getInternalNode(nodeIndex).rightChildIdx; - float leftNodeImportance = computeImportance(posW, normalW, leftNodeIndex); - float rightNodeImportance = computeImportance(posW, normalW, rightNodeIndex); + float leftNodeImportance = computeImportance(posW, normalW, onSurface, leftNodeIndex); + float rightNodeImportance = computeImportance(posW, normalW, onSurface, rightNodeIndex); float totalImportance = leftNodeImportance + rightNodeImportance; @@ -301,10 +322,11 @@ struct LightBVHSampler : IEmissiveLightSampler /** Compute the importance for the given triangle as seen from a given shading point. \param[in] posW Shading point in world space. \param[in] normalW Normal at the shading point in world space. + \param[in] onSurface True if only upper hemisphere should be considered. \param[in] triangleIndex The index of the triangle to compute the importance for. \return True if we found any triangle(s) to sample, false otherwise. */ - float computeTriangleImportance(const float3 posW, const float3 normalW, const uint triangleIndex) + float computeTriangleImportance(const float3 posW, const float3 normalW, const bool onSurface, const uint triangleIndex) { const EmissiveTriangle tri = gScene.lightCollection.getTriangle(triangleIndex); @@ -315,89 +337,100 @@ struct LightBVHSampler : IEmissiveLightSampler // TODO: Use an approximation for this. The exact formulation is math heavy. float distSqr = max(1e-5f, computeSquaredMinDistanceToTriangle(tri.posW, posW)); - // Compute conservative bounds for dot(N,L) at the shading point. - float NdotL = 0.0f; - [unroll] - // TODO: Not conservative! - // NdotL > 0 if triangle is visible so it's safe to use, but it may be an under-estimation. - // Consider the case where the closest point is inside the triangle and all vertices are far away. - // Their dot products will be small, but the dot product to a sample inside can be larger. - for (uint i = 0; i < 3; ++i) + if (onSurface) { - NdotL = max(NdotL, dot(normalW, normalize(tri.posW[i] - posW))); + // Compute conservative bounds for dot(N,L) at the shading point. + float NdotL = 0.0f; + [unroll] + // TODO: Not conservative! + // NdotL > 0 if triangle is visible so it's safe to use, but it may be an under-estimation. + // Consider the case where the closest point is inside the triangle and all vertices are far away. + // Their dot products will be small, but the dot product to a sample inside can be larger. + for (uint i = 0; i < 3; ++i) + { + NdotL = max(NdotL, dot(normalW, normalize(tri.posW[i] - posW))); + } + NdotL = saturate(NdotL); + return NdotL / distSqr; + } + else + { + return 1.f / distSqr; } - NdotL = saturate(NdotL); - - return NdotL / distSqr; } /** Pick a triangle in a leaf node to sample. \param[in] posW Shading point in world space. \param[in] normalW Normal at the shading point in world space. + \param[in] onSurface True if only upper hemisphere should be considered. \param[in] nodeIndex The index of the BVH leaf node. \param[in] u Uniform random number. \param[out] pdf Probabiliy of the sampled triangle, only valid if true is returned. \param[out] triangleIndex Index of the sampled triangle, only valid if true is returned. \return True if a triangle was sampled, false otherwise. */ - bool pickTriangle(const float3 posW, const float3 normalW, const uint nodeIndex, const float u, out float pdf, out uint triangleIndex) + bool pickTriangle(const float3 posW, const float3 normalW, const bool onSurface, const uint nodeIndex, const float u, out float pdf, out uint triangleIndex) { const LeafNode node = _lightBVH.getLeafNode(nodeIndex); - #if _USE_UNIFORM_TRIANGLE_SAMPLING - uint idx = min((uint)(u * node.triangleCount), node.triangleCount - 1); // Safety precaution in case u == 1.0 (it shouldn't be). - triangleIndex = _lightBVH.getNodeTriangleIndex(node, idx); - pdf = 1.0f / (float)node.triangleCount; - return true; - #else - float pdfs[_ACTUAL_MAX_TRIANGLES_PER_NODE]; - float totalImportance = 0.0f; - - for (uint i = 0; i < node.triangleCount; ++i) + if (kUseUniformTriangleSampling) { - uint triIdx = _lightBVH.getNodeTriangleIndex(node, i); - pdfs[i] = computeTriangleImportance(posW, normalW, triIdx); - totalImportance += pdfs[i]; + uint idx = min((uint)(u * node.triangleCount), node.triangleCount - 1); // Safety precaution in case u == 1.0 (it shouldn't be). + triangleIndex = _lightBVH.getNodeTriangleIndex(node, idx); + pdf = 1.0f / (float)node.triangleCount; + return true; } + else + { + float pdfs[kActualMaxTrianglesPerNode]; + float totalImportance = 0.0f; - // If the total importance is zero, none of the triangles matter so just bail out. - if (totalImportance == 0.0f) return false; + for (uint i = 0; i < node.triangleCount; ++i) + { + uint triIdx = _lightBVH.getNodeTriangleIndex(node, i); + pdfs[i] = computeTriangleImportance(posW, normalW, onSurface, triIdx); + totalImportance += pdfs[i]; + } - float uScaled = u * totalImportance; - float cdf = 0.0f; + // If the total importance is zero, none of the triangles matter so just bail out. + if (totalImportance == 0.0f) return false; - uint idx = 0; - for (; idx < node.triangleCount; ++idx) - { - cdf += pdfs[idx]; - if (uScaled < cdf) break; - } + float uScaled = u * totalImportance; + float cdf = 0.0f; - idx = min(idx, node.triangleCount - 1); // Safety precaution in case uScaled == cdf (it shouldn't be). - triangleIndex = _lightBVH.getNodeTriangleIndex(node, idx); - pdf = pdfs[idx] / totalImportance; - return true; - #endif + uint idx = 0; + for (; idx < node.triangleCount; ++idx) + { + cdf += pdfs[idx]; + if (uScaled < cdf) break; + } + + idx = min(idx, node.triangleCount - 1); // Safety precaution in case uScaled == cdf (it shouldn't be). + triangleIndex = _lightBVH.getNodeTriangleIndex(node, idx); + pdf = pdfs[idx] / totalImportance; + return true; + } } /** Samples a light using the BVH. \param[in] posW Shading point in world space. \param[in] normalW Normal at the shading point in world space. + \param[in] onSurface True if only upper hemisphere should be considered. \param[in] u Uniform random number. \param[out] pdf Probabiliy of the sampled triangle, only valid if true is returned. \param[out] triangleIndex Index of the sampled triangle, only valid if true is returned. \return True if a triangle was sampled, false otherwise. */ - bool sampleLightViaBVH(const float3 posW, const float3 normalW, float u, out float pdf, out uint triangleIndex) + bool sampleLightViaBVH(const float3 posW, const float3 normalW, const bool onSurface, float u, out float pdf, out uint triangleIndex) { // Traverse BVH to select a leaf node with N triangles based on estimated probabilities during traversal. float leafPdf; uint leafNodeIndex; - if (!traverseTree(posW, normalW, u, leafPdf, leafNodeIndex)) return false; + if (!traverseTree(posW, normalW, onSurface, u, leafPdf, leafNodeIndex)) return false; // Within the selected leaf, pick one out of the N triangles to sample. float trianglePdf; - if (!pickTriangle(posW, normalW, leafNodeIndex, u, trianglePdf, triangleIndex)) return false; + if (!pickTriangle(posW, normalW, onSurface, leafNodeIndex, u, trianglePdf, triangleIndex)) return false; pdf = leafPdf * trianglePdf; return true; @@ -406,10 +439,11 @@ struct LightBVHSampler : IEmissiveLightSampler /** Returns the PDF of selecting the specified leaf node by traversing the tree. \param[in] posW Shading point in world space. \param[in] normalW Normal at the shading point in world space. + \param[in] onSurface True if only upper hemisphere should be considered. \param[in] bitmask The bit pattern describing at each level which child was chosen in order to reach the specifide leaf node. \param[out] nodeIndex The node index at which the given leaf node is located. */ - float evalBVHTraversalPdf(const float3 posW, const float3 normalW, uint64_t bitmask, out uint nodeIndex) + float evalBVHTraversalPdf(const float3 posW, const float3 normalW, const bool onSurface, uint64_t bitmask, out uint nodeIndex) { float traversalPdf = 1.0f; nodeIndex = 0; @@ -420,8 +454,8 @@ struct LightBVHSampler : IEmissiveLightSampler uint leftNodeIndex = nodeIndex + 1; uint rightNodeIndex = _lightBVH.getInternalNode(nodeIndex).rightChildIdx; - float leftNodeImportance = computeImportance(posW, normalW, leftNodeIndex); - float rightNodeImportance = computeImportance(posW, normalW, rightNodeIndex); + float leftNodeImportance = computeImportance(posW, normalW, onSurface, leftNodeIndex); + float rightNodeImportance = computeImportance(posW, normalW, onSurface, rightNodeIndex); float totalImportance = leftNodeImportance + rightNodeImportance; if (totalImportance == 0.f) return 0.0f; @@ -451,37 +485,41 @@ struct LightBVHSampler : IEmissiveLightSampler /** Returns the PDF of selecting the specified triangle inside the specified leaf node as seen from a given shading point. \param[in] posW Shading point in world space. \param[in] normalW Normal at the shading point in world space. + \param[in] onSurface True if only upper hemisphere should be considered. \param[in] nodeIndex The index at which the given leaf node is located. \param[in] triangleIndex The global index of the triangle that was selected. \return Probability density for selecting the given triangle. */ - float evalNodeSamplingPdf(const float3 posW, const float3 normalW, const uint nodeIndex, const uint triangleIndex) + float evalNodeSamplingPdf(const float3 posW, const float3 normalW, const bool onSurface, const uint nodeIndex, const uint triangleIndex) { const LeafNode node = _lightBVH.getLeafNode(nodeIndex); - #if _USE_UNIFORM_TRIANGLE_SAMPLING == 1 - return 1.0f / ((float)node.triangleCount); - #else - float triangleImportance = 0.0f; - float totalImportance = 0.0f; - for (uint i = 0; i < node.triangleCount; ++i) + if (kUseUniformTriangleSampling) { - uint localTriangleIndex = _lightBVH.getNodeTriangleIndex(node, i); - float importance = computeTriangleImportance(posW, normalW, localTriangleIndex); - if (triangleIndex == localTriangleIndex) + return 1.0f / ((float)node.triangleCount); + } + else + { + float triangleImportance = 0.0f; + float totalImportance = 0.0f; + for (uint i = 0; i < node.triangleCount; ++i) { - triangleImportance = importance; + uint localTriangleIndex = _lightBVH.getNodeTriangleIndex(node, i); + float importance = computeTriangleImportance(posW, normalW, onSurface, localTriangleIndex); + if (triangleIndex == localTriangleIndex) + { + triangleImportance = importance; + } + totalImportance += importance; } - totalImportance += importance; - } - // If the total importance is 0, none of the triangles matter so just bail out. - if (totalImportance == 0.0f) - { - return 0.0f; - } + // If the total importance is 0, none of the triangles matter so just bail out. + if (totalImportance == 0.0f) + { + return 0.0f; + } - return triangleImportance / totalImportance; - #endif + return triangleImportance / totalImportance; + } } }; diff --git a/Source/Falcor/Experimental/Scene/Lights/LightCollection.cpp b/Source/Falcor/Experimental/Scene/Lights/LightCollection.cpp index 9775b02920..39d8c9cf9b 100644 --- a/Source/Falcor/Experimental/Scene/Lights/LightCollection.cpp +++ b/Source/Falcor/Experimental/Scene/Lights/LightCollection.cpp @@ -55,6 +55,9 @@ namespace Falcor { PROFILE("LightCollection::update()"); + auto pScene = mpScene.lock(); + if (!pScene) return false; + if (pUpdateStatus) { pUpdateStatus->lightsUpdateInfo.clear(); @@ -68,11 +71,11 @@ namespace Falcor for (uint32_t lightIdx = 0; lightIdx < mMeshLights.size(); ++lightIdx) { - const MeshInstanceData& instanceData = mpScene->getMeshInstance(mMeshLights[lightIdx].meshInstanceID); + const MeshInstanceData& instanceData = pScene->getMeshInstance(mMeshLights[lightIdx].meshInstanceID); UpdateFlags updateFlags = UpdateFlags::None; // Check if instance transform changed. - if (mpScene->getAnimationController()->didMatrixChanged(instanceData.globalMatrixID)) updateFlags |= UpdateFlags::MatrixChanged; + if (pScene->getAnimationController()->isMatrixChanged(instanceData.globalMatrixID)) updateFlags |= UpdateFlags::MatrixChanged; // Store update status. if (updateFlags != UpdateFlags::None) updatedLights.push_back(lightIdx); @@ -82,7 +85,7 @@ namespace Falcor // Update light data if needed. if (!updatedLights.empty()) { - updateTrianglePositions(pRenderContext, updatedLights); + updateTrianglePositions(pRenderContext, *pScene, updatedLights); return true; } @@ -95,14 +98,14 @@ namespace Falcor mpScene = pScene; // Setup the lights. - if (!setupMeshLights()) return false; + if (!setupMeshLights(*pScene)) return false; // Create program for integrating emissive textures. // This should be done after lights are setup, so that we know which sampler state etc. to use. - if (!initIntegrator()) return false; + if (!initIntegrator(*pScene)) return false; // Create programs for building/updating the mesh lights. - Shader::DefineList defines = mpScene->getSceneDefines(); + Shader::DefineList defines = pScene->getSceneDefines(); mpTriangleListBuilder = ComputePass::create(kBuildTriangleListFile, "buildTriangleList", defines); mpTrianglePositionUpdater = ComputePass::create(kUpdateTriangleVerticesFile, "updateTriangleVertices", defines); mpFinalizeIntegration = ComputePass::create(kFinalizeIntegrationFile, "finalizeIntegration", defines); @@ -110,12 +113,12 @@ namespace Falcor mpStagingFence = GpuFence::create(); // Now build the mesh light data. - build(pRenderContext); + build(pRenderContext, *pScene); return true; } - bool LightCollection::initIntegrator() + bool LightCollection::initIntegrator(const Scene& scene) { // The current algorithm rasterizes emissive triangles in texture space, // and uses atomic operations to sum up the contribution from all covered texels. @@ -132,7 +135,7 @@ namespace Falcor // Create program. Program::Desc desc; desc.addShaderLibrary(kEmissiveIntegratorFile).vsEntry("vsMain").psEntry("psMain"); - mIntegrator.pProgram = GraphicsProgram::create(desc, mpScene->getSceneDefines()); + mIntegrator.pProgram = GraphicsProgram::create(desc, scene.getSceneDefines()); // Create graphics state. mIntegrator.pState = GraphicsState::create(); @@ -166,18 +169,18 @@ namespace Falcor return true; } - bool LightCollection::setupMeshLights() + bool LightCollection::setupMeshLights(const Scene& scene) { mMeshLights.clear(); mpSamplerState = nullptr; mTriangleCount = 0; // Create mesh lights for all emissive mesh instances. - for (uint32_t meshInstanceID = 0; meshInstanceID < mpScene->getMeshInstanceCount(); meshInstanceID++) + for (uint32_t meshInstanceID = 0; meshInstanceID < scene.getMeshInstanceCount(); meshInstanceID++) { - const MeshInstanceData& instanceData = mpScene->getMeshInstance(meshInstanceID); - const MeshDesc& meshData = mpScene->getMesh(instanceData.meshID); - const Material::SharedPtr& pMaterial = mpScene->getMaterial(instanceData.materialID); + const MeshInstanceData& instanceData = scene.getMeshInstance(meshInstanceID); + const MeshDesc& meshData = scene.getMesh(instanceData.meshID); + const Material::SharedPtr& pMaterial = scene.getMaterial(instanceData.materialID); assert(pMaterial); if (pMaterial->isEmissive()) @@ -212,9 +215,9 @@ namespace Falcor return true; } - void LightCollection::build(RenderContext* pRenderContext) + void LightCollection::build(RenderContext* pRenderContext, const Scene& scene) { - prepareMeshData(); + prepareMeshData(scene); if (mTriangleCount == 0) { @@ -229,11 +232,11 @@ namespace Falcor else { // Prepare GPU buffers. - prepareTriangleData(pRenderContext); + prepareTriangleData(pRenderContext, scene); // Pre-integrate emissive triangles. // TODO: We might want to redo this in update() for animated meshes or after scale changes as that affects the flux. - integrateEmissive(pRenderContext); + integrateEmissive(pRenderContext, scene); mCPUInvalidData = CPUOutOfDateFlags::All; mStagingBufferValid = false; @@ -246,7 +249,7 @@ namespace Falcor } } - void LightCollection::prepareTriangleData(RenderContext* pRenderContext) + void LightCollection::prepareTriangleData(RenderContext* pRenderContext, const Scene& scene) { assert(mTriangleCount > 0); @@ -260,10 +263,10 @@ namespace Falcor if (mpFluxData->getStructSize() != sizeof(EmissiveFlux)) throw std::exception("Struct EmissiveFlux size mismatch between CPU/GPU"); // Compute triangle data (vertices, uv-coordinates, materialID) for all mesh lights. - buildTriangleList(pRenderContext); + buildTriangleList(pRenderContext, scene); } - void LightCollection::prepareMeshData() + void LightCollection::prepareMeshData(const Scene& scene) { // Create buffer for the mesh data if needed. if (!mMeshLights.empty()) @@ -285,7 +288,7 @@ namespace Falcor // Build a lookup table from mesh instance ID to emissive triangle offset. // This is useful in ray tracing for locating the emissive triangle that was hit for MIS computation etc. - uint32_t instanceCount = mpScene->getMeshInstanceCount(); + uint32_t instanceCount = scene.getMeshInstanceCount(); assert(instanceCount > 0); std::vector triangleOffsets(instanceCount, MeshLightData::kInvalidIndex); for (const auto& it : mMeshLights) @@ -299,7 +302,7 @@ namespace Falcor assert(mpPerMeshInstanceOffset->getSize() == triangleOffsets.size() * sizeof(triangleOffsets[0])); } - void LightCollection::integrateEmissive(RenderContext* pRenderContext) + void LightCollection::integrateEmissive(RenderContext* pRenderContext, const Scene& scene) { assert(mTriangleCount > 0); assert(mMeshLights.size() > 0); @@ -321,7 +324,7 @@ namespace Falcor mIntegrator.pVars = GraphicsVars::create(mIntegrator.pProgram.get()); // Bind scene. - mIntegrator.pVars["gScene"] = mpScene->getParameterBlock(); + mIntegrator.pVars["gScene"] = scene.getParameterBlock(); // Bind light collection. setShaderData(mIntegrator.pVars["gLightCollection"]); @@ -337,7 +340,7 @@ namespace Falcor // 2nd pass: Finalize the per-triangle flux values. { // Bind scene. - mpFinalizeIntegration["gScene"] = mpScene->getParameterBlock(); + mpFinalizeIntegration["gScene"] = scene.getParameterBlock(); mpFinalizeIntegration["gTexelSum"] = mIntegrator.pResultBuffer; mpFinalizeIntegration["gTriangleData"] = mpTriangleData; @@ -356,6 +359,9 @@ namespace Falcor { if (mStatsValid) return; + auto pScene = mpScene.lock(); + assert(pScene); + // Read back the current data. This is potentially expensive. syncCPUData(); @@ -367,7 +373,7 @@ namespace Falcor uint32_t trianglesTotal = 0; for (const auto& meshLight : mMeshLights) { - bool isTextured = mpScene->getMaterial(meshLight.materialID)->getEmissiveTexture() != nullptr; + bool isTextured = pScene->getMaterial(meshLight.materialID)->getEmissiveTexture() != nullptr; if (isTextured) { @@ -390,7 +396,7 @@ namespace Falcor { // TODO: Currently we don't detect uniform radiance for textured lights, so just look at whether the mesh light is textured or not. // This code will change when we tag individual triangles as textured vs non-textured. - bool isTextured = mpScene->getMaterial(mMeshLights[tri.lightIdx].materialID)->getEmissiveTexture() != nullptr; + bool isTextured = pScene->getMaterial(mMeshLights[tri.lightIdx].materialID)->getEmissiveTexture() != nullptr; if (isTextured) stats.trianglesActiveTextured++; else stats.trianglesActiveUniform++; @@ -402,12 +408,12 @@ namespace Falcor mStatsValid = true; } - void LightCollection::buildTriangleList(RenderContext* pRenderContext) + void LightCollection::buildTriangleList(RenderContext* pRenderContext, const Scene& scene) { assert(mMeshLights.size() > 0); // Bind scene. - mpTriangleListBuilder["gScene"] = mpScene->getParameterBlock(); + mpTriangleListBuilder["gScene"] = scene.getParameterBlock(); // Bind our output buffer. mpTriangleListBuilder["gTriangleData"] = mpTriangleData; @@ -470,7 +476,7 @@ namespace Falcor } } - void LightCollection::updateTrianglePositions(RenderContext* pRenderContext, const std::vector& updatedLights) + void LightCollection::updateTrianglePositions(RenderContext* pRenderContext, const Scene& scene, const std::vector& updatedLights) { // This pass pre-transforms all emissive triangles into world space and updates their area and face normals. // It is executed if any geometry in the scene has moved, which is wasteful since it will update also things @@ -480,7 +486,7 @@ namespace Falcor assert(!updatedLights.empty()); // Bind scene. - mpTrianglePositionUpdater["gScene"] = mpScene->getParameterBlock(); + mpTrianglePositionUpdater["gScene"] = scene.getParameterBlock(); // Bind our resources. mpTrianglePositionUpdater["gTriangleData"] = mpTriangleData; @@ -627,4 +633,17 @@ namespace Falcor mpStagingBuffer->unmap(); mCPUInvalidData = CPUOutOfDateFlags::None; } + + uint64_t LightCollection::getMemoryUsageInBytes() const + { + uint64_t m = 0; + if (mpTriangleData) m += mpTriangleData->getSize(); + if (mpActiveTriangleList) m += mpActiveTriangleList->getSize(); + if (mpFluxData) m += mpFluxData->getSize(); + if (mpMeshData) m += mpMeshData->getSize(); + if (mpPerMeshInstanceOffset) m += mpPerMeshInstanceOffset->getSize(); + if (mpStagingBuffer) m += mpStagingBuffer->getSize(); + if (mIntegrator.pResultBuffer) m += mIntegrator.pResultBuffer->getSize(); + return m; + } } diff --git a/Source/Falcor/Experimental/Scene/Lights/LightCollection.h b/Source/Falcor/Experimental/Scene/Lights/LightCollection.h index e12f31653f..6f594241ee 100644 --- a/Source/Falcor/Experimental/Scene/Lights/LightCollection.h +++ b/Source/Falcor/Experimental/Scene/Lights/LightCollection.h @@ -121,7 +121,6 @@ namespace Falcor bool update(RenderContext* pRenderContext, UpdateStatus* pUpdateStatus = nullptr); /** Bind the light collection data to a given shader var - Note that prepareProgram() must have been called before this function. \param[in] var The shader variable to set the data into. \return True if successful, false otherwise. */ @@ -157,6 +156,10 @@ namespace Falcor */ void prepareSyncCPUData(RenderContext* pRenderContext) const { copyDataToStagingBuffer(pRenderContext); } + /** Get the total GPU memory usage in bytes. + */ + uint64_t getMemoryUsageInBytes() const; + // Internal update flags. This only public for enum_class_operators() to work. enum class CPUOutOfDateFlags : uint32_t { @@ -171,22 +174,22 @@ namespace Falcor LightCollection() = default; bool init(RenderContext* pRenderContext, const std::shared_ptr& pScene); - bool initIntegrator(); - bool setupMeshLights(); - void build(RenderContext* pRenderContext); - void prepareTriangleData(RenderContext* pRenderContext); - void prepareMeshData(); - void integrateEmissive(RenderContext* pRenderContext); + bool initIntegrator(const Scene& scene); + bool setupMeshLights(const Scene& scene); + void build(RenderContext* pRenderContext, const Scene& scene); + void prepareTriangleData(RenderContext* pRenderContext, const Scene& scene); + void prepareMeshData(const Scene& scene); + void integrateEmissive(RenderContext* pRenderContext, const Scene& scene); void computeStats() const; - void buildTriangleList(RenderContext* pRenderContext); + void buildTriangleList(RenderContext* pRenderContext, const Scene& scene); void updateActiveTriangleList(); - void updateTrianglePositions(RenderContext* pRenderContext, const std::vector& updatedLights); + void updateTrianglePositions(RenderContext* pRenderContext, const Scene& scene, const std::vector& updatedLights); void copyDataToStagingBuffer(RenderContext* pRenderContext) const; void syncCPUData() const; // Internal state - std::shared_ptr mpScene; + std::weak_ptr mpScene; ///< Weak pointer to scene (scene owns LightCollection). std::vector mMeshLights; ///< List of all mesh lights. uint32_t mTriangleCount = 0; ///< Total number of triangles in all mesh lights (= mMeshLightTriangles.size()). This may include culled triangles. diff --git a/Source/Falcor/Experimental/Scene/Lights/LightCollection.slang b/Source/Falcor/Experimental/Scene/Lights/LightCollection.slang index 40c5b6c8b7..b9910030aa 100644 --- a/Source/Falcor/Experimental/Scene/Lights/LightCollection.slang +++ b/Source/Falcor/Experimental/Scene/Lights/LightCollection.slang @@ -81,6 +81,15 @@ struct LightCollection return triangleData[triIdx].unpack(); } + /** Returns the average radiance for a given triangle. + \param[in] triIdx Emissive triangle index. + \return Average radiance. + */ + float3 getAverageRadiance(uint triIdx) + { + return fluxData[triIdx].averageRadiance; + } + /** Returns a triangle index given the mesh instance ID and primitive index. \param[in] meshInstanceID Global mesh instance ID. \param[in] primitiveIndex Primitive index in the given mesh. diff --git a/Source/Falcor/Experimental/Scene/Lights/LightCollectionShared.slang b/Source/Falcor/Experimental/Scene/Lights/LightCollectionShared.slang index f36c74fd13..2e1dddae2a 100644 --- a/Source/Falcor/Experimental/Scene/Lights/LightCollectionShared.slang +++ b/Source/Falcor/Experimental/Scene/Lights/LightCollectionShared.slang @@ -86,13 +86,16 @@ struct PackedEmissiveTriangle uint encodeTexCoord(float2 t) CONST_FUNCTION { - uint2 c = f32tof16(t); - return (c.y << 16) | c.x; + uint x = f32tof16(t.x); + uint y = f32tof16(t.y); + return (y << 16) | x; } float2 decodeTexCoord(uint p) CONST_FUNCTION { - return f16tof32(uint2(p & 0xffff, p >> 16)); + float x = f16tof32(p & 0xffff); + float y = f16tof32(p >> 16); + return float2(x, y); } #ifndef HOST_CODE diff --git a/Source/Falcor/Experimental/Scene/Lights/LightHelpers.slang b/Source/Falcor/Experimental/Scene/Lights/LightHelpers.slang index b937e582d9..bd3f17476a 100644 --- a/Source/Falcor/Experimental/Scene/Lights/LightHelpers.slang +++ b/Source/Falcor/Experimental/Scene/Lights/LightHelpers.slang @@ -39,7 +39,7 @@ import Scene.Lights.LightData; import Utils.Math.MathHelpers; -import Utils.Sampling.SampleGenerator; +import Utils.Sampling.SampleGeneratorInterface; static const float kMinLightDistSqr = 1e-9f; static const float kMaxLightDistance = FLT_MAX; @@ -91,7 +91,7 @@ bool finalizeAreaLightSample(const float3 shadingPosW, const LightData light, in \param[out] ls Light sample struct. \return True if a sample was generated, false otherwise. */ -bool sampleRectAreaLight(const float3 shadingPosW, const LightData light, inout SampleGenerator sg, out AnalyticLightSample ls) +bool sampleRectAreaLight(const float3 shadingPosW, const LightData light, inout S sg, out AnalyticLightSample ls) { // Pick a random sample on the quad. // The quad is from (-1,-1,0) to (1,1,0) in object space, but may be scaled by its transform matrix. @@ -102,11 +102,8 @@ bool sampleRectAreaLight(const float3 shadingPosW, const LightData light, inout ls.posW = mul(float4(pos, 1.f), light.transMat).xyz; // Setup world space normal. - // TODO: Should use light.dirW. - float3 tangentW = mul(float4(1.f, 0.f, 0.f, 0.f), light.transMat).xyz; - float3 bitangentW = mul(float4(0.f, 1.f, 0.f, 0.f), light.transMat).xyz; // TODO: normalW is not correctly oriented for mesh instances that have flipped triangle winding. - ls.normalW = normalize(cross(tangentW, bitangentW)); + ls.normalW = normalize(mul(float4(0.f, 0.f, 1.f, 0.f), light.transMatIT).xyz); return finalizeAreaLightSample(shadingPosW, light, ls); } @@ -118,7 +115,7 @@ bool sampleRectAreaLight(const float3 shadingPosW, const LightData light, inout \param[out] ls Light sample struct. \return True if a sample was generated, false otherwise. */ -bool sampleSphereAreaLight(const float3 shadingPosW, const LightData light, inout SampleGenerator sg, out AnalyticLightSample ls) +bool sampleSphereAreaLight(const float3 shadingPosW, const LightData light, inout S sg, out AnalyticLightSample ls) { // Sample a random point on the sphere. // TODO: We should pick a random point on the hemisphere facing the shading point. @@ -141,7 +138,7 @@ bool sampleSphereAreaLight(const float3 shadingPosW, const LightData light, inou \param[out] ls Light sample struct. \return True if a sample was generated, false otherwise. */ -bool sampleDiscAreaLight(const float3 shadingPosW, const LightData light, inout SampleGenerator sg, out AnalyticLightSample ls) +bool sampleDiscAreaLight(const float3 shadingPosW, const LightData light, inout S sg, out AnalyticLightSample ls) { // Sample a random point on the disk. // TODO: Fix spelling disagreement between disc vs disk. @@ -164,7 +161,7 @@ bool sampleDiscAreaLight(const float3 shadingPosW, const LightData light, inout \param[out] ls Light sample struct. \return True if a sample was generated, false otherwise. */ -bool sampleDistantLight(const float3 shadingPosW, const LightData light, inout SampleGenerator sg, out AnalyticLightSample ls) +bool sampleDistantLight(const float3 shadingPosW, const LightData light, inout S sg, out AnalyticLightSample ls) { // A distant light doesn't have a position. Just clear to zero. ls.posW = float3(0.f, 0.f, 0.f); @@ -251,7 +248,7 @@ bool samplePointLight(const float3 shadingPosW, const LightData light, out Analy \param[out] ls Sampled point on the light and associated sample data, only valid if true is returned. \return True if a sample was generated, false otherwise. */ -bool sampleLight(const float3 shadingPosW, const LightData light, inout SampleGenerator sg, out AnalyticLightSample ls) +bool sampleLight(const float3 shadingPosW, const LightData light, inout S sg, out AnalyticLightSample ls) { // Sample the light based on its type: point, directional, or area. switch (light.type) @@ -269,6 +266,7 @@ bool sampleLight(const float3 shadingPosW, const LightData light, inout SampleGe case LightType::Distant: return sampleDistantLight(shadingPosW, light, sg, ls); default: + ls = {}; return false; // Should not happen } } diff --git a/Source/Falcor/Experimental/Scene/Material/BCSDF.slang b/Source/Falcor/Experimental/Scene/Material/BCSDF.slang new file mode 100644 index 0000000000..91c4b22319 --- /dev/null +++ b/Source/Falcor/Experimental/Scene/Material/BCSDF.slang @@ -0,0 +1,131 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#include "Utils/Math/MathConstants.slangh" +#include "BCSDFConfig.slangh" + +import Scene.ShadingData; +import Utils.Math.MathHelpers; +import Experimental.Scene.Material.HairChiang16; +import Experimental.Scene.Material.MaterialHelpers; +import Experimental.Scene.Material.MaterialShading; + +/******************************************************************* + BCSDF functions +*******************************************************************/ + +/** Evaluates the BCSDF multiplied by NdotL for a given incident direction. + + \param[in] sd Shading point data. + \param[in] L Normalized incident direction from shading point towards light source. + \return f * saturate(dot(N,L)) +*/ +float3 evalBCSDFCosine(const ShadingData sd, float3 L) +{ +#if BCSDF == HairChiang + return evalHairChiang16(sd, L); +#else + // Diffuse BRDF + return sd.diffuse * M_1_PI * saturate(dot(sd.N, L)); +#endif +} + +/** Importance sampling of the BCSDF. + + Note: The evaluated pdf for the generated sample is expensive to compute. If the caller + doesn't explicitly need the probability, they should be careful not to touch the value + so that the compiler can do dead code elimination. + + \param[in] sd Shading point data. + \param[in] sg Sample generator. + \param[out] result Generated sample. Only valid if true is returned. + \return True if a sample was generated, false otherwise. +*/ +bool sampleBCSDF(const ShadingData sd, inout S sg, out BSDFSample result) +{ +#if BCSDF == HairChiang + return sampleHairChiang16(sd, sg, result); +#else + // Diffuse BRDF + float3 wi = sample_cosine_hemisphere_concentric(sampleNext2D(sg), result.pdf); + result.wi = fromLocal(wi, sd); + result.weight = sd.diffuse * M_1_PI; + result.lobe = (uint)LobeType::DiffuseReflection; + return true; +#endif +} + +/** Evaluates the probability density function. + \param[in] sd Describes the shading point. + \param[in] L The normalized incident direction for which to evaluate the pdf. + \return Probability density with respect to solid angle from the shading point. +*/ +float evalPdfBCSDF(const ShadingData sd, float3 L) +{ +#if BCSDF == HairChiang + return evalPdfHairChiang16(sd, L); +#else + // Diffuse BRDF + return saturate(dot(sd.N, L)); +#endif +} + +/******************************************************************* + Interface wrappers +*******************************************************************/ + +float3 evalHairChiang16(const ShadingData sd, float3 L) +{ + float3 wo = toLocal(sd.V, sd); + float3 wi = toLocal(L, sd); + + HairChiang16 bcsdf; + bcsdf.setup(sd); + return bcsdf.eval(wo, wi); +} + +bool sampleHairChiang16(const ShadingData sd, inout S sg, out BSDFSample result) +{ + float3 wo = toLocal(sd.V, sd); + float3 wi; + + HairChiang16 bcsdf; + bcsdf.setup(sd); + bool valid = bcsdf.sample(wo, wi, result.pdf, result.weight, result.lobe, sg); + result.wi = fromLocal(wi, sd); + return valid; +} + +float evalPdfHairChiang16(const ShadingData sd, float3 L) +{ + float3 wo = toLocal(sd.V, sd); + float3 wi = toLocal(L, sd); + + HairChiang16 bcsdf; + bcsdf.setup(sd); + return bcsdf.evalPdf(wo, wi); +} diff --git a/Source/Falcor/Experimental/Scene/Material/BCSDFConfig.slangh b/Source/Falcor/Experimental/Scene/Material/BCSDFConfig.slangh new file mode 100644 index 0000000000..a0affaf4de --- /dev/null +++ b/Source/Falcor/Experimental/Scene/Material/BCSDFConfig.slangh @@ -0,0 +1,42 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#pragma once + +/** Static configuration of the BCSDF models. + + The defaults can be overridden by passing in defines from the host. + + TODO: This file will be removed when we've settled on a new standard material definition. +*/ + +#define DiffuseLambert 0 +#define HairChiang 1 + +#ifndef BCSDF +#define BCSDF HairChiang +#endif diff --git a/Source/Falcor/Experimental/Scene/Material/BxDF.slang b/Source/Falcor/Experimental/Scene/Material/BxDF.slang index 2eb896d480..1cb6e436fc 100644 --- a/Source/Falcor/Experimental/Scene/Material/BxDF.slang +++ b/Source/Falcor/Experimental/Scene/Material/BxDF.slang @@ -31,7 +31,7 @@ import Scene.ShadingData; import Utils.Math.MathHelpers; import Utils.Color.ColorHelpers; -import Utils.Sampling.SampleGenerator; +import Utils.Sampling.SampleGeneratorInterface; import Experimental.Scene.Material.Fresnel; import Experimental.Scene.Material.Microfacet; __exported import Experimental.Scene.Material.BxDFTypes; @@ -82,10 +82,10 @@ interface IBxDF \param[out] pdf pdf with respect to solid angle for sampling incident direction wi (0 if a delta event is sampled). \param[out] weight Sample weight f(wo, wi) * dot(wi, n) / pdf(wi). \param[out] lobe Sampled lobe. - \param[inout] sg Sample generator. + \param[in,out] sg Sample generator. \return Returns true if successful. */ - bool sample(float3 wo, out float3 wi, out float pdf, out float3 weight, out uint lobe, inout SampleGenerator sg); + bool sample(float3 wo, out float3 wi, out float pdf, out float3 weight, out uint lobe, inout S sg); /** Evaluates the BxDF directional pdf for sampling incident direction wi. \param[in] wo Outgoing direction. @@ -109,14 +109,18 @@ struct DiffuseReflectionLambert : IBxDF return M_1_PI * albedo * wi.z; } - bool sample(float3 wo, out float3 wi, out float pdf, out float3 weight, out uint lobe, inout SampleGenerator sg) + bool sample(float3 wo, out float3 wi, out float pdf, out float3 weight, out uint lobe, inout S sg) { wi = sample_cosine_hemisphere_concentric(sampleNext2D(sg), pdf); + lobe = (uint)LobeType::DiffuseReflection; - if (min(wo.z, wi.z) < kMinCosTheta) return false; + if (min(wo.z, wi.z) < kMinCosTheta) + { + weight = {}; + return false; + } weight = albedo; - lobe = (uint)LobeType::DiffuseReflection; return true; } @@ -143,14 +147,18 @@ struct DiffuseReflectionDisney : IBxDF return evalWeight(wo, wi) * M_1_PI * wi.z; } - bool sample(float3 wo, out float3 wi, out float pdf, out float3 weight, out uint lobe, inout SampleGenerator sg) + bool sample(float3 wo, out float3 wi, out float pdf, out float3 weight, out uint lobe, inout S sg) { wi = sample_cosine_hemisphere_concentric(sampleNext2D(sg), pdf); + lobe = (uint)LobeType::DiffuseReflection; - if (min(wo.z, wi.z) < kMinCosTheta) return false; + if (min(wo.z, wi.z) < kMinCosTheta) + { + weight = {}; + return false; + } weight = evalWeight(wo, wi); - lobe = (uint)LobeType::DiffuseReflection; return true; } @@ -192,14 +200,18 @@ struct DiffuseReflectionFrostbite : IBxDF return evalWeight(wo, wi) * M_1_PI * wi.z; } - bool sample(float3 wo, out float3 wi, out float pdf, out float3 weight, out uint lobe, inout SampleGenerator sg) + bool sample(float3 wo, out float3 wi, out float pdf, out float3 weight, out uint lobe, inout S sg) { wi = sample_cosine_hemisphere_concentric(sampleNext2D(sg), pdf); + lobe = (uint)LobeType::DiffuseReflection; - if (min(wo.z, wi.z) < kMinCosTheta) return false; + if (min(wo.z, wi.z) < kMinCosTheta) + { + weight = {}; + return false; + } weight = evalWeight(wo, wi); - lobe = (uint)LobeType::DiffuseReflection; return true; } @@ -231,8 +243,11 @@ struct DiffuseReflectionFrostbite : IBxDF */ struct SpecularReflectionMicrofacet : IBxDF { - float3 albedo; ///< Specular albedo. - float alpha; ///< GGX width parameter. + float3 albedo; ///< Specular albedo. + float alpha; ///< GGX width parameter. + uint activeLobes; ///< BSDF lobes to include for sampling and evaluation. See LobeType in BxDFTypes.slang. + + bool hasLobe(LobeType lobe) { return activeLobes & (uint)lobe; } float3 eval(float3 wo, float3 wi) { @@ -243,6 +258,8 @@ struct SpecularReflectionMicrofacet : IBxDF if (alpha == 0) return float3(0); #endif + if (!hasLobe(LobeType::SpecularReflection)) return float3(0); + float3 h = normalize(wo + wi); float woDotH = dot(wo, h); @@ -256,14 +273,22 @@ struct SpecularReflectionMicrofacet : IBxDF return F * D * G * 0.25 / wo.z; } - bool sample(float3 wo, out float3 wi, out float pdf, out float3 weight, out uint lobe, inout SampleGenerator sg) + bool sample(float3 wo, out float3 wi, out float pdf, out float3 weight, out uint lobe, inout S sg) { + // Default initialization to avoid divergence at returns. + wi = {}; + weight = {}; + pdf = 0.f; + lobe = (uint)LobeType::SpecularReflection; + if (wo.z < kMinCosTheta) return false; #if EnableDeltaBSDF // Handle delta reflection. if (alpha == 0) { + if (!hasLobe(LobeType::DeltaReflection)) return false; + wi = float3(-wo.x, -wo.y, wo.z); pdf = 0; weight = evalFresnelSchlick(albedo, 1, wo.z); @@ -272,6 +297,8 @@ struct SpecularReflectionMicrofacet : IBxDF } #endif + if (!hasLobe(LobeType::SpecularReflection)) return false; + // Sample the GGX distribution to find a microfacet normal (half vector). #if EnableVNDFSampling float3 h = sampleGGX_VNDF(alpha, wo, sampleNext2D(sg), pdf); // pdf = G1(wo) * D(h) * max(0,dot(wo,h)) / wo.z @@ -320,6 +347,8 @@ struct SpecularReflectionMicrofacet : IBxDF if (alpha == 0) return 0; #endif + if (!hasLobe(LobeType::SpecularReflection)) return 0; + float3 h = normalize(wo + wi); float woDotH = dot(wo, h); #if EnableVNDFSampling @@ -335,8 +364,11 @@ struct SpecularReflectionMicrofacet : IBxDF */ struct SpecularReflectionTransmissionMicrofacet : IBxDF { - float alpha; ///< GGX width parameter. - float eta; ///< Relative index of refraction (e.g. etaI / etaT). + float alpha; ///< GGX width parameter. + float eta; ///< Relative index of refraction (e.g. etaI / etaT). + uint activeLobes; ///< BSDF lobes to include for sampling and evaluation. See LobeType in BxDFTypes.slang. + + bool hasLobe(LobeType lobe) { return activeLobes & (uint)lobe; } float3 eval(float3 wo, float3 wi) { @@ -348,6 +380,8 @@ struct SpecularReflectionTransmissionMicrofacet : IBxDF #endif bool isReflection = wi.z > 0; + if ((isReflection && !hasLobe(LobeType::SpecularReflection)) || + (!isReflection && !hasLobe(LobeType::SpecularTransmission))) return float3(0); float3 h = isReflection ? @@ -377,17 +411,34 @@ struct SpecularReflectionTransmissionMicrofacet : IBxDF } } - bool sample(float3 wo, out float3 wi, out float pdf, out float3 weight, out uint lobe, inout SampleGenerator sg) + bool sample(float3 wo, out float3 wi, out float pdf, out float3 weight, out uint lobe, inout S sg) { + // Default initialization to avoid divergence at returns. + wi = {}; + weight = {}; + pdf = 0.f; + lobe = (uint)LobeType::SpecularReflection; + if (wo.z < kMinCosTheta) return false; + // Get a random number to decide what lobe to sample. + float lobeSample = sampleNext1D(sg); + #if EnableDeltaBSDF // Handle delta reflection/transmission. if (alpha == 0) { + const bool hasReflection = hasLobe(LobeType::DeltaReflection); + const bool hasTransmission = hasLobe(LobeType::DeltaTransmission); + if (!(hasReflection || hasTransmission)) return false; + float cosThetaT; float F = evalFresnelDielectric(eta, wo.z, cosThetaT); - bool isReflection = sampleNext1D(sg) < F; + + if (!hasReflection) F = 0.f; + else if (!hasTransmission) F = 1.f; + + bool isReflection = lobeSample < F; pdf = 0; weight = float3(1); @@ -400,6 +451,10 @@ struct SpecularReflectionTransmissionMicrofacet : IBxDF } #endif + const bool hasReflection = hasLobe(LobeType::SpecularReflection); + const bool hasTransmission = hasLobe(LobeType::SpecularTransmission); + if (!(hasReflection || hasTransmission)) return false; + // Sample the GGX distribution of (visible) normals. This is our half vector. #if EnableVNDFSampling float3 h = sampleGGX_VNDF(alpha, wo, sampleNext2D(sg), pdf); // pdf = G1(wo) * D(h) * max(0,dot(wo,h)) / wo.z @@ -412,7 +467,11 @@ struct SpecularReflectionTransmissionMicrofacet : IBxDF float cosThetaT; float F = evalFresnelDielectric(eta, woDotH, cosThetaT); - bool isReflection = sampleNext1D(sg) < F; + + if (!hasReflection) F = 0.f; + else if (!hasTransmission) F = 1.f; + + bool isReflection = lobeSample < F; wi = isReflection ? 2 * woDotH * h - wo : @@ -468,6 +527,9 @@ struct SpecularReflectionTransmissionMicrofacet : IBxDF #endif bool isReflection = wi.z > 0; + const bool hasReflection = hasLobe(LobeType::SpecularReflection); + const bool hasTransmission = hasLobe(LobeType::SpecularTransmission); + if ((isReflection && !hasReflection) || (!isReflection && !hasTransmission)) return 0; float3 h = isReflection ? @@ -479,6 +541,9 @@ struct SpecularReflectionTransmissionMicrofacet : IBxDF float F = evalFresnelDielectric(eta, woDotH); + if (!hasReflection) F = 0.f; + else if (!hasTransmission) F = 1.f; + #if EnableVNDFSampling float pdf = evalPdfGGX_VNDF(alpha, wo, h); #else @@ -486,12 +551,12 @@ struct SpecularReflectionTransmissionMicrofacet : IBxDF #endif if (isReflection) { - return F * pdf / (4 * wiDotH); + return pdf * F / (4 * wiDotH); // Jacobian of the reflection operator. } else { float sqrtDenom = woDotH + eta * wiDotH; - return (1 - F) * pdf * eta * eta * wiDotH / (sqrtDenom * sqrtDenom); + return pdf * (1 - F) * eta * eta * wiDotH / (sqrtDenom * sqrtDenom); // Jacobian of the refraction operator. } } }; @@ -545,9 +610,11 @@ struct FalcorBSDF : IBxDF specularReflection.albedo = sd.specular; specularReflection.alpha = alpha; + specularReflection.activeLobes = sd.activeLobes; specularReflectionTransmission.alpha = alpha; specularReflectionTransmission.eta = sd.eta; + specularReflectionTransmission.activeLobes = sd.activeLobes; specularTransmission = sd.specularTransmission; @@ -601,8 +668,14 @@ struct FalcorBSDF : IBxDF return result; } - bool sample(float3 wo, out float3 wi, out float pdf, out float3 weight, out uint lobe, inout SampleGenerator sg) + bool sample(float3 wo, out float3 wi, out float pdf, out float3 weight, out uint lobe, inout S sg) { + // Default initialization to avoid divergence at returns. + wi = {}; + weight = {}; + pdf = 0.f; + lobe = (uint)LobeType::DiffuseReflection; + bool valid = false; float uSelect = sampleNext1D(sg); diff --git a/Source/Falcor/Experimental/Scene/Material/Fresnel.slang b/Source/Falcor/Experimental/Scene/Material/Fresnel.slang index e0028b34a7..1d72f1fcb8 100644 --- a/Source/Falcor/Experimental/Scene/Material/Fresnel.slang +++ b/Source/Falcor/Experimental/Scene/Material/Fresnel.slang @@ -42,6 +42,11 @@ float3 evalFresnelSchlick(float3 f0, float3 f90, float cosTheta) return f0 + (f90 - f0) * pow(max(1 - cosTheta, 0), 5); // Clamp to avoid NaN if cosTheta = 1+epsilon } +float evalFresnelSchlick(float f0, float f90, float cosTheta) +{ + return f0 + (f90 - f0) * pow(max(1 - cosTheta, 0), 5); // Clamp to avoid NaN if cosTheta = 1+epsilon +} + /** Evaluates the Fresnel term using dieletric fresnel equations. Based on http://www.pbr-book.org/3ed-2018/Reflection_Models/Specular_Reflection_and_Transmission.html diff --git a/Source/Falcor/Experimental/Scene/Material/HairChiang16.slang b/Source/Falcor/Experimental/Scene/Material/HairChiang16.slang new file mode 100644 index 0000000000..998fdc6bbf --- /dev/null +++ b/Source/Falcor/Experimental/Scene/Material/HairChiang16.slang @@ -0,0 +1,499 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#include "Utils/Math/MathConstants.slangh" + +import Scene.ShadingData; +import Utils.Math.MathHelpers; +import Utils.Color.ColorHelpers; +import Utils.Sampling.SampleGeneratorInterface; +import Experimental.Scene.Material.Fresnel; +import Experimental.Scene.Material.BxDF; +__exported import Experimental.Scene.Material.BxDFTypes; + +// Enable explicitly computing sampling weights using eval(wo, wi) / evalPdf(wo, wi). +// This is for testing only, as many terms of the equation cancel out allowing to save on computation. +#define USE_BCSDF_IMPORTANCE_SAMPLING 1 + +/** Hair BCSDF from "A Practical and Controllable Hair and Fur Model for Production Path Tracing", Chiang et al. 2016. + Implementation is adapted from pbrt-v3. +*/ +struct HairChiang16 : IBxDF +{ + // Max number of scattering events that are explicitly computed. + // All higher-order scattering terms (>= kMaxScatterEvents) will be represented by a single one. + static const uint kMaxScatterEvents = 3; + + static const float kSqrtPiOver8 = 0.626657069f; + + float betaM, betaN, alpha, IoR; + float3 sigmaA; + float h; + float eta; + + float gammaO; + float v[kMaxScatterEvents + 1]; + float s; + float sin2kAlpha[3], cos2kAlpha[3]; + + [mutating] void precompute() + { + gammaO = asin(clamp(h, -1.f, 1.f)); + + float tmp = 0.726f * betaM + 0.812f * betaM * betaM + 3.7f * pow(betaM, 20.f); + v[0] = tmp * tmp; + v[1] = 0.25f * v[0]; + v[2] = 4 * v[0]; + [unroll] + for (uint p = 3; p <= kMaxScatterEvents; p++) v[p] = v[2]; + + // Compute azimuthal logistic scale factor + s = kSqrtPiOver8 * (0.265f * betaN + 1.194f * betaN * betaN + 5.372f * pow(betaN, 22.f)); + + // Compute alpha terms for hair scales + sin2kAlpha[0] = sin(alpha / 180.f * M_PI); + cos2kAlpha[0] = sqrt(max(0.f, 1.f - sin2kAlpha[0] * sin2kAlpha[0])); + [unroll] + for (uint i = 1; i < 3; i++) + { + sin2kAlpha[i] = 2 * cos2kAlpha[i - 1] * sin2kAlpha[i - 1]; + cos2kAlpha[i] = cos2kAlpha[i - 1] * cos2kAlpha[i - 1] - sin2kAlpha[i - 1] * sin2kAlpha[i - 1]; + } + } + + [mutating] void setup(const ShadingData sd) + { + betaM = sd.specular.x; + betaN = sd.specular.y; + sigmaA = sigmaAFromColor(sd.diffuse, betaN); + alpha = sd.specular.z; + IoR = sd.IoR; + eta = sd.eta; + + // Compute offset h azimuthally with the unit circle cross section. + float3 woProj = normalize(sd.V - dot(sd.V, sd.T) * sd.T); // Project wo to the (B, N) plane. + float3 woProjPerp = cross(woProj, sd.T); + h = dot(sd.N, woProjPerp); + + precompute(); + } + + float3 eval(float3 wo, float3 wi) + { + float sinThetaO = wo.x; + float cosThetaO = sqrt(max(0.f, 1.f - sinThetaO * sinThetaO)); + float phiO = atan2(wo.z, wo.y); + + float sinThetaI = wi.x; + float cosThetaI = sqrt(max(0.f, 1.f - sinThetaI * sinThetaI)); + float phiI = atan2(wi.z, wi.y); + + // Compute refracted ray. + float sinThetaT = sinThetaO / IoR; + float cosThetaT = sqrt(max(0.f, 1.f - sinThetaT * sinThetaT)); + + float etap = sqrt(IoR * IoR - sinThetaO * sinThetaO) / cosThetaO; + float sinGammaT = h / etap; + float cosGammaT = sqrt(max(0.f, 1.f - sinGammaT * sinGammaT)); + float gammaT = asin(clamp(sinGammaT, -1.f, 1.f)); + + // Compute the transmittance T of a single path through the cylinder. + float tmp = -2.f * cosGammaT / cosThetaT; + float3 T = exp(sigmaA * tmp); + + // Evaluate hair BCSDF for each lobe. + float phi = phiI - phiO; + float3 ap[kMaxScatterEvents + 1]; + Ap(cosThetaO, T, ap); + float3 result = float3(0.f); + + [unroll] + for (int p = 0; p < kMaxScatterEvents; p++) + { + float sinThetaOp, cosThetaOp; + if (p == 0) + { + sinThetaOp = sinThetaO * cos2kAlpha[1] - cosThetaO * sin2kAlpha[1]; + cosThetaOp = cosThetaO * cos2kAlpha[1] + sinThetaO * sin2kAlpha[1]; + } + else if (p == 1) + { + sinThetaOp = sinThetaO * cos2kAlpha[0] + cosThetaO * sin2kAlpha[0]; + cosThetaOp = cosThetaO * cos2kAlpha[0] - sinThetaO * sin2kAlpha[0]; + } + else if (p == 2) + { + sinThetaOp = sinThetaO * cos2kAlpha[2] + cosThetaO * sin2kAlpha[2]; + cosThetaOp = cosThetaO * cos2kAlpha[2] - sinThetaO * sin2kAlpha[2]; + } + else + { + sinThetaOp = sinThetaO; + cosThetaOp = cosThetaO; + } + + cosThetaOp = abs(cosThetaOp); + result += ap[p] * Mp(cosThetaI, cosThetaOp, sinThetaI, sinThetaOp, v[p]) * Np(phi, p, s, gammaO, gammaT); + } + + // Compute contribution of remaining terms after kMaxScatterEvents. + result += ap[kMaxScatterEvents] * Mp(cosThetaI, cosThetaO, sinThetaI, sinThetaO, v[kMaxScatterEvents]) * M_1_2PI; + + result = isnan(luminance(result)) ? float3(0.f) : result; + return result; + } + + bool sample(float3 wo, out float3 wi, out float pdf, out float3 weight, out uint lobe, inout S sg) + { +#if !USE_BCSDF_IMPORTANCE_SAMPLING + wi = sample_sphere(sampleNext2D(sg)); + pdf = M_1_4PI; + weight = eval(wo, wi) / pdf; + lobe = wi.z > 0 ? (uint)LobeType::SpecularReflection : (uint)LobeType::SpecularTransmission; + return true; +#endif + + float sinThetaO = wo.x; + float cosThetaO = sqrt(max(0.f, 1.f - sinThetaO * sinThetaO)); + float phiO = atan2(wo.z, wo.y); + + float2 u[2] = { sampleNext2D(sg), sampleNext2D(sg) }; + + // Determine which term p to sample for hair scattering. + float apPdf[kMaxScatterEvents + 1]; + computeApPdf(cosThetaO, apPdf); + uint p = 0; + while (p < kMaxScatterEvents && u[0].x >= apPdf[p]) + { + u[0].x -= apPdf[p]; + p++; + } + + float sinThetaOp, cosThetaOp; + if (p == 0) + { + sinThetaOp = sinThetaO * cos2kAlpha[1] - cosThetaO * sin2kAlpha[1]; + cosThetaOp = cosThetaO * cos2kAlpha[1] + sinThetaO * sin2kAlpha[1]; + } + else if (p == 1) + { + sinThetaOp = sinThetaO * cos2kAlpha[0] + cosThetaO * sin2kAlpha[0]; + cosThetaOp = cosThetaO * cos2kAlpha[0] - sinThetaO * sin2kAlpha[0]; + } + else if (p == 2) + { + sinThetaOp = sinThetaO * cos2kAlpha[2] + cosThetaO * sin2kAlpha[2]; + cosThetaOp = cosThetaO * cos2kAlpha[2] - sinThetaO * sin2kAlpha[2]; + } + else + { + sinThetaOp = sinThetaO; + cosThetaOp = cosThetaO; + } + + // Sample Mp to compute thetaI. + u[1].x = max(u[1].x, 1e-5f); + float cosTheta = 1.f + v[p] * log(u[1].x + (1.f - u[1].x) * exp(-2.f / v[p])); + float sinTheta = sqrt(max(0.f, 1.f - cosTheta * cosTheta)); + float cosPhi = cos(u[1].y * M_2PI); + float sinThetaI = -cosTheta * sinThetaOp + sinTheta * cosPhi * cosThetaOp; + float cosThetaI = sqrt(max(0.f, 1.f - sinThetaI * sinThetaI)); + + // Sample Np to compute dphi. + float etap = sqrt(IoR * IoR - sinThetaO * sinThetaO) / cosThetaO; + float sinGammaT = h / etap; + float gammaT = asin(clamp(sinGammaT, -1.f, 1.f)); + float dphi; + if (p < kMaxScatterEvents) + { + dphi = phiFunction(p, gammaO, gammaT) + sampleTrimmedLogistic(u[0].y, s, -M_PI, M_PI); + } + else + { + dphi = u[0].y * M_2PI; + } + + float phiI = phiO + dphi; + wi = float3(sinThetaI, cosThetaI * cos(phiI), cosThetaI * sin(phiI)); + lobe = wi.z > 0 ? (uint)LobeType::SpecularReflection : (uint)LobeType::SpecularTransmission; + + // Compute pdf. + pdf = 0; + [unroll] + for (uint i = 0; i < kMaxScatterEvents; i++) + { + float sinThetaOp, cosThetaOp; + if (i == 0) + { + sinThetaOp = sinThetaO * cos2kAlpha[1] - cosThetaO * sin2kAlpha[1]; + cosThetaOp = cosThetaO * cos2kAlpha[1] + sinThetaO * sin2kAlpha[1]; + } + else if (i == 1) + { + sinThetaOp = sinThetaO * cos2kAlpha[0] + cosThetaO * sin2kAlpha[0]; + cosThetaOp = cosThetaO * cos2kAlpha[0] - sinThetaO * sin2kAlpha[0]; + } + else if (i == 2) + { + sinThetaOp = sinThetaO * cos2kAlpha[2] + cosThetaO * sin2kAlpha[2]; + cosThetaOp = cosThetaO * cos2kAlpha[2] - sinThetaO * sin2kAlpha[2]; + } + else + { + sinThetaOp = sinThetaO; + cosThetaOp = cosThetaO; + } + + cosThetaOp = abs(cosThetaOp); + pdf += Mp(cosThetaI, cosThetaOp, sinThetaI, sinThetaOp, v[i]) * apPdf[i] * Np(dphi, i, s, gammaO, gammaT); + } + pdf += Mp(cosThetaI, cosThetaO, sinThetaI, sinThetaO, v[kMaxScatterEvents]) * apPdf[kMaxScatterEvents] * M_1_2PI; + + if (!isnan(pdf)) + { + weight = eval(wo, wi) / pdf; + } + else + { + pdf = 0.f; + weight = float3(0.f); + } + return (pdf > 0.f); + } + + float evalPdf(float3 wo, float3 wi) + { +#if !USE_BCSDF_IMPORTANCE_SAMPLING + return M_1_4PI; +#endif + + float sinThetaO = wo.x; + float cosThetaO = sqrt(max(0.f, 1.f - sinThetaO * sinThetaO)); + float phiO = atan2(wo.z, wo.y); + + float sinThetaI = wi.x; + float cosThetaI = sqrt(max(0.f, 1.f - sinThetaI * sinThetaI)); + float phiI = atan2(wi.z, wi.y); + + // Compute refracted ray. + float etap = sqrt(IoR * IoR - sinThetaO * sinThetaO) / cosThetaO; + float sinGammaT = h / etap; + float gammaT = asin(clamp(sinGammaT, -1.f, 1.f)); + + float apPdf[kMaxScatterEvents + 1]; + computeApPdf(cosThetaO, apPdf); + + // Compute pdf. + float phi = phiI - phiO; + float pdf = 0; + + [unroll] + for (int p = 0; p < kMaxScatterEvents; p++) + { + float sinThetaOp, cosThetaOp; + if (p == 0) + { + sinThetaOp = sinThetaO * cos2kAlpha[1] - cosThetaO * sin2kAlpha[1]; + cosThetaOp = cosThetaO * cos2kAlpha[1] + sinThetaO * sin2kAlpha[1]; + } + else if (p == 1) + { + sinThetaOp = sinThetaO * cos2kAlpha[0] + cosThetaO * sin2kAlpha[0]; + cosThetaOp = cosThetaO * cos2kAlpha[0] - sinThetaO * sin2kAlpha[0]; + } + else if (p == 2) + { + sinThetaOp = sinThetaO * cos2kAlpha[2] + cosThetaO * sin2kAlpha[2]; + cosThetaOp = cosThetaO * cos2kAlpha[2] - sinThetaO * sin2kAlpha[2]; + } + else + { + sinThetaOp = sinThetaO; + cosThetaOp = cosThetaO; + } + + cosThetaOp = abs(cosThetaOp); + pdf += apPdf[p] * Mp(cosThetaI, cosThetaOp, sinThetaI, sinThetaOp, v[p]) * Np(phi, p, s, gammaO, gammaT); + } + + // Compute contribution of remaining terms after kMaxScatterEvents. + pdf += apPdf[kMaxScatterEvents] * Mp(cosThetaI, cosThetaO, sinThetaI, sinThetaO, v[kMaxScatterEvents]) * M_1_2PI; + + pdf = isnan(pdf) ? 0.f : pdf; + return pdf; + } + + // private + + /** Attenuation function Ap. + */ + void Ap(float cosThetaO, float3 T, out float3 ap[kMaxScatterEvents + 1]) + { + float cosGammaO = sqrt(max(0.f, 1.f - h * h)); + float cosTheta = cosThetaO * cosGammaO; + float f = evalFresnelDielectric(eta, cosTheta); + + ap[0] = float3(f); + ap[1] = T * (1 - f) * (1 - f); + [unroll] + for (uint p = 2; p < kMaxScatterEvents; p++) ap[p] = ap[p - 1] * T * f; + + // Compute attenuation term accounting for remaining orders of scattering. + ap[kMaxScatterEvents] = ap[kMaxScatterEvents - 1] * T * f / (float3(1.f) - T * f); + } + + /** Compute a discrete pdf for sampling Ap (which BCSDF lobe). + */ + void computeApPdf(float cosThetaO, out float apPdf[kMaxScatterEvents + 1]) + { + float sinThetaO = sqrt(max(0.f, 1.f - cosThetaO * cosThetaO)); + + // Compute refracted ray. + float sinThetaT = sinThetaO / IoR; + float cosThetaT = sqrt(max(0.f, 1.f - sinThetaT * sinThetaT)); + + float etap = sqrt(IoR * IoR - sinThetaO * sinThetaO) / cosThetaO; + float sinGammaT = h / etap; + float cosGammaT = sqrt(max(0.f, 1.f - sinGammaT * sinGammaT)); + + // Compute the transmittance T of a single path through the cylinder. + float tmp = -2.f * cosGammaT / cosThetaT; + float3 T = exp(sigmaA * tmp); + + float3 ap[kMaxScatterEvents + 1]; + Ap(cosThetaO, T, ap); + + // Compute apPdf from individal ap terms. + float sumY = 0.f; + [unroll] + for (uint p = 0; p <= kMaxScatterEvents; p++) + { + apPdf[p] = luminance(ap[p]); + sumY += apPdf[p]; + } + + float invSumY = 1.f / sumY; + [unroll] + for (uint p = 0; p <= kMaxScatterEvents; p++) apPdf[p] *= invSumY; + } +}; + +/******************************************************************* + Helper functions +*******************************************************************/ + +/** Longitudinal scattering function Mp. +*/ +float Mp(float cosThetaI, float cosThetaO, float sinThetaI, float sinThetaO, float v) +{ + float a = cosThetaI * cosThetaO / v; + float b = sinThetaI * sinThetaO / v; + float mp = (v <= 0.1f) ? exp(logI0(a) - b - 1.f / v + 0.6931f + log(0.5f / v)) : (exp(-b) * I0(a)) / (sinh(1.f / v) * 2.f * v); + return mp; +} + +float I0(float x) +{ + float val = 0.f; + float x2i = 1.f; + float ifact = 1.f; + uint i4 = 1; + + [unroll] + for (uint i = 0; i < 10; i++) + { + if (i > 1) ifact *= i; + val += x2i / (ifact * ifact * i4); + x2i *= x * x; + i4 *= 4; + } + return val; +} + +float logI0(float x) +{ + if (x > 12) + { + return x + 0.5f * (-log(M_2PI) + log(1.f / x) + 0.125f / x); + } + else + { + return log(I0(x)); + } +} + +/** Azimuthal scattering function Np. +*/ +float Np(float phi, int p, float s, float gammaO, float gammaT) +{ + float dphi = phi - phiFunction(p, gammaO, gammaT); + + // Remap dphi to [-pi, pi]. + dphi = fmod(dphi, M_2PI); + if (dphi > M_PI) dphi -= M_2PI; + if (dphi < -M_PI) dphi += M_2PI; + + return trimmedLogistic(dphi, s, -M_PI, M_PI); +} + +float phiFunction(int p, float gammaO, float gammaT) +{ + return 2.f * p * gammaT - 2.f * gammaO + p * M_PI; +} + +float logistic(float x, float s) +{ + x = abs(x); + float tmp = exp(-x / s); + return tmp / (s * (1.f + tmp) * (1.f + tmp)); +} + +float logisticCDF(float x, float s) +{ + return 1.f / (1.f + exp(-x / s)); +} + +float trimmedLogistic(float x, float s, float a, float b) +{ + return logistic(x, s) / (logisticCDF(b, s) - logisticCDF(a, s)); +} + +float sampleTrimmedLogistic(float u, float s, float a, float b) +{ + float k = logisticCDF(b, s) - logisticCDF(a, s); + float x = -s * log(1.f / (u * k + logisticCDF(a, s)) - 1.f); + return clamp(x, a, b); +} + +/** Mapping from color to sigmaA. +*/ +float3 sigmaAFromColor(float3 color, float betaN) +{ + float tmp = 5.969f - 0.215f * betaN + 2.532f * betaN * betaN - 10.73f * pow(betaN, 3) + 5.574f * pow(betaN, 4) + 0.245f * pow(betaN, 5); + float3 sqrtSigmaA = log(max(color, 1e-4f)) / tmp; + return sqrtSigmaA * sqrtSigmaA; +} diff --git a/Source/Falcor/Experimental/Scene/Material/MaterialShading.slang b/Source/Falcor/Experimental/Scene/Material/MaterialShading.slang index 6ce9427d6a..ce45e7dfdf 100644 --- a/Source/Falcor/Experimental/Scene/Material/MaterialShading.slang +++ b/Source/Falcor/Experimental/Scene/Material/MaterialShading.slang @@ -43,7 +43,7 @@ import Scene.ShadingData; import Utils.Math.MathHelpers; import Utils.Color.ColorHelpers; -__exported import Utils.Sampling.SampleGenerator; +__exported import Utils.Sampling.SampleGeneratorInterface; __exported import Experimental.Scene.Material.BxDF; __exported import Experimental.Scene.Material.MaterialHelpers; @@ -111,7 +111,7 @@ float3 evalBSDFCosine(const ShadingData sd, float3 L) \param[out] result Generated sample. Only valid if true is returned. \return True if a sample was generated, false otherwise. */ -bool sampleBSDF(const ShadingData sd, inout SampleGenerator sg, out BSDFSample result) +bool sampleBSDF(const ShadingData sd, inout S sg, out BSDFSample result) { float3 wo = toLocal(sd.V, sd); float3 wi; @@ -145,7 +145,7 @@ float evalPdfBSDF(const ShadingData sd, float3 L) \param[out] result Generated sample. Only valid if true is returned. \return True if a sample was generated, false otherwise. */ -bool sampleBSDF_Reference(const ShadingData sd, inout SampleGenerator sg, out BSDFSample result) +bool sampleBSDF_Reference(const ShadingData sd, inout S sg, out BSDFSample result) { float3 wo = toLocal(sd.V, sd); float3 wi = sample_cosine_hemisphere_concentric(sampleNext2D(sg), result.pdf); // pdf = cos(theta) / pi @@ -451,6 +451,7 @@ void sampleDiffuse(const ShadingData sd, const float2 u, out BSDFSample result) // The Disney diffuse is a Lambert times a Fresnel term to increase grazing retroreflection. The latter is not included in the pdf. // TODO: Derive sampling method that better approminates the Disney diffuse lobe. result.wi = sampleHemisphereCosine(sd, u, result.pdf); + result.lobe = (uint)LobeType::DiffuseReflection; // Check that L and V are in the positive hemisphere. float NdotL = dot(sd.N, result.wi); @@ -463,7 +464,6 @@ void sampleDiffuse(const ShadingData sd, const float2 u, out BSDFSample result) // Compute weight. Note that NdotL cancels out by the pdf. result.weight = evalBSDFLobes(sd, result.wi, (uint)LobeType::DiffuseReflection) * M_PI; - result.lobe = (uint)LobeType::DiffuseReflection; } /** Evaluates the probability density function for the specular sampling strategy. @@ -501,6 +501,7 @@ void sampleSpecular(const ShadingData sd, const float2 u, out BSDFSample result) float alpha = max(kMinGGXAlpha, sd.ggxAlpha); float VdotH, NdotH; result.wi = sampleNdfGGX_Walter(sd, u, alpha, result.pdf, VdotH, NdotH); + result.lobe = (uint)LobeType::SpecularReflection; // Check that L and V are in the positive hemisphere. float NdotL = dot(sd.N, result.wi); @@ -527,7 +528,6 @@ void sampleSpecular(const ShadingData sd, const float2 u, out BSDFSample result) float3 F = evalFresnelSchlick(sd.specular, 1, VdotH); result.weight = F * (G * NdotL * VdotH * 4.f) / NdotH; //result.weight = evalBSDFCosine(sd, result.wi, (uint)LobeType::SpecularReflection) / result.pdf; - result.lobe = (uint)LobeType::SpecularReflection; } float getDiffuseProbability(const ShadingData sd) @@ -562,7 +562,7 @@ float evalPdfBSDF(const ShadingData sd, float3 L) \param[out] result Generated sample. Only valid if true is returned. \return True if a sample was generated, false otherwise. */ -bool sampleBSDF(const ShadingData sd, inout SampleGenerator sg, out BSDFSample result) +bool sampleBSDF(const ShadingData sd, inout S sg, out BSDFSample result) { // Draw uniform random numbers for lobe selection (1D) and sampling (2D). const float2 u = sampleNext2D(sg); @@ -609,7 +609,7 @@ bool sampleBSDF(const ShadingData sd, inout SampleGenerator sg, out BSDFSample r \param[out] result Generated sample. Only valid if true is returned. \return True if a sample was generated, false otherwise. */ -bool sampleBSDF_Reference(const ShadingData sd, inout SampleGenerator sg, out BSDFSample result) +bool sampleBSDF_Reference(const ShadingData sd, inout S sg, out BSDFSample result) { float pdf; float3 dir = sampleHemisphereCosine(sd, sampleNext2D(sg), pdf); // pdf = cos(theta) / pi diff --git a/Source/Falcor/Experimental/Scene/Material/TexLODHelpers.slang b/Source/Falcor/Experimental/Scene/Material/TexLODHelpers.slang index 7aa2db3025..200a6e1f02 100644 --- a/Source/Falcor/Experimental/Scene/Material/TexLODHelpers.slang +++ b/Source/Falcor/Experimental/Scene/Material/TexLODHelpers.slang @@ -27,7 +27,7 @@ **************************************************************************/ /** Helper functions for the texture level-of-detail (LOD) system. - + Supports texture LOD both for ray differentials (Igehy, SIGGRAPH 1999) and a method based on ray cones, described in "Strategies for Texture Level-of-Detail for Real-Time Ray Tracing," by Tomas Akenine-Moller et al., Ray Tracing Gems, 2019 and @@ -64,7 +64,7 @@ struct RayCone #else uint widthSpreadAngleFP16; float getWidth() { return f16tof32(widthSpreadAngleFP16 >> 16); } - float getSpreadAngle() { return f16tof32(widthSpreadAngleFP16); } + float getSpreadAngle() { return f16tof32(widthSpreadAngleFP16); } #endif /** Propagate the raycone to the next hit point (hitT distance away) and with the current spreadAngle + update ray cone angle with the surfaceSpreadAngle. @@ -143,7 +143,7 @@ struct RayCone } }; - + /** Compute the triangle LOD value based on triangle vertices and texture coordinates, used by ray cones. \param[in] vertices Triangle vertices. \param[in] txcoords Texture coordinates at triangle vertices. @@ -198,7 +198,7 @@ float computeScreenSpaceSurfaceSpreadAngle(float3 positionW, float3 normalW, flo float3 dNdy = ddy(normalW); float3 dPdx = ddx(positionW); float3 dPdy = ddy(positionW); - + float beta = sqrt(dot(dNdx, dNdx) + dot(dNdy, dNdy)) * sign(dot(dNdx, dPdx) + dot(dNdy, dPdy)); return 2.0f * beta * betaFactorK1 + betaFactorK2; @@ -289,7 +289,7 @@ void computeAnisotropicEllipseAxes(float3 intersectionPoint, float3 faceNormal, float oneOverAreaTriangle = 1.0f / dot(faceNormal, cross(edge01, edge02)); // Compute barycentrics. - edgeP = d + ellipseAxis0; + edgeP = d + ellipseAxis0; u = dot(faceNormal, cross(edgeP, edge02)) * oneOverAreaTriangle; v = dot(faceNormal, cross(edge01, edgeP)) * oneOverAreaTriangle; texGradientX = (1.0f - u - v) * txcoords[0] + u * txcoords[1] + v * txcoords[2] - interpolatedTexCoordsAtIntersection; diff --git a/Source/Falcor/Experimental/Scene/Volume/PhaseFunctions.slang b/Source/Falcor/Experimental/Scene/Volume/PhaseFunctions.slang new file mode 100644 index 0000000000..53161032f7 --- /dev/null +++ b/Source/Falcor/Experimental/Scene/Volume/PhaseFunctions.slang @@ -0,0 +1,71 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#include "Utils/Math/MathConstants.slangh" + +import Utils.Math.MathHelpers; + +/** Evaluates the isotropic (uniform) phase function. +*/ +float evalPhaseIsotropic() { return M_1_4PI; } + +/** Evaluates the anisotropic Henyey-Greenstein phase function. + Note: This function reduces to isotropic phase at g = 0 and has singularities at g = -1 and g = 1. + \param[in] cos_t Cosine between unscattered and scattered direction. + \param[in] g Anisotropy parameter in (-1, 1), where positive values promote forward scattering. +*/ +float evalPhaseHenyeyGreenstein(const float cos_t, const float g) +{ + const float denom = 1 + g * g + 2 * g * cos_t; + return M_1_4PI * (1 - g * g) / (denom * sqrt(denom)); +} + +/** Samples a direction according to the isotropic phase function, uniformly distributed over the sphere. + \param[in] u Uniform random samples in [0, 1). +*/ +float3 samplePhaseIsotropic(const float2 u) { return sample_sphere(u); } + +/** Samples a direction according to the anisotropic Henyey-Greenstein phase function, distributed around a given direction. + Note: This function reduces to isotropic phase at g = 0 and has singularities at g = -1 and g = 1. + \param[in] dir Unscattered ray direction, pointing towards the scatter location. + \param[in] g Anisotropy parameter in (-1, 1), where positive values promote forward scattering. + \param[in] u Uniform random samples in [0, 1). +*/ +float3 samplePhaseHenyeyGreenstein(const float3 dir, const float g, const float2 u) +{ + // Sample phase in tangent space. + const float phi = M_2PI * u.y; + const float sqr = (1.f - g * g) / (1.f - g + 2.f * g * u.x); + const float cosTheta = abs(g) < 1e-3f ? 1.f - 2.f * u.x : (1.f + g * g - sqr * sqr) / (2.f * g); + const float sinTheta = sqrt(max(0.f, 1.f - cosTheta * cosTheta)); + const float3 phase = float3(sinTheta * cos(phi), sinTheta * sin(phi), cosTheta); + // Build tangent frame. + const float3 tangent = perp_stark(dir); + const float3 bitangent = cross(dir, tangent); + // Tangent to world transformation. + return normalize(phase.x * tangent + phase.y * bitangent + phase.z * dir); +} diff --git a/Source/Falcor/Experimental/Scene/Volume/VolumeSampler.cpp b/Source/Falcor/Experimental/Scene/Volume/VolumeSampler.cpp new file mode 100644 index 0000000000..0c8ba2666d --- /dev/null +++ b/Source/Falcor/Experimental/Scene/Volume/VolumeSampler.cpp @@ -0,0 +1,90 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#include "stdafx.h" +#include "VolumeSampler.h" + +namespace Falcor +{ + namespace + { + const Gui::DropdownList kTransmittanceEstimatorList = + { + { (uint32_t)TransmittanceEstimator::DeltaTracking, "Delta Tracking" }, + { (uint32_t)TransmittanceEstimator::RatioTracking, "Ratio Tracking" }, + }; + } + + VolumeSampler::SharedPtr VolumeSampler::create(RenderContext* pRenderContext, Scene::SharedPtr pScene, const Options& options) + { + return SharedPtr(new VolumeSampler(pRenderContext, pScene, options)); + } + + Program::DefineList VolumeSampler::getDefines() const + { + Program::DefineList defines; + defines.add("VOLUME_SAMPLER_TRANSMITTANCE_ESTIMATOR", std::to_string((uint32_t)mOptions.transmittanceEstimator)); + return defines; + } + + void VolumeSampler::setShaderData(const ShaderVar& var) const + { + assert(var.isValid()); + } + + bool VolumeSampler::renderUI(Gui::Widgets& widget) + { + bool dirty = false; + + if (widget.dropdown("Transmittance Estimator", kTransmittanceEstimatorList, reinterpret_cast(mOptions.transmittanceEstimator))) + { + dirty = true; + } + + return dirty; + } + + VolumeSampler::VolumeSampler(RenderContext* pRenderContext, Scene::SharedPtr pScene, const Options& options) + : mpScene(pScene) + , mOptions(options) + { + assert(pScene); + } + + SCRIPT_BINDING(VolumeSampler) + { + pybind11::enum_ transmittanceEstimator(m, "TransmittanceEstimator"); + transmittanceEstimator.value("DeltaTracking", TransmittanceEstimator::DeltaTracking); + transmittanceEstimator.value("RatioTracking", TransmittanceEstimator::RatioTracking); + + // TODO use a nested class in the bindings when supported. + ScriptBindings::SerializableStruct options(m, "VolumeSamplerOptions"); +#define field(f_) field(#f_, &VolumeSampler::Options::f_) + options.field(transmittanceEstimator); +#undef field + } +} diff --git a/Source/Falcor/Experimental/Scene/Volume/VolumeSampler.h b/Source/Falcor/Experimental/Scene/Volume/VolumeSampler.h new file mode 100644 index 0000000000..4d58398ec2 --- /dev/null +++ b/Source/Falcor/Experimental/Scene/Volume/VolumeSampler.h @@ -0,0 +1,84 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#pragma once +#include "Scene/Scene.h" +#include "VolumeSamplerParams.slang" + +namespace Falcor +{ + /** Volume sampler. + Utily class for evaluating transmittance and sampling scattering in volumes. + */ + class dlldecl VolumeSampler + { + public: + using SharedPtr = std::shared_ptr; + + /** Volume sampler configuration options. + */ + struct Options + { + TransmittanceEstimator transmittanceEstimator = TransmittanceEstimator::RatioTracking; + }; + + virtual ~VolumeSampler() = default; + + /** Create a new object. + \param[in] pRenderContext A render-context that will be used for processing. + \param[in] pScene The scene. + \param[in] options Configuration options. + */ + static SharedPtr create(RenderContext* pRenderContext, Scene::SharedPtr pScene, const Options& options = Options()); + + /** Get a list of shader defines for using the volume sampler. + \return Returns a list of defines. + */ + Program::DefineList getDefines() const; + + /** Bind the volume sampler to a given shader variable. + \param[in] var Shader variable. + */ + void setShaderData(const ShaderVar& var) const; + + /** Render the GUI. + \return True if options were changed, false otherwise. + */ + bool renderUI(Gui::Widgets& widget); + + /** Returns the current configuration. + */ + const Options& getOptions() const { return mOptions; } + + protected: + VolumeSampler(RenderContext* pRenderContext, Scene::SharedPtr pScene, const Options& options); + + Scene::SharedPtr mpScene; ///< Scene. + + Options mOptions; + }; +} diff --git a/Source/Falcor/Experimental/Scene/Volume/VolumeSampler.slang b/Source/Falcor/Experimental/Scene/Volume/VolumeSampler.slang new file mode 100644 index 0000000000..858bdc3460 --- /dev/null +++ b/Source/Falcor/Experimental/Scene/Volume/VolumeSampler.slang @@ -0,0 +1,256 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#include "Utils/Math/MathConstants.slangh" +import Scene.Scene; +import Utils.Helpers; +import Utils.Math.MathHelpers; +import Utils.Sampling.SampleGeneratorInterface; +import Utils.Sampling.TinyUniformSampleGenerator; +import Utils.Math.HashUtils; +import RenderPasses.Shared.PathTracer.PixelStats; +import PhaseFunctions; +import VolumeSamplerParams; + +#ifndef VOLUME_SAMPLER_TRANSMITTANCE_ESTIMATOR +// VOLUME_SAMPLER_TRANSMITTANCE_ESTIMATOR must be defined in order to use this module. +#error "VOLUME_SAMPLER_TRANSMITTANCE_ESTIMATOR not defined!" +#endif + +/** Helper class for sampling volumes in the scene. + Note: For simplicity, this sampler only uses the first volume in the scene. +*/ +struct VolumeSampler +{ + static const TransmittanceEstimator kTransmittanceEstimator = TransmittanceEstimator(VOLUME_SAMPLER_TRANSMITTANCE_ESTIMATOR); + + uint4 _dummy; + + struct DistanceSample + { + float t; ///< Sampled distance. + float3 thp; ///< Througput. + }; + + /** Checks if a ray potentially intersects a volume. + \param[in] rayOrigin Ray origin. + \param[in] rayDir Ray direction. + \param[in] minT Ray minimum t. + \param[in] maxT Ray maximum t. + \return Returns true if a volume potentially intersects the ray. + */ + bool intersectsVolumes(const float3 rayOrigin, const float3 rayDir, const float minT, const float maxT) + { + if (gScene.getVolumeCount() == 0) return false; + float2 nearFar; + return intersectVolume(gScene.getVolume(0), rayOrigin, rayDir, minT, maxT, nearFar); + } + + /** Evaluate transmittance along a ray through volumes. + \param[in] rayOrigin Ray origin. + \param[in] rayDir Ray direction. + \param[in] minT Ray minimum t. + \param[in] maxT Ray maximum t. + \param[in,out] sg Sample generator. + \return Returns the transmittance. + */ + float evalTransmittance(const float3 rayOrigin, const float3 rayDir, const float minT, const float maxT, inout S sg) + { + if (gScene.getVolumeCount() == 0) return 1.f; + + Volume volume = gScene.getVolume(0); + if (!volume.hasDensityGrid()) return 1.f; + + float2 nearFar; + if (!intersectVolume(volume, rayOrigin, rayDir, minT, maxT, nearFar)) return 1.f; + + TinyUniformSampleGenerator tsg = TinyUniformSampleGenerator::create(jenkinsHash(sg.next())); + + switch (kTransmittanceEstimator) + { + case TransmittanceEstimator::DeltaTracking: + return evalTransmittanceDeltaTracking(volume, rayOrigin, rayDir, nearFar, tsg); + case TransmittanceEstimator::RatioTracking: + return evalTransmittanceRatioTracking(volume, rayOrigin, rayDir, nearFar, tsg); + default: + return 1.f; + } + } + + /** Sample a scattering distance along a ray through volumes. + \param[in] rayOrigin Ray origin. + \param[in] rayDir Ray direction. + \param[in] minT Ray minimum t. + \param[in] maxT Ray maximum t. + \param[in,out] sg Sample generator. + \param[out] ds Distance sample. + \return Returns true if a valid scattering distance was sampled. + */ + bool sampleDistance(const float3 rayOrigin, const float3 rayDir, const float minT, const float maxT, inout S sg, out DistanceSample ds) + { + ds = {}; + + if (gScene.getVolumeCount() == 0) return false; + + Volume volume = gScene.getVolume(0); + if (!volume.hasDensityGrid()) return false; + + float2 nearFar; + if (!intersectVolume(volume, rayOrigin, rayDir, minT, maxT, nearFar)) return false; + + TinyUniformSampleGenerator tsg = TinyUniformSampleGenerator::create(jenkinsHash(sg.next())); + return sampleDistanceDeltaTracking(volume, rayOrigin, rayDir, nearFar, tsg, ds); + } + + /** Sample a scattering direction. + \param[in] rayDir Ray direction. + \param[input] sg Sample generator. + \param[out] scatterDir Sampled scatter direction. + \return Returns true if successful. + */ + bool samplePhaseFunction(const float3 rayDir, inout S sg, out float3 scatterDir) + { + scatterDir = {}; + + if (gScene.getVolumeCount() == 0) return false; + + Volume volume = gScene.getVolume(0); + scatterDir = samplePhaseHenyeyGreenstein(rayDir, volume.data.anisotropy, sampleNext2D(sg)); + + return true; + } + + /** Evaluate the directional probability for sampling a scattering direction. + \param[in] rayDir Ray direction. + \param[in] scatterDir Scatter direction. + \return Returns the directional probability. + */ + float evalPhaseFunction(const float3 rayDir, const float3 scatterDir) + { + if (gScene.getVolumeCount() == 0) return 0.f; + + Volume volume = gScene.getVolume(0); + return evalPhaseHenyeyGreenstein(dot(-rayDir, scatterDir), volume.data.anisotropy); + } + + float lookupDensity(const Volume volume, const float3 pos, const float3 u, const Grid densityGrid, inout Grid::Accessor densityAccessor) + { + logVolumeLookup(); + return volume.data.densityScale * densityGrid.lookupStochasticWorld(mul(float4(pos, 1.f), volume.data.invTransform).xyz, u, densityAccessor); + } + + bool intersectVolume(const Volume volume, const float3 rayOrigin, const float3 rayDir, const float minT, const float maxT, out float2 nearFar) + { + // Intersect with volume bounds and get intersection interval along the view ray. + AABB bounds = volume.getBounds(); + intersectRayAABB(rayOrigin, rayDir, bounds.minPoint, bounds.maxPoint, nearFar); + nearFar.x = max(nearFar.x, minT); + nearFar.y = min(nearFar.y, maxT); + return nearFar.x < nearFar.y; + } + + float evalTransmittanceDeltaTracking(const Volume volume, const float3 rayOrigin, const float3 rayDir, const float2 nearFar, inout S sg) + { + // Setup access to density grid. + Grid densityGrid; + gScene.getGrid(volume.getDensityGrid(), densityGrid); + Grid::Accessor densityAccessor = densityGrid.createAccessor(); + + float Tr = 1.f; + const float invMajorant = 1.f / (volume.data.densityScale * densityGrid.getMaxValue()); + + // Delta tracking. + float t = nearFar.x; + while (t < nearFar.y) + { + t -= log(1 - sampleNext1D(sg)) * invMajorant; + const float d = lookupDensity(volume, rayOrigin + t * rayDir, sampleNext3D(sg), densityGrid, densityAccessor); + Tr *= 1.f - max(0.f, d * invMajorant); + // Russian roulette. + if (sampleNext1D(sg) < d * invMajorant) return 0.f; + } + return 1.f; + } + + float evalTransmittanceRatioTracking(const Volume volume, const float3 rayOrigin, const float3 rayDir, const float2 nearFar, inout S sg) + { + // Setup access to density grid. + Grid densityGrid; + gScene.getGrid(volume.getDensityGrid(), densityGrid); + Grid::Accessor densityAccessor = densityGrid.createAccessor(); + + float Tr = 1.f; + const float invMajorant = 1.f / (volume.data.densityScale * densityGrid.getMaxValue()); + + // Ratio tracking. + float t = nearFar.x; + while (t < nearFar.y) + { + t -= log(1 - sampleNext1D(sg)) * invMajorant; + const float d = lookupDensity(volume, rayOrigin + t * rayDir, sampleNext3D(sg), densityGrid, densityAccessor); + Tr *= 1.f - max(0.f, d * invMajorant); + if (Tr < 0.1f) + { + // Russian roulette. + const float prob = 1 - Tr; + if (sampleNext1D(sg) < prob) return 0.f; + Tr /= 1.f - prob; + } + } + + return Tr; + } + + bool sampleDistanceDeltaTracking(const Volume volume, const float3 rayOrigin, const float3 rayDir, const float2 nearFar, inout S sg, out DistanceSample ds) + { + ds = {}; + + // Setup access to density grid. + Grid densityGrid; + gScene.getGrid(volume.getDensityGrid(), densityGrid); + Grid::Accessor densityAccessor = densityGrid.createAccessor(); + + const float invMajorant = 1.f / (volume.data.densityScale * densityGrid.getMaxValue()); + + // Delta tracking. + float t = nearFar.x; + while (t < nearFar.y) + { + t -= log(1 - sampleNext1D(sg)) * invMajorant; + const float d = lookupDensity(volume, rayOrigin + t * rayDir, sampleNext3D(sg), densityGrid, densityAccessor); + // Scatter on real collision. + if (sampleNext1D(sg) < d * invMajorant) + { + ds.t = t; + ds.thp = volume.data.albedo; + return true; + } + } + + return false; + } +}; diff --git a/Source/Falcor/Scene/Lights/LightProbeData.slang b/Source/Falcor/Experimental/Scene/Volume/VolumeSamplerParams.slang similarity index 62% rename from Source/Falcor/Scene/Lights/LightProbeData.slang rename to Source/Falcor/Experimental/Scene/Volume/VolumeSamplerParams.slang index 720eb68312..7f780147c2 100644 --- a/Source/Falcor/Scene/Lights/LightProbeData.slang +++ b/Source/Falcor/Experimental/Scene/Volume/VolumeSamplerParams.slang @@ -30,44 +30,12 @@ BEGIN_NAMESPACE_FALCOR -#ifdef HOST_CODE -#define SamplerState std::shared_ptr -#define Texture2D std::shared_ptr -#endif - -/** This is a host/device structure that describes light probe resources. -*/ -struct LightProbeResources -{ - Texture2D origTexture; ///< The original texture - Texture2D diffuseTexture; ///< Texture containing pre-integrated diffuse (LD) term - Texture2D specularTexture; ///< Texture containing pre-integrated specular (LD) term - SamplerState sampler; -}; - -/** This is a host/device structure that describes shared light probe resources. +/** Enumeration of available volume transmittance estimators. */ -struct LightProbeSharedResources +enum class TransmittanceEstimator { - Texture2D dfgTexture; ///< Texture containing shared pre-integrated (DFG) term - SamplerState dfgSampler; + DeltaTracking, + RatioTracking, }; -/** This is a host/device structure that describes light probe data. -*/ -struct LightProbeData -{ - float3 posW = float3(0); - float radius = -1.0f; - float3 intensity = float3(1.0f); - - LightProbeResources resources; - LightProbeSharedResources sharedResources; -}; - -#ifdef HOST_CODE -#undef SamplerState -#undef Texture2D -#endif - END_NAMESPACE_FALCOR diff --git a/Source/Falcor/Falcor.h b/Source/Falcor/Falcor.h index 4df69438dd..d84e8839d3 100644 --- a/Source/Falcor/Falcor.h +++ b/Source/Falcor/Falcor.h @@ -111,7 +111,6 @@ #include "Scene/Camera/Camera.h" #include "Scene/Camera/CameraController.h" #include "Scene/Lights/Light.h" -#include "Scene/Lights/LightProbe.h" #include "Scene/Material/Material.h" #include "Scene/Animation/Animation.h" #include "Scene/Animation/AnimationController.h" @@ -121,6 +120,7 @@ #include "Utils/Math/AABB.h" #include "Utils/BinaryFileStream.h" #include "Utils/Logger.h" +#include "Utils/NumericRange.h" #include "Utils/StringUtils.h" #include "Utils/TermColor.h" #include "Utils/Threading.h" @@ -128,6 +128,7 @@ #include "Utils/Algorithm/DirectedGraphTraversal.h" #include "Utils/Algorithm/ParallelReduction.h" #include "Utils/Image/Bitmap.h" +#include "Utils/Image/ImageIO.h" #include "Utils/Math/CubicSpline.h" #include "Utils/Math/FalcorMath.h" #include "Utils/Scripting/Dictionary.h" @@ -162,5 +163,5 @@ #endif #define FALCOR_MAJOR_VERSION 4 -#define FALCOR_REVISION 2 -#define FALCOR_VERSION_STRING "4.2" +#define FALCOR_REVISION 3 +#define FALCOR_VERSION_STRING "4.3" diff --git a/Source/Falcor/Falcor.props b/Source/Falcor/Falcor.props index 9eeb9d64c7..5852b7b503 100644 --- a/Source/Falcor/Falcor.props +++ b/Source/Falcor/Falcor.props @@ -14,18 +14,30 @@ Level3 true - $(FALCOR_CORE_DIRECTORY)\Falcor;$(FALCOR_CORE_DIRECTORY)\Externals\.packman\nvapi;$(FALCOR_CORE_DIRECTORY)\Externals\.packman\GLM;$(FALCOR_CORE_DIRECTORY)\Externals\.packman\VulkanSDK\Include;$(FALCOR_CORE_DIRECTORY)\Externals\.packman\RapidJson\include;$(FALCOR_CORE_DIRECTORY)\Externals\.packman\pybind11\include;$(FALCOR_CORE_DIRECTORY)\Externals\.packman\Python\include;$(FALCOR_CORE_DIRECTORY)\Externals\.packman\WinPixEventRuntime\Include\WinPixEventRuntime;$(FALCOR_CORE_DIRECTORY)\Externals;$(FALCOR_CORE_DIRECTORY)\Externals\.packman;$(FALCOR_CORE_DIRECTORY)\Externals\args;%(AdditionalIncludeDirectories) + $(FALCOR_CORE_DIRECTORY)\Falcor;$(FALCOR_CORE_DIRECTORY)\Externals\.packman\deps\include;$(FALCOR_CORE_DIRECTORY)\Externals\.packman;$(FALCOR_CORE_DIRECTORY)\Externals;$(FALCOR_CORE_DIRECTORY)\Externals\.packman\nvapi;$(FALCOR_CORE_DIRECTORY)\Externals\.packman\vulkansdk\Include;$(FALCOR_CORE_DIRECTORY)\Externals\.packman\python\include;$(FALCOR_CORE_DIRECTORY)\Externals\.packman\WinPixEventRuntime\Include\WinPixEventRuntime;$(FALCOR_CORE_DIRECTORY)\Externals;$(FALCOR_CORE_DIRECTORY)\Externals\.packman\nanovdb\include;%(AdditionalIncludeDirectories) _$(OutputType);_CRT_SECURE_NO_WARNINGS;_SCL_SECURE_NO_WARNINGS;GLM_FORCE_DEPTH_ZERO_TO_ONE;$(FALCOR_BACKEND);_UNICODE;UNICODE;%(PreprocessorDefinitions) stdcpp17 - $(FALCOR_CORE_DIRECTORY)\Externals\.packman\FreeImage;$(FALCOR_CORE_DIRECTORY)\Externals\.packman\Assimp\lib\$(PlatformName);$(FALCOR_CORE_DIRECTORY)\Externals\.packman\FFMpeg\lib\$(PlatformName);$(FALCOR_CORE_DIRECTORY)\Externals\.packman\nvapi\amd64;$(FALCOR_CORE_DIRECTORY)\Externals\.packman\VulkanSDK\Lib;$(FALCOR_CORE_DIRECTORY)\Externals\.packman\Slang\bin\windows-x64\release;$(FALCOR_CORE_DIRECTORY)\Externals\.packman\GLFW\lib;$(FALCOR_CORE_DIRECTORY)\Externals\.packman\Python\libs;$(FALCOR_CORE_DIRECTORY)\Externals\.packman\WinPixEventRuntime\bin\x64;%(AdditionalLibraryDirectories) - WinPixEventRuntime.lib;glfw3dll.lib;slang.lib;Comctl32.lib;Shlwapi.lib;assimp-vc141-mt.lib;freeimage.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;avcodec.lib;avutil.lib;avformat.lib;swscale.lib;Shcore.lib;%(AdditionalDependencies) + $(FALCOR_CORE_DIRECTORY)\Externals\.packman\nvapi\amd64;$(FALCOR_CORE_DIRECTORY)\Externals\.packman\vulkansdk\Lib;$(FALCOR_CORE_DIRECTORY)\Externals\.packman\slang\bin\windows-x64\release;$(FALCOR_CORE_DIRECTORY)\Externals\.packman\python\libs;$(FALCOR_CORE_DIRECTORY)\Externals\.packman\WinPixEventRuntime\bin\x64;%(AdditionalLibraryDirectories) + WinPixEventRuntime.lib;slang.lib;Comctl32.lib;Shlwapi.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;Shcore.lib;%(AdditionalDependencies) call $(FALCOR_CORE_DIRECTORY)\..\Build\deployproject.bat $(ProjectDir) $(OutDir) + + + $(FALCOR_CORE_DIRECTORY)\Externals\.packman\deps\debug\lib;$(FALCOR_CORE_DIRECTORY)\Externals\.packman\deps\lib;%(AdditionalLibraryDirectories) + glfw3dll.lib;FreeImaged.lib;%(AdditionalDependencies) + + + + + $(FALCOR_CORE_DIRECTORY)\Externals\.packman\deps\lib;%(AdditionalLibraryDirectories) + glfw3dll.lib;FreeImage.lib;%(AdditionalDependencies) + + Project diff --git a/Source/Falcor/Falcor.vcxproj b/Source/Falcor/Falcor.vcxproj index 573c1f0f12..978f57e221 100644 --- a/Source/Falcor/Falcor.vcxproj +++ b/Source/Falcor/Falcor.vcxproj @@ -25,9 +25,7 @@ - - @@ -108,6 +106,7 @@ + @@ -124,20 +123,26 @@ + + + + + + @@ -151,6 +156,7 @@ + @@ -158,9 +164,15 @@ + + + + + + @@ -177,14 +189,17 @@ + + + - + @@ -207,17 +222,21 @@ - + + + + + @@ -230,7 +249,6 @@ - @@ -240,10 +258,14 @@ + + + + @@ -252,16 +274,15 @@ - - + - + @@ -270,12 +291,14 @@ + + @@ -293,6 +316,7 @@ + @@ -324,12 +348,6 @@ NotUsing NotUsing - - NotUsing - NotUsing - NotUsing - NotUsing - @@ -583,6 +601,7 @@ + @@ -595,13 +614,16 @@ + + + @@ -613,10 +635,13 @@ + + + @@ -636,10 +661,13 @@ - + + + + Create Create @@ -651,15 +679,18 @@ + - + + + @@ -725,7 +756,7 @@ {2C535635-E4C5-4098-A928-574F0E7CD5F9} Win32Proj Falcor - 10.0.18362.0 + 10.0.19041.0 @@ -795,7 +826,7 @@ Level3 Disabled FALCOR_DLL;IMGUI_API=__declspec(dllexport);_PROJECT_DIR_=R"($(ProjectDir))";_SCL_SECURE_NO_WARNINGS;_CRT_SECURE_NO_WARNINGS;FALCOR_D3D12;WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions);GLM_FORCE_DEPTH_ZERO_TO_ONE;_$(OutputType) - $(ProjectDir);$(ProjectDir)\..\Externals\.packman\GLM;$(ProjectDir)\..\Externals\.packman\GLFW\include;$(ProjectDir)\..\Externals\.packman\FreeImage;$(ProjectDir)\..\Externals\.packman\ASSIMP\include;$(ProjectDir)\..\Externals\.packman\FFMpeg\include;$(ProjectDir)\..\Externals\.packman\RapidJson\include;$(ProjectDir)\..\Externals\.packman\VulkanSDK\Include;$(ProjectDir)\..\Externals\.packman\Python\Include;$(ProjectDir)\..\Externals\.packman\pybind11\include;$(ProjectDir)\..\Externals\;$(ProjectDir)\..\Externals\.packman\nvapi;$(ProjectDir)\..\Externals\.packman + $(ProjectDir);$(ProjectDir)\..\Externals\.packman\deps\include;$(ProjectDir)\..\Externals\.packman;$(ProjectDir)\..\Externals;$(ProjectDir)\..\Externals\.packman\vulkansdk\Include;$(ProjectDir)\..\Externals\.packman\python\Include;$(ProjectDir)\..\Externals\;$(ProjectDir)\..\Externals\.packman\nvapi;$(ProjectDir)\..\Externals\.packman\nanovdb\include; true true stdcpp17 @@ -806,8 +837,8 @@ Windows true - $(ProjectDir)..\Externals\.packman\FreeImage;$(ProjectDir)..\Externals\.packman\Assimp\lib\$(PlatformName)\;$(ProjectDir)..\Externals\.packman\FFMpeg\lib\$(PlatformName);$(ProjectDir)..\Externals\.packman\nvapi\amd64;$(ProjectDir)..\Externals\.packman\VulkanSDK\Lib;$(ProjectDir)..\Externals\.packman\Slang\bin\windows-x64\release;$(ProjectDir)..\Externals\.packman\GLFW\lib;$(ProjectDir)..\Externals\.packman\Python\libs;$(ProjectDir)..\Externals\.packman\WinPixEventRuntime\bin\x64 - WinPixEventRuntime.lib;glfw3dll.lib;slang.lib;Comctl32.lib;Shlwapi.lib;assimp-vc141-mt.lib;freeimage.lib;avcodec.lib;avutil.lib;avformat.lib;swscale.lib;Shcore.lib;%(AdditionalDependencies) + $(ProjectDir)..\Externals\.packman\deps\debug\lib;$(ProjectDir)..\Externals\.packman\deps\lib;$(ProjectDir)..\Externals\.packman\nvapi\amd64;$(ProjectDir)..\Externals\.packman\vulkansdk\Lib;$(ProjectDir)..\Externals\.packman\slang\bin\windows-x64\release;$(ProjectDir)..\Externals\.packman\python\libs;$(ProjectDir)..\Externals\.packman\WinPixEventRuntime\bin\x64;$(ProjectDir)..\Externals\.packman\Cuda\lib\x64;%(AdditionalLibraryDirectories) + WinPixEventRuntime.lib;glfw3dll.lib;slang.lib;Comctl32.lib;Shlwapi.lib;assimp-vc142-mt.lib;mikktspaced.lib;FreeImaged.lib;avcodec.lib;avutil.lib;avformat.lib;swscale.lib;Shcore.lib;DirectXTex.lib;Half-2_5_d.lib;openvdb.lib;%(AdditionalDependencies) call $(ProjectDir)\..\..\Build\prebuild.bat $(ProjectDir)\..\ $(SolutionDir) $(ProjectDir) $(PlatformName) $(PlatformShortName) $(Configuration) $(OutDir) @@ -833,7 +864,7 @@ Level3 Disabled FALCOR_DLL;IMGUI_API=__declspec(dllexport);_PROJECT_DIR_=R"($(ProjectDir))";_SCL_SECURE_NO_WARNINGS;_CRT_SECURE_NO_WARNINGS;FALCOR_VK;WIN32;_DEBUG;_LIB;%(PreprocessorDefinitions);GLM_FORCE_DEPTH_ZERO_TO_ONE - $(ProjectDir);$(ProjectDir)\..\Externals\.packman\GLM;$(ProjectDir)\..\Externals\.packman\GLFW\include;$(ProjectDir)\..\Externals\.packman\FreeImage;$(ProjectDir)\..\Externals\.packman\ASSIMP\include;$(ProjectDir)\..\Externals\.packman\FFMpeg\include;$(ProjectDir)\..\Externals\.packman\RapidJson\include;$(ProjectDir)\..\Externals\.packman\VulkanSDK\Include;$(ProjectDir)\..\Externals\.packman\Python\Include;$(ProjectDir)\..\Externals\.packman\pybind11\include;$(ProjectDir)\..\Externals\;$(ProjectDir)\..\Externals\.packman\nvapi;$(ProjectDir)\..\Externals\.packman + $(ProjectDir);$(ProjectDir)\..\Externals\.packman\deps\include;$(ProjectDir)\..\Externals\.packman;$(ProjectDir)\..\Externals;$(ProjectDir)\..\Externals\.packman\vulkansdk\Include;$(ProjectDir)\..\Externals\.packman\python\Include;$(ProjectDir)\..\Externals\;$(ProjectDir)\..\Externals\.packman\nvapi;$(ProjectDir)\..\Externals\.packman\nanovdb\include; true true stdcpp17 @@ -844,8 +875,8 @@ Windows true - $(ProjectDir)..\Externals\.packman\FreeImage;$(ProjectDir)..\Externals\.packman\Assimp\lib\$(PlatformName)\;$(ProjectDir)..\Externals\.packman\FFMpeg\lib\$(PlatformName);$(ProjectDir)..\Externals\.packman\nvapi\amd64;$(ProjectDir)..\Externals\.packman\VulkanSDK\Lib;$(ProjectDir)..\Externals\.packman\Slang\bin\windows-x64\release;$(ProjectDir)..\Externals\.packman\GLFW\lib;$(ProjectDir)..\Externals\.packman\Python\libs;$(ProjectDir)..\Externals\.packman\WinPixEventRuntime\bin\x64 - WinPixEventRuntime.lib;glfw3dll.lib;slang.lib;Comctl32.lib;Shlwapi.lib;assimp-vc141-mt.lib;freeimage.lib;avcodec.lib;avutil.lib;avformat.lib;swscale.lib;Shcore.lib;%(AdditionalDependencies) + $(ProjectDir)..\Externals\.packman\deps\debug\lib;$(ProjectDir)..\Externals\.packman\deps\lib;$(ProjectDir)..\Externals\.packman\nvapi\amd64;$(ProjectDir)..\Externals\.packman\vulkansdk\Lib;$(ProjectDir)..\Externals\.packman\slang\bin\windows-x64\release;$(ProjectDir)..\Externals\.packman\python\libs;$(ProjectDir)..\Externals\.packman\WinPixEventRuntime\bin\x64; + WinPixEventRuntime.lib;glfw3dll.lib;slang.lib;Comctl32.lib;Shlwapi.lib;assimp-vc142-mt.lib;mikktspaced.lib;FreeImaged.lib;avcodec.lib;avutil.lib;avformat.lib;swscale.lib;Shcore.lib;DirectXTex.lib;Half-2_5_d.lib;openvdb.lib;%(AdditionalDependencies) call $(ProjectDir)\..\..\Build\prebuild.bat $(ProjectDir)\..\ $(SolutionDir) $(ProjectDir) $(PlatformName) $(PlatformShortName) $(Configuration) $(OutDir) @@ -874,7 +905,7 @@ MaxSpeed true FALCOR_DLL;IMGUI_API=__declspec(dllexport);_PROJECT_DIR_=R"($(ProjectDir))";_SCL_SECURE_NO_WARNINGS;_CRT_SECURE_NO_WARNINGS;FALCOR_D3D12;WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions);GLM_FORCE_DEPTH_ZERO_TO_ONE - $(ProjectDir);$(ProjectDir)\..\Externals\.packman\GLM;$(ProjectDir)\..\Externals\.packman\GLFW\include;$(ProjectDir)\..\Externals\.packman\FreeImage;$(ProjectDir)\..\Externals\.packman\ASSIMP\include;$(ProjectDir)\..\Externals\.packman\FFMpeg\include;$(ProjectDir)\..\Externals\.packman\RapidJson\include;$(ProjectDir)\..\Externals\.packman\VulkanSDK\Include;$(ProjectDir)\..\Externals\.packman\Python\Include;$(ProjectDir)\..\Externals\.packman\pybind11\include;$(ProjectDir)\..\Externals\;$(ProjectDir)\..\Externals\.packman\nvapi;$(ProjectDir)\..\Externals\.packman + $(ProjectDir);$(ProjectDir)\..\Externals\.packman\deps\include;$(ProjectDir)\..\Externals\.packman;$(ProjectDir)\..\Externals;$(ProjectDir)\..\Externals\.packman\vulkansdk\Include;$(ProjectDir)\..\Externals\.packman\python\Include;$(ProjectDir)\..\Externals\;$(ProjectDir)\..\Externals\.packman\nvapi;$(ProjectDir)\..\Externals\.packman\nanovdb\include; true true stdcpp17 @@ -887,8 +918,8 @@ true true true - $(ProjectDir)..\Externals\.packman\FreeImage;$(ProjectDir)..\Externals\.packman\Assimp\lib\$(PlatformName)\;$(ProjectDir)..\Externals\.packman\FFMpeg\lib\$(PlatformName);$(ProjectDir)..\Externals\.packman\nvapi\amd64;$(ProjectDir)..\Externals\.packman\VulkanSDK\Lib;$(ProjectDir)..\Externals\.packman\Slang\bin\windows-x64\release;$(ProjectDir)..\Externals\.packman\GLFW\lib;$(ProjectDir)..\Externals\.packman\Python\libs;$(ProjectDir)..\Externals\.packman\WinPixEventRuntime\bin\x64;%(AdditionalLibraryDirectories) - WinPixEventRuntime.lib;glfw3dll.lib;slang.lib;Comctl32.lib;Shlwapi.lib;assimp-vc141-mt.lib;freeimage.lib;avcodec.lib;avutil.lib;avformat.lib;swscale.lib;Shcore.lib;%(AdditionalDependencies) + $(ProjectDir)..\Externals\.packman\deps\lib;$(ProjectDir)..\Externals\.packman\nvapi\amd64;$(ProjectDir)..\Externals\.packman\vulkansdk\Lib;$(ProjectDir)..\Externals\.packman\slang\bin\windows-x64\release;$(ProjectDir)..\Externals\.packman\python\libs;$(ProjectDir)..\Externals\.packman\WinPixEventRuntime\bin\x64;$(ProjectDir)..\Externals\.packman\Cuda\lib\x64;%(AdditionalLibraryDirectories) + WinPixEventRuntime.lib;glfw3dll.lib;slang.lib;Comctl32.lib;Shlwapi.lib;assimp-vc142-mt.lib;mikktspace.lib;FreeImage.lib;avcodec.lib;avutil.lib;avformat.lib;swscale.lib;Shcore.lib;DirectXTex.lib;Half-2_5.lib;openvdb.lib;%(AdditionalDependencies) call $(ProjectDir)\..\..\Build\prebuild.bat $(ProjectDir)\..\ $(SolutionDir) $(ProjectDir) $(PlatformName) $(PlatformShortName) $(Configuration) $(OutDir) @@ -916,7 +947,7 @@ MaxSpeed true FALCOR_DLL;IMGUI_API=__declspec(dllexport);_PROJECT_DIR_=R"($(ProjectDir))";_SCL_SECURE_NO_WARNINGS;_CRT_SECURE_NO_WARNINGS;FALCOR_VK;WIN32;NDEBUG;_LIB;%(PreprocessorDefinitions);GLM_FORCE_DEPTH_ZERO_TO_ONE - $(ProjectDir);$(ProjectDir)\..\Externals\.packman\GLM;$(ProjectDir)\..\Externals\.packman\GLFW\include;$(ProjectDir)\..\Externals\.packman\FreeImage;$(ProjectDir)\..\Externals\.packman\ASSIMP\include;$(ProjectDir)\..\Externals\.packman\FFMpeg\include;$(ProjectDir)\..\Externals\.packman\RapidJson\include;$(ProjectDir)\..\Externals\.packman\VulkanSDK\Include;$(ProjectDir)\..\Externals\.packman\Python\Include;$(ProjectDir)\..\Externals\.packman\pybind11\include;$(ProjectDir)\..\Externals\;$(ProjectDir)\..\Externals\.packman\nvapi;$(ProjectDir)\..\Externals\.packman + $(ProjectDir);$(ProjectDir)\..\Externals\.packman\deps\include;$(ProjectDir)\..\Externals\.packman;$(ProjectDir)\..\Externals;$(ProjectDir)\..\Externals\.packman\vulkansdk\Include;$(ProjectDir)\..\Externals\.packman\python\Include;$(ProjectDir)\..\Externals\;$(ProjectDir)\..\Externals\.packman\nvapi;$(ProjectDir)\..\Externals\.packman\nanovdb\include; true true stdcpp17 @@ -929,8 +960,8 @@ true true true - $(ProjectDir)..\Externals\.packman\FreeImage;$(ProjectDir)..\Externals\.packman\Assimp\lib\$(PlatformName)\;$(ProjectDir)..\Externals\.packman\FFMpeg\lib\$(PlatformName);$(ProjectDir)..\Externals\.packman\nvapi\amd64;$(ProjectDir)..\Externals\.packman\VulkanSDK\Lib;$(ProjectDir)..\Externals\.packman\Slang\bin\windows-x64\release;$(ProjectDir)..\Externals\.packman\GLFW\lib;$(ProjectDir)..\Externals\.packman\Python\libs;$(ProjectDir)..\Externals\.packman\WinPixEventRuntime\bin\x64 - WinPixEventRuntime.lib;glfw3dll.lib;slang.lib;Comctl32.lib;Shlwapi.lib;assimp-vc141-mt.lib;freeimage.lib;avcodec.lib;avutil.lib;avformat.lib;swscale.lib;Shcore.lib;%(AdditionalDependencies) + $(ProjectDir)..\Externals\.packman\deps\lib$(ProjectDir)..\Externals\.packman\nvapi\amd64;$(ProjectDir)..\Externals\.packman\vulkansdk\Lib;$(ProjectDir)..\Externals\.packman\slang\bin\windows-x64\release;$(ProjectDir)..\Externals\.packman\python\libs;$(ProjectDir)..\Externals\.packman\WinPixEventRuntime\bin\x64; + WinPixEventRuntime.lib;glfw3dll.lib;slang.lib;Comctl32.lib;Shlwapi.lib;assimp-vc142-mt.lib;mikktspace.lib;FreeImage.lib;avcodec.lib;avutil.lib;avformat.lib;swscale.lib;Shcore.lib;DirectXTex.lib;Half-2_5.lib;openvdb.lib;%(AdditionalDependencies) call $(ProjectDir)\..\..\Build\prebuild.bat $(ProjectDir)\..\ $(SolutionDir) $(ProjectDir) $(PlatformName) $(PlatformShortName) $(Configuration) $(OutDir) diff --git a/Source/Falcor/Falcor.vcxproj.filters b/Source/Falcor/Falcor.vcxproj.filters index 8aa3dd6b12..363164178b 100644 --- a/Source/Falcor/Falcor.vcxproj.filters +++ b/Source/Falcor/Falcor.vcxproj.filters @@ -160,9 +160,6 @@ Scene\Lights - - Scene\Lights - Scene\Material @@ -213,12 +210,6 @@ Utils\Image - - Utils\Image - - - Utils\Image - Utils\Algorithm @@ -364,9 +355,6 @@ Scene - - Externals\mikktspace - Utils\SampleGenerators @@ -436,9 +424,6 @@ Utils\Math - - Utils\Math - Utils\Math @@ -490,9 +475,6 @@ Utils\Math - - Externals\args - Experimental\Scene\Lights @@ -508,6 +490,51 @@ Scene + + Utils + + + Scene\Curves + + + Utils\Scripting + + + Utils\Math + + + Scene\Material + + + Scene + + + Scene + + + Scene\Volume + + + Scene\Volume + + + Utils\Image + + + Core\Program + + + Experimental\Scene\Lights + + + Experimental\Scene\Volume + + + Experimental\Scene\Lights + + + Utils\Sampling + @@ -606,9 +633,6 @@ {a5e366f9-af02-4a41-a630-621da069c661} - - {32845222-2924-47c4-8b2e-43bf44e7a85a} - {6d9bd53c-0171-4f34-80ed-77d948789c2b} @@ -654,6 +678,15 @@ {5cbaba11-d15e-4d2c-8f4b-abaedd57c5c6} + + {078c1d08-979e-42d3-bee0-adbbae09f8e0} + + + {dab1a8c2-24d6-4768-9cf8-eccf8df38c81} + + + {b71b1c11-7080-4491-9571-9daad77e3a75} + @@ -902,9 +935,6 @@ Scene\Camera - - Scene\Lights - Scene\Lights @@ -941,9 +971,6 @@ Utils\Image - - Utils\Image - Utils\Algorithm @@ -1089,9 +1116,6 @@ Scene - - Externals\mikktspace - Utils\SampleGenerators @@ -1197,6 +1221,51 @@ Scene + + Scene + + + Utils + + + Scene\Curves + + + Scene\Material + + + Scene + + + Scene + + + Scene\Volume + + + Scene\Volume + + + Utils\Image + + + Core\Program + + + Utils\Math + + + Experimental\Scene\Lights + + + Experimental\Scene\Volume + + + Experimental\Scene\Lights + + + Utils\Sampling + @@ -1316,9 +1385,6 @@ Utils\Algorithm - - Scene\Lights - Utils\UI @@ -1397,9 +1463,6 @@ Scene\Material - - Scene\Lights - Scene\Lights @@ -1460,6 +1523,9 @@ Scene + + Scene + Scene\Camera @@ -1523,6 +1589,9 @@ RenderPasses\Shared\PathTracer + + Utils\Sampling\Pseudorandom + Utils\Timing @@ -1532,5 +1601,53 @@ RenderPasses\Shared\PathTracer + + Scene\Volume + + + Scene\Volume + + + Scene\Volume + + + Scene + + + Experimental\Scene\Lights + + + Experimental\Scene\Lights + + + Experimental\Scene\Volume + + + Experimental\Scene\Volume + + + Experimental\Scene\Volume + + + Experimental\Scene\Lights + + + Experimental\Scene\Material + + + Experimental\Scene\Material + + + Experimental\Scene\Material + + + Scene + + + Scene + + + Utils\Sampling + \ No newline at end of file diff --git a/Source/Falcor/FalcorExperimental.h b/Source/Falcor/FalcorExperimental.h index ac77549a30..17afd902ba 100644 --- a/Source/Falcor/FalcorExperimental.h +++ b/Source/Falcor/FalcorExperimental.h @@ -27,5 +27,6 @@ **************************************************************************/ #pragma once #include "Experimental/Scene/Lights/EnvMapSampler.h" +#include "Experimental/Scene/Lights/EnvMapLighting.h" #include "Experimental/Scene/Lights/EmissiveLightSampler.h" #include "Experimental/Scene/Lights/EmissiveUniformSampler.h" diff --git a/Source/Falcor/Raytracing/RtProgram/RtProgram.cpp b/Source/Falcor/Raytracing/RtProgram/RtProgram.cpp index ee4523063a..09f1dbf787 100644 --- a/Source/Falcor/Raytracing/RtProgram/RtProgram.cpp +++ b/Source/Falcor/Raytracing/RtProgram/RtProgram.cpp @@ -71,7 +71,6 @@ namespace Falcor logError("already have a miss shader at that index"); } - auto entryPointIndex = int32_t(mBaseDesc.mEntryPoints.size()); mBaseDesc.beginEntryPointGroup(); mBaseDesc.entryPoint(ShaderType::Miss, miss); @@ -80,37 +79,107 @@ namespace Falcor return *this; } - RtProgram::Desc& RtProgram::Desc::addHitGroup(uint32_t hitIndex, const std::string& closestHit, const std::string& anyHit, const std::string& intersection /* = "" */) + RtProgram::Desc& RtProgram::Desc::addHitGroup(uint32_t hitIndex, const std::string& closestHit, const std::string& anyHit) { - if(hitIndex >= mHitGroups.size()) + if (hitIndex >= mHitGroups.size()) { - mHitGroups.resize(hitIndex+1); + mHitGroups.resize(hitIndex + 1); } - else if(mHitGroups[hitIndex].groupIndex >= 0) + else if (mHitGroups[hitIndex].groupIndex >= 0) { logError("already have a hit group at that index"); } - auto groupIndex = int32_t(mBaseDesc.mGroups.size()); mBaseDesc.beginEntryPointGroup(); - if(closestHit.length()) + if (closestHit.length()) { mBaseDesc.entryPoint(ShaderType::ClosestHit, closestHit); } - if(anyHit.length()) + if (anyHit.length()) { mBaseDesc.entryPoint(ShaderType::AnyHit, anyHit); } - if(intersection.length()) - { - mBaseDesc.entryPoint(ShaderType::Intersection, intersection); - } DescExtra::GroupInfo info = { mBaseDesc.mActiveGroup }; mHitGroups[hitIndex] = info; return *this; } + RtProgram::Desc& RtProgram::Desc::addAABBHitGroup(uint32_t hitIndex, const std::string& closestHit, const std::string& anyHit /*= ""*/) + { + if (hitIndex >= mAABBHitGroupEntryPoints.size()) + { + mAABBHitGroupEntryPoints.resize(hitIndex + 1); + } + else + { + auto& group = mAABBHitGroupEntryPoints[hitIndex]; + if (group.closestHit != uint32_t(-1) || group.anyHit != uint32_t(-1)) + { + throw std::exception(("There is already an AABB hit group defined at index " + std::to_string(hitIndex)).c_str()); + } + } + + auto& group = mAABBHitGroupEntryPoints[hitIndex]; + + if (!closestHit.empty()) + { + group.closestHit = mBaseDesc.declareEntryPoint(ShaderType::ClosestHit, closestHit); + } + + if (!anyHit.empty()) + { + group.anyHit = mBaseDesc.declareEntryPoint(ShaderType::AnyHit, anyHit); + } + + return *this; + } + + RtProgram::Desc& RtProgram::Desc::addIntersection(uint32_t typeIndex, const std::string& intersection) + { + if (typeIndex >= mIntersectionEntryPoints.size()) + { + mIntersectionEntryPoints.resize(typeIndex + 1); + } + else if (mIntersectionEntryPoints[typeIndex] != uint32_t(-1)) + { + throw std::exception(("There is already an intersection shader defined at primitive type index " + std::to_string(typeIndex)).c_str()); + } + + assert(!intersection.empty()); + mIntersectionEntryPoints[typeIndex] = mBaseDesc.declareEntryPoint(ShaderType::Intersection, intersection); + + return *this; + } + + void RtProgram::Desc::resolveAABBHitGroups() + { + // Every intersection shader defines a custom primitive type, so we need permutations where each CHS/AHS is paired + // with each intersection shader. This is required by state object creation time so we need to generate all groups up front. + for (auto& intersection : mIntersectionEntryPoints) + { + for (auto& hitGroup : mAABBHitGroupEntryPoints) + { + auto& closestHit = hitGroup.closestHit; + auto& anyHit = hitGroup.anyHit; + + // Save index of the group being added + DescExtra::GroupInfo groupInfo; + groupInfo.groupIndex = (int32_t)mBaseDesc.mGroups.size(); + + // Add entry point group containing each shader in the hit group + Program::Desc::EntryPointGroup group; + if (closestHit != uint32_t(-1)) group.entryPoints.push_back(closestHit); + if (anyHit != uint32_t(-1)) group.entryPoints.push_back(anyHit); + group.entryPoints.push_back(intersection); // TODO: This has to go last. Why? + mBaseDesc.mGroups.push_back(group); + + // Save association of hit group index -> program entry point group + mAABBHitGroups.push_back(groupInfo); + } + } + } + RtProgram::Desc& RtProgram::Desc::addDefine(const std::string& name, const std::string& value) { mDefineList.add(name, value); @@ -123,7 +192,7 @@ namespace Falcor return *this; } - RtProgram::SharedPtr RtProgram::create(const Desc& desc, uint32_t maxPayloadSize, uint32_t maxAttributesSize) + RtProgram::SharedPtr RtProgram::create(Desc desc, uint32_t maxPayloadSize, uint32_t maxAttributesSize) { size_t rayGenCount = desc.mRayGenEntryPoints.size(); if (rayGenCount == 0) @@ -135,7 +204,18 @@ namespace Falcor throw std::exception("Can't create an RtProgram with more than one ray generation shader"); } - SharedPtr pProg = SharedPtr(new RtProgram(desc, maxPayloadSize, maxAttributesSize)); + if (!desc.mAABBHitGroupEntryPoints.empty() && (desc.mHitGroups.size() != desc.mAABBHitGroupEntryPoints.size())) + { + logWarning("There are not corresponding hit shaders for each ray type defined for custom primitives."); + } + + // Both intersection shaders and hit groups must be defined for custom primitives for it to be valid/complete + if (!desc.mIntersectionEntryPoints.empty() && !desc.mAABBHitGroupEntryPoints.empty()) + { + desc.resolveAABBHitGroups(); + } + + SharedPtr pProg = SharedPtr(new RtProgram(maxPayloadSize, maxAttributesSize)); pProg->init(desc); pProg->addDefine("_MS_DISABLE_ALPHA_TEST"); pProg->addDefine("_DEFAULT_ALPHA_TEST"); @@ -149,7 +229,7 @@ namespace Falcor mDescExtra = desc; } - RtProgram::RtProgram(const Desc& desc, uint32_t maxPayloadSize, uint32_t maxAttributesSize) + RtProgram::RtProgram(uint32_t maxPayloadSize, uint32_t maxAttributesSize) : Program() , mMaxPayloadSize(maxPayloadSize) , mMaxAttributesSize(maxAttributesSize) diff --git a/Source/Falcor/Raytracing/RtProgram/RtProgram.h b/Source/Falcor/Raytracing/RtProgram/RtProgram.h index 32458ce93d..5a58ad0c66 100644 --- a/Source/Falcor/Raytracing/RtProgram/RtProgram.h +++ b/Source/Falcor/Raytracing/RtProgram/RtProgram.h @@ -28,8 +28,8 @@ #pragma once #include "Core/Program/Program.h" #include "Core/API/RootSignature.h" -#include "../RtStateObject.h" -#include "../ShaderTable.h" +#include "Raytracing/RtStateObject.h" +#include "Raytracing/ShaderTable.h" #include "Scene/Scene.h" namespace Falcor @@ -56,9 +56,21 @@ namespace Falcor */ void setMaxTraceRecursionDepth(uint32_t maxDepth) { mMaxTraceRecursionDepth = maxDepth; } + struct HitGroupEntryPoints + { + uint32_t closestHit = -1; + uint32_t anyHit = -1; + }; + + // Stored indices for entry points in the Desc. Used to generate groups right before program creation. + std::vector mAABBHitGroupEntryPoints; + std::vector mIntersectionEntryPoints; + + // Entry points and hit groups they have been added to the Program::Desc and which entry point group they are std::vector mRayGenEntryPoints; std::vector mMissEntryPoints; std::vector mHitGroups; + std::vector mAABBHitGroups; uint32_t mMaxTraceRecursionDepth = 1; }; @@ -72,7 +84,10 @@ namespace Falcor Desc& setRayGen(const std::string& raygen); Desc& addRayGen(const std::string& raygen); Desc& addMiss(uint32_t missIndex, const std::string& miss); - Desc& addHitGroup(uint32_t hitIndex, const std::string& closestHit, const std::string& anyHit = "", const std::string& intersection = ""); + Desc& addHitGroup(uint32_t hitIndex, const std::string& closestHit, const std::string& anyHit = ""); + + Desc& addAABBHitGroup(uint32_t hitIndex, const std::string& closestHit, const std::string& anyHit = ""); + Desc& addIntersection(uint32_t typeIndex, const std::string& intersection); Desc& addDefine(const std::string& define, const std::string& value); Desc& addDefines(const DefineList& defines); @@ -84,6 +99,7 @@ namespace Falcor friend class RtProgram; void init(); + void resolveAABBHitGroups(); Program::Desc mBaseDesc; DefineList mDefineList; @@ -95,7 +111,7 @@ namespace Falcor \param[in] maxAttributesSize The maximum attributes size in bytes. \return A new object, or an exception is thrown if creation failed. */ - static RtProgram::SharedPtr create(const Desc& desc, uint32_t maxPayloadSize = FALCOR_RT_MAX_PAYLOAD_SIZE_IN_BYTES, uint32_t maxAttributesSize = D3D12_RAYTRACING_MAX_ATTRIBUTE_SIZE_IN_BYTES); + static RtProgram::SharedPtr create(Desc desc, uint32_t maxPayloadSize = FALCOR_RT_MAX_PAYLOAD_SIZE_IN_BYTES, uint32_t maxAttributesSize = D3D12_RAYTRACING_MAX_ATTRIBUTE_SIZE_IN_BYTES); /** Get the max recursion depth */ @@ -113,6 +129,9 @@ namespace Falcor uint32_t getHitProgramCount() const { return (uint32_t) mDescExtra.mHitGroups.size(); } uint32_t getHitIndex(uint32_t index) const { return mDescExtra.mHitGroups[index].groupIndex; } + uint32_t getAABBHitProgramCount() const { return (uint32_t)mDescExtra.mAABBHitGroups.size(); } + uint32_t getAABBHitIndex(uint32_t index) const { return mDescExtra.mAABBHitGroups[index].groupIndex; } + // Miss uint32_t getMissProgramCount() const { return (uint32_t) mDescExtra.mMissEntryPoints.size(); } uint32_t getMissIndex(uint32_t index) const { return mDescExtra.mMissEntryPoints[index].groupIndex; } @@ -134,7 +153,7 @@ namespace Falcor RtProgram(RtProgram const&) = delete; RtProgram& operator=(RtProgram const&) = delete; - RtProgram(const Desc& desc, uint32_t maxPayloadSize = FALCOR_RT_MAX_PAYLOAD_SIZE_IN_BYTES, uint32_t maxAttributesSize = D3D12_RAYTRACING_MAX_ATTRIBUTE_SIZE_IN_BYTES); + RtProgram(uint32_t maxPayloadSize = FALCOR_RT_MAX_PAYLOAD_SIZE_IN_BYTES, uint32_t maxAttributesSize = D3D12_RAYTRACING_MAX_ATTRIBUTE_SIZE_IN_BYTES); DescExtra mDescExtra; diff --git a/Source/Falcor/Raytracing/RtProgramVars.cpp b/Source/Falcor/Raytracing/RtProgramVars.cpp index 0b6cd653ce..675705b68e 100644 --- a/Source/Falcor/Raytracing/RtProgramVars.cpp +++ b/Source/Falcor/Raytracing/RtProgramVars.cpp @@ -84,11 +84,26 @@ namespace Falcor // rebuild on parameter changes. It might make sense for ray-gen programs // to be more flexibly allocated. // - uint32_t rayGenProgCount = uint32_t(descExtra.mRayGenEntryPoints.size()); - uint32_t missProgCount = uint32_t(descExtra.mMissEntryPoints.size()); + uint32_t rayGenProgCount = uint32_t(descExtra.mRayGenEntryPoints.size()); mRayGenVars.resize(rayGenProgCount); + for (uint32_t i = 0; i < rayGenProgCount; ++i) + { + auto& info = descExtra.mRayGenEntryPoints[i]; + if (info.groupIndex < 0) continue; + + mRayGenVars[i].pVars = EntryPointGroupVars::create(pReflector->getEntryPointGroup(info.groupIndex), info.groupIndex); + } + + uint32_t missProgCount = uint32_t(descExtra.mMissEntryPoints.size()); mMissVars.resize(missProgCount); + for (uint32_t i = 0; i < missProgCount; ++i) + { + auto& info = descExtra.mMissEntryPoints[i]; + if (info.groupIndex < 0) continue; + + mMissVars[i].pVars = EntryPointGroupVars::create(pReflector->getEntryPointGroup(info.groupIndex), info.groupIndex); + } // Hit groups are more complicated than ray generation and miss shaders. // We typically want a distinct parameter block per declared hit group @@ -99,20 +114,10 @@ namespace Falcor // uint32_t descHitGroupCount = uint32_t(descExtra.mHitGroups.size()); uint32_t blockCountPerHitGroup = mpScene->getMeshCount(); - uint32_t totalHitBlockCount = descHitGroupCount * blockCountPerHitGroup; - - mHitVars.resize(totalHitBlockCount); mDescHitGroupCount = descHitGroupCount; - for (uint32_t i = 0; i < rayGenProgCount; ++i) - { - auto& info = descExtra.mRayGenEntryPoints[i]; - if (info.groupIndex < 0) continue; - - mRayGenVars[i].pVars = EntryPointGroupVars::create(pReflector->getEntryPointGroup(info.groupIndex), info.groupIndex); - } - + mHitVars.resize(totalHitBlockCount); for (uint32_t i = 0; i < descHitGroupCount; ++i) { auto& info = descExtra.mHitGroups[i]; @@ -124,19 +129,48 @@ namespace Falcor } } - for (uint32_t i = 0; i < missProgCount; ++i) + // Hit Groups for procedural primitives are different than for triangles. + // + // There must be a set of vars for every geometry defined in the BLAS (i.e. every prim added to the scene). + // All intersection shader x hit-shader permutations are already generated in Program creation, so we look up entry points based on each + // each geometry's type index. + // + // Hit groups in the program are ordered in the following way: + // + // [ Intersection Shader 0 | Intersection Shader 1 | ... | Intersection Shader N ] + // [ with | with | | with ] + // [ Ray0 | Ray1 | ... | RayN | Ray0 | Ray1 | ... | RayN | ... | Ray0 | Ray1 | ... | RayN ] + // + // So the index of any specific hit group is calculated using: (IntersectionShaderIdx * RayCount + RayIdx) + // + // For each primitive, the hit groups for the corresponding intersection shader are looked up and appended to the vars. + // + uint32_t intersectionShaderCount = (uint32_t)descExtra.mIntersectionEntryPoints.size(); + uint32_t proceduralPrimHitVarCount = mpScene->getProceduralPrimitiveCount() * descHitGroupCount; // Total Var Count = Prim Count * Ray Count + mAABBHitVars.resize(proceduralPrimHitVarCount); + uint32_t currAABBHitVar = 0; + for (uint32_t i = 0; i < mpScene->getProceduralPrimitiveCount(); i++) { - auto& info = descExtra.mMissEntryPoints[i]; - if (info.groupIndex < 0) continue; + uint32_t intersectionShaderId = mpScene->getProceduralPrimitive(i).typeID; + assert(intersectionShaderId < intersectionShaderCount); - mMissVars[i].pVars = EntryPointGroupVars::create(pReflector->getEntryPointGroup(info.groupIndex), info.groupIndex); + // For this primitive's intersection shader group/type, get the hit vars for each ray type + for (uint32_t j = 0; j < descHitGroupCount; j++) + { + auto& info = descExtra.mAABBHitGroups[intersectionShaderId * descHitGroupCount + j]; + if (info.groupIndex < 0) continue; + + mAABBHitVars[currAABBHitVar++].pVars = EntryPointGroupVars::create(pReflector->getEntryPointGroup(info.groupIndex), info.groupIndex); + } } for (auto entryPointGroupInfo : mRayGenVars) mpEntryPointGroupVars.push_back(entryPointGroupInfo.pVars); + for (auto entryPointGroupInfo : mMissVars) + mpEntryPointGroupVars.push_back(entryPointGroupInfo.pVars); for (auto entryPointGroupInfo : mHitVars) mpEntryPointGroupVars.push_back(entryPointGroupInfo.pVars); - for (auto entryPointGroupInfo : mMissVars) + for (auto entryPointGroupInfo : mAABBHitVars) mpEntryPointGroupVars.push_back(entryPointGroupInfo.pVars); } @@ -244,6 +278,7 @@ namespace Falcor if (!applyVarsToTable(ShaderTable::SubTableType::RayGen, 0, mRayGenVars, pRtso)) return false; if (!applyVarsToTable(ShaderTable::SubTableType::Miss, 0, mMissVars, pRtso)) return false; if (!applyVarsToTable(ShaderTable::SubTableType::Hit, 0, mHitVars, pRtso)) return false; + if (!applyVarsToTable(ShaderTable::SubTableType::Hit, (uint32_t)mHitVars.size(), mAABBHitVars, pRtso)) return false; mpShaderTable->flushBuffer(pCtx); } diff --git a/Source/Falcor/Raytracing/RtProgramVars.h b/Source/Falcor/Raytracing/RtProgramVars.h index 776a789329..1f59de83de 100644 --- a/Source/Falcor/Raytracing/RtProgramVars.h +++ b/Source/Falcor/Raytracing/RtProgramVars.h @@ -50,6 +50,7 @@ namespace Falcor const EntryPointGroupVars::SharedPtr& getRayGenVars(uint32_t index = 0) { return mRayGenVars[index].pVars; } const EntryPointGroupVars::SharedPtr& getMissVars(uint32_t rayID) { return mMissVars[rayID].pVars; } const EntryPointGroupVars::SharedPtr& getHitVars(uint32_t rayID, uint32_t meshID) { return mHitVars[meshID * mDescHitGroupCount + rayID].pVars; } + const EntryPointGroupVars::SharedPtr& getAABBHitVars(uint32_t rayID, uint32_t primitiveIndex) { return mAABBHitVars[primitiveIndex * mDescHitGroupCount + rayID].pVars; } bool apply(RenderContext* pCtx, RtStateObject* pRtso); @@ -58,6 +59,7 @@ namespace Falcor uint32_t getRayGenVarsCount() const { return uint32_t(mRayGenVars.size()); } uint32_t getMissVarsCount() const { return uint32_t(mMissVars.size()); } uint32_t getTotalHitVarsCount() const { return uint32_t(mHitVars.size()); } + uint32_t getAABBHitVarsCount() const { return uint32_t(mAABBHitVars.size()); } uint32_t getDescHitGroupCount() const { return mDescHitGroupCount; } Scene::SharedPtr getSceneForGeometryIndices() const { return mpSceneForGeometryIndices.lock(); } @@ -84,8 +86,9 @@ namespace Falcor mutable ShaderTable::SharedPtr mpShaderTable; VarsVector mRayGenVars; - VarsVector mHitVars; VarsVector mMissVars; + VarsVector mHitVars; + VarsVector mAABBHitVars; RtVarsContext::SharedPtr mpRtVarsHelper; diff --git a/Source/Falcor/Raytracing/RtStateObject.cpp b/Source/Falcor/Raytracing/RtStateObject.cpp index 5fab533469..66db14a153 100644 --- a/Source/Falcor/Raytracing/RtStateObject.cpp +++ b/Source/Falcor/Raytracing/RtStateObject.cpp @@ -29,9 +29,7 @@ #include "RtStateObject.h" #include "RtStateObjectHelper.h" #include "Utils/StringUtils.h" -#include "Core/API/Device.h" #include "Core/API/D3D12/D3D12NvApiExDesc.h" -#include "ShaderTable.h" namespace Falcor { @@ -48,6 +46,19 @@ namespace Falcor SharedPtr pState = SharedPtr(new RtStateObject(desc)); RtStateObjectHelper rtsoHelper; + std::set configuredShaders; + + auto configureShader = [&](ID3DBlobPtr pBlob, const std::wstring& shaderName, RtEntryPointGroupKernels* pEntryPointGroup) + { + if (pBlob && !shaderName.empty() && !configuredShaders.count(shaderName)) + { + rtsoHelper.addProgramDesc(pBlob, shaderName); + rtsoHelper.addLocalRootSignature(&shaderName, 1, pEntryPointGroup->getLocalRootSignature()->getApiHandle().GetInterfacePtr()); + rtsoHelper.addShaderConfig(&shaderName, 1, pEntryPointGroup->getMaxPayloadSize(), pEntryPointGroup->getMaxAttributesSize()); + configuredShaders.insert(shaderName); + } + }; + // Pipeline config rtsoHelper.addPipelineConfig(desc.mMaxTraceRecursionDepth); @@ -85,25 +96,11 @@ namespace Falcor const std::wstring& ahsExport = pAhs ? string_2_wstring(pAhs->getEntryPoint()) : L""; const std::wstring& chsExport = pChs ? string_2_wstring(pChs->getEntryPoint()) : L""; - rtsoHelper.addHitProgramDesc(pAhsBlob, ahsExport, pChsBlob, chsExport, pIntersectionBlob, intersectionExport, exportName); - - if (intersectionExport.size()) - { - rtsoHelper.addLocalRootSignature(&intersectionExport, 1, pEntryPointGroup->getLocalRootSignature()->getApiHandle().GetInterfacePtr()); - rtsoHelper.addShaderConfig(&intersectionExport, 1, pEntryPointGroup->getMaxPayloadSize(), pEntryPointGroup->getMaxAttributesSize()); - } - - if (ahsExport.size()) - { - rtsoHelper.addLocalRootSignature(&ahsExport, 1, pEntryPointGroup->getLocalRootSignature()->getApiHandle().GetInterfacePtr()); - rtsoHelper.addShaderConfig(&ahsExport, 1, pEntryPointGroup->getMaxPayloadSize(), pEntryPointGroup->getMaxAttributesSize()); - } - - if (chsExport.size()) - { - rtsoHelper.addLocalRootSignature(&chsExport, 1, pEntryPointGroup->getLocalRootSignature()->getApiHandle().GetInterfacePtr()); - rtsoHelper.addShaderConfig(&chsExport, 1, pEntryPointGroup->getMaxPayloadSize(), pEntryPointGroup->getMaxAttributesSize()); - } + configureShader(pIntersectionBlob, intersectionExport, pEntryPointGroup); + configureShader(pAhsBlob, ahsExport, pEntryPointGroup); + configureShader(pChsBlob, chsExport, pEntryPointGroup); + + rtsoHelper.addHitGroupDesc(ahsExport, chsExport, intersectionExport, exportName); } break; @@ -111,7 +108,6 @@ namespace Falcor { const std::wstring& exportName = string_2_wstring(pEntryPointGroup->getExportName()); - const Shader* pShader = pEntryPointGroup->getShaderByIndex(0); rtsoHelper.addProgramDesc(pShader->getD3DBlob(), exportName); @@ -136,7 +132,7 @@ namespace Falcor MAKE_SMART_COM_PTR(ID3D12StateObjectProperties); ID3D12StateObjectPropertiesPtr pRtsoProps = pState->getApiHandle(); - for( const auto& pBaseEntryPointGroup : pKernels->getUniqueEntryPointGroups() ) + for (const auto& pBaseEntryPointGroup : pKernels->getUniqueEntryPointGroups()) { assert(dynamic_cast(pBaseEntryPointGroup.get())); auto pEntryPointGroup = static_cast(pBaseEntryPointGroup.get()); diff --git a/Source/Falcor/Raytracing/RtStateObjectHelper.h b/Source/Falcor/Raytracing/RtStateObjectHelper.h index c787d11868..78112c0c0c 100644 --- a/Source/Falcor/Raytracing/RtStateObjectHelper.h +++ b/Source/Falcor/Raytracing/RtStateObjectHelper.h @@ -50,9 +50,9 @@ namespace Falcor mDirty = true; } - void addHitProgramDesc(ID3DBlobPtr pAhsBlob, const std::wstring& ahsExportName, ID3DBlobPtr pChsBlob, const std::wstring& chsExportName, ID3DBlobPtr pIntersectionBlob, const std::wstring& intersectionExportName, const std::wstring& name) + void addHitGroupDesc(const std::wstring& ahsExportName, const std::wstring& chsExportName, const std::wstring& intersectionExportName, const std::wstring& name) { - addSubobject(pAhsBlob, ahsExportName, pChsBlob, chsExportName, pIntersectionBlob, intersectionExportName, name); + addSubobject(ahsExportName, chsExportName, intersectionExportName, name); mDirty = true; } @@ -121,7 +121,7 @@ namespace Falcor subobject.pDesc = &config; } virtual ~PipelineConfig() = default; - D3D12_RAYTRACING_PIPELINE_CONFIG config = {}; + D3D12_RAYTRACING_PIPELINE_CONFIG config = {}; }; struct ProgramDesc : public RtStateSubobjectBase @@ -154,40 +154,38 @@ namespace Falcor std::wstring exportName; }; - struct HitProgramDesc : public RtStateSubobjectBase + struct HitGroupDesc : public RtStateSubobjectBase { - HitProgramDesc( - ID3DBlobPtr pAhsBlob, const std::wstring& ahsExportName, - ID3DBlobPtr pChsBlob, const std::wstring& chsExportName, - ID3DBlobPtr pIntersectionBlob, const std::wstring& intersectionExportName, - const std::wstring& name) : - anyHitShader(pAhsBlob, ahsExportName), - closestHitShader(pChsBlob, chsExportName), - intersectionShader(pIntersectionBlob, intersectionExportName), - exportName(name) + HitGroupDesc( + const std::wstring& ahsExportName, + const std::wstring& chsExportName, + const std::wstring& intersectionExportName, + const std::wstring& name) + : exportName(name) + , ahsName(ahsExportName) + , chsName(chsExportName) + , intersectionName(intersectionExportName) { - desc.IntersectionShaderImport = pIntersectionBlob ? intersectionShader.exportName.c_str() : nullptr; - desc.AnyHitShaderImport = pAhsBlob ? anyHitShader.exportName.c_str() : nullptr; - desc.ClosestHitShaderImport = pChsBlob ? closestHitShader.exportName.c_str() : nullptr; + desc.Type = intersectionName.empty() ? D3D12_HIT_GROUP_TYPE_TRIANGLES : D3D12_HIT_GROUP_TYPE_PROCEDURAL_PRIMITIVE; + desc.IntersectionShaderImport = intersectionName.empty() ? nullptr : intersectionName.c_str(); + desc.AnyHitShaderImport = ahsName.empty() ? nullptr : ahsName.c_str(); + desc.ClosestHitShaderImport = chsName.empty() ? nullptr : chsName.c_str(); desc.HitGroupExport = exportName.c_str(); subobject.Type = D3D12_STATE_SUBOBJECT_TYPE_HIT_GROUP; subobject.pDesc = &desc; } - virtual ~HitProgramDesc() = default; + virtual ~HitGroupDesc() = default; std::wstring exportName; - ProgramDesc anyHitShader; - ProgramDesc closestHitShader; - ProgramDesc intersectionShader; + std::wstring ahsName; + std::wstring chsName; + std::wstring intersectionName; D3D12_HIT_GROUP_DESC desc = {}; virtual void addToVector(SubobjectVector& vec) override { - if (desc.AnyHitShaderImport) anyHitShader.addToVector(vec); - if (desc.ClosestHitShaderImport) closestHitShader.addToVector(vec); - if (desc.IntersectionShaderImport) intersectionShader.addToVector(vec); vec.push_back(subobject); } }; diff --git a/Source/Falcor/Raytracing/ShaderTable.cpp b/Source/Falcor/Raytracing/ShaderTable.cpp index 058190c6a2..a7bf6662b7 100644 --- a/Source/Falcor/Raytracing/ShaderTable.cpp +++ b/Source/Falcor/Raytracing/ShaderTable.cpp @@ -68,7 +68,7 @@ namespace Falcor mSubTables[uint32_t(SubTableType::RayGen)].recordCount = pVars->getRayGenVarsCount(); mSubTables[uint32_t(SubTableType::Miss)].recordCount = pVars->getMissVarsCount(); - mSubTables[uint32_t(SubTableType::Hit)].recordCount = pVars->getTotalHitVarsCount(); + mSubTables[uint32_t(SubTableType::Hit)].recordCount = pVars->getTotalHitVarsCount() + pVars->getAABBHitVarsCount(); for (auto pUniqueEntryPointGroup : pKernels->getUniqueEntryPointGroups()) { diff --git a/Source/Falcor/RenderGraph/BasePasses/RasterScenePass.cpp b/Source/Falcor/RenderGraph/BasePasses/RasterScenePass.cpp index b6ad507a22..d57be8076c 100644 --- a/Source/Falcor/RenderGraph/BasePasses/RasterScenePass.cpp +++ b/Source/Falcor/RenderGraph/BasePasses/RasterScenePass.cpp @@ -56,7 +56,7 @@ namespace Falcor void RasterScenePass::renderScene(RenderContext* pContext, const Fbo::SharedPtr& pDstFbo) { mpState->setFbo(pDstFbo); - mpScene->render(pContext, mpState.get(), mpVars.get()); + mpScene->rasterize(pContext, mpState.get(), mpVars.get()); } bool RasterScenePass::onMouseEvent(const MouseEvent& mouseEvent) diff --git a/Source/Falcor/RenderGraph/RenderGraph.cpp b/Source/Falcor/RenderGraph/RenderGraph.cpp index 375fece083..a56fe09710 100644 --- a/Source/Falcor/RenderGraph/RenderGraph.cpp +++ b/Source/Falcor/RenderGraph/RenderGraph.cpp @@ -785,8 +785,6 @@ namespace Falcor if (!pPass) throw std::exception(("Can't create a render pass named '" + passName + "'. Make sure the required DLL was loaded.").c_str()); return pPass; }; - renderPass.def(pybind11::init(createRenderPass), "name"_a, "dict"_a = pybind11::dict()); // PYTHONDEPRECATED - m.def("createPass", createRenderPass, "name"_a, "dict"_a = pybind11::dict()); const auto& loadPassLibrary = [](const std::string& library) diff --git a/Source/Falcor/RenderGraph/RenderGraphExe.cpp b/Source/Falcor/RenderGraph/RenderGraphExe.cpp index ebbcba9187..18e9471672 100644 --- a/Source/Falcor/RenderGraph/RenderGraphExe.cpp +++ b/Source/Falcor/RenderGraph/RenderGraphExe.cpp @@ -53,7 +53,7 @@ namespace Falcor { // If you are thinking about displaying the profiler results next to the group label, it won't work. Since the times change every frame, IMGUI thinks it's a different group and will not expand it const auto& desc = pPass->getDesc(); - if (desc.size()) passGroup.tooltip(desc.c_str()); + if (desc.size()) passGroup.tooltip(desc); pPass->renderUI(passGroup); } } diff --git a/Source/Falcor/RenderGraph/RenderGraphIR.cpp b/Source/Falcor/RenderGraph/RenderGraphIR.cpp index 0f8554bf5d..dfb871ad17 100644 --- a/Source/Falcor/RenderGraph/RenderGraphIR.cpp +++ b/Source/Falcor/RenderGraph/RenderGraphIR.cpp @@ -43,7 +43,6 @@ namespace Falcor const char* RenderGraphIR::kUpdatePass = "updatePass"; const char* RenderGraphIR::kLoadPassLibrary = "loadRenderPassLibrary"; const char* RenderGraphIR::kCreatePass = "createPass"; - const char* RenderGraphIR::kRenderPass = "RenderPass"; // PYTHONDEPRECATED const char* RenderGraphIR::kRenderGraph = "RenderGraph"; std::string RenderGraphIR::getFuncName(const std::string& graphName) @@ -59,7 +58,7 @@ namespace Falcor mIR += "def " + getFuncName(mName) + "():\n"; mIndentation = " "; mGraphPrefix += mIndentation; - mIR += mIndentation + "g" + " = " + Scripting::makeFunc(kRenderGraph, mName); + mIR += mIndentation + "g" + " = " + ScriptWriter::makeFunc(kRenderGraph, mName); } mGraphPrefix += "g."; }; @@ -74,52 +73,52 @@ namespace Falcor mIR += mIndentation + passName + " = "; if (dictionary.size()) { - mIR += Scripting::makeFunc(RenderGraphIR::kCreatePass, passClass, dictionary); + mIR += ScriptWriter::makeFunc(RenderGraphIR::kCreatePass, passClass, dictionary); } else { - mIR += Scripting::makeFunc(RenderGraphIR::kCreatePass, passClass); + mIR += ScriptWriter::makeFunc(RenderGraphIR::kCreatePass, passClass); } - mIR += mGraphPrefix + RenderGraphIR::kAddPass + "(" + passName + ", " + Scripting::getArgString(passName) + ")\n"; + mIR += mGraphPrefix + RenderGraphIR::kAddPass + "(" + passName + ", " + ScriptWriter::getArgString(passName) + ")\n"; } void RenderGraphIR::updatePass(const std::string& passName, const Dictionary& dictionary) { - mIR += mGraphPrefix + Scripting::makeFunc(RenderGraphIR::kUpdatePass, passName, dictionary); + mIR += mGraphPrefix + ScriptWriter::makeFunc(RenderGraphIR::kUpdatePass, passName, dictionary); } void RenderGraphIR::removePass(const std::string& passName) { - mIR += mGraphPrefix + Scripting::makeFunc(RenderGraphIR::kRemovePass, passName); + mIR += mGraphPrefix + ScriptWriter::makeFunc(RenderGraphIR::kRemovePass, passName); } void RenderGraphIR::addEdge(const std::string& src, const std::string& dst) { - mIR += mGraphPrefix + Scripting::makeFunc(RenderGraphIR::kAddEdge, src, dst); + mIR += mGraphPrefix + ScriptWriter::makeFunc(RenderGraphIR::kAddEdge, src, dst); } void RenderGraphIR::removeEdge(const std::string& src, const std::string& dst) { - mIR += mGraphPrefix + Scripting::makeFunc(RenderGraphIR::kRemoveEdge, src, dst); + mIR += mGraphPrefix + ScriptWriter::makeFunc(RenderGraphIR::kRemoveEdge, src, dst); } void RenderGraphIR::markOutput(const std::string& name) { - mIR += mGraphPrefix + Scripting::makeFunc(RenderGraphIR::kMarkOutput, name); + mIR += mGraphPrefix + ScriptWriter::makeFunc(RenderGraphIR::kMarkOutput, name); } void RenderGraphIR::unmarkOutput(const std::string& name) { - mIR += mGraphPrefix + Scripting::makeFunc(RenderGraphIR::kUnmarkOutput, name); + mIR += mGraphPrefix + ScriptWriter::makeFunc(RenderGraphIR::kUnmarkOutput, name); } void RenderGraphIR::loadPassLibrary(const std::string& name) { - mIR += mIndentation + Scripting::makeFunc(RenderGraphIR::kLoadPassLibrary, name); + mIR += mIndentation + ScriptWriter::makeFunc(RenderGraphIR::kLoadPassLibrary, name); } void RenderGraphIR::autoGenEdges() { - mIR += mGraphPrefix + Scripting::makeFunc(RenderGraphIR::kAutoGenEdges); + mIR += mGraphPrefix + ScriptWriter::makeFunc(RenderGraphIR::kAutoGenEdges); } } diff --git a/Source/Falcor/RenderGraph/RenderGraphImportExport.cpp b/Source/Falcor/RenderGraph/RenderGraphImportExport.cpp index fbfa9ec3ec..0e63c0baf0 100644 --- a/Source/Falcor/RenderGraph/RenderGraphImportExport.cpp +++ b/Source/Falcor/RenderGraph/RenderGraphImportExport.cpp @@ -71,9 +71,10 @@ namespace Falcor updateGraphStrings(graphName, filename, funcName); std::string custom; if (funcName.size()) custom += "\n" + graphName + '=' + funcName + "()"; + // TODO: Rendergraph scripts should be executed in an isolated scripting context. runScriptFile(filename, custom); - auto pGraph = Scripting::getGlobalContext().getObject(graphName); + auto pGraph = Scripting::getDefaultContext().getObject(graphName); if (!pGraph) throw("Unspecified error"); pGraph->setName(graphName); @@ -92,8 +93,9 @@ namespace Falcor { try { + // TODO: Rendergraph scripts should be executed in an isolated scripting context. runScriptFile(filename, {}); - auto scriptObj = Scripting::getGlobalContext().getObjects(); + auto scriptObj = Scripting::getDefaultContext().getObjects(); std::vector res; res.reserve(scriptObj.size()); diff --git a/Source/Falcor/RenderGraph/RenderGraphUI.cpp b/Source/Falcor/RenderGraph/RenderGraphUI.cpp index cb5c6266d4..d879095eca 100644 --- a/Source/Falcor/RenderGraph/RenderGraphUI.cpp +++ b/Source/Falcor/RenderGraph/RenderGraphUI.cpp @@ -570,7 +570,8 @@ namespace Falcor if (mRecordUpdates) mUpdateCommands += newCommands; // update reference graph to check if valid before sending to next - Scripting::getGlobalContext().setObject("g", mpRenderGraph); + // TODO: Rendergraph scripts should be executed in an isolated scripting context. + Scripting::getDefaultContext().setObject("g", mpRenderGraph); Scripting::runScript(newCommands); if(newCommands.size()) mLogString += newCommands; diff --git a/Source/Falcor/RenderGraph/RenderPass.h b/Source/Falcor/RenderGraph/RenderPass.h index 8eb70bbc81..3c73ebae53 100644 --- a/Source/Falcor/RenderGraph/RenderPass.h +++ b/Source/Falcor/RenderGraph/RenderPass.h @@ -59,6 +59,10 @@ namespace Falcor */ InternalDictionary& getDictionary() const { return (*mpDictionary); } + /** Get the global dictionary. You can use it to pass data between different passes + */ + InternalDictionary::SharedPtr getDictionaryPtr() const { return mpDictionary; } + /** Get the default dimensions used for Texture2Ds (when `0` is specified as the dimensions in `RenderPassReflection`) */ const uint2& getDefaultTextureDims() const { return mDefaultTexDims; } diff --git a/Source/Falcor/RenderGraph/RenderPassLibrary.cpp b/Source/Falcor/RenderGraph/RenderPassLibrary.cpp index 2c118a3f98..b13411e7e5 100644 --- a/Source/Falcor/RenderGraph/RenderPassLibrary.cpp +++ b/Source/Falcor/RenderGraph/RenderPassLibrary.cpp @@ -186,6 +186,10 @@ namespace Falcor func(lib); for (auto& p : lib.mPasses) registerInternal(p.second.className, p.second.desc, p.second.func, l); + + // Re-import falcor package to current (executing) scripting context. + auto ctx = Scripting::getCurrentContext(); + if (Scripting::isRunning()) Scripting::runScript("from falcor import *", ctx); } void RenderPassLibrary::releaseLibrary(const std::string& filename) diff --git a/Source/Falcor/RenderGraph/RenderPassReflection.cpp b/Source/Falcor/RenderGraph/RenderPassReflection.cpp index 9acacec2e6..274fbf2556 100644 --- a/Source/Falcor/RenderGraph/RenderPassReflection.cpp +++ b/Source/Falcor/RenderGraph/RenderPassReflection.cpp @@ -195,6 +195,15 @@ namespace Falcor return nullptr; } + RenderPassReflection::Field* RenderPassReflection::getField(const std::string& name) + { + for (auto& field : mFields) + { + if (field.getName() == name) return &field; + } + return nullptr; + } + RenderPassReflection::Field& RenderPassReflection::Field::merge(const RenderPassReflection::Field& other) { auto err = [&](const std::string& msg) diff --git a/Source/Falcor/RenderGraph/RenderPassReflection.h b/Source/Falcor/RenderGraph/RenderPassReflection.h index 0277f28e16..9aca101872 100644 --- a/Source/Falcor/RenderGraph/RenderPassReflection.h +++ b/Source/Falcor/RenderGraph/RenderPassReflection.h @@ -135,6 +135,7 @@ namespace Falcor size_t getFieldCount() const { return mFields.size(); } const Field* getField(size_t f) const { return f <= mFields.size() ? &mFields[f] : nullptr; } const Field* getField(const std::string& name) const; + Field* getField(const std::string& name); Field& addField(const Field& field); bool operator==(const RenderPassReflection& other) const; diff --git a/Source/Falcor/RenderGraph/RenderPassStandardFlags.h b/Source/Falcor/RenderGraph/RenderPassStandardFlags.h index 0584314a49..8fa802c49d 100644 --- a/Source/Falcor/RenderGraph/RenderPassStandardFlags.h +++ b/Source/Falcor/RenderGraph/RenderPassStandardFlags.h @@ -48,5 +48,9 @@ namespace Falcor */ static const char kRenderPassPRNGDimension[] = "_prngDimension"; + /** Adjust shading normals on primary hits. + */ + static const char kRenderPassGBufferAdjustShadingNormals[] = "_gbufferAdjustShadingNormals"; + enum_class_operators(RenderPassRefreshFlags); } diff --git a/Source/Falcor/RenderPasses/Shared/PathTracer/InteriorList.slang b/Source/Falcor/RenderPasses/Shared/PathTracer/InteriorList.slang index f08f949e38..d6c2693cee 100644 --- a/Source/Falcor/RenderPasses/Shared/PathTracer/InteriorList.slang +++ b/Source/Falcor/RenderPasses/Shared/PathTracer/InteriorList.slang @@ -27,11 +27,11 @@ **************************************************************************/ /** Slots on the interior list have the following bit layout. - 0-26 materialID - 27 active bit + 0-27 materialID 28-31 nestedPriority We put the nestedPriority in the highest value bits in order to simplify sorting the list. + Internally the value 0 is reserved for empty slots. */ #ifndef INTERIOR_LIST_SLOT_COUNT @@ -42,29 +42,27 @@ struct InteriorList { static const uint kNoMaterial = 0xffffffff; - static const uint kMaterialBits = 27; - static const uint kActiveBits = 1; + static const uint kMaterialBits = 28; static const uint kNestedPriorityBits = 4; static const uint kMaterialOffset = 0; - static const uint kActiveOffset = kMaterialOffset + kMaterialBits; - static const uint kNestedPriorityOffset = kActiveOffset + kActiveBits; + static const uint kNestedPriorityOffset = kMaterialOffset + kMaterialBits; static const uint kMaterialMask = ((1 << kMaterialBits) - 1) << kMaterialOffset; - static const uint kActiveMask = ((1 << kActiveBits) - 1) << kActiveOffset; static const uint kNestedPriorityMask = ((1 << kNestedPriorityBits) - 1) << kNestedPriorityOffset; + static const uint kMaxNestedPriority = ((1 << kNestedPriorityBits) - 1); + uint slots[INTERIOR_LIST_SLOT_COUNT]; - /** Make an active material slot given the material. + /** Make an active material slot given the material and priority. \param[in] materialID Material ID. - \param[in] nestedPriority Nested priority. + \param[in] nestedPriority Nested priority, 0 is reserved for empty slots. \return Returns the encoded slot. */ uint makeSlot(uint materialID, uint nestedPriority) { - // Get nested priority of given material. - return (nestedPriority << kNestedPriorityOffset) | kActiveMask | (materialID & kMaterialMask); + return (nestedPriority << kNestedPriorityOffset) | (materialID & kMaterialMask); } /** Check if a slot is active. @@ -73,7 +71,7 @@ struct InteriorList */ bool isSlotActive(uint slot) { - return slot & kActiveMask; + return slot != 0; } /** Check if the interior list is empty. @@ -131,24 +129,28 @@ struct InteriorList /** Check if an intersection with a given surface is a true intersection. True intersection occurs if nested priority of intersected mesh is equal or higher priority than the highest nested priority on the interior list. - \param[in] nestedPriority Nested priority of intersected surface. + \param[in] nestedPriority Nested priority of intersected surface, with 0 reserved for the highest possible priority. \return Returns true if intersection is a true intersection, false otherwise. */ bool isTrueIntersection(uint nestedPriority) { - // Get nested priority of given material. - return nestedPriority >= getTopNestedPriority(); + // Compare nested priority to current top of stack. + return nestedPriority == 0 || nestedPriority >= getTopNestedPriority(); } /** Handle an intersection with a given material: If material is already on interior list -> remove it. If material is not on interior list -> add it. \param[in] materialID Material ID of intersected material. - \param[in] nestedPriority Nested priority of intersected surface. + \param[in] nestedPriority Nested priority of intersected surface, with 0 reserved for the highest possible priority. \param[in] entering True if material is entered, false if material is left. */ [mutating] void handleIntersection(uint materialID, uint nestedPriority, bool entering) { + // Remap priority 0 to the highest priority to allow sorting by high->low priority, + // and as internally 0 is reserved for empty slots. + if (nestedPriority == 0) nestedPriority = kMaxNestedPriority; + for (uint slotIndex = 0; slotIndex < INTERIOR_LIST_SLOT_COUNT; ++slotIndex) { uint slot = slots[slotIndex]; diff --git a/Source/Falcor/RenderPasses/Shared/PathTracer/LoadShadingData.slang b/Source/Falcor/RenderPasses/Shared/PathTracer/LoadShadingData.slang index 50ae77394c..c0d8d140cd 100644 --- a/Source/Falcor/RenderPasses/Shared/PathTracer/LoadShadingData.slang +++ b/Source/Falcor/RenderPasses/Shared/PathTracer/LoadShadingData.slang @@ -42,21 +42,21 @@ import Utils.Math.MathHelpers; #if USE_VBUFFER // V-buffer inputs -Texture2D gVBuffer; +Texture2D gVBuffer; #else import Experimental.Scene.Material.MaterialHelpers; // G-buffer inputs Texture2D gWorldPosition; Texture2D gWorldShadingNormal; -Texture2D gWorldShadingTangent; // Optional -Texture2D gWorldView; // Optional +Texture2D gWorldShadingTangent; // Optional +Texture2D gWorldView; // Optional Texture2D gWorldFaceNormal; Texture2D gMaterialDiffuseOpacity; Texture2D gMaterialSpecularRoughness; Texture2D gMaterialEmissive; Texture2D gMaterialExtraParams; -Texture2D gVBuffer; // Optional +Texture2D gVBuffer; // Optional #endif #define isValid(name) (is_valid_##name != 0) @@ -87,7 +87,7 @@ float3 getPrimaryRayDir(uint2 pixel, uint2 frameDim, const Camera camera) \param[in] frameDim Frame dimensions in pixel. \param[in] camera Current camera. \param[out] sd ShadingData struct. - \param[out] hit HitInfo struct returned with geometry fetched from vbuffer if available. + \param[out] hit HitInfo struct returned with geometry fetched from vbuffer if available. Only valid if true is returned. \return True if the pixel has valid data (not a background pixel). Note sd.V is always valid. */ bool loadShadingData(uint2 pixel, uint2 frameDim, const Camera camera, out ShadingData sd, out HitInfo hit) @@ -103,16 +103,13 @@ bool loadShadingData(uint2 pixel, uint2 frameDim, const Camera camera, out Shadi // Evaluate Falcor's material parameters at the hit point. // TODO: Implement texLOD to enable texture filtering in prepareShadingData(). VertexData v = gScene.getVertexData(hit); - const uint materialID = gScene.getMaterialID(hit.meshInstanceID); + const uint materialID = gScene.getMaterialID(hit.instanceID); sd = prepareShadingData(v, materialID, gScene.materials[materialID], gScene.materialResources[materialID], -rayDir, 0.f); - // Compute tangent space if it is invalid. - if (!(dot(sd.T, sd.T) > 0.f)) // Note: Comparison written so that NaNs trigger - { - sd.T = perp_stark(sd.N); - sd.B = cross(sd.N, sd.T); - } - + // Adjust shading normals if GBuffer pass has flag enabled. +#if GBUFFER_ADJUST_SHADING_NORMALS + adjustShadingNormal(sd, v); +#endif valid = true; } #else @@ -132,15 +129,16 @@ bool loadShadingData(uint2 pixel, uint2 frameDim, const Camera camera, out Shadi matParams.extraParams = gMaterialExtraParams[pixel]; sd = prepareShadingData(geoParams, matParams); + valid = true; + if (isValid(gVBuffer)) { - if (hit.decode(gVBuffer[pixel])) sd.materialID = gScene.getMaterialID(hit.meshInstanceID); - } - else - { - hit = { HitInfo::kInvalidIndex }; + if (hit.decode(gVBuffer[pixel])) + { + sd.materialID = gScene.getMaterialID(hit.instanceID); + } + else valid = false; // Shouldn't happen } - valid = true; } #endif diff --git a/Source/Falcor/RenderPasses/Shared/PathTracer/PathTracer.cpp b/Source/Falcor/RenderPasses/Shared/PathTracer/PathTracer.cpp index ee0652ba58..6295631d1f 100644 --- a/Source/Falcor/RenderPasses/Shared/PathTracer/PathTracer.cpp +++ b/Source/Falcor/RenderPasses/Shared/PathTracer/PathTracer.cpp @@ -52,12 +52,12 @@ namespace Falcor { "mtlSpecRough", "gMaterialSpecularRoughness", "Material specular color (xyz) and roughness (w)" }, { "mtlEmissive", "gMaterialEmissive", "Material emissive color (xyz)" }, { "mtlParams", "gMaterialExtraParams", "Material parameters (IoR, flags etc)" }, - { "vbuffer", "gVBuffer", "Visibility buffer in packed 64-bit format", true /* optional */, ResourceFormat::RG32Uint }, + { "vbuffer", "gVBuffer", "Visibility buffer in packed format", true /* optional */, ResourceFormat::Unknown }, }; const Falcor::ChannelList kVBufferInputChannels = { - { "vbuffer", "gVBuffer", "Visibility buffer in packed 64-bit format", false, ResourceFormat::RG32Uint }, + { "vbuffer", "gVBuffer", "Visibility buffer in packed format", false, ResourceFormat::Unknown }, }; const Falcor::ChannelList kPixelStatsOutputChannels = @@ -78,6 +78,7 @@ namespace Falcor { { (uint32_t)EmissiveLightSamplerType::Uniform, "Uniform" }, { (uint32_t)EmissiveLightSamplerType::LightBVH, "LightBVH" }, + { (uint32_t)EmissiveLightSamplerType::Power, "Power" }, }; const Gui::DropdownList kRayFootprintModeList = @@ -173,6 +174,9 @@ namespace Falcor dirty |= widget.var("Threshold", mSharedParams.clampThreshold, 0.f, std::numeric_limits::max(), mSharedParams.clampThreshold * 0.01f); } + dirty |= widget.checkbox("Adjust shading normals on secondary hits", mSharedParams.adjustShadingNormals); + widget.tooltip("Enables adjustment of the shading normals to reduce the risk of black pixels due to back-facing vectors.\nDoes not apply to primary hits which is configured in GBuffer.", true); + dirty |= widget.checkbox("Force alpha to 1.0", mSharedParams.forceAlphaOne); widget.tooltip("Forces the output alpha channel to 1.0.\n" "Otherwise the background will be 0.0 and the foreground 1.0 to allow separate compositing.", true); @@ -267,6 +271,8 @@ namespace Falcor case EmissiveLightSamplerType::LightBVH: mLightBVHSamplerOptions = std::static_pointer_cast(mpEmissiveSampler)->getOptions(); break; + case EmissiveLightSamplerType::Power: + break; default: should_not_get_here(); } @@ -290,7 +296,7 @@ namespace Falcor } } - dirty |= samplingGroup.checkbox("Use lights in volumes", mSharedParams.useLightsInVolumes); + dirty |= samplingGroup.checkbox("Use lights in dielectric volumes", mSharedParams.useLightsInDielectricVolumes); samplingGroup.tooltip("Use lights inside of volumes (transmissive materials). We typically don't want this because lights are occluded by the interface.", true); dirty |= samplingGroup.checkbox("Disable caustics", mSharedParams.disableCaustics); @@ -379,12 +385,6 @@ namespace Falcor logError("'specularRoughnessThreshold' has invalid value. Clamping to the range [0,1]."); mSharedParams.specularRoughnessThreshold = std::clamp(mSharedParams.specularRoughnessThreshold, 0.f, 1.f); } - - if (mSharedParams.useLightsInVolumes == false) - { - logWarning("'useLightsInVolumes' can cause instability when disabled (todo fix). Forcing the value to true."); - mSharedParams.useLightsInVolumes = true; - } } bool PathTracer::initLights(RenderContext* pRenderContext) @@ -397,12 +397,6 @@ namespace Falcor // If we have no scene, we're done. if (mpScene == nullptr) return true; - // Create environment map sampler if scene uses an environment map. - if (mpScene->getEnvMap()) - { - mpEnvMapSampler = EnvMapSampler::create(pRenderContext, mpScene->getEnvMap()); - } - return true; } @@ -418,8 +412,34 @@ namespace Falcor return false; } + bool lightingChanged = false; + + if (is_set(mpScene->getUpdates(), Scene::UpdateFlags::EnvMapChanged)) + { + mpEnvMapSampler = nullptr; + lightingChanged = true; + } + // Configure light sampling. mUseAnalyticLights = mpScene->useAnalyticLights(); + + // Configure env map sampling. + if (mpScene->useEnvLight()) + { + if (!mpEnvMapSampler) + { + mpEnvMapSampler = EnvMapSampler::create(pRenderContext, mpScene->getEnvMap()); + lightingChanged = true; + } + } + else + { + if (mpEnvMapSampler) + { + mpEnvMapSampler = nullptr; + lightingChanged = true; + } + } mUseEnvLight = mpScene->useEnvLight() && mpEnvMapSampler != nullptr; // Request the light collection if emissive lights are enabled. @@ -428,7 +448,6 @@ namespace Falcor mpScene->getLightCollection(pRenderContext); } - bool lightingChanged = false; if (!mpScene->useEmissiveLights()) { mUseEmissiveLights = mUseEmissiveSampler = false; @@ -456,6 +475,9 @@ namespace Falcor case EmissiveLightSamplerType::LightBVH: mpEmissiveSampler = LightBVHSampler::create(pRenderContext, mpScene, mLightBVHSamplerOptions); break; + case EmissiveLightSamplerType::Power: + mpEmissiveSampler = EmissivePowerSampler::create(pRenderContext, mpScene); + break; default: logError("Unknown emissive light sampler type"); } @@ -511,6 +533,9 @@ namespace Falcor mOptionsChanged = false; } + // Check if GBuffer has adjusted shading normals enabled. + mGBufferAdjustShadingNormals = dict.getValue(Falcor::kRenderPassGBufferAdjustShadingNormals, false); + // If we have no scene, just clear the outputs and return. if (!mpScene) { @@ -599,6 +624,7 @@ namespace Falcor defines.add("MAX_BOUNCES", std::to_string(mSharedParams.maxBounces)); defines.add("MAX_NON_SPECULAR_BOUNCES", std::to_string(mSharedParams.maxNonSpecularBounces)); defines.add("USE_ALPHA_TEST", mSharedParams.useAlphaTest ? "1" : "0"); + defines.add("ADJUST_SHADING_NORMALS", mSharedParams.adjustShadingNormals ? "1" : "0"); defines.add("FORCE_ALPHA_ONE", mSharedParams.forceAlphaOne ? "1" : "0"); defines.add("USE_ANALYTIC_LIGHTS", mUseAnalyticLights ? "1" : "0"); defines.add("USE_EMISSIVE_LIGHTS", mUseEmissiveLights ? "1" : "0"); @@ -611,12 +637,14 @@ namespace Falcor defines.add("USE_RUSSIAN_ROULETTE", mSharedParams.useRussianRoulette ? "1" : "0"); defines.add("USE_VBUFFER", mSharedParams.useVBuffer ? "1" : "0"); defines.add("USE_NESTED_DIELECTRICS", mSharedParams.useNestedDielectrics ? "1" : "0"); - defines.add("USE_LIGHTS_IN_VOLUMES", mSharedParams.useLightsInVolumes ? "1" : "0"); + defines.add("USE_LIGHTS_IN_DIELECTRIC_VOLUMES", mSharedParams.useLightsInDielectricVolumes ? "1" : "0"); defines.add("DISABLE_CAUSTICS", mSharedParams.disableCaustics ? "1" : "0"); // Defines in MaterialShading.slang. defines.add("_USE_LEGACY_SHADING_CODE", mSharedParams.useLegacyBSDF ? "1" : "0"); + defines.add("GBUFFER_ADJUST_SHADING_NORMALS", mGBufferAdjustShadingNormals ? "1" : "0"); + // Defines for ray footprint. defines.add("RAY_FOOTPRINT_MODE", std::to_string(mSharedParams.rayFootprintMode)); defines.add("RAY_CONE_MODE", std::to_string(mSharedParams.rayConeMode)); @@ -638,6 +666,7 @@ namespace Falcor params.field(useVBuffer); params.field(useAlphaTest); + params.field(adjustShadingNormals); params.field(forceAlphaOne); params.field(clampSamples); @@ -657,7 +686,7 @@ namespace Falcor params.field(useLegacyBSDF); params.field(useNestedDielectrics); - params.field(useLightsInVolumes); + params.field(useLightsInDielectricVolumes); params.field(disableCaustics); // Ray footprint diff --git a/Source/Falcor/RenderPasses/Shared/PathTracer/PathTracer.h b/Source/Falcor/RenderPasses/Shared/PathTracer/PathTracer.h index ead2586fbe..f1be8190b6 100644 --- a/Source/Falcor/RenderPasses/Shared/PathTracer/PathTracer.h +++ b/Source/Falcor/RenderPasses/Shared/PathTracer/PathTracer.h @@ -33,6 +33,7 @@ #include "Experimental/Scene/Lights/EnvMapSampler.h" #include "Experimental/Scene/Lights/EmissiveUniformSampler.h" #include "Experimental/Scene/Lights/LightBVHSampler.h" +#include "Experimental/Scene/Lights/EmissivePowerSampler.h" #include "RenderGraph/RenderPassHelpers.h" #include "PathTracerParams.slang" #include "PixelStats.h" @@ -98,7 +99,8 @@ namespace Falcor bool mUseEmissiveLights = false; ///< True if emissive lights should be taken into account. See compile-time constant in StaticParams.slang. bool mUseEmissiveSampler = false; ///< True if emissive light sampler should be used for the current frame. See compile-time constant in StaticParams.slang. uint32_t mMaxRaysPerPixel = 0; ///< Maximum number of rays per pixel that will be traced. This is computed based on the current configuration. - bool mIsRayFootprintSupported = true; ///< Globally enable/disable ray footprint. Requires v-buffer. Set to false if any requirement is not met. + bool mGBufferAdjustShadingNormals = false; ///< True if GBuffer/VBuffer has adjusted shading normals enabled. + bool mIsRayFootprintSupported = true;///< Globally enable/disable ray footprint. Requires v-buffer. Set to false if any requirement is not met. // Scripting #define serialize(var) \ diff --git a/Source/Falcor/RenderPasses/Shared/PathTracer/PathTracerHelpers.slang b/Source/Falcor/RenderPasses/Shared/PathTracer/PathTracerHelpers.slang index 9b03f7414b..41a79df0a8 100644 --- a/Source/Falcor/RenderPasses/Shared/PathTracer/PathTracerHelpers.slang +++ b/Source/Falcor/RenderPasses/Shared/PathTracer/PathTracerHelpers.slang @@ -38,6 +38,7 @@ __exported import Experimental.Scene.Lights.EmissiveLightSampler; __exported import Experimental.Scene.Lights.LightHelpers; __exported import RenderPasses.Shared.PathTracer.PathData; __exported import RenderPasses.Shared.PathTracer.PathTracerParams; +__exported import Utils.Sampling.SampleGeneratorInterface; static const float3 kDefaultBackgroundColor = float3(0, 0, 0); static const float kRayTMax = FLT_MAX; @@ -182,7 +183,7 @@ float evalPdfScatter(const ShadingData sd, const float3 dir) \param[out] ls Generated light sample. Only valid if true is returned. \return True if a sample was generated, false otherwise. */ -bool sampleSceneLights(const PathTracerParams params, const EnvMapSampler envMapSampler, const EmissiveLightSampler emissiveSampler, const ShadingData sd, const float3 rayOrigin, const uint numSamples, inout SampleGenerator sg, out SceneLightSample ls) +bool sampleSceneLights(const PathTracerParams params, const EnvMapSampler envMapSampler, const EmissiveLightSampler emissiveSampler, const ShadingData sd, const float3 rayOrigin, const uint numSamples, inout S sg, out SceneLightSample ls) { // Set relative probabilities of the different sampling techniques. // TODO: These should use estimated irradiance from each light type. Using equal probabilities for now. @@ -279,7 +280,7 @@ bool sampleSceneLights(const PathTracerParams params, const EnvMapSampler envMap // Sample emissive lights. TriangleLightSample lightSample; - bool valid = emissiveSampler.sampleLight(rayOrigin, sd.N, sg, lightSample); + bool valid = emissiveSampler.sampleLight(rayOrigin, sd.N, true, sg, lightSample); // Reject sample if lower hemisphere. if (!valid || dot(sd.N, lightSample.dir) < kMinCosTheta) return false; @@ -323,14 +324,15 @@ bool sampleSceneLights(const PathTracerParams params, const EnvMapSampler envMap \param[in] sd Shading data. \param[in] i The sample index in the range [0, kLightSamplesPerVertex). \param[in,out] path Path data. The path flags will be updated to enable the i:th shadow ray if a sample was generated. + \param[in,out] sg Sample generator. \param[in,out] shadowRay Shadow ray parameters and unoccluded contribution for the generated sample. \return True if a sample was generated, false otherwise. */ -bool generateShadowRay(const PathTracerParams params, const EnvMapSampler envMapSampler, const EmissiveLightSampler emissiveSampler, const ShadingData sd, const uint i, inout PathData path, inout ShadowRay shadowRay) +bool generateShadowRay(const PathTracerParams params, const EnvMapSampler envMapSampler, const EmissiveLightSampler emissiveSampler, const ShadingData sd, const uint i, inout PathData path, inout S sg, inout ShadowRay shadowRay) { // Sample the scene lights. SceneLightSample ls; - bool valid = sampleSceneLights(params, envMapSampler, emissiveSampler, sd, path.origin, kLightSamplesPerVertex, path.sg, ls); + bool valid = sampleSceneLights(params, envMapSampler, emissiveSampler, sd, path.origin, kLightSamplesPerVertex, sg, ls); if (valid && any(ls.Li > 0.f)) { @@ -354,15 +356,16 @@ bool generateShadowRay(const PathTracerParams params, const EnvMapSampler envMap \param[in] params Path tracer parameters. \param[in] sd Shading data. \param[in,out] path Path data. + \param[in,out] sg Sample generator. \return True if a sample was generated and path should continue, false otherwise. */ -bool generateScatterRay(const PathTracerParams params, const ShadingData sd, inout PathData path) +bool generateScatterRay(const PathTracerParams params, const ShadingData sd, inout PathData path, inout S sg) { // Generate next path segment. BSDFSample result; bool valid; - if (kUseBRDFSampling) valid = sampleBSDF(sd, path.sg, result); - else valid = sampleBSDF_Reference(sd, path.sg, result); + if (kUseBRDFSampling) valid = sampleBSDF(sd, sg, result); + else valid = sampleBSDF_Reference(sd, sg, result); if (valid) { diff --git a/Source/Falcor/RenderPasses/Shared/PathTracer/PathTracerParams.slang b/Source/Falcor/RenderPasses/Shared/PathTracer/PathTracerParams.slang index e2a3f6d9b1..ae0fc8e462 100644 --- a/Source/Falcor/RenderPasses/Shared/PathTracer/PathTracerParams.slang +++ b/Source/Falcor/RenderPasses/Shared/PathTracer/PathTracerParams.slang @@ -37,6 +37,7 @@ static const uint kMaxPathFlagsBits = 16; static const uint kMaxPathLengthBits = 8; static const uint kMaxPathLength = (1 << kMaxPathLengthBits) - 1; static const uint kMaxLightSamplesPerVertex = 8; +static const uint kMaxRejectedHits = 16; // Maximum number of rejected hits along a path. The path is terminated if the limit is reached to avoid getting stuck in pathological cases. // Define ray indices. static const uint32_t kRayTypeScatter = 0; @@ -70,7 +71,7 @@ struct PathTracerParams int useVBuffer = 1; ///< Use a V-buffer as input. Use compile-time constant kUseVBuffer (or preprocessor define USE_VBUFFER) in shader. int useAlphaTest = 1; ///< Use alpha testing on non-opaque triangles. Use compile-time constant kUseAlphaTest (or preprocessor define USE_ALPHA_TEST) in shader. - int _removed; + int adjustShadingNormals = false; ///< Adjust shading normals on secondary hits. Use compile-time constant kAdjustShadingNormals in shader. int forceAlphaOne = true; ///< Force the alpha channel to 1.0. Otherwise background will have alpha 0.0 and covered samples 1.0 to allow compositing. Use compile-time constant kForceAlphaOne in shader. int clampSamples = false; ///< Clamp the per-path contribution to 'clampThreshold' to reduce fireflies. @@ -91,7 +92,7 @@ struct PathTracerParams int useLegacyBSDF = false; ///< Use legacy BRDF sampling code (no support for specular transmission). int useNestedDielectrics = true; ///< Use algorithm to handle nested dielectric materials. Use compile-time constant kUseNestedDielectrics in shader. - int useLightsInVolumes = false; ///< Use lights inside of volumes (transmissive materials). We typically don't want this because lights are occluded by the interface. Use compile-time constant kUseLightsInVolumes in shader. + int useLightsInDielectricVolumes = false; ///< Use lights inside of volumes (transmissive materials). We typically don't want this because lights are occluded by the interface. Use compile-time constant kUseLightsInDielectricVolumes in shader. int disableCaustics = false; ///< Disable sampling of caustics. Use compile-time constant kDisableCaustics in shader. // Ray footprint diff --git a/Source/Falcor/RenderPasses/Shared/PathTracer/PixelStats.cpp b/Source/Falcor/RenderPasses/Shared/PathTracer/PixelStats.cpp index 1f76de093e..fcff79bdd5 100644 --- a/Source/Falcor/RenderPasses/Shared/PathTracer/PixelStats.cpp +++ b/Source/Falcor/RenderPasses/Shared/PathTracer/PixelStats.cpp @@ -61,13 +61,13 @@ namespace Falcor mStatsBuffersValid = false; mRayCountTextureValid = false; - if (mStatsEnabled) + if (mEnabled) { // Create parallel reduction helper. if (!mpParallelReduction) { mpParallelReduction = ComputeParallelReduction::create(); - mpReductionResult = Buffer::create((kRayTypeCount + 1) * sizeof(uint4), ResourceBindFlags::None, Buffer::CpuAccess::Read); + mpReductionResult = Buffer::create((kRayTypeCount + 3) * sizeof(uint4), ResourceBindFlags::None, Buffer::CpuAccess::Read); } // Prepare stats buffers. @@ -78,6 +78,8 @@ namespace Falcor mpStatsRayCount[i] = Texture::create2D(frameDim.x, frameDim.y, ResourceFormat::R32Uint, 1, 1, nullptr, ResourceBindFlags::ShaderResource | ResourceBindFlags::UnorderedAccess); } mpStatsPathLength = Texture::create2D(frameDim.x, frameDim.y, ResourceFormat::R32Uint, 1, 1, nullptr, ResourceBindFlags::ShaderResource | ResourceBindFlags::UnorderedAccess); + mpStatsPathVertexCount = Texture::create2D(frameDim.x, frameDim.y, ResourceFormat::R32Uint, 1, 1, nullptr, ResourceBindFlags::ShaderResource | ResourceBindFlags::UnorderedAccess); + mpStatsVolumeLookupCount = Texture::create2D(frameDim.x, frameDim.y, ResourceFormat::R32Uint, 1, 1, nullptr, ResourceBindFlags::ShaderResource | ResourceBindFlags::UnorderedAccess); } for (uint32_t i = 0; i < kRayTypeCount; i++) @@ -85,6 +87,8 @@ namespace Falcor pRenderContext->clearUAV(mpStatsRayCount[i]->getUAV().get(), uint4(0, 0, 0, 0)); } pRenderContext->clearUAV(mpStatsPathLength->getUAV().get(), uint4(0, 0, 0, 0)); + pRenderContext->clearUAV(mpStatsPathVertexCount->getUAV().get(), uint4(0, 0, 0, 0)); + pRenderContext->clearUAV(mpStatsVolumeLookupCount->getUAV().get(), uint4(0, 0, 0, 0)); } } @@ -93,7 +97,7 @@ namespace Falcor assert(mRunning); mRunning = false; - if (mStatsEnabled) + if (mEnabled) { // Create fence first time we need it. if (!mpFence) mpFence = GpuFence::create(); @@ -104,6 +108,8 @@ namespace Falcor mpParallelReduction->execute(pRenderContext, mpStatsRayCount[i], ComputeParallelReduction::Type::Sum, nullptr, mpReductionResult, i * sizeof(uint4)); } mpParallelReduction->execute(pRenderContext, mpStatsPathLength, ComputeParallelReduction::Type::Sum, nullptr, mpReductionResult, kRayTypeCount * sizeof(uint4)); + mpParallelReduction->execute(pRenderContext, mpStatsPathVertexCount, ComputeParallelReduction::Type::Sum, nullptr, mpReductionResult, (kRayTypeCount + 1) * sizeof(uint4)); + mpParallelReduction->execute(pRenderContext, mpStatsVolumeLookupCount, ComputeParallelReduction::Type::Sum, nullptr, mpReductionResult, (kRayTypeCount + 2) * sizeof(uint4)); // Submit command list and insert signal. pRenderContext->flush(false); @@ -118,7 +124,7 @@ namespace Falcor { assert(mRunning); - if (mStatsEnabled) + if (mEnabled) { pProgram->addDefine("_PIXEL_STATS_ENABLED"); for (uint32_t i = 0; i < kRayTypeCount; i++) @@ -126,6 +132,8 @@ namespace Falcor var["gStatsRayCount"][i] = mpStatsRayCount[i]; } var["gStatsPathLength"] = mpStatsPathLength; + var["gStatsPathVertexCount"] = mpStatsPathVertexCount; + var["gStatsVolumeLookupCount"] = mpStatsVolumeLookupCount; } else { @@ -136,22 +144,40 @@ namespace Falcor void PixelStats::renderUI(Gui::Widgets& widget) { // Configuration. - widget.checkbox("Pixel stats", mStatsEnabled); + widget.checkbox("Ray stats", mEnabled); widget.tooltip("Collects ray tracing traversal stats on the GPU.\nNote that this option slows down the performance."); // Fetch data and show stats if available. copyStatsToCPU(); if (mStatsValid) { + widget.text("Stats:"); + widget.tooltip("All averages are per pixel on screen.\n" + "\n" + "The path vertex count includes:\n" + " - Primary hits\n" + " - Secondary hits on geometry\n" + " - Secondary misses on envmap\n" + "\n" + "Note that the camera/sensor is not included, nor misses when there is no envmap (no-op miss shader)."); + std::ostringstream oss; oss << "Path length (avg): " << std::fixed << std::setprecision(3) << mStats.avgPathLength << "\n" - << "Total rays (avg): " << std::fixed << std::setprecision(3) << mStats.avgTotalRaysPerPixel << "\n" - << "Shadow rays (avg): " << std::fixed << std::setprecision(3) << mStats.avgShadowRaysPerPixel << "\n" - << "ClosestHit rays (avg): " << std::fixed << std::setprecision(3) << mStats.avgClosestHitRaysPerPixel << "\n" + << "Path vertices (avg): " << std::fixed << std::setprecision(3) << mStats.avgPathVertices << "\n" + << "Total rays (avg): " << std::fixed << std::setprecision(3) << mStats.avgTotalRays << "\n" + << "Shadow rays (avg): " << std::fixed << std::setprecision(3) << mStats.avgShadowRays << "\n" + << "ClosestHit rays (avg): " << std::fixed << std::setprecision(3) << mStats.avgClosestHitRays << "\n" + << "Path vertices: " << mStats.pathVertices << "\n" << "Total rays: " << mStats.totalRays << "\n" << "Shadow rays: " << mStats.shadowRays << "\n" - << "ClosestHit rays: " << mStats.closestHitRays << "\n"; - widget.text(oss.str().c_str()); + << "ClosestHit rays: " << mStats.closestHitRays << "\n" + << "Volume lookups: " << mStats.volumeLookups << "\n" + << "Volume lookups (avg): " << mStats.avgVolumeLookups << "\n"; + + widget.checkbox("Enable logging", mEnableLogging); + widget.text(oss.str()); + + if (mEnableLogging) logInfo("\n" + oss.str()); } } @@ -207,6 +233,18 @@ namespace Falcor return mStatsBuffersValid ? mpStatsPathLength : nullptr; } + const Texture::SharedPtr PixelStats::getPathVertexCountTexture() const + { + assert(!mRunning); + return mStatsBuffersValid ? mpStatsPathVertexCount : nullptr; + } + + const Texture::SharedPtr PixelStats::getVolumeLookupCountTexture() const + { + assert(!mRunning); + return mStatsBuffersValid ? mpStatsVolumeLookupCount : nullptr; + } + void PixelStats::copyStatsToCPU() { assert(!mRunning); @@ -216,27 +254,63 @@ namespace Falcor mpFence->syncCpu(); mWaitingForData = false; - if (mStatsEnabled) + if (mEnabled) { // Map the stats buffer. const uint4* result = static_cast(mpReductionResult->map(Buffer::MapType::Read)); assert(result); const uint32_t totalPathLength = result[kRayTypeCount].x; + const uint32_t totalPathVertices = result[kRayTypeCount + 1].x; + const uint32_t totalVolumeLookups = result[kRayTypeCount + 2].x; const uint32_t numPixels = mFrameDim.x * mFrameDim.y; assert(numPixels > 0); mStats.shadowRays = result[(uint32_t)PixelStatsRayType::Shadow].x; mStats.closestHitRays = result[(uint32_t)PixelStatsRayType::ClosestHit].x; mStats.totalRays = mStats.shadowRays + mStats.closestHitRays; - mStats.avgShadowRaysPerPixel = (float)mStats.shadowRays / numPixels; - mStats.avgClosestHitRaysPerPixel = (float)mStats.closestHitRays / numPixels; - mStats.avgTotalRaysPerPixel = (float)mStats.totalRays / numPixels; + mStats.pathVertices = totalPathVertices; + mStats.volumeLookups = totalVolumeLookups; + mStats.avgShadowRays = (float)mStats.shadowRays / numPixels; + mStats.avgClosestHitRays = (float)mStats.closestHitRays / numPixels; + mStats.avgTotalRays = (float)mStats.totalRays / numPixels; mStats.avgPathLength = (float)totalPathLength / numPixels; + mStats.avgPathVertices = (float)totalPathVertices / numPixels; + mStats.avgVolumeLookups = (float)totalVolumeLookups / numPixels; mpReductionResult->unmap(); mStatsValid = true; } } } + + pybind11::dict PixelStats::Stats::toPython() const + { + pybind11::dict d; + + d["shadowRays"] = shadowRays; + d["closestHitRays"] = closestHitRays; + d["totalRays"] = totalRays; + d["pathVertices"] = pathVertices; + d["volumeLookups"] = volumeLookups; + d["avgShadowRays"] = avgShadowRays; + d["avgClosestHitRays"] = avgClosestHitRays; + d["avgTotalRays"] = avgTotalRays; + d["avgPathLength"] = avgPathLength; + d["avgPathVertices"] = avgPathVertices; + d["avgVolumeLookups"] = avgVolumeLookups; + + return d; + } + + SCRIPT_BINDING(PixelStats) + { + pybind11::class_ pixelStats(m, "PixelStats"); + pixelStats.def_property("enabled", &PixelStats::isEnabled, &PixelStats::setEnabled); + pixelStats.def_property_readonly("stats", [](PixelStats* pPixelStats) { + PixelStats::Stats stats; + pPixelStats->getStats(stats); + return stats.toPython(); + }); + } } diff --git a/Source/Falcor/RenderPasses/Shared/PathTracer/PixelStats.cs.slang b/Source/Falcor/RenderPasses/Shared/PathTracer/PixelStats.cs.slang index 659e7d22a4..085c534615 100644 --- a/Source/Falcor/RenderPasses/Shared/PathTracer/PixelStats.cs.slang +++ b/Source/Falcor/RenderPasses/Shared/PathTracer/PixelStats.cs.slang @@ -46,7 +46,7 @@ void main(uint3 dispatchThreadId : SV_DispatchThreadID) uint totalRays = 0; for (uint i = 0; i < (uint)PixelStatsRayType::Count; i++) { - totalRays = gStatsRayCount[i][pixel]; + totalRays += gStatsRayCount[i][pixel]; } gStatsRayCountTotal[pixel] = totalRays; } diff --git a/Source/Falcor/RenderPasses/Shared/PathTracer/PixelStats.h b/Source/Falcor/RenderPasses/Shared/PathTracer/PixelStats.h index da78db73d6..399c5ca528 100644 --- a/Source/Falcor/RenderPasses/Shared/PathTracer/PixelStats.h +++ b/Source/Falcor/RenderPasses/Shared/PathTracer/PixelStats.h @@ -46,10 +46,18 @@ namespace Falcor uint32_t shadowRays = 0; uint32_t closestHitRays = 0; uint32_t totalRays = 0; - float avgShadowRaysPerPixel = 0.f; - float avgClosestHitRaysPerPixel = 0.f; - float avgTotalRaysPerPixel = 0.f; + uint32_t pathVertices = 0; + uint32_t volumeLookups = 0; + float avgShadowRays = 0.f; + float avgClosestHitRays = 0.f; + float avgTotalRays = 0.f; float avgPathLength = 0.f; + float avgPathVertices = 0.f; + float avgVolumeLookups = 0.f; + + /** Convert to python dict. + */ + pybind11::dict toPython() const; }; using SharedPtr = std::shared_ptr; @@ -57,8 +65,8 @@ namespace Falcor static SharedPtr create(); - void setEnabled(bool enabled) { mStatsEnabled = enabled; } - bool isEnabled() const { return mStatsEnabled; } + void setEnabled(bool enabled) { mEnabled = enabled; } + bool isEnabled() const { return mEnabled; } void beginFrame(RenderContext* pRenderContext, const uint2& frameDim); void endFrame(RenderContext* pRenderContext); @@ -84,6 +92,16 @@ namespace Falcor */ const Texture::SharedPtr getPathLengthTexture() const; + /** Returns the per-pixel path vertex count texture or nullptr if not available. + \return Texture in R32Uint format containing per-pixel path vertex counts, or nullptr if not available. + */ + const Texture::SharedPtr getPathVertexCountTexture() const; + + /** Returns the per-pixel volume lookup count texture or nullptr if not available. + \return Texture in R32Uint format containing per-pixel volume lookup counts, or nullptr if not available. + */ + const Texture::SharedPtr getVolumeLookupCountTexture() const; + protected: PixelStats(); void copyStatsToCPU(); @@ -97,7 +115,8 @@ namespace Falcor GpuFence::SharedPtr mpFence; ///< GPU fence for sychronizing readback. // Configuration - bool mStatsEnabled = false; ///< UI variable to turn logging on/off. + bool mEnabled = false; ///< Enable pixel statistics. + bool mEnableLogging = false; ///< Enable printing to logfile. // Runtime data bool mRunning = false; ///< True inbetween begin() / end() calls. @@ -111,6 +130,8 @@ namespace Falcor Texture::SharedPtr mpStatsRayCount[kRayTypeCount]; ///< Buffers for per-pixel ray count stats. Texture::SharedPtr mpStatsRayCountTotal; ///< Buffer for per-pixel total ray count. Only generated if getRayCountTexture() is called. Texture::SharedPtr mpStatsPathLength; ///< Buffer for per-pixel path length stats. + Texture::SharedPtr mpStatsPathVertexCount; ///< Buffer for per-pixel path vertex count. + Texture::SharedPtr mpStatsVolumeLookupCount; ///< Buffer for per-pixel volume lookup count. bool mStatsBuffersValid = false; ///< True if per-pixel stats buffers contain valid data. ComputePass::SharedPtr mpComputeRayCount; ///< Pass for computing per-pixel total ray count. diff --git a/Source/Falcor/RenderPasses/Shared/PathTracer/PixelStats.slang b/Source/Falcor/RenderPasses/Shared/PathTracer/PixelStats.slang index b9a849cd02..91328fac14 100644 --- a/Source/Falcor/RenderPasses/Shared/PathTracer/PixelStats.slang +++ b/Source/Falcor/RenderPasses/Shared/PathTracer/PixelStats.slang @@ -37,6 +37,8 @@ __exported import PixelStatsShared; RWTexture2D gStatsRayCount[(uint)PixelStatsRayType::Count]; // Per-pixel ray count stats. RWTexture2D gStatsPathLength; // Per-pixel path length. +RWTexture2D gStatsPathVertexCount; // Per-pixel path vertex count. +RWTexture2D gStatsVolumeLookupCount; // Per-pixel volume lookup count. #ifdef _PIXEL_STATS_ENABLED static uint2 gPixelStatsPixel; @@ -62,3 +64,17 @@ void logPathLength(uint pathLength) gStatsPathLength[gPixelStatsPixel] = pathLength; #endif } + +void logPathVertex() +{ +#ifdef _PIXEL_STATS_ENABLED + InterlockedAdd(gStatsPathVertexCount[gPixelStatsPixel], 1); +#endif +} + +void logVolumeLookup() +{ +#ifdef _PIXEL_STATS_ENABLED + InterlockedAdd(gStatsVolumeLookupCount[gPixelStatsPixel], 1); +#endif +} diff --git a/Source/Falcor/RenderPasses/Shared/PathTracer/RayFootprint.slang b/Source/Falcor/RenderPasses/Shared/PathTracer/RayFootprint.slang index 136241d32a..f411ff5f54 100644 --- a/Source/Falcor/RenderPasses/Shared/PathTracer/RayFootprint.slang +++ b/Source/Falcor/RenderPasses/Shared/PathTracer/RayFootprint.slang @@ -118,9 +118,9 @@ extension RayFootprint float surfaceSpreadAngle; if (kRayConeMode == RayConeMode::Combo) { - const float4x4 worldMat = gScene.getWorldMatrix(hit.meshInstanceID); - const float3x3 worldInvTransposeMat = gScene.getInverseTransposeWorldMatrix(hit.meshInstanceID); - const uint3 vertexIndices = gScene.getIndices(hit.meshInstanceID, hit.primitiveIndex); + const float4x4 worldMat = gScene.getWorldMatrix(hit.instanceID); + const float3x3 worldInvTransposeMat = gScene.getInverseTransposeWorldMatrix(hit.instanceID); + const uint3 vertexIndices = gScene.getIndices(hit.instanceID, hit.primitiveIndex); const float3 barycentrics = hit.getBarycentricWeights(); float3 unnormalizedN, normals[3], dNdx, dNdy, edge1, edge2; float2 txcoords[3], dBarydx, dBarydy, dUVdx, dUVdy; @@ -143,7 +143,7 @@ extension RayFootprint { // Various curvature estimation modes are available. cf. RAY_FOOTPRINT_CURVATURE_ESTIMATOR_PRIMARY define at the top of the file. RAY_FOOTPRINT_CURVATURE_ESTIMATOR_PRIMARY(rayDir, rayConeWidth, screenSpacePixelSpreadAngle); // Declares tce, the triangle curvature estimator to use. - float curvature = gScene.computeCurvatureGeneric(hit.meshInstanceID, hit.primitiveIndex, tce); + float curvature = gScene.computeCurvatureGeneric(hit.instanceID, hit.primitiveIndex, tce); surfaceSpreadAngle = computeSpreadAngleFromCurvatureIso(curvature, rayConeWidth, rayDir, normal); } @@ -172,7 +172,7 @@ extension RayFootprint void hitSurface(inout VertexData v, StaticVertexData triangleVertices[3], HitInfo hit, float3 rayOrg, float3 rayDir) { // Adds Ray Cone data to previously fetched VertexData. - v.coneTexLODValue = computeRayConeTriangleLODValue(triangleVertices, hit.meshInstanceID, float3x3(gScene.getWorldMatrix(hit.meshInstanceID))); + v.coneTexLODValue = computeRayConeTriangleLODValue(triangleVertices, hit.instanceID, float3x3(gScene.getWorldMatrix(hit.instanceID))); float hitT = length(v.posW - rayOrg); // Propagate the cone to current surface. @@ -184,7 +184,7 @@ extension RayFootprint // Compute texture LOD for prepareShadingData(). float lambda = rayCone.computeLOD(v.coneTexLODValue, rayDir, v.normalW); - const uint materialID = gScene.getMaterialID(hit.meshInstanceID); + const uint materialID = gScene.getMaterialID(hit.instanceID); float3 viewDir = -rayDir; return prepareShadingDataUsingRayConesLOD(v, materialID, gScene.materials[materialID], gScene.materialResources[materialID], viewDir, lambda); } @@ -202,7 +202,7 @@ extension RayFootprint float rayConeWidth = rayCone.getWidth(); // Declares tce, the triangle curvature estimator to use. RAY_FOOTPRINT_CURVATURE_ESTIMATOR_SECONDARY(rayDir, rayConeWidth, rayCone.getSpreadAngle()); - float curvature = gScene.computeCurvatureGeneric(hit.meshInstanceID, hit.primitiveIndex, tce); + float curvature = gScene.computeCurvatureGeneric(hit.instanceID, hit.primitiveIndex, tce); surfaceSpreadAngle = computeSpreadAngleFromCurvatureIso(curvature, rayConeWidth, rayDirIn, normal); } @@ -249,9 +249,9 @@ extension RayFootprint res.rayDiff = res.rayDiff.propagate(worldPos.xyz, rayDir, hitT, faceNormal); // Propagate the ray differential to the current hit point. // Bounce using primary hit geometry. - const float4x4 worldMat = gScene.getWorldMatrix(hit.meshInstanceID); - const float3x3 worldInvTransposeMat = gScene.getInverseTransposeWorldMatrix(hit.meshInstanceID); - const uint3 vertexIndices = gScene.getIndices(hit.meshInstanceID, hit.primitiveIndex); + const float4x4 worldMat = gScene.getWorldMatrix(hit.instanceID); + const float3x3 worldInvTransposeMat = gScene.getInverseTransposeWorldMatrix(hit.instanceID); + const uint3 vertexIndices = gScene.getIndices(hit.instanceID, hit.primitiveIndex); const float3 barycentrics = hit.getBarycentricWeights(); float3 unnormalizedN, normals[3], dNdx, dNdy, edge1, edge2; float2 txcoords[3], dBarydx, dBarydy, dUVdx, dUVdy; @@ -270,7 +270,7 @@ extension RayFootprint { // Propagate to hit point. float hitT = length(v.posW - rayOrg); - float3 geometricNormal = gScene.getFaceNormalW(hit.meshInstanceID, hit.primitiveIndex); + float3 geometricNormal = gScene.getFaceNormalW(hit.instanceID, hit.primitiveIndex); this.rayDiff = this.rayDiff.propagate(rayOrg, rayDir, hitT, geometricNormal); // Propagate the ray differential to the current hit point. } @@ -280,13 +280,13 @@ extension RayFootprint // Get hit point adapted parameters. float2 dUVdx, dUVdy; // Ray differential variables for the texture lookup. - prepareRayDiffAtHitPoint(v, triangleVertices, hit.getBarycentricWeights(), rayDir, hitT, gScene.getWorldMatrix(hit.meshInstanceID), gScene.getInverseTransposeWorldMatrix(hit.meshInstanceID), + prepareRayDiffAtHitPoint(v, triangleVertices, hit.getBarycentricWeights(), rayDir, hitT, gScene.getWorldMatrix(hit.instanceID), gScene.getInverseTransposeWorldMatrix(hit.instanceID), this.rayDiff, dUVdx, dUVdy); // Compute shading data. float3 viewDir = -rayDir; ShadingData sd; - const uint materialID = gScene.getMaterialID(hit.meshInstanceID); + const uint materialID = gScene.getMaterialID(hit.instanceID); if (kRayFootprintMode == TexLODMode::RayDiffsIsotropic) { // When using prepareShadingDataUsingRayDiffsLOD(), the texture sampler will compute a single lambda for texture LOD @@ -310,7 +310,7 @@ extension RayFootprint // TODO: Avoid recomputing everything (dUVdx, dUVdy and dBarydx, dBarydy) by keeping intermediate variables live in registers. float2 dUVdx, dUVdy; // Ray differential variables for the texture lookup. - reflectRayDiffUsingVertexData(v, triangleVertices, hit.getBarycentricWeights(), rayDirIn, gScene.getWorldMatrix(hit.meshInstanceID), gScene.getInverseTransposeWorldMatrix(hit.meshInstanceID), + reflectRayDiffUsingVertexData(v, triangleVertices, hit.getBarycentricWeights(), rayDirIn, gScene.getWorldMatrix(hit.instanceID), gScene.getInverseTransposeWorldMatrix(hit.instanceID), this.rayDiff, dUVdx, dUVdy); } }; @@ -340,7 +340,7 @@ extension RayFootprint ShadingData computeShadingData(VertexData v, StaticVertexData triangleVertices[3], HitInfo hit, float3 rayOrg, float3 rayDir) { - const uint materialID = gScene.getMaterialID(hit.meshInstanceID); + const uint materialID = gScene.getMaterialID(hit.instanceID); return prepareShadingData(v, materialID, gScene.materials[materialID], gScene.materialResources[materialID], -rayDir, 0.f); } }; diff --git a/Source/Falcor/RenderPasses/Shared/PathTracer/StaticParams.slang b/Source/Falcor/RenderPasses/Shared/PathTracer/StaticParams.slang index ceb8f4a78b..11b23e7c00 100644 --- a/Source/Falcor/RenderPasses/Shared/PathTracer/StaticParams.slang +++ b/Source/Falcor/RenderPasses/Shared/PathTracer/StaticParams.slang @@ -37,6 +37,7 @@ MAX_BOUNCES Maximum number of indirect bounces (0 means no indirect). MAX_NON_SPECULAR_BOUNCES Maximum number of non-specular indirect bounces (0 means no indirect). USE_VBUFFER Use a V-buffer as input. + ADJUST_SHADING_NORMALS Adjust shading normals on secondary hits. FORCE_ALPHA_ONE Force the alpha channel to 1.0. USE_ANALYTIC_LIGHTS Enables Falcor's analytic lights (point, directional). USE_EMISSIVE_LIGHTS Enables use of emissive geometry as light sources. @@ -48,7 +49,7 @@ USE_RUSSIAN_ROULETTE Enables Russian roulette for path termination. MIS_HEURISTIC MIS heuristic enum value. USE_NESTED_DIELECTRICS Enables nested dielectrics handling. - USE_LIGHTS_IN_VOLUMES Enables lights in volumes. + USE_LIGHTS_IN_DIELECTRIC_VOLUMES Enables lights in dielectric volumes. DISABLE_CAUSTICS Disables caustics. */ @@ -62,6 +63,7 @@ static const uint kMaxBounces = MAX_BOUNCES; static const uint kMaxNonSpecularBounces = MAX_NON_SPECULAR_BOUNCES; static const bool kUseVBuffer = USE_VBUFFER; static const bool kUseAlphaTest = USE_ALPHA_TEST; +static const bool kAdjustShadingNormals = ADJUST_SHADING_NORMALS; static const bool kForceAlphaOne = FORCE_ALPHA_ONE; static const bool kUseAnalyticLights = USE_ANALYTIC_LIGHTS; static const bool kUseEmissiveLights = USE_EMISSIVE_LIGHTS; @@ -74,6 +76,6 @@ static const bool kUseRussianRoulette = USE_RUSSIAN_ROULETTE; static const uint kMISHeuristic = MIS_HEURISTIC; //static const MISHeuristic kMISHeuristic = (MISHeuristic) MIS_HEURISTIC; // TODO: Use enum instead of uint when Slang supports it. static const bool kUseNestedDielectrics = USE_NESTED_DIELECTRICS; -static const bool kUseLightsInVolumes = USE_LIGHTS_IN_VOLUMES; +static const bool kUseLightsInDielectricVolumes = USE_LIGHTS_IN_DIELECTRIC_VOLUMES; static const bool kDisableCaustics = DISABLE_CAUSTICS; diff --git a/Source/Falcor/Scene/Animation/Animation.cpp b/Source/Falcor/Scene/Animation/Animation.cpp index 82c1f28151..a75b973270 100644 --- a/Source/Falcor/Scene/Animation/Animation.cpp +++ b/Source/Falcor/Scene/Animation/Animation.cpp @@ -27,195 +27,250 @@ **************************************************************************/ #include "stdafx.h" #include "Animation.h" +#include "AnimationController.h" #include "glm/gtc/quaternion.hpp" #include "glm/gtx/transform.hpp" -#include "AnimationController.h" namespace Falcor { - // Bezier form hermite spline - static float3 interpolateHermite(const float3& p0, const float3& p1, const float3& p2, const float3& p3, float t) + namespace { - float3 v1 = (p2 - p0) * 0.5f; - float3 v2 = (p3 - p1) * 0.5f; + const double kEpsilonTime = 1e-5f; - float3 b0 = p1; - float3 b1 = p1 + (p2 - p0) * 0.5f / 3.f; - float3 b2 = p2 - (p3 - p1) * 0.5f / 3.f; - float3 b3 = p2; + const Gui::DropdownList kChannelLoopModeDropdown = + { + { (uint32_t)Animation::Behavior::Constant, "Constant" }, + { (uint32_t)Animation::Behavior::Linear, "Linear" }, + { (uint32_t)Animation::Behavior::Cycle, "Cycle" }, + { (uint32_t)Animation::Behavior::Oscillate, "Oscillate" }, + }; - float3 q0 = lerp(b0, b1, t); - float3 q1 = lerp(b1, b2, t); - float3 q2 = lerp(b2, b3, t); + // Bezier form hermite spline + float3 interpolateHermite(const float3& p0, const float3& p1, const float3& p2, const float3& p3, float t) + { + float3 b0 = p1; + float3 b1 = p1 + (p2 - p0) * 0.5f / 3.f; + float3 b2 = p2 - (p3 - p1) * 0.5f / 3.f; + float3 b3 = p2; - float3 qq0 = lerp(q0, q1, t); - float3 qq1 = lerp(q1, q2, t); + float3 q0 = lerp(b0, b1, t); + float3 q1 = lerp(b1, b2, t); + float3 q2 = lerp(b2, b3, t); - return lerp(qq0, qq1, t); - } + float3 qq0 = lerp(q0, q1, t); + float3 qq1 = lerp(q1, q2, t); - // Bezier hermite slerp - static glm::quat interpolateHermite(const glm::quat& r0, const glm::quat& r1, const glm::quat& r2, const glm::quat& r3, float t) - { - glm::quat b0 = r1; - glm::quat b1 = r1 + (r2 - r0) * 0.5f / 3.0f; - glm::quat b2 = r2 - (r3 - r1) * 0.5f / 3.0f; - glm::quat b3 = r2; + return lerp(qq0, qq1, t); + } - glm::quat q0 = slerp(b0, b1, t); - glm::quat q1 = slerp(b1, b2, t); - glm::quat q2 = slerp(b2, b3, t); + // Bezier hermite slerp + glm::quat interpolateHermite(const glm::quat& r0, const glm::quat& r1, const glm::quat& r2, const glm::quat& r3, float t) + { + glm::quat b0 = r1; + glm::quat b1 = r1 + (r2 - r0) * 0.5f / 3.0f; + glm::quat b2 = r2 - (r3 - r1) * 0.5f / 3.0f; + glm::quat b3 = r2; - glm::quat qq0 = slerp(q0, q1, t); - glm::quat qq1 = slerp(q1, q2, t); + glm::quat q0 = slerp(b0, b1, t); + glm::quat q1 = slerp(b1, b2, t); + glm::quat q2 = slerp(b2, b3, t); - return slerp(qq0, qq1, t); - } + glm::quat qq0 = slerp(q0, q1, t); + glm::quat qq1 = slerp(q1, q2, t); - static Animation::Keyframe interpolateLinear(const Animation::Keyframe& k0, const Animation::Keyframe& k1, float t) - { - assert(t >= 0.f && t <= 1.f); - Animation::Keyframe result; - result.translation = lerp(k0.translation, k1.translation, t); - result.scaling = lerp(k0.scaling, k1.scaling, t); - result.rotation = slerp(k0.rotation, k1.rotation, t); - return result; - } + return slerp(qq0, qq1, t); + } - static Animation::Keyframe interpolateHermite(const Animation::Keyframe& k0, const Animation::Keyframe& k1, const Animation::Keyframe& k2, const Animation::Keyframe& k3, float t) - { - assert(t >= 0.f && t <= 1.f); - Animation::Keyframe result; - result.translation = interpolateHermite(k0.translation, k1.translation, k2.translation, k3.translation, t); - result.scaling = lerp(k1.scaling, k2.scaling, t); - result.rotation = interpolateHermite(k0.rotation, k1.rotation, k2.rotation, k3.rotation, t); - return result; + // This function performs linear extrapolation when either t < 0 or t > 1 + Animation::Keyframe interpolateLinear(const Animation::Keyframe& k0, const Animation::Keyframe& k1, float t) + { + Animation::Keyframe result; + result.translation = lerp(k0.translation, k1.translation, t); + result.scaling = lerp(k0.scaling, k1.scaling, t); + result.rotation = slerp(k0.rotation, k1.rotation, t); + result.time = glm::lerp(k0.time, k1.time, (double)t); + return result; + } + + Animation::Keyframe interpolateHermite(const Animation::Keyframe& k0, const Animation::Keyframe& k1, const Animation::Keyframe& k2, const Animation::Keyframe& k3, float t) + { + assert(t >= 0.f && t <= 1.f); + Animation::Keyframe result; + result.translation = interpolateHermite(k0.translation, k1.translation, k2.translation, k3.translation, t); + result.scaling = lerp(k1.scaling, k2.scaling, t); + result.rotation = interpolateHermite(k0.rotation, k1.rotation, k2.rotation, k3.rotation, t); + result.time = glm::lerp(k1.time, k2.time, (double)t); + return result; + } } - Animation::SharedPtr Animation::create(const std::string& name, double durationInSeconds) + Animation::SharedPtr Animation::create(const std::string& name, uint32_t nodeID, double duration) { - return SharedPtr(new Animation(name, durationInSeconds)); + return SharedPtr(new Animation(name, nodeID, duration)); } - Animation::Animation(const std::string& name, double durationInSeconds) : mName(name), mDurationInSeconds(durationInSeconds) {} + Animation::Animation(const std::string& name, uint32_t nodeID, double duration) + : mName(name) + , mNodeID(nodeID) + , mDuration(duration) + {} - size_t Animation::findChannelFrame(const Channel& c, double time) const + glm::mat4 Animation::animate(double currentTime) { - size_t frameID = (time < c.lastUpdateTime) ? 0 : c.lastKeyframeUsed; - while (frameID < c.keyframes.size() - 1) + // Calculate the sample time. + double time = currentTime; + if (time < mKeyframes.front().time || time > mKeyframes.back().time) + { + time = calcSampleTime(currentTime); + } + + // Determine if the animation behaves linearly outside of defined keyframes. + bool isLinearPostInfinity = time > mKeyframes.back().time && this->getPostInfinityBehavior() == Behavior::Linear; + bool isLinearPreInfinity = time < mKeyframes.front().time && this->getPreInfinityBehavior() == Behavior::Linear; + + Keyframe interpolated; + + if (isLinearPreInfinity && mKeyframes.size() > 1) + { + const auto& k0 = mKeyframes.front(); + auto k1 = interpolate(mInterpolationMode, k0.time + kEpsilonTime); + double segmentDuration = k1.time - k0.time; + float t = (float)((time - k0.time) / segmentDuration); + interpolated = interpolateLinear(k0, k1, t); + } + else if (isLinearPostInfinity && mKeyframes.size() > 1) + { + const auto& k1 = mKeyframes.back(); + auto k0 = interpolate(mInterpolationMode, k1.time - kEpsilonTime); + double segmentDuration = k1.time - k0.time; + float t = (float)((time - k0.time) / segmentDuration); + interpolated = interpolateLinear(k0, k1, t); + } + else { - if (c.keyframes[frameID + 1].time > time) break; - frameID++; + interpolated = interpolate(mInterpolationMode, time); } - // Cache last used key frame. - c.lastUpdateTime = time; - c.lastKeyframeUsed = frameID; + glm::mat4 T = translate(interpolated.translation); + glm::mat4 R = mat4_cast(interpolated.rotation); + glm::mat4 S = scale(interpolated.scaling); + glm::mat4 transform = T * R * S; - return frameID; + return transform; } - glm::mat4 Animation::animateChannel(const Channel& c, double time) const + Animation::Keyframe Animation::interpolate(InterpolationMode mode, double time) const { - auto mode = c.interpolationMode; + assert(!mKeyframes.empty()); - // Use linear interpolation if there are less than 4 keyframes. - if (c.keyframes.size() < 4) mode = InterpolationMode::Linear; + // Validate cached frame index. + size_t frameIndex = clamp(mCachedFrameIndex, (size_t)0, mKeyframes.size() - 1); + if (time < mKeyframes[frameIndex].time) frameIndex = 0; - Keyframe interpolated; + // Find frame index. + while (frameIndex < mKeyframes.size() - 1) + { + if (mKeyframes[frameIndex + 1].time > time) break; + frameIndex++; + } + + // Cache frame index; + mCachedFrameIndex = frameIndex; // Compute index of adjacent frame including optional warping. - auto adjacentFrame = [] (const Channel& c, size_t frame, int32_t offset = 1) + auto adjacentFrame = [this] (size_t frame, int32_t offset = 1) { - size_t count = c.keyframes.size(); - if ((int64_t)frame + offset < 0) frame += count; - return c.enableWarping ? (frame + offset) % count : std::min(frame + offset, count - 1); + size_t count = mKeyframes.size(); + return mEnableWarping ? (frame + count + offset) % count : clamp(frame + offset, (size_t)0, count - 1); }; - if (mode == InterpolationMode::Linear) + if (mode == InterpolationMode::Linear || mKeyframes.size() < 4) { - size_t i0 = findChannelFrame(c, time); - size_t i1 = adjacentFrame(c, i0); + size_t i0 = frameIndex; + size_t i1 = adjacentFrame(i0); - const Keyframe& k0 = c.keyframes[i0]; - const Keyframe& k1 = c.keyframes[i1]; + const Keyframe& k0 = mKeyframes[i0]; + const Keyframe& k1 = mKeyframes[i1]; double segmentDuration = k1.time - k0.time; - if (c.enableWarping && segmentDuration < 0.0) segmentDuration += mDurationInSeconds; - float t = (float)clamp(segmentDuration > 0.0 ? (time - k0.time) / segmentDuration : 1.0, 0.0, 1.0); + if (mEnableWarping && segmentDuration < 0.0) segmentDuration += mDuration; + float t = (float)clamp((segmentDuration > 0.0 ? (time - k0.time) / segmentDuration : 1.0), 0.0, 1.0); - interpolated = interpolateLinear(k0, k1, t); + return interpolateLinear(k0, k1, t); } else if (mode == InterpolationMode::Hermite) { - size_t i1 = findChannelFrame(c, time); - size_t i0 = adjacentFrame(c, i1, -1); - size_t i2 = adjacentFrame(c, i1, 1); - size_t i3 = adjacentFrame(c, i1, 2); + size_t i1 = frameIndex; + size_t i0 = adjacentFrame(i1, -1); + size_t i2 = adjacentFrame(i1, 1); + size_t i3 = adjacentFrame(i1, 2); - const Keyframe& k0 = c.keyframes[i0]; - const Keyframe& k1 = c.keyframes[i1]; - const Keyframe& k2 = c.keyframes[i2]; - const Keyframe& k3 = c.keyframes[i3]; + const Keyframe& k0 = mKeyframes[i0]; + const Keyframe& k1 = mKeyframes[i1]; + const Keyframe& k2 = mKeyframes[i2]; + const Keyframe& k3 = mKeyframes[i3]; double segmentDuration = k2.time - k1.time; - if (c.enableWarping && segmentDuration < 0.0) segmentDuration += mDurationInSeconds; + if (mEnableWarping && segmentDuration < 0.0) segmentDuration += mDuration; float t = (float)clamp(segmentDuration > 0.0 ? (time - k1.time) / segmentDuration : 1.0, 0.0, 1.0); - interpolated = interpolateHermite(k0, k1, k2, k3, t); + return interpolateHermite(k0, k1, k2, k3, t); } - - glm::mat4 T = translate(interpolated.translation); - glm::mat4 R = mat4_cast(interpolated.rotation); - glm::mat4 S = scale(interpolated.scaling); - glm::mat4 transform = T * R * S; - - return transform; - } - - void Animation::animate(double totalTime, std::vector& matrices) - { - // Calculate the relative time - double modTime = std::fmod(totalTime, mDurationInSeconds); - for (auto& c : mChannels) + else { - matrices[c.matrixID] = animateChannel(c, modTime); + throw std::exception("Unknown interpolation mode"); } } - uint32_t Animation::addChannel(uint32_t matrixID) + // Calculates the sample time within the keyframe range if the current time lies outside and + // the animation does not behave linearly. If the animation behaves linearly, then the + // current time is returned. This function should not be used if the current time lies + // within the range of defined keyframe times. + double Animation::calcSampleTime(double currentTime) { - mChannels.push_back(Channel(matrixID)); - return (uint32_t)(mChannels.size() - 1); - } + double modifiedTime = currentTime; + double firstKeyframeTime = mKeyframes.front().time; + double lastKeyframeTime = mKeyframes.back().time; + double duration = lastKeyframeTime - firstKeyframeTime; - uint32_t Animation::getChannel(uint32_t matrixID) const - { - for (uint32_t i = 0; i < mChannels.size(); ++i) + assert(currentTime < firstKeyframeTime || currentTime > lastKeyframeTime); + + Behavior behavior = (currentTime < firstKeyframeTime) ? mPreInfinityBehavior : mPostInfinityBehavior; + switch (behavior) { - if (mChannels[i].matrixID == matrixID) return i; + case Behavior::Constant: + modifiedTime = clamp(currentTime, firstKeyframeTime, lastKeyframeTime); + break; + case Behavior::Cycle: + // Calculate the relative time + modifiedTime = firstKeyframeTime + std::fmod(currentTime - firstKeyframeTime, duration); + if (modifiedTime < firstKeyframeTime) modifiedTime += duration; + break; + case Behavior::Oscillate: + // Calculate the relative time + double offset = std::fmod(currentTime - firstKeyframeTime, 2 * duration); + if (offset < 0) offset += 2 * duration; + if (offset > duration) offset = 2 * duration - offset; + modifiedTime = firstKeyframeTime + offset; } - return kInvalidChannel; + + return modifiedTime; } - void Animation::addKeyframe(uint32_t channelID, const Keyframe& keyframe) + void Animation::addKeyframe(const Keyframe& keyframe) { - assert(channelID < mChannels.size()); - assert(keyframe.time <= mDurationInSeconds); + assert(keyframe.time <= mDuration); - mChannels[channelID].lastKeyframeUsed = 0; - auto& channelFrames = mChannels[channelID].keyframes; - - if (channelFrames.size() == 0 || channelFrames[0].time > keyframe.time) + if (mKeyframes.size() == 0 || mKeyframes[0].time > keyframe.time) { - channelFrames.insert(channelFrames.begin(), keyframe); + mKeyframes.insert(mKeyframes.begin(), keyframe); return; } else { - for (size_t i = 0; i < channelFrames.size(); i++) + for (size_t i = 0; i < mKeyframes.size(); i++) { - auto& current = channelFrames[i]; + auto& current = mKeyframes[i]; // If we already have a key-frame at the same time, replace it if (current.time == keyframe.time) { @@ -224,47 +279,70 @@ namespace Falcor } // If this is not the last frame, Check if we are in between frames - if (i < channelFrames.size() - 1) + if (i < mKeyframes.size() - 1) { - auto& Next = channelFrames[i + 1]; + auto& Next = mKeyframes[i + 1]; if (current.time < keyframe.time && Next.time > keyframe.time) { - channelFrames.insert(channelFrames.begin() + i + 1, keyframe); + mKeyframes.insert(mKeyframes.begin() + i + 1, keyframe); return; } } } // If we got here, need to push it to the end of the list - channelFrames.push_back(keyframe); + mKeyframes.push_back(keyframe); } } - const Animation::Keyframe& Animation::getKeyframe(uint32_t channelID, double time) const + const Animation::Keyframe& Animation::getKeyframe(double time) const { - assert(channelID < mChannels.size()); - for (const auto& k : mChannels[channelID].keyframes) + for (const auto& k : mKeyframes) { if (k.time == time) return k; } throw std::runtime_error(("Animation::getKeyframe() - can't find a keyframe at time " + std::to_string(time)).c_str()); } - bool Animation::doesKeyframeExists(uint32_t channelID, double time) const + bool Animation::doesKeyframeExists(double time) const { - assert(channelID < mChannels.size()); - for (const auto& k : mChannels[channelID].keyframes) + for (const auto& k : mKeyframes) { if (k.time == time) return true; } return false; } - void Animation::setInterpolationMode(uint32_t channelID, InterpolationMode mode, bool enableWarping) + void Animation::renderUI(Gui::Widgets& widget) { - assert(channelID < mChannels.size()); - mChannels[channelID].interpolationMode = mode; - mChannels[channelID].enableWarping = enableWarping; + widget.dropdown("Pre-Infinity Behavior", kChannelLoopModeDropdown, reinterpret_cast(mPreInfinityBehavior)); + widget.dropdown("Post-Infinity Behavior", kChannelLoopModeDropdown, reinterpret_cast(mPostInfinityBehavior)); } + SCRIPT_BINDING(Animation) + { + pybind11::class_ animation(m, "Animation"); + animation.def_property_readonly("name", &Animation::getName); + animation.def_property_readonly("nodeID", &Animation::getNodeID); + animation.def_property_readonly("duration", &Animation::getDuration); + animation.def_property("preInfinityBehavior", &Animation::getPreInfinityBehavior, &Animation::setPreInfinityBehavior); + animation.def_property("postInfinityBehavior", &Animation::getPostInfinityBehavior, &Animation::setPostInfinityBehavior); + animation.def_property("interpolationMode", &Animation::getInterpolationMode, &Animation::setInterpolationMode); + animation.def_property("enableWarping", &Animation::isWarpingEnabled, &Animation::setEnableWarping); + animation.def(pybind11::init(&Animation::create), "name"_a, "nodeID"_a, "duration"_a); + animation.def("addKeyframe", [] (Animation* pAnimation, double time, const Transform& transform) { + Animation::Keyframe keyframe{ time, transform.getTranslation(), transform.getScaling(), transform.getRotation() }; + pAnimation->addKeyframe(keyframe); + }); + + pybind11::enum_ interpolationMode(animation, "InterpolationMode"); + interpolationMode.value("Linear", Animation::InterpolationMode::Linear); + interpolationMode.value("Hermite", Animation::InterpolationMode::Hermite); + + pybind11::enum_ behavior(animation, "Behavior"); + behavior.value("Constant", Animation::Behavior::Constant); + behavior.value("Linear", Animation::Behavior::Linear); + behavior.value("Cycle", Animation::Behavior::Cycle); + behavior.value("Oscillate", Animation::Behavior::Oscillate); + } } diff --git a/Source/Falcor/Scene/Animation/Animation.h b/Source/Falcor/Scene/Animation/Animation.h index 489b9f933a..398fe0585e 100644 --- a/Source/Falcor/Scene/Animation/Animation.h +++ b/Source/Falcor/Scene/Animation/Animation.h @@ -37,14 +37,20 @@ namespace Falcor public: using SharedPtr = std::shared_ptr; - static const uint32_t kInvalidChannel = -1; - enum class InterpolationMode { Linear, Hermite, }; + enum class Behavior + { + Constant, + Linear, + Cycle, + Oscillate, + }; + struct Keyframe { double time = 0; @@ -53,79 +59,104 @@ namespace Falcor glm::quat rotation = glm::quat(1, 0, 0, 0); }; - /** Create a new object + /** Create a new animation. + \param[in] name Animation name. + \param[in] nodeID ID of the animated node. + \param[in] Animation duration in seconds. + \return Returns a new animation. */ - static SharedPtr create(const std::string& name, double durationInSeconds); + static SharedPtr create(const std::string& name, uint32_t nodeID, double duration); - /** Get the animation's name + /** Get the animation name. */ const std::string& getName() const { return mName; } - /** Add a new channel + /** Get the animated node. */ - uint32_t addChannel(uint32_t matrixID); + uint32_t getNodeID() const { return mNodeID; } - /** Get the channel for a given matrix ID or kInvalidChannel if not available + /** Get the animation duration in seconds. */ - uint32_t getChannel(uint32_t matrixID) const; + double getDuration() const { return mDuration; } - /** Get the channel count + /** Get the animation's behavior before the first keyframe. */ - size_t getChannelCount() const { return mChannels.size(); } + Behavior getPreInfinityBehavior() const { return mPreInfinityBehavior; } - /** Add a keyframe. - If there's already a keyframe at the requested time, this call will override the existing frame + /** Set the animation's behavior before the first keyframe. */ - void addKeyframe(uint32_t channelID, const Keyframe& keyframe); + void setPreInfinityBehavior(Behavior behavior) { mPreInfinityBehavior = behavior; } - /** Get the keyframe from a specific time. - If the keyframe doesn't exists, the function will throw an exception. If you don't want to handle exceptions, call doesKeyframeExist() first + /** Get the animation's behavior after the last keyframe. */ - const Keyframe& getKeyframe(uint32_t channelID, double time) const; + Behavior getPostInfinityBehavior() const { return mPostInfinityBehavior; } - /** Check if a keyframe exists in a specific time + /** Set the animation's behavior after the last keyframe. */ - bool doesKeyframeExists(uint32_t channelID, double time) const; + void setPostInfinityBehavior(Behavior behavior) { mPostInfinityBehavior = behavior; } - /** Set the interpolation mode and enable/disable warping for a given channel. + /** Get the interpolation mode. */ - void setInterpolationMode(uint32_t channelID, InterpolationMode mode, bool enableWarping); + InterpolationMode getInterpolationMode() const { return mInterpolationMode; } - /** Run the animation - \param currentTime The current time in seconds. This can be larger then the animation time, in which case the animation will loop - \param matrices The array of global matrices to update + /** Set the interpolation mode. */ - void animate(double currentTime, std::vector& matrices); + void setInterpolationMode(InterpolationMode interpolationMode) { mInterpolationMode = interpolationMode; } - /** Get the matrixID affected by a channel + /** Return true if warping is enabled. */ - uint32_t getChannelMatrixID(uint32_t channel) const { return mChannels[channel].matrixID; } + bool isWarpingEnabled() const { return mEnableWarping; } + + /** Enable/disable warping. + */ + void setEnableWarping(bool enableWarping) { mEnableWarping = enableWarping; } + + /** Add a keyframe. + If there's already a keyframe at the requested time, this call will override the existing frame. + \param[in] keyframe Keyframe. + */ + void addKeyframe(const Keyframe& keyframe); + + /** Get the keyframe at the specified time. + If the keyframe doesn't exists, the function will throw an exception. If you don't want to handle exceptions, call doesKeyframeExist() first. + \param[in] time Time of the keyframe. + \return Returns the keyframe. + */ + const Keyframe& getKeyframe(double time) const; + + /** Check if a keyframe exists at the specified time. + \param[in] time Time of the keyframe. + \return Returns true if keyframe exists. + */ + bool doesKeyframeExists(double time) const; + + /** Compute the animation. + \param time The current time in seconds. This can be larger then the animation time, in which case the animation will loop. + \return Returns the animation's transform matrix for the specified time. + */ + glm::mat4 animate(double currentTime); + + /* Render the UI. + */ + void renderUI(Gui::Widgets& widget); private: - Animation(const std::string& name, double durationInSeconds); + Animation(const std::string& name, uint32_t nodeID, double duration); - struct Channel - { - Channel(uint32_t matrixID, InterpolationMode interpolationMode = InterpolationMode::Linear, bool enableWarping = true) - : matrixID(matrixID) - , interpolationMode(interpolationMode) - , enableWarping(enableWarping) - {}; - - uint32_t matrixID; - InterpolationMode interpolationMode; - bool enableWarping; - std::vector keyframes; - mutable size_t lastKeyframeUsed = 0; - mutable double lastUpdateTime = 0; - }; + Keyframe interpolate(InterpolationMode mode, double time) const; + double calcSampleTime(double currentTime); - std::vector mChannels; const std::string mName; - double mDurationInSeconds = 0; + uint32_t mNodeID; + double mDuration; // Includes any time before the first keyframe. May be Assimp or FBX specific. + + Behavior mPreInfinityBehavior = Behavior::Constant; // How the animation behaves before the first keyframe + Behavior mPostInfinityBehavior = Behavior::Constant; // How the animation behaves after the last keyframe + + InterpolationMode mInterpolationMode = InterpolationMode::Linear; + bool mEnableWarping = false; - glm::mat4 animateChannel(const Channel& c, double time) const; - size_t findChannelFrame(const Channel& c, double time) const; - glm::mat4 interpolate(const Keyframe& start, const Keyframe& end, double curTime) const; + std::vector mKeyframes; + mutable size_t mCachedFrameIndex = 0; }; } diff --git a/Source/Falcor/Scene/Animation/AnimationController.cpp b/Source/Falcor/Scene/Animation/AnimationController.cpp index 870b6ff57b..8c75242efc 100644 --- a/Source/Falcor/Scene/Animation/AnimationController.cpp +++ b/Source/Falcor/Scene/Animation/AnimationController.cpp @@ -33,17 +33,22 @@ namespace Falcor { namespace { - const static std::string kWorldMatrices = "worldMatrices"; - const static std::string kInverseTransposeWorldMatrices = "inverseTransposeWorldMatrices"; - const static std::string kPreviousFrameWorldMatrices = "previousFrameWorldMatrices"; + const std::string kWorldMatrices = "worldMatrices"; + const std::string kInverseTransposeWorldMatrices = "inverseTransposeWorldMatrices"; + const std::string kPreviousFrameWorldMatrices = "previousFrameWorldMatrices"; } - AnimationController::AnimationController(Scene* pScene, const StaticVertexVector& staticVertexData, const DynamicVertexVector& dynamicVertexData) + AnimationController::AnimationController(Scene* pScene, const StaticVertexVector& staticVertexData, const DynamicVertexVector& dynamicVertexData, const std::vector& animations) : mpScene(pScene) , mLocalMatrices(pScene->mSceneGraph.size()) , mInvTransposeGlobalMatrices(pScene->mSceneGraph.size()) + , mMatricesAnimated(pScene->mSceneGraph.size()) , mMatricesChanged(pScene->mSceneGraph.size()) + , mAnimations(animations) { + initFlags(); + + // Create GPU resources. assert(mLocalMatrices.size() * 4 <= std::numeric_limits::max()); uint32_t float4Count = (uint32_t)mLocalMatrices.size() * 4; @@ -53,17 +58,19 @@ namespace Falcor mpPrevWorldMatricesBuffer->setName("AnimationController::mpPrevWorldMatricesBuffer"); mpInvTransposeWorldMatricesBuffer = Buffer::createStructured(sizeof(float4), float4Count, Resource::BindFlags::ShaderResource, Buffer::CpuAccess::None, nullptr, false); mpInvTransposeWorldMatricesBuffer->setName("AnimationController::mpInvTransposeWorldMatricesBuffer"); + createSkinningPass(staticVertexData, dynamicVertexData); - } - AnimationController::UniquePtr AnimationController::create(Scene* pScene, const StaticVertexVector& staticVertexData, const DynamicVertexVector& dynamicVertexData) - { - return UniquePtr(new AnimationController(pScene, staticVertexData, dynamicVertexData)); + // Determine length of global animation loop. + for (const auto& animation : mAnimations) + { + if (animation->getDuration() > mGlobalAnimationLength) mGlobalAnimationLength = animation->getDuration(); + } } - void AnimationController::addAnimation(const Animation::SharedPtr& pAnimation) + AnimationController::UniquePtr AnimationController::create(Scene* pScene, const StaticVertexVector& staticVertexData, const DynamicVertexVector& dynamicVertexData, const std::vector& animations) { - mAnimations.push_back(pAnimation); + return UniquePtr(new AnimationController(pScene, staticVertexData, dynamicVertexData, animations)); } void AnimationController::setEnabled(bool enabled) @@ -74,6 +81,28 @@ namespace Falcor mAnimationChanged = true; } + void AnimationController::initFlags() + { + std::fill(mMatricesAnimated.begin(), mMatricesAnimated.end(), false); + + // Tag all matrices affected by an animation. + for (const auto& pAnimation : mAnimations) + { + mMatricesAnimated[pAnimation->getNodeID()] = true; + } + + // Traverse the scene graph hierarchy to propagate the flags. + assert(mpScene->mSceneGraph.size() == mMatricesAnimated.size()); + for (size_t i = 0; i < mMatricesAnimated.size(); i++) + { + if (uint32_t parent = mpScene->mSceneGraph[i].parent; parent != SceneBuilder::kInvalidNode) + { + assert(parent < i); + mMatricesAnimated[i] = mMatricesAnimated[i] || mMatricesAnimated[parent]; + } + } + } + void AnimationController::initLocalMatrices() { for (size_t i = 0; i < mLocalMatrices.size(); i++) @@ -105,13 +134,12 @@ namespace Falcor if (mEnabled) { + double time = (mLoopAnimations == true) ? std::fmod(currentTime, mGlobalAnimationLength) : currentTime; for (auto& pAnimation : mAnimations) { - pAnimation->animate(currentTime, mLocalMatrices); - for (uint32_t i = 0; i < pAnimation->getChannelCount(); i++) - { - mMatricesChanged[pAnimation->getChannelMatrixID(i)] = true; - } + uint32_t nodeID = pAnimation->getNodeID(); + mLocalMatrices[nodeID] = pAnimation->animate(time); + mMatricesChanged[nodeID] = true; } } @@ -134,11 +162,12 @@ namespace Falcor { mGlobalMatrices[i] = mGlobalMatrices[mpScene->mSceneGraph[i].parent] * mGlobalMatrices[i]; mMatricesChanged[i] = mMatricesChanged[i] || mMatricesChanged[mpScene->mSceneGraph[i].parent]; + assert(!mMatricesChanged[i] || mMatricesAnimated[i]); } mInvTransposeGlobalMatrices[i] = transpose(inverse(mGlobalMatrices[i])); - if(mpSkinningPass) + if (mpSkinningPass) { mSkinningMatrices[i] = mGlobalMatrices[i] * mpScene->mSceneGraph[i].localToBindSpace; mInvTransposeSkinningMatrices[i] = transpose(inverse(mSkinningMatrices[i])); @@ -157,50 +186,67 @@ namespace Falcor pBlock->setBuffer(kInverseTransposeWorldMatrices, mpInvTransposeWorldMatricesBuffer); } + uint64_t AnimationController::getMemoryUsageInBytes() const + { + uint64_t m = 0; + m += mpWorldMatricesBuffer ? mpWorldMatricesBuffer->getSize() : 0; + m += mpPrevWorldMatricesBuffer ? mpPrevWorldMatricesBuffer->getSize() : 0; + m += mpInvTransposeWorldMatricesBuffer ? mpInvTransposeWorldMatricesBuffer->getSize() : 0; + m += mpSkinningMatricesBuffer ? mpSkinningMatricesBuffer->getSize() : 0; + m += mpInvTransposeSkinningMatricesBuffer ? mpInvTransposeSkinningMatricesBuffer->getSize() : 0; + m += mpSkinningStaticVertexData ? mpSkinningStaticVertexData->getSize() : 0; + m += mpSkinningDynamicVertexData ? mpSkinningDynamicVertexData->getSize() : 0; + m += mpPrevVertexData ? mpPrevVertexData->getSize() : 0; + return m; + } + void AnimationController::createSkinningPass(const std::vector& staticVertexData, const std::vector& dynamicVertexData) { - // We always copy the static data, to initialize the non-skinned vertices + // We always copy the static data, to initialize the non-skinned vertices. const Buffer::SharedPtr& pVB = mpScene->mpVao->getVertexBuffer(Scene::kStaticDataBufferIndex); assert(pVB->getSize() == staticVertexData.size() * sizeof(staticVertexData[0])); pVB->setBlob(staticVertexData.data(), 0, pVB->getSize()); - // Initialize the previous positions for non-skinned vertices. - std::vector prevVertexData(staticVertexData.size()); - for (size_t i = 0; i < staticVertexData.size(); i++) - { - prevVertexData[i].position = staticVertexData[i].position; - } - const Buffer::SharedPtr& pPrevVB = mpScene->mpVao->getVertexBuffer(Scene::kPrevVertexBufferIndex); - assert(pPrevVB->getSize() == prevVertexData.size() * sizeof(prevVertexData[0])); - pPrevVB->setBlob(prevVertexData.data(), 0, pPrevVB->getSize()); - - if (dynamicVertexData.size()) + if (!dynamicVertexData.empty()) { mSkinningMatrices.resize(mpScene->mSceneGraph.size()); mInvTransposeSkinningMatrices.resize(mSkinningMatrices.size()); mpSkinningPass = ComputePass::create("Scene/Animation/Skinning.slang"); auto block = mpSkinningPass->getVars()["gData"]; - block["skinnedVertices"] = pVB; - block["prevSkinnedVertices"] = pPrevVB; - auto createBuffer = [&](const std::string& name, const auto& initData) + // Initialize the previous positions for skinned vertices. + // This ensures we have valid data in the buffer before the skinning pass runs for the first time. + std::vector prevVertexData(dynamicVertexData.size()); + for (size_t i = 0; i < dynamicVertexData.size(); i++) { - auto pBuffer = Buffer::createStructured(block[name], (uint32_t)initData.size(), ResourceBindFlags::ShaderResource, Buffer::CpuAccess::None, nullptr, false); - pBuffer->setName(name); - pBuffer->setBlob(initData.data(), 0, pBuffer->getSize()); - block[name] = pBuffer; - }; + uint32_t staticIndex = dynamicVertexData[i].staticIndex; + prevVertexData[i].position = staticVertexData[staticIndex].position; + } - createBuffer("staticData", staticVertexData); - createBuffer("dynamicData", dynamicVertexData); + // Bind vertex data. + assert(staticVertexData.size() <= std::numeric_limits::max()); + assert(dynamicVertexData.size() <= std::numeric_limits::max()); + mpSkinningStaticVertexData = Buffer::createStructured(block["staticData"], (uint32_t)staticVertexData.size(), ResourceBindFlags::ShaderResource, Buffer::CpuAccess::None, staticVertexData.data(), false); + mpSkinningStaticVertexData->setName("AnimationController::mpSkinningStaticVertexData"); + mpSkinningDynamicVertexData = Buffer::createStructured(block["dynamicData"], (uint32_t)dynamicVertexData.size(), ResourceBindFlags::ShaderResource, Buffer::CpuAccess::None, dynamicVertexData.data(), false); + mpSkinningDynamicVertexData->setName("AnimationController::mpSkinningDynamicVertexData"); + mpPrevVertexData = Buffer::createStructured(block["prevSkinnedVertices"], (uint32_t)dynamicVertexData.size(), ResourceBindFlags::ShaderResource | ResourceBindFlags::UnorderedAccess, Buffer::CpuAccess::None, prevVertexData.data(), false); + mpPrevVertexData->setName("AnimationController::mpPrevVertexData"); + + block["staticData"] = mpSkinningStaticVertexData; + block["dynamicData"] = mpSkinningDynamicVertexData; + block["skinnedVertices"] = pVB; + block["prevSkinnedVertices"] = mpPrevVertexData; + // Bind transforms. assert(mSkinningMatrices.size() * 4 < std::numeric_limits::max()); uint32_t float4Count = (uint32_t)mSkinningMatrices.size() * 4; mpSkinningMatricesBuffer = Buffer::createStructured(sizeof(float4), float4Count, ResourceBindFlags::ShaderResource, Buffer::CpuAccess::None, nullptr, false); mpSkinningMatricesBuffer->setName("AnimationController::mpSkinningMatricesBuffer"); mpInvTransposeSkinningMatricesBuffer = Buffer::createStructured(sizeof(float4), float4Count, ResourceBindFlags::ShaderResource, Buffer::CpuAccess::None, nullptr, false); mpInvTransposeSkinningMatricesBuffer->setName("AnimationController::mpInvTransposeSkinningMatricesBuffer"); + block["boneMatrices"].setBuffer(mpSkinningMatricesBuffer); block["inverseTransposeBoneMatrices"].setBuffer(mpInvTransposeSkinningMatricesBuffer); block["inverseTransposeWorldMatrices"].setBuffer(mpInvTransposeWorldMatricesBuffer); @@ -217,4 +263,18 @@ namespace Falcor mpInvTransposeSkinningMatricesBuffer->setBlob(mInvTransposeSkinningMatrices.data(), 0, mpInvTransposeSkinningMatricesBuffer->getSize()); mpSkinningPass->execute(pContext, mSkinningDispatchSize, 1, 1); } + + void AnimationController::renderUI(Gui::Widgets& widget) + { + widget.checkbox("Loop Animations", mLoopAnimations); + widget.tooltip("Enable/disable global animation looping."); + + for (auto& animation : mAnimations) + { + if (auto animGroup = widget.group(animation->getName())) + { + animation->renderUI(animGroup); + } + } + } } diff --git a/Source/Falcor/Scene/Animation/AnimationController.h b/Source/Falcor/Scene/Animation/AnimationController.h index 8be29a1907..f6e559b68a 100644 --- a/Source/Falcor/Scene/Animation/AnimationController.h +++ b/Source/Falcor/Scene/Animation/AnimationController.h @@ -59,18 +59,19 @@ namespace Falcor using StaticVertexVector = std::vector; using DynamicVertexVector = std::vector; - /** Create a new object + /** Create a new object. + \return A new object, or throws an exception if creation failed. */ - static UniquePtr create(Scene* pScene, const StaticVertexVector& staticVertexData, const DynamicVertexVector& dynamicVertexData); - - /** Add an animation - */ - void addAnimation(const Animation::SharedPtr& pAnimation); + static UniquePtr create(Scene* pScene, const StaticVertexVector& staticVertexData, const DynamicVertexVector& dynamicVertexData, const std::vector& animations); /** Returns true if controller contains animations. */ bool hasAnimations() const { return mAnimations.size() > 0; } + /** Returns a list of all animations. + */ + std::vector& getAnimations() { return mAnimations; } + /** Enable/disable animations. */ void setEnabled(bool enabled); @@ -79,35 +80,70 @@ namespace Falcor */ bool isEnabled() const { return mEnabled; }; + /** Enable/disable globally looping animations. + */ + void setIsLooped(bool looped) { mLoopAnimations = looped; } + + /** Returns true if animations are currently globally looped. + */ + bool isLooped() { return mLoopAnimations; } + /** Run the animation \return true if a change occurred, otherwise false */ bool animate(RenderContext* pContext, double currentTime); - /** Check if a matrix changed + /** Check if a matrix is animated. + */ + bool isMatrixAnimated(size_t matrixID) const { return mMatricesAnimated[matrixID]; } + + /** Check if a matrix changed since last frame. */ - bool didMatrixChanged(size_t matrixID) const { return mMatricesChanged[matrixID]; } + bool isMatrixChanged(size_t matrixID) const { return mMatricesChanged[matrixID]; } - /** Get the global matrices + /** Get the global matrices. */ const std::vector& getGlobalMatrices() const { return mGlobalMatrices; } + /** Render the UI. + */ + void renderUI(Gui::Widgets& widget); + + + /** Get the previous vertex data buffer for dynamic meshes. + \return Buffer containing the previous vertex data, or nullptr if no dynamic meshes exist. + */ + Buffer::SharedPtr getPrevVertexData() const { return mpPrevVertexData; } + + /** Get the total GPU memory usage in bytes. + */ + uint64_t getMemoryUsageInBytes() const; + private: friend class SceneBuilder; - AnimationController(Scene* pScene, const StaticVertexVector& staticVertexData, const DynamicVertexVector& dynamicVertexData); + AnimationController(Scene* pScene, const StaticVertexVector& staticVertexData, const DynamicVertexVector& dynamicVertexData, const std::vector& animations); + void initFlags(); void bindBuffers(); void updateMatrices(); + void createSkinningPass(const std::vector& staticVertexData, const std::vector& dynamicVertexData); + void executeSkinningPass(RenderContext* pContext); + void initLocalMatrices(); + + // Animation std::vector mAnimations; std::vector mLocalMatrices; std::vector mGlobalMatrices; std::vector mInvTransposeGlobalMatrices; - std::vector mMatricesChanged; + std::vector mMatricesAnimated; ///< Flag per matrix, true if matrix is affected by animations. + std::vector mMatricesChanged; ///< Flag per matrix, true if matrix changed since last frame. bool mEnabled = true; bool mAnimationChanged = true; double mLastAnimationTime = 0; + bool mLoopAnimations = true; + double mGlobalAnimationLength = 0; Scene* mpScene = nullptr; Buffer::SharedPtr mpWorldMatricesBuffer; @@ -119,11 +155,11 @@ namespace Falcor std::vector mSkinningMatrices; std::vector mInvTransposeSkinningMatrices; uint32_t mSkinningDispatchSize = 0; - void createSkinningPass(const std::vector& staticVertexData, const std::vector& dynamicVertexData); - void executeSkinningPass(RenderContext* pContext); Buffer::SharedPtr mpSkinningMatricesBuffer; Buffer::SharedPtr mpInvTransposeSkinningMatricesBuffer; - void initLocalMatrices(); + Buffer::SharedPtr mpSkinningStaticVertexData; + Buffer::SharedPtr mpSkinningDynamicVertexData; + Buffer::SharedPtr mpPrevVertexData; }; } diff --git a/Source/Falcor/Scene/Animation/Skinning.slang b/Source/Falcor/Scene/Animation/Skinning.slang index b3b1a9fb73..d4499473a9 100644 --- a/Source/Falcor/Scene/Animation/Skinning.slang +++ b/Source/Falcor/Scene/Animation/Skinning.slang @@ -27,17 +27,27 @@ **************************************************************************/ import Scene.SceneTypes; +/** Compute pass for skinned vertex animation. + + The dispatch size is one thread per dynamic vertex. +*/ + struct SkinningData { - StructuredBuffer staticData; - StructuredBuffer dynamicData; - RWStructuredBuffer skinnedVertices; - RWStructuredBuffer prevSkinnedVertices; + // Vertex data + StructuredBuffer staticData; ///< Original global vertex buffer. This holds the unmodified input vertices. + StructuredBuffer dynamicData; ///< Bone IDs and weights for all dynamic vertices. + RWStructuredBuffer skinnedVertices; ///< Skinned global vertex buffer. We'll update the positions only for the dynamic meshes. + RWStructuredBuffer prevSkinnedVertices; ///< Previous frame vertex positions for dynamic meshes. Same size as 'dynamicData'. + + // Transforms StructuredBuffer boneMatrices; StructuredBuffer inverseTransposeBoneMatrices; StructuredBuffer worldMatrices; StructuredBuffer inverseTransposeWorldMatrices; + // Accessors + float4x4 getTransposeWorldMatrix(uint matrixID) { float4x4 m = float4x4(worldMatrices[matrixID * 4 + 0], @@ -113,7 +123,7 @@ struct SkinningData void storeSkinnedVertexData(uint vertexId, StaticVertexData data, PrevVertexData prevData) { gData.skinnedVertices[getStaticVertexID(vertexId)].pack(data); - gData.prevSkinnedVertices[getStaticVertexID(vertexId)] = prevData; + gData.prevSkinnedVertices[vertexId] = prevData; } float3 getCurrentPosition(uint vertexId) diff --git a/Source/Falcor/Scene/Camera/Camera.cpp b/Source/Falcor/Scene/Camera/Camera.cpp index 4db5c5e693..7bb8980d35 100644 --- a/Source/Falcor/Scene/Camera/Camera.cpp +++ b/Source/Falcor/Scene/Camera/Camera.cpp @@ -47,16 +47,14 @@ namespace Falcor // Default dimensions of full frame cameras and 35mm film const float Camera::kDefaultFrameHeight = 24.0f; - Camera::Camera() + Camera::Camera(const std::string& name) + : mName(name) { } - Camera::~Camera() = default; - - Camera::SharedPtr Camera::create() + Camera::SharedPtr Camera::create(const std::string& name) { - Camera* pCamera = new Camera; - return SharedPtr(pCamera); + return SharedPtr(new Camera(name)); } Camera::Changes Camera::beginFrame(bool firstFrame) @@ -73,6 +71,7 @@ namespace Falcor if (firstFrame) mPrevData = mData; // Keep copies of the transforms used for the previous frame. We need these for computing motion vectors etc. + mData.prevViewMat = mPrevData.viewMat; mData.prevViewProjMatNoJitter = mPrevData.viewProjMatNoJitter; mChanges = is_set(mChanges, Changes::Movement | Changes::Frustum) ? Changes::History : Changes::None; @@ -197,6 +196,11 @@ namespace Falcor return mData.viewMat; } + const glm::mat4& Camera::getPrevViewMatrix() const + { + return mData.prevViewMat; + } + const glm::mat4& Camera::getProjMatrix() const { calculateCameraParameters(); @@ -239,7 +243,7 @@ namespace Falcor mEnablePersistentViewMat = persistent; } - bool Camera::isObjectCulled(const BoundingBox& box) const + bool Camera::isObjectCulled(const AABB& box) const { calculateCameraParameters(); @@ -248,8 +252,8 @@ namespace Falcor // See method 4b: https://fgiesen.wordpress.com/2010/10/17/view-frustum-culling/ for (int plane = 0; plane < 6; plane++) { - float3 signedExtent = box.extent * mFrustumPlanes[plane].sign; - float dr = glm::dot(box.center + signedExtent, mFrustumPlanes[plane].xyz); + float3 signedHalfExtent = 0.5f * box.extent() * mFrustumPlanes[plane].sign; + float dr = glm::dot(box.center() + signedHalfExtent, mFrustumPlanes[plane].xyz); isInside = isInside && (dr > mFrustumPlanes[plane].negW); } @@ -347,14 +351,14 @@ namespace Falcor if (hasAnimation() && !isAnimated()) { - c += Scripting::makeSetProperty(cameraVar, kAnimated, false); + c += ScriptWriter::makeSetProperty(cameraVar, kAnimated, false); } if (!hasAnimation() || !isAnimated()) { - c += Scripting::makeSetProperty(cameraVar, kPosition, getPosition()); - c += Scripting::makeSetProperty(cameraVar, kTarget, getTarget()); - c += Scripting::makeSetProperty(cameraVar, kUp, getUpVector()); + c += ScriptWriter::makeSetProperty(cameraVar, kPosition, getPosition()); + c += ScriptWriter::makeSetProperty(cameraVar, kTarget, getTarget()); + c += ScriptWriter::makeSetProperty(cameraVar, kUp, getUpVector()); } return c; @@ -363,7 +367,7 @@ namespace Falcor SCRIPT_BINDING(Camera) { pybind11::class_ camera(m, "Camera"); - camera.def_property_readonly("name", &Camera::getName); + camera.def_property("name", &Camera::getName, &Camera::setName); camera.def_property("aspectRatio", &Camera::getAspectRatio, &Camera::setAspectRatio); camera.def_property("focalLength", &Camera::getFocalLength, &Camera::setFocalLength); camera.def_property("frameHeight", &Camera::getFrameHeight, &Camera::setFrameHeight); @@ -377,5 +381,6 @@ namespace Falcor camera.def_property(kPosition.c_str(), &Camera::getPosition, &Camera::setPosition); camera.def_property(kTarget.c_str(), &Camera::getTarget, &Camera::setTarget); camera.def_property(kUp.c_str(), &Camera::getUpVector, &Camera::setUpVector); + camera.def(pybind11::init(&Camera::create), "name"_a = ""); } } diff --git a/Source/Falcor/Scene/Camera/Camera.h b/Source/Falcor/Scene/Camera/Camera.h index 6b5b4f14a2..671450243b 100644 --- a/Source/Falcor/Scene/Camera/Camera.h +++ b/Source/Falcor/Scene/Camera/Camera.h @@ -30,6 +30,7 @@ #include "Scene/Animation/Animatable.h" #include "Utils/SampleGenerators/CPUSampleGenerator.h" #include "Core/BufferTypes/ParameterBlock.h" +#include "Utils/Math/AABB.h" namespace Falcor { @@ -50,8 +51,8 @@ namespace Falcor /** Create a new camera object. */ - static SharedPtr create(); - ~Camera(); + static SharedPtr create(const std::string& name = ""); + ~Camera() = default; /** Name the camera. */ @@ -195,6 +196,10 @@ namespace Falcor */ const glm::mat4& getViewMatrix() const; + /** Get the previous frame view matrix, which possibly includes the previous frame's camera jitter. + */ + const glm::mat4& getPrevViewMatrix() const; + /** Get the projection matrix. */ const glm::mat4& getProjMatrix() const; @@ -224,7 +229,7 @@ namespace Falcor /** Check if an object should be culled \param[in] box Bounding box of the object to check */ - bool isObjectCulled(const BoundingBox& box) const; + bool isObjectCulled(const AABB& box) const; /** Set the camera into a shader var */ @@ -265,7 +270,7 @@ namespace Falcor std::string getScript(const std::string& cameraVar); private: - Camera(); + Camera(const std::string& name); Changes mChanges = Changes::None; mutable bool mDirty = true; diff --git a/Source/Falcor/Scene/Camera/Camera.slang b/Source/Falcor/Scene/Camera/Camera.slang index f1dad929db..299b994785 100644 --- a/Source/Falcor/Scene/Camera/Camera.slang +++ b/Source/Falcor/Scene/Camera/Camera.slang @@ -39,6 +39,11 @@ struct CameraRay { return { origin, tMin, dir, tMax }; } + + float3 eval(float t) + { + return origin + t * dir; + } }; struct Camera diff --git a/Source/Falcor/Scene/Camera/CameraController.h b/Source/Falcor/Scene/Camera/CameraController.h index d47198c67b..cc02fc4782 100644 --- a/Source/Falcor/Scene/Camera/CameraController.h +++ b/Source/Falcor/Scene/Camera/CameraController.h @@ -46,7 +46,7 @@ namespace Falcor */ virtual bool onMouseEvent(const MouseEvent& mouseEvent) { return false; } - /* Handle keyboard events + /** Handle keyboard events */ virtual bool onKeyEvent(const KeyboardEvent& keyboardEvent) { return false; } diff --git a/Source/Falcor/Scene/Camera/CameraData.slang b/Source/Falcor/Scene/Camera/CameraData.slang index e6d25164f1..5433bba9de 100644 --- a/Source/Falcor/Scene/Camera/CameraData.slang +++ b/Source/Falcor/Scene/Camera/CameraData.slang @@ -35,6 +35,7 @@ BEGIN_NAMESPACE_FALCOR struct CameraData { float4x4 viewMat; ///< Camera view matrix. + float4x4 prevViewMat; ///< Camera view matrix associated to previous frame. float4x4 projMat; ///< Camera projection matrix. float4x4 viewProjMat; ///< Camera view-projection matrix. float4x4 invViewProj; ///< Camera inverse view-projection matrix. diff --git a/Source/Falcor/Scene/Curves/CurveTessellation.cpp b/Source/Falcor/Scene/Curves/CurveTessellation.cpp new file mode 100644 index 0000000000..b5c5351d43 --- /dev/null +++ b/Source/Falcor/Scene/Curves/CurveTessellation.cpp @@ -0,0 +1,247 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#include "stdafx.h" +#include "CurveTessellation.h" +#include "Utils/Math/MathHelpers.h" +#define _USE_MATH_DEFINES +#include + +namespace Falcor +{ + namespace + { + float4 transformSphere(const glm::mat4& xform, const float4& sphere) + { + // Spheres are represented as (center.x, center.y, center.z, radius). + // Assume the scaling is isotropic, i.e., the end points are still spheres after transformation. + float3 q = sphere.xyz + float3(sphere.w, 0, 0); + float4 xp = xform * float4(sphere.xyz, 1.f); + float4 xq = xform * float4(q, 1.f); + float xr = glm::length(xq - xp); + return float4(xp.xyz, xr); + } + } + + CurveTessellation::SweptSphereResult CurveTessellation::convertToLinearSweptSphere(size_t strandCount, const int* vertexCountsPerStrand, const float3* controlPoints, const float* widths, const float2* UVs, uint32_t degree, uint32_t subdivPerSegment, const glm::mat4& xform) + { + SweptSphereResult result; + + // Only support linear tube segments now. + // TODO: Add quadratic or cubic tube segments if necessary. + assert(degree == 1); + result.degree = degree; + + uint32_t pointCounts = 0; + uint32_t segCounts = 0; + for (uint32_t i = 0; i < strandCount; i++) + { + pointCounts += subdivPerSegment * (vertexCountsPerStrand[i] - 1) + 1; + segCounts += pointCounts - 1; + } + result.indices.reserve(segCounts); + result.points.reserve(pointCounts); + result.radius.reserve(pointCounts); + result.tangents.reserve(pointCounts); + result.normals.reserve(pointCounts); + result.texCrds.reserve(pointCounts); + + uint32_t pointOffset = 0; + for (uint32_t i = 0; i < strandCount; i++) + { + CubicSpline strandPoints(controlPoints + pointOffset, vertexCountsPerStrand[i]); + CubicSpline strandWidths(widths + pointOffset, vertexCountsPerStrand[i]); + + uint32_t resOffset = (uint32_t)result.points.size(); + for (uint32_t j = 0; j < (uint32_t)vertexCountsPerStrand[i] - 1; j++) + { + for (uint32_t k = 0; k < subdivPerSegment; k++) + { + float t = (float)k / (float)subdivPerSegment; + result.indices.push_back((uint32_t)result.points.size()); + + // Pre-transform curve points. + float4 sph = transformSphere(xform, float4(strandPoints.interpolate(j, t), strandWidths.interpolate(j, t) * 0.5f)); + result.points.push_back(sph.xyz); + result.radius.push_back(sph.w); + } + } + + float4 sph = transformSphere(xform, float4(strandPoints.interpolate(vertexCountsPerStrand[i] - 2, 1.f), strandWidths.interpolate(vertexCountsPerStrand[i] - 2, 1.f) * 0.5f)); + result.points.push_back(sph.xyz); + result.radius.push_back(sph.w); + + // Compute tangents and normals. + for (uint32_t j = resOffset; j < result.points.size(); j++) + { + float3 fwd, s, t; + if (j < result.points.size() - 1) + { + fwd = normalize(result.points[j + 1] - result.points[j]); + } + else + { + fwd = normalize(result.points[j] - result.points[j - 1]); + } + buildFrame(fwd, s, t); + + result.tangents.push_back(fwd); + result.normals.push_back(s); + } + + // Texture coordinates. + if (UVs) + { + CubicSpline strandUVs(UVs + pointOffset, vertexCountsPerStrand[i]); + for (uint32_t j = 0; j < (uint32_t)vertexCountsPerStrand[i] - 1; j++) + { + for (uint32_t k = 0; k < subdivPerSegment; k++) + { + float t = (float)k / (float)subdivPerSegment; + result.texCrds.push_back(strandUVs.interpolate(j, t)); + } + } + result.texCrds.push_back(strandUVs.interpolate(vertexCountsPerStrand[i] - 2, 1.f)); + } + + pointOffset += vertexCountsPerStrand[i]; + } + + return result; + } + + CurveTessellation::MeshResult CurveTessellation::convertToMesh(size_t strandCount, const int* vertexCountsPerStrand, const float3* controlPoints, const float* widths, const float2* UVs, uint32_t subdivPerSegment, uint32_t pointCountPerCrossSection) + { + MeshResult result; + uint32_t vertexCounts = 0; + uint32_t faceCounts = 0; + for (uint32_t i = 0; i < strandCount; i++) + { + vertexCounts += pointCountPerCrossSection * subdivPerSegment * (vertexCountsPerStrand[i] - 1) + 1; + faceCounts += 2 * pointCountPerCrossSection * subdivPerSegment * (vertexCountsPerStrand[i] - 1); + } + result.vertices.reserve(vertexCounts); + result.normals.reserve(vertexCounts); + result.tangents.reserve(vertexCounts); + result.faceVertexCounts.reserve(faceCounts); + result.faceVertexIndices.reserve(faceCounts * 3); + result.texCrds.reserve(vertexCounts); + + uint32_t pointOffset = 0; + uint32_t meshVertexOffset = 0; + for (uint32_t i = 0; i < strandCount; i++) + { + CubicSpline strandPoints(controlPoints + pointOffset, vertexCountsPerStrand[i]); + CubicSpline strandWidths(widths + pointOffset, vertexCountsPerStrand[i]); + + std::vector curvePoints; + std::vector curveRadius; + std::vector curveUVs; + + curvePoints.push_back(strandPoints.interpolate(0, 0.f)); + curveRadius.push_back(strandWidths.interpolate(0, 0.f) * 0.5f); + + for (uint32_t j = 0; j < (uint32_t)vertexCountsPerStrand[i] - 1; j++) + { + for (uint32_t k = 1; k <= subdivPerSegment; k++) + { + float t = (float)k / (float)subdivPerSegment; + curvePoints.push_back(strandPoints.interpolate(j, t)); + curveRadius.push_back(strandWidths.interpolate(j, t) * 0.5f); + } + } + + // Texture coordinates. + if (UVs) + { + CubicSpline strandUVs(UVs + pointOffset, vertexCountsPerStrand[i]); + curveUVs.push_back(strandUVs.interpolate(0, 0.f)); + for (uint32_t j = 0; j < (uint32_t)vertexCountsPerStrand[i] - 1; j++) + { + for (uint32_t k = 1; k <= subdivPerSegment; k++) + { + float t = (float)k / (float)subdivPerSegment; + curveUVs.push_back(strandUVs.interpolate(j, t)); + } + } + } + + pointOffset += vertexCountsPerStrand[i]; + + // Create mesh. + for (uint32_t j = 0; j < curvePoints.size(); j++) + { + float3 fwd, s, t; + if (j < curvePoints.size() - 1) + { + fwd = normalize(curvePoints[j + 1] - curvePoints[j]); + } + else + { + fwd = normalize(curvePoints[j] - curvePoints[j - 1]); + } + buildFrame(fwd, s, t); + + // Mesh vertices, normals, tangents, and texCrds (if any). + for (uint32_t k = 0; k < pointCountPerCrossSection; k++) + { + float phi = (float)k / (float)pointCountPerCrossSection * (float)M_PI * 2.f; + float3 vNormal = std::cos(phi) * s + std::sin(phi) * t; + + result.vertices.push_back(curvePoints[j] + curveRadius[j] * vNormal); + result.normals.push_back(vNormal); + result.tangents.push_back(float4(fwd.x, fwd.y, fwd.z, 1)); + + if (UVs) + { + result.texCrds.push_back(curveUVs[j]); + } + } + + // Mesh faces. + if (j < curvePoints.size() - 1) + { + for (uint32_t k = 0; k < pointCountPerCrossSection; k++) + { + result.faceVertexCounts.push_back(3); + result.faceVertexIndices.push_back(meshVertexOffset + j * pointCountPerCrossSection + k); + result.faceVertexIndices.push_back(meshVertexOffset + j * pointCountPerCrossSection + (k + 1) % pointCountPerCrossSection); + result.faceVertexIndices.push_back(meshVertexOffset + (j + 1) * pointCountPerCrossSection + (k + 1) % pointCountPerCrossSection); + + result.faceVertexCounts.push_back(3); + result.faceVertexIndices.push_back(meshVertexOffset + j * pointCountPerCrossSection + k); + result.faceVertexIndices.push_back(meshVertexOffset + (j + 1) * pointCountPerCrossSection + (k + 1) % pointCountPerCrossSection); + result.faceVertexIndices.push_back(meshVertexOffset + (j + 1) * pointCountPerCrossSection + k); + } + } + } + + meshVertexOffset += pointCountPerCrossSection * (uint32_t)curvePoints.size(); + } + return result; + } +} diff --git a/Source/Falcor/Scene/Curves/CurveTessellation.h b/Source/Falcor/Scene/Curves/CurveTessellation.h new file mode 100644 index 0000000000..15cde3374f --- /dev/null +++ b/Source/Falcor/Scene/Curves/CurveTessellation.h @@ -0,0 +1,91 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#pragma once +#include "Utils/Math/CubicSpline.h" + +namespace Falcor +{ + class dlldecl CurveTessellation + { + public: + // Swept spheres + + struct SweptSphereResult + { + uint32_t degree; + std::vector indices; + std::vector points; + std::vector radius; + std::vector tangents; + std::vector normals; + std::vector texCrds; + }; + + /** Convert cubic B-splines to a couple of linear swept sphere segments. + \param[in] strandCount Number of curve strands. + \param[in] vertexCountsPerStrand Number of control points per strand. + \param[in] controlPoints Array of control points. + \param[in] widths Array of curve widths, i.e., diameters of swept spheres. + \param[in] UVs Array of texture coordinates. + \param[in] degree Polynomial degree of strand (linear -- cubic). + \param[in] subdivPerSegment Number of sub-segments within each cubic bspline segment (defined by 4 control points). + \param[in] xform Row-major 4x4 transformation matrix. We apply pre-transformation to curve geometry. + \return Linear swept sphere segments. + */ + static SweptSphereResult convertToLinearSweptSphere(size_t strandCount, const int* vertexCountsPerStrand, const float3* controlPoints, const float* widths, const float2* UVs, uint32_t degree, uint32_t subdivPerSegment, const glm::mat4& xform); + + // Tessellated mesh + + struct MeshResult + { + std::vector vertices; + std::vector normals; + std::vector tangents; + std::vector faceVertexCounts; + std::vector faceVertexIndices; + std::vector texCrds; + }; + + /** Tessellate cubic B-splines to a triangular mesh. + \param[in] strandCount Number of curve strands. + \param[in] vertexCountsPerStrand Number of control points per strand. + \param[in] controlPoints Array of control points. + \param[in] widths Array of curve widths, i.e., diameters of swept spheres. + \param[in] UVs Array of texture coordinates. + \param[in] subdivPerSegment Number of sub-segments within each cubic bspline segment (defined by 4 control points). + \param[in] pointCountPerCrossSection Number of points sampled at each cross-section. + \return Tessellated mesh. + */ + static MeshResult convertToMesh(size_t strandCount, const int* vertexCountsPerStrand, const float3* controlPoints, const float* widths, const float2* UVs, uint32_t subdivPerSegment, uint32_t pointCountPerCrossSection); + + private: + CurveTessellation() = default; + CurveTessellation(const CurveTessellation&) = delete; + void operator=(const CurveTessellation&) = delete; + }; +} diff --git a/Source/Falcor/Scene/HitInfo.cpp b/Source/Falcor/Scene/HitInfo.cpp new file mode 100644 index 0000000000..88828caf58 --- /dev/null +++ b/Source/Falcor/Scene/HitInfo.cpp @@ -0,0 +1,92 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#include "stdafx.h" +#include "HitInfo.h" +#include "HitInfoType.slang" +#include "Scene.h" + +namespace Falcor +{ + namespace + { + uint32_t allocateBits(const uint32_t count) + { + if (count <= 1) return 0; + uint32_t maxValue = count - 1; + return bitScanReverse(maxValue) + 1; + } + } + + void HitInfo::init(const Scene& scene) + { + // Setup bit allocations for encoding the hit information. + // The shader code will choose either a 64-bit or 96-bit format depending on requirements. + // The barycentrics are encoded in 32 bits, while instance type and instance/primitive index are encoded in 32-64 bits. + + uint32_t typeCount = (uint32_t)InstanceType::Count; + mInstanceTypeBits = allocateBits(typeCount); + + uint32_t instanceCount = std::max(scene.getMeshInstanceCount(), scene.getCurveInstanceCount()); + mInstanceIndexBits = allocateBits(instanceCount); + + uint32_t maxPrimitiveCount = 0; + for (uint32_t meshID = 0; meshID < scene.getMeshCount(); meshID++) + { + uint32_t triangleCount = scene.getMesh(meshID).getTriangleCount(); + maxPrimitiveCount = std::max(maxPrimitiveCount, triangleCount); + } + for (uint32_t curveID = 0; curveID < scene.getCurveCount(); curveID++) + { + uint32_t curveSegmentCount = scene.getCurve(curveID).getSegmentCount(); + maxPrimitiveCount = std::max(maxPrimitiveCount, curveSegmentCount); + } + mPrimitiveIndexBits = allocateBits(maxPrimitiveCount); + + // Handle special case to reserve 'kInvalidIndex' from being used. + uint32_t maxTypeID = typeCount > 0 ? typeCount - 1 : 0; + uint32_t maxInstanceID = instanceCount > 0 ? instanceCount - 1 : 0; + uint32_t maxPrimitiveID = maxPrimitiveCount > 0 ? maxPrimitiveCount - 1 : 0; + uint32_t packedInstance = (maxTypeID << mInstanceIndexBits) | maxInstanceID; + + if (mInstanceTypeBits + mInstanceIndexBits + mPrimitiveIndexBits == 32) + { + uint32_t packed = (packedInstance << mPrimitiveIndexBits) | maxPrimitiveID; + if (packed == kInvalidIndex) mInstanceIndexBits++; + } + if (mInstanceTypeBits + mInstanceIndexBits == 32) + { + if (packedInstance == kInvalidIndex) mInstanceIndexBits++; + } + + // Check that the final bit allocation fits. + if (mPrimitiveIndexBits > 32 || (mInstanceTypeBits + mInstanceIndexBits) > 32) + { + throw std::exception("Scene requires > 96 bits for encoding hit information. This is currently not supported."); + } + } +} diff --git a/Source/Falcor/Scene/HitInfo.h b/Source/Falcor/Scene/HitInfo.h index b866e5c400..b3fafa21a4 100644 --- a/Source/Falcor/Scene/HitInfo.h +++ b/Source/Falcor/Scene/HitInfo.h @@ -30,42 +30,43 @@ namespace Falcor { + class Scene; + class HitInfo { public: static const uint32_t kInvalidIndex = 0xffffffff; + static const uint32_t kMaxPackedSizeInBytes = 12; + static const ResourceFormat kDefaultFormat = ResourceFormat::RG32Uint; /** Returns defines needed packing/unpacking a HitInfo struct. */ - static Shader::DefineList getDefines(const Scene* pScene) + Shader::DefineList getDefines() const { - // Setup bit allocations for encoding the meshInstanceID and primitive indices. - - uint32_t meshInstanceCount = pScene->getMeshInstanceCount(); - uint32_t maxInstanceID = meshInstanceCount > 0 ? meshInstanceCount - 1 : 0; - uint32_t instanceIndexBits = maxInstanceID > 0 ? bitScanReverse(maxInstanceID) + 1 : 0; - - uint32_t maxTriangleCount = 0; - for (uint32_t meshID = 0; meshID < pScene->getMeshCount(); meshID++) - { - uint32_t triangleCount = pScene->getMesh(meshID).getTriangleCount(); - maxTriangleCount = std::max(triangleCount, maxTriangleCount); - } - uint32_t maxTriangleID = maxTriangleCount > 0 ? maxTriangleCount - 1 : 0; - uint32_t triangleIndexBits = maxTriangleID > 0 ? bitScanReverse(maxTriangleID) + 1 : 0; - - if (instanceIndexBits + triangleIndexBits > 32 || - (instanceIndexBits + triangleIndexBits == 32 && ((maxInstanceID << triangleIndexBits) | maxTriangleID) == kInvalidIndex)) - { - logError("Scene requires > 32 bits for encoding meshInstanceID/triangleIndex. This is currently not supported."); - } - - // Setup defines for the shader program. + assert((mInstanceTypeBits + mInstanceIndexBits) <= 32 && mPrimitiveIndexBits <= 32); Shader::DefineList defines; - defines.add("HIT_INSTANCE_INDEX_BITS", std::to_string(instanceIndexBits)); - defines.add("HIT_TRIANGLE_INDEX_BITS", std::to_string(triangleIndexBits)); - + defines.add("HIT_INSTANCE_TYPE_BITS", std::to_string(mInstanceTypeBits)); + defines.add("HIT_INSTANCE_INDEX_BITS", std::to_string(mInstanceIndexBits)); + defines.add("HIT_PRIMITIVE_INDEX_BITS", std::to_string(mPrimitiveIndexBits)); return defines; } + + /** Returns the resource format required for encoding packed hit information. + */ + ResourceFormat getFormat() const + { + assert((mInstanceTypeBits + mInstanceIndexBits) <= 32 && mPrimitiveIndexBits <= 32); + if (mInstanceTypeBits + mInstanceIndexBits + mPrimitiveIndexBits <= 32) return ResourceFormat::RG32Uint; + else return ResourceFormat::RGBA32Uint; // RGB32Uint can't be used for UAV writes + } + + HitInfo() = default; + HitInfo(const Scene & scene) { init(scene); } + void init(const Scene& scene); + + private: + uint32_t mInstanceTypeBits = 0; + uint32_t mInstanceIndexBits = 0; + uint32_t mPrimitiveIndexBits = 0; }; } diff --git a/Source/Falcor/Scene/HitInfo.slang b/Source/Falcor/Scene/HitInfo.slang index 3a5bcca233..fd8378f9b1 100644 --- a/Source/Falcor/Scene/HitInfo.slang +++ b/Source/Falcor/Scene/HitInfo.slang @@ -26,29 +26,50 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **************************************************************************/ import Utils.Math.FormatConversion; +__exported import Scene.HitInfoType; /** Ray hit information. - The fields in this struct uniquely identifies a hit point in terms - of mesh instance ID and primitive index, together with barycentrics. + The fields in the 'HitInfo' struct uniquely identifies a hit point in terms + of instance ID and primitive index, together with barycentrics. If the host sets the following defines, the struct includes helpers - for packing/unpacking the hit information into a 64-bit value. + for packing/unpacking the hit information. Use 'PackedHitInfo' in your code. - - HIT_INSTANCE_INDEX_BITS Bits needed to encode the mesh instance ID of the hit - - HIT_TRIANGLE_INDEX_BITS Bits needed to encode the primitive index of the hit + - HIT_INSTANCE_TYPE_BITS Bits needed to encode the instance type. + - HIT_INSTANCE_INDEX_BITS Bits needed to encode the mesh instance ID of the hit. + - HIT_PRIMITIVE_INDEX_BITS Bits needed to encode the primitive index of the hit. - Together the bit allocations must be 32 or less. + If a define is zero, no bits are needed (the field has only one possible value = 0). */ + +#if defined(HIT_INSTANCE_TYPE_BITS) && defined(HIT_INSTANCE_INDEX_BITS) && defined(HIT_PRIMITIVE_INDEX_BITS) +#if ((HIT_INSTANCE_TYPE_BITS) + (HIT_INSTANCE_INDEX_BITS) + (HIT_PRIMITIVE_INDEX_BITS)) <= 32 + typedef uint2 PackedHitInfo; +#elif ((HIT_INSTANCE_TYPE_BITS) + (HIT_INSTANCE_INDEX_BITS)) <= 32 && (HIT_PRIMITIVE_INDEX_BITS) <= 32 + typedef uint3 PackedHitInfo; +#else + #error HitInfo instance or primitive index bits exceed the maximum supported +#endif +#endif + struct HitInfo { - uint meshInstanceID; ///< Mesh instance ID at hit. - uint primitiveIndex; ///< Primitive index at hit. - float2 barycentrics; ///< Barycentric coordinates at ray hit, always in [0,1]. + InstanceType type = InstanceType::TriangleMesh; ///< Type of instance. + uint instanceID; ///< Instance ID at hit. + uint primitiveIndex; ///< Primitive index at hit. + float2 barycentrics; ///< Barycentric coordinates at ray hit, always in [0,1]. static const uint kInvalidIndex = 0xffffffff; + /** Return instance type that was hit. + */ + InstanceType getType() + { + return type; + } + /** Return the barycentric weights. */ float3 getBarycentricWeights() @@ -56,30 +77,64 @@ struct HitInfo return float3(1.f - barycentrics.x - barycentrics.y, barycentrics.x, barycentrics.y); } -#if defined(HIT_INSTANCE_INDEX_BITS) && defined(HIT_TRIANGLE_INDEX_BITS) -#if ((HIT_INSTANCE_INDEX_BITS) + (HIT_TRIANGLE_INDEX_BITS)) > 32 - #error HitInfo instance/primitive index bits exceed 32 bits -#endif +#if defined(HIT_INSTANCE_TYPE_BITS) && defined(HIT_INSTANCE_INDEX_BITS) && defined(HIT_PRIMITIVE_INDEX_BITS) + + static const uint kInstanceTypeBits = HIT_INSTANCE_TYPE_BITS; + static const uint kInstanceIndexBits = HIT_INSTANCE_INDEX_BITS; + static const uint kPrimitiveIndexBits = HIT_PRIMITIVE_INDEX_BITS; - /** Encode hit information to packed format. +#if ((HIT_INSTANCE_TYPE_BITS) + (HIT_INSTANCE_INDEX_BITS) + (HIT_PRIMITIVE_INDEX_BITS)) <= 32 + + static const uint kInstanceIndexOffset = kPrimitiveIndexBits; + static const uint kInstanceTypeOffset = kInstanceIndexOffset + kInstanceIndexBits; + + /** Encode hit information to packed 64-bit format. */ - uint2 encode() + PackedHitInfo encode() { - uint2 packed; - packed.x = (meshInstanceID << (HIT_TRIANGLE_INDEX_BITS)) | primitiveIndex; + PackedHitInfo packed; + packed.x = (uint(type) << kInstanceTypeOffset) | (instanceID << kInstanceIndexOffset) | primitiveIndex; packed.y = packUnorm2x16_unsafe(barycentrics); return packed; } - /** Decode hit information from packed format. + /** Decode hit information from packed 64-bit format. \return True if the hit information is valid. */ - [mutating] bool decode(uint2 packed) + [mutating] bool decode(PackedHitInfo packed) { - meshInstanceID = packed.x >> (HIT_TRIANGLE_INDEX_BITS); - primitiveIndex = packed.x & ((1 << (HIT_TRIANGLE_INDEX_BITS)) - 1); + type = InstanceType(packed.x >> kInstanceTypeOffset); + instanceID = (packed.x >> kInstanceIndexOffset) & ((1 << kInstanceIndexBits) - 1); + primitiveIndex = packed.x & ((1 << kPrimitiveIndexBits) - 1); barycentrics = unpackUnorm2x16(packed.y); return packed.x != kInvalidIndex; } + +#elif ((HIT_INSTANCE_TYPE_BITS) + (HIT_INSTANCE_INDEX_BITS)) <= 32 && (HIT_PRIMITIVE_INDEX_BITS) <= 32 + + /** Encode hit information to packed 96-bit format. + */ + PackedHitInfo encode() + { + PackedHitInfo packed; + packed.x = (uint(type) << kInstanceIndexBits) | instanceID; + packed.y = primitiveIndex; + packed.z = packUnorm2x16_unsafe(barycentrics); + return packed; + } + + /** Decode hit information from packed 96-bit format. + \return True if the hit information is valid. + */ + [mutating] bool decode(PackedHitInfo packed) + { + type = InstanceType(packed.x >> kInstanceIndexBits); + instanceID = packed.x & ((1 << kInstanceIndexBits) - 1); + primitiveIndex = packed.y; + barycentrics = unpackUnorm2x16(packed.z); + return packed.x != kInvalidIndex; + } + +#endif #endif }; diff --git a/Source/Falcor/Scene/HitInfoType.slang b/Source/Falcor/Scene/HitInfoType.slang new file mode 100644 index 0000000000..584667eeda --- /dev/null +++ b/Source/Falcor/Scene/HitInfoType.slang @@ -0,0 +1,45 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#pragma once +#include "Utils/HostDeviceShared.slangh" + +BEGIN_NAMESPACE_FALCOR + +enum InstanceType : uint32_t +{ + TriangleMesh = 0, ///< Triangle mesh instance. + Curve = 1, ///< Curve instance. + + // + // Add new instance types here + // + + Count // Must be last +}; + +END_NAMESPACE_FALCOR diff --git a/Source/Falcor/Scene/Importers/AssimpImporter.cpp b/Source/Falcor/Scene/Importers/AssimpImporter.cpp index 79d08794f7..3b2536c9d3 100644 --- a/Source/Falcor/Scene/Importers/AssimpImporter.cpp +++ b/Source/Falcor/Scene/Importers/AssimpImporter.cpp @@ -36,6 +36,8 @@ #include "Core/API/Device.h" #include "Scene/SceneBuilder.h" +#include + namespace Falcor { namespace @@ -140,7 +142,6 @@ namespace Falcor SceneBuilder& builder; std::map materialMap; std::map meshMap; // Assimp mesh index to Falcor mesh ID - std::map textureCache; const SceneBuilder::InstanceMatrices& modelInstances; std::map localToBindPoseMatrices; @@ -219,24 +220,28 @@ namespace Falcor resetTime(pAiNode->mScalingKeys, pAiNode->mNumScalingKeys); } - Animation::SharedPtr createAnimation(ImporterData& data, const aiAnimation* pAiAnim) + void createAnimation(ImporterData& data, const aiAnimation* pAiAnim) { assert(pAiAnim->mNumMeshChannels == 0); double duration = pAiAnim->mDuration; double ticksPerSecond = pAiAnim->mTicksPerSecond ? pAiAnim->mTicksPerSecond : 25; double durationInSeconds = duration / ticksPerSecond; - Animation::SharedPtr pAnimation = Animation::create(pAiAnim->mName.C_Str(), durationInSeconds); - for (uint32_t i = 0; i < pAiAnim->mNumChannels; i++) { aiNodeAnim* pAiNode = pAiAnim->mChannels[i]; resetNegativeKeyframeTimes(pAiNode); - std::vector channels; + std::vector animations; for (uint32_t i = 0; i < data.getNodeInstanceCount(pAiNode->mNodeName.C_Str()); i++) { - channels.push_back(pAnimation->addChannel(data.getFalcorNodeID(pAiNode->mNodeName.C_Str(), i))); + Animation::SharedPtr pAnimation = Animation::create( + std::string(pAiNode->mNodeName.C_Str()) + "." + std::to_string(i), + data.getFalcorNodeID(pAiNode->mNodeName.C_Str(), i), + durationInSeconds + ); + animations.push_back(pAnimation); + data.builder.addAnimation(pAnimation); } uint32_t pos = 0, rot = 0, scale = 0; @@ -263,11 +268,9 @@ namespace Falcor done = parseAnimationChannel(pAiNode->mPositionKeys, pAiNode->mNumPositionKeys, time, pos, keyframe.translation); done = parseAnimationChannel(pAiNode->mRotationKeys, pAiNode->mNumRotationKeys, time, rot, keyframe.rotation) && done; done = parseAnimationChannel(pAiNode->mScalingKeys, pAiNode->mNumScalingKeys, time, scale, keyframe.scaling) && done; - for(auto c : channels) pAnimation->addKeyframe(c, keyframe); + for(auto pAnimation : animations) pAnimation->addKeyframe(keyframe); } } - - return pAnimation; } bool createCameras(ImporterData& data, ImportMode importMode) @@ -317,7 +320,6 @@ namespace Falcor bool addLightCommon(const Light::SharedPtr& pLight, const glm::mat4& baseMatrix, ImporterData& data, const aiLight* pAiLight) { - pLight->setName(pAiLight->mName.C_Str()); assert(pAiLight->mColorDiffuse == pAiLight->mColorSpecular); pLight->setIntensity(aiCast(pAiLight->mColorSpecular)); @@ -340,7 +342,7 @@ namespace Falcor bool createDirLight(ImporterData& data, const aiLight* pAiLight) { - DirectionalLight::SharedPtr pLight = DirectionalLight::create(); + DirectionalLight::SharedPtr pLight = DirectionalLight::create(pAiLight->mName.C_Str()); float3 direction = normalize(aiCast(pAiLight->mDirection)); pLight->setWorldDirection(direction); glm::mat4 base; @@ -350,7 +352,7 @@ namespace Falcor bool createPointLight(ImporterData& data, const aiLight* pAiLight) { - PointLight::SharedPtr pLight = PointLight::create(); + PointLight::SharedPtr pLight = PointLight::create(pAiLight->mName.C_Str()); float3 position = aiCast(pAiLight->mPosition); float3 lookAt = normalize(aiCast(pAiLight->mDirection)); float3 up = normalize(aiCast(pAiLight->mUp)); @@ -396,8 +398,7 @@ namespace Falcor { for (uint32_t i = 0; i < data.pScene->mNumAnimations; i++) { - Animation::SharedPtr pAnimation = createAnimation(data, data.pScene->mAnimations[i]); - data.builder.addAnimation(pAnimation); + createAnimation(data, data.pScene->mAnimations[i]); } return true; } @@ -446,9 +447,13 @@ namespace Falcor void loadBones(const aiMesh* pAiMesh, const ImporterData& data, std::vector& weights, std::vector& ids) { const uint32_t vertexCount = pAiMesh->mNumVertices; + weights.resize(vertexCount); ids.resize(vertexCount); + std::fill(weights.begin(), weights.end(), float4(0.f)); + std::fill(ids.begin(), ids.end(), uint4(Scene::kInvalidBone)); + for (uint32_t bone = 0; bone < pAiMesh->mNumBones; bone++) { const aiBone* pAiBone = pAiMesh->mBones[bone]; @@ -462,6 +467,9 @@ namespace Falcor // Get the vertex the current weight affects const aiVertexWeight& aiWeight = pAiBone->mWeights[weightID]; + // Skip zero weights + if (aiWeight.mWeight == 0.f) continue; + // Get the address of the Bone ID and weight for the current vertex uint4& vertexIds = ids[aiWeight.mVertexId]; float4& vertexWeights = weights[aiWeight.mVertexId]; @@ -470,7 +478,7 @@ namespace Falcor bool emptySlotFound = false; for (uint32_t j = 0; j < Scene::kMaxBonesPerVertex; j++) { - if (vertexWeights[j] == 0) + if (vertexIds[j] == Scene::kInvalidBone) { vertexIds[j] = aiBoneID; vertexWeights[j] = aiWeight.mWeight; @@ -498,30 +506,12 @@ namespace Falcor const aiScene* pScene = data.pScene; const bool loadTangents = is_set(data.builder.getFlags(), SceneBuilder::Flags::UseOriginalTangentSpace); - // Find the largest mesh. - uint64_t largestIndexCount = 0; - uint64_t largestVertexCount = 0; - - for (uint32_t i = 0; i < pScene->mNumMeshes; i++) - { - const aiMesh* pAiMesh = pScene->mMeshes[i]; - uint64_t indexCount = pAiMesh->mNumFaces * pAiMesh->mFaces[0].mNumIndices; - - largestIndexCount = std::max(largestIndexCount, indexCount); - largestVertexCount = std::max(largestVertexCount, (uint64_t)pAiMesh->mNumVertices); - } - - // Reserve memory for the vertex and index data. - std::vector indexList; - std::vector texCrds; - std::vector tangents; - indexList.reserve(largestIndexCount); - texCrds.reserve(largestVertexCount); - if (loadTangents) tangents.reserve(largestVertexCount); + uint32_t meshCount = pScene->mNumMeshes; - // Add all the meshes. - for (uint32_t i = 0; i < pScene->mNumMeshes; i++) - { + // Pre-process meshes. + std::vector processedMeshes(meshCount); + auto range = NumericRange(0, meshCount); + std::for_each(std::execution::par, range.begin(), range.end(), [&] (uint32_t i) { const aiMesh* pAiMesh = pScene->mMeshes[i]; const uint32_t perFaceIndexCount = pAiMesh->mFaces[0].mNumIndices; @@ -529,6 +519,13 @@ namespace Falcor mesh.name = pAiMesh->mName.C_Str(); mesh.faceCount = pAiMesh->mNumFaces; + // Temporary memory for the vertex and index data. + std::vector indexList; + std::vector texCrds; + std::vector tangents; + std::vector boneIds; + std::vector boneWeights; + // Indices createIndexList(pAiMesh, indexList); assert(indexList.size() <= std::numeric_limits::max()); @@ -561,9 +558,6 @@ namespace Falcor mesh.tangents.frequency = SceneBuilder::Mesh::AttributeFrequency::Vertex; } - std::vector boneIds; - std::vector boneWeights; - if (pAiMesh->HasBones()) { loadBones(pAiMesh, data, boneWeights, boneIds); @@ -584,9 +578,18 @@ namespace Falcor } mesh.pMaterial = data.materialMap.at(pAiMesh->mMaterialIndex); - assert(mesh.pMaterial); - uint32_t meshID = data.builder.addMesh(mesh); - data.meshMap[i] = meshID; + + processedMeshes[i] = data.builder.processMesh(mesh); + }); + + // Add meshes to the scene. + // We retain a deterministic order of the meshes in the global scene buffer by adding + // them sequentially after being processed in parallel. + uint32_t i = 0; + for (const auto& mesh : processedMeshes) + { + uint32_t meshID = data.builder.addProcessedMesh(mesh); + data.meshMap[i++] = meshID; } } @@ -724,7 +727,7 @@ namespace Falcor } } - void loadTextures(ImporterData& data, const aiMaterial* pAiMaterial, const std::string& folder, Material* pMaterial, ImportMode importMode, bool useSrgb) + void loadTextures(ImporterData& data, const aiMaterial* pAiMaterial, const std::string& folder, const Material::SharedPtr& pMaterial, ImportMode importMode) { const auto& textureMappings = kTextureMappings[int(importMode)]; @@ -743,34 +746,13 @@ namespace Falcor continue; } - // Check if the texture was already loaded - Texture::SharedPtr pTex; - const auto& cacheItem = data.textureCache.find(path); - if (cacheItem != data.textureCache.end()) - { - pTex = cacheItem->second; - } - else - { - // create a new texture - std::string fullpath = folder + '/' + path; - fullpath = replaceSubstring(fullpath, "\\", "/"); - pTex = Texture::createFromFile(fullpath, true, useSrgb && pMaterial->isSrgbTextureRequired(source.targetType)); - if (pTex) - { - data.textureCache[path] = pTex; - } - } - - assert(pTex != nullptr); - pMaterial->setTexture(source.targetType, pTex); + // Load the texture + std::string filename = canonicalizeFilename(folder + '/' + path); + data.builder.loadMaterialTexture(pMaterial, source.targetType, filename); } - - // Flush upload heap after every material so we don't accumulate a ton of memory usage when loading a model with a lot of textures - gpDevice->flushAndSync(); } - Material::SharedPtr createMaterial(ImporterData& data, const aiMaterial* pAiMaterial, const std::string& folder, ImportMode importMode, bool useSrgb) + Material::SharedPtr createMaterial(ImporterData& data, const aiMaterial* pAiMaterial, const std::string& folder, ImportMode importMode) { aiString name; pAiMaterial->Get(AI_MATKEY_NAME, name); @@ -794,7 +776,7 @@ namespace Falcor } // Load textures. Note that loading is affected by the current shading model. - loadTextures(data, pAiMaterial, folder, pMaterial.get(), importMode, useSrgb); + loadTextures(data, pAiMaterial, folder, pMaterial, importMode); // Opacity float opacity = 1.f; @@ -913,12 +895,10 @@ namespace Falcor bool createAllMaterials(ImporterData& data, const std::string& modelFolder, ImportMode importMode) { - bool useSrgb = !is_set(data.builder.getFlags(), SceneBuilder::Flags::AssumeLinearSpaceTextures); - for (uint32_t i = 0; i < data.pScene->mNumMaterials; i++) { const aiMaterial* pAiMaterial = data.pScene->mMaterials[i]; - auto pMaterial = createMaterial(data, pAiMaterial, modelFolder, importMode, useSrgb); + auto pMaterial = createMaterial(data, pAiMaterial, modelFolder, importMode); if (pMaterial == nullptr) { logError("Can't allocate memory for material"); @@ -1031,7 +1011,7 @@ namespace Falcor assimpFlags &= ~(aiProcess_CalcTangentSpace); // Never use Assimp's tangent gen code assimpFlags &= ~(aiProcess_FindDegenerates); // Avoid converting degenerated triangles to lines assimpFlags &= ~(aiProcess_OptimizeGraph); // Never use as it doesn't handle transforms with negative determinants - assimpFlags &= ~(aiProcess_RemoveRedundantMaterials); // Avoid merging materials + assimpFlags &= ~(aiProcess_RemoveRedundantMaterials); // Avoid merging materials as it doesn't load all fields we care about, we merge in 'SceneBuilder' instead. assimpFlags &= ~(aiProcess_SplitLargeMeshes); // Avoid splitting large meshes if (is_set(builderFlags, SceneBuilder::Flags::DontMergeMeshes)) assimpFlags &= ~aiProcess_OptimizeMeshes; // Avoid merging original meshes diff --git a/Source/Falcor/Scene/Importers/PythonImporter.cpp b/Source/Falcor/Scene/Importers/PythonImporter.cpp index da153a6475..ac2bae8f8c 100644 --- a/Source/Falcor/Scene/Importers/PythonImporter.cpp +++ b/Source/Falcor/Scene/Importers/PythonImporter.cpp @@ -28,110 +28,71 @@ #include "stdafx.h" #include "PythonImporter.h" #include +#include namespace Falcor { - class PythonImporterImpl + namespace { - public: - PythonImporterImpl(SceneBuilder& builder) : mBuilder(builder) {} - bool load(const std::string& filename); - bool importScene(std::string& filename, const pybind11::dict& dict); - private: - Scripting::Context mScriptingContext; - SceneBuilder& mBuilder; - }; - - bool PythonImporterImpl::importScene(std::string& filename, const pybind11::dict& dict) - { - bool success = true; - - std::string extension = std::filesystem::path(filename).extension().string(); - if (extension == ".pyscene") + /** Parse the legacy header on the first line of the script with the syntax: + # filename.extension + */ + static std::optional parseLegacyHeader(const std::string& script) { - logError("Python scene files cannot be imported from python scene files."); - success = false; - } - else - { - SceneBuilder::InstanceMatrices mats; - success = mBuilder.import(filename, mats, Dictionary(dict)); - - if (success) + if (size_t endOfFirstLine = script.find_first_of("\n\r"); endOfFirstLine != std::string::npos) { - if (mScriptingContext.containsObject("scene")) - { - // Warn if a scene had previously been imported by this script - logWarning("More than one scene loaded from python script. Discarding previously loaded scene."); + const std::regex headerRegex(R"""(#\s+([\w-]+\.[\w]{1,10}))"""); + + std::smatch match; + if (std::regex_match(script.begin(), script.begin() + endOfFirstLine, match, headerRegex)) { + if (match.size() > 1) return match[1].str(); } - mScriptingContext.setObject("scene", mBuilder.getScene()); } + + return {}; } - return success; } - bool PythonImporterImpl::load(const std::string& filename) + bool PythonImporter::import(const std::string& filename, SceneBuilder& builder, const SceneBuilder::InstanceMatrices& instances, const Dictionary& dict) { + bool success = false; + + if (!instances.empty()) logWarning("Python importer does not support instancing."); + std::string fullpath; if (findFileInDataDirectories(filename, fullpath)) { - // Get the directory of the script file - const std::string directory = fullpath.substr(0, fullpath.find_last_of("/\\")); + // Add script directory to search paths (add it to the front to make it highest priority). + const std::string directory = getDirectoryFromFile(fullpath); + addDataDirectory(directory, true); // Load the script file - const std::string script = removeLeadingWhitespaces(readFile(fullpath)); + const std::string script = readFile(fullpath); - addDataDirectory(directory); - - bool success = true; - - // Get filename of referenced scene from first line "# filename.{fbx,fscene}", if any - size_t endOfFirstLine = script.find_first_of("\r\n"); - if (script.length() >= 2 && script[0] == '#' && script[1] == ' ' && endOfFirstLine != std::string::npos) + // Check for legacy .pyscene file format. + if (auto sceneFile = parseLegacyHeader(script)) { - const std::string sceneFile = script.substr(2, endOfFirstLine - 2); - std::string extension = std::filesystem::path(sceneFile).extension().string(); - if (extension != ".pyscene") - { - // Load referenced scene - success = mBuilder.import(sceneFile.c_str()); - if (success) - { - mScriptingContext.setObject("scene", mBuilder.getScene()); - } - } + logError("Python scene file '" + fullpath + "' is using old header comment syntax. Use the new 'sceneBuilder' object instead."); } - - if (success) + else { - // Execute scene script. - mScriptingContext.setObject("importer", this); - Scripting::runScriptFromFile(fullpath, mScriptingContext); + Scripting::Context context; + context.setObject("sceneBuilder", &builder); + Scripting::runScript("from falcor import *", context); + Scripting::runScriptFromFile(fullpath, context); + success = true; } + // Remove script directory from search path. removeDataDirectory(directory); - return success; } else { logError("Error when loading scene file '" + filename + "'. File not found."); - return false; } - } - bool PythonImporter::import(const std::string& filename, SceneBuilder& builder, const SceneBuilder::InstanceMatrices& instances, const Dictionary& dict) - { - if (!instances.empty()) logWarning("Python importer does not support instancing."); - - PythonImporterImpl importer(builder); - return importer.load(filename); - } - - SCRIPT_BINDING(PythonImporterImpl) - { - pybind11::class_ importer(m, "PythonImporterImpl"); - importer.def("importScene", &PythonImporterImpl::importScene, "filename"_a, "dictionary"_a = pybind11::dict()); + return success; } REGISTER_IMPORTER( diff --git a/Source/Falcor/Scene/Importers/SceneImporter.cpp b/Source/Falcor/Scene/Importers/SceneImporter.cpp index c790ef8d27..221dca9c6d 100644 --- a/Source/Falcor/Scene/Importers/SceneImporter.cpp +++ b/Source/Falcor/Scene/Importers/SceneImporter.cpp @@ -41,12 +41,6 @@ namespace Falcor static const char* kInclude = "include"; - // Not supported in exporter yet - static const char* kLightProbes = "light_probes"; - static const char* kLightProbeRadius = "radius"; - static const char* kLightProbeDiffSamples = "diff_samples"; - static const char* kLightProbeSpecSamples = "spec_samples"; - // Keys for values in older scene versions that are not exported anymore static const char* kCamFovY = "fovY"; static const char* kActivePath = "active_path"; @@ -118,7 +112,6 @@ namespace Falcor bool parseSceneUnit(const rapidjson::Value& jsonVal); bool parseModels(const rapidjson::Value& jsonVal); bool parseLights(const rapidjson::Value& jsonVal); - bool parseLightProbes(const rapidjson::Value& jsonVal); bool parseCameras(const rapidjson::Value& jsonVal); bool parseCamera(const rapidjson::Value& jsonVal); bool parseAmbientIntensity(const rapidjson::Value& jsonVal); @@ -362,7 +355,7 @@ namespace Falcor } assert(std::filesystem::path(file).extension() != ".fscene"); // #SCENE this will cause an endless recursion. We may want to fix it - mBuilder.import(file.c_str(), instances); + mBuilder.import(file, instances); return true; } @@ -583,14 +576,12 @@ namespace Falcor auto typeKey = jsonLight.FindMember(SceneKeys::kType); if (typeKey == jsonLight.MemberEnd() || !typeKey->value.IsString()) error("Area light missing/invalid '" + std::string(SceneKeys::kType) + "' key"); - LightType type; - if (typeKey->value.GetString() == SceneKeys::kAreaLightRect) type = LightType::Rect; - else if (typeKey->value.GetString() == SceneKeys::kAreaLightSphere) type = LightType::Sphere; - else if (typeKey->value.GetString() == SceneKeys::kAreaLightDisc) type = LightType::Disc; - else return error("Invalid area light type"); - // Create the light. - auto pAreaLight = AnalyticAreaLight::create(type); + AnalyticAreaLight::SharedPtr pAreaLight; + if (typeKey->value.GetString() == SceneKeys::kAreaLightRect) pAreaLight = RectLight::create(); + else if (typeKey->value.GetString() == SceneKeys::kAreaLightSphere) pAreaLight = SphereLight::create(); + else if (typeKey->value.GetString() == SceneKeys::kAreaLightDisc) pAreaLight = DiscLight::create(); + else return error("Invalid area light type"); float3 scaling(1, 1, 1); float3 translation(0, 0, 0); @@ -721,92 +712,6 @@ namespace Falcor return true; } - bool SceneImporterImpl::parseLightProbes(const rapidjson::Value& jsonVal) - { - if (jsonVal.IsArray() == false) - { - return error("Light probes should be an array of objects."); - } - - for (uint32_t i = 0; i < jsonVal.Size(); i++) - { - const auto& lightProbe = jsonVal[i]; - - if (lightProbe.HasMember(SceneKeys::kFilename) == false) - { - return error("An image file must be specified for a light probe."); - } - - // Check if path is relative, if not, assume full path - std::string imagePath = lightProbe[SceneKeys::kFilename].GetString(); - std::string actualPath = mDirectory + '/' + imagePath; - if (doesFileExist(actualPath) == false) - { - actualPath = imagePath; - } - - float3 position; - float3 intensity(1.0f); - float radius = -1; - uint32_t diffuseSamples = LightProbe::kDefaultDiffSamples; - uint32_t specSamples = LightProbe::kDefaultSpecSamples; - - for (auto m = lightProbe.MemberBegin(); m < lightProbe.MemberEnd(); m++) - { - std::string key = m->name.GetString(); - const auto& value = m->value; - if (key == SceneKeys::kLightIntensity) - { - if (getFloatVec<3>(value, "Light probe intensity", &intensity[0]) == false) - { - return false; - } - } - else if (key == SceneKeys::kLightPos) - { - if (getFloatVec<3>(value, "Light probe world position", &position[0]) == false) - { - return false; - } - } - else if (key == SceneKeys::kLightProbeRadius) - { - if (value.IsUint() == false) - { - error("Light Probe radius must be a float."); - return false; - } - radius = float(value.GetDouble()); - } - else if (key == SceneKeys::kLightProbeDiffSamples) - { - if (value.IsUint() == false) - { - error("Light Probe diffuse sample count must be a uint."); - return false; - } - diffuseSamples = value.GetUint(); - } - else if (key == SceneKeys::kLightProbeSpecSamples) - { - if (value.IsUint() == false) - { - error("Light Probe specular sample count must be a uint."); - return false; - } - specSamples = value.GetUint(); - } - } - - LightProbe::SharedPtr pLightProbe = LightProbe::create(gpDevice->getRenderContext(), actualPath, true, ResourceFormat::RGBA16Float, diffuseSamples, specSamples); - pLightProbe->setPosW(position); - pLightProbe->setIntensity(intensity); - mBuilder.setLightProbe(pLightProbe); - } - - return true; - } - bool SceneImporterImpl::parsePaths(const rapidjson::Value& jsonVal) { if (jsonVal.IsArray() == false) @@ -1021,8 +926,9 @@ namespace Falcor return error("Selected camera should be a name."); } - std::string s = (std::string)(jsonVal.GetString()); - mBuilder.setCamera(s); + std::string name(jsonVal.GetString()); + auto it = std::find_if(mBuilder.getCameras().begin(), mBuilder.getCameras().end(), [&name] (const Camera::SharedPtr& pCamera) { return pCamera->getName() == name; }); + if (it != mBuilder.getCameras().end()) mBuilder.setSelectedCamera(*it); return true; } @@ -1124,7 +1030,6 @@ namespace Falcor {SceneKeys::kModels, &SceneImporterImpl::parseModels}, {SceneKeys::kLights, &SceneImporterImpl::parseLights}, - {SceneKeys::kLightProbes, &SceneImporterImpl::parseLightProbes}, {SceneKeys::kCameras, &SceneImporterImpl::parseCameras}, {SceneKeys::kCamera, &SceneImporterImpl::parseCamera}, {SceneKeys::kActiveCamera, &SceneImporterImpl::parseActiveCamera}, // Should come after ParseCameras @@ -1186,7 +1091,12 @@ namespace Falcor bool SceneImporter::import(const std::string& filename, SceneBuilder& builder, const SceneBuilder::InstanceMatrices& instances, const Dictionary& dict) { - logWarning("fscene files are no longer supported in Falcor 4.0. Some properties may not be loaded."); + logWarning("\n" + "-------------------------------------------------------------------------------\n" + " DEPRECATION WARNING \n" + "fscene files are no longer supported and will be removed in the next release. \n" + "Some properties may not be loaded. Please convert your file to .pyscene format.\n" + "-------------------------------------------------------------------------------"); if (!instances.empty()) logWarning("Scene importer does not support instancing."); SceneImporterImpl importer(builder); diff --git a/Source/Falcor/Scene/Intersection.slang b/Source/Falcor/Scene/Intersection.slang new file mode 100644 index 0000000000..7026ae8255 --- /dev/null +++ b/Source/Falcor/Scene/Intersection.slang @@ -0,0 +1,52 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +import Utils.Helpers; + +#ifndef ENABLE_CURVE_SPHERE_JOINTS +#define ENABLE_CURVE_SPHERE_JOINTS 0 +#endif + +/** Linear swept sphere intersection function wrapper. + \param[in] rayOrigin Ray origin position. + \param[in] rayDir Unit ray direction vector. + \param[in] sphereA Sphere (3D position + radius) at one end point. + \param[in] sphereB Sphere at the other end point. + \param[out] result The closest intersection distance t, and a parameter u for linear interpolation (between 0 and 1). + \return True if the ray intersects the linear swept sphere segment. +*/ +bool intersectLinearSweptSphere(float3 rayOrigin, float3 rayDir, float4 sphereA, float4 sphereB, out float2 result) +{ + bool useSphereJoints; +#if ENABLE_CURVE_SPHERE_JOINTS + useSphereJoints = true; +#else + useSphereJoints = false; +#endif + + return intersectLinearSweptSphereHan19(rayOrigin, rayDir, sphereA, sphereB, useSphereJoints, result); +} diff --git a/Source/Falcor/Scene/Lights/Light.cpp b/Source/Falcor/Scene/Lights/Light.cpp index 877ea5885b..af260ca8c0 100644 --- a/Source/Falcor/Scene/Lights/Light.cpp +++ b/Source/Falcor/Scene/Lights/Light.cpp @@ -42,6 +42,8 @@ namespace Falcor return true; } + // Light + void Light::setActive(bool active) { if (active != mActive) @@ -163,64 +165,25 @@ namespace Falcor } } - Light::Light(LightType type) + Light::Light(const std::string& name, LightType type) + : mName(name) { mData.type = (uint32_t)type; } + // PointLight - DirectionalLight::DirectionalLight() - : Light(LightType::Directional) - { - } - - DirectionalLight::SharedPtr DirectionalLight::create() - { - DirectionalLight* pLight = new DirectionalLight; - return SharedPtr(pLight); - } - - DirectionalLight::~DirectionalLight() = default; - - void DirectionalLight::renderUI(Gui::Widgets& widget) + PointLight::SharedPtr PointLight::create(const std::string& name) { - Light::renderUI(widget); - - if (widget.direction("Direction", mData.dirW)) - { - setWorldDirection(mData.dirW); - } - } - - void DirectionalLight::setWorldDirection(const float3& dir) - { - if (!(glm::length(dir) > 0.f)) // NaNs propagate - { - logWarning("Can't set light direction to zero length vector. Ignoring call."); - return; - } - mData.dirW = normalize(dir); - } - - void DirectionalLight::updateFromAnimation(const glm::mat4& transform) - { - float3 fwd = float3(transform[2]); - setWorldDirection(fwd); + return SharedPtr(new PointLight(name)); } - PointLight::SharedPtr PointLight::create() - { - PointLight* pLight = new PointLight; - return SharedPtr(pLight); - } - - PointLight::PointLight() - : Light(LightType::Point) + PointLight::PointLight(const std::string& name) + : Light(name, LightType::Point) { + mPrevData = mData; } - PointLight::~PointLight() = default; - void PointLight::setWorldDirection(const float3& dir) { if (!(glm::length(dir) > 0.f)) // NaNs propagate @@ -264,7 +227,7 @@ namespace Falcor if (openingAngle == mData.openingAngle) return; mData.openingAngle = openingAngle; - /* Prepare an auxiliary cosine of the opening angle to quickly check whether we're within the cone of a spot light */ + // Prepare an auxiliary cosine of the opening angle to quickly check whether we're within the cone of a spot light. mData.cosOpeningAngle = std::cos(openingAngle); } @@ -283,22 +246,61 @@ namespace Falcor setWorldDirection(fwd); } - DistantLight::SharedPtr DistantLight::create() + // DirectionalLight + + DirectionalLight::DirectionalLight(const std::string& name) + : Light(name, LightType::Directional) + { + mPrevData = mData; + } + + DirectionalLight::SharedPtr DirectionalLight::create(const std::string& name) + { + return SharedPtr(new DirectionalLight(name)); + } + + void DirectionalLight::renderUI(Gui::Widgets& widget) + { + Light::renderUI(widget); + + if (widget.direction("Direction", mData.dirW)) + { + setWorldDirection(mData.dirW); + } + } + + void DirectionalLight::setWorldDirection(const float3& dir) + { + if (!(glm::length(dir) > 0.f)) // NaNs propagate + { + logWarning("Can't set light direction to zero length vector. Ignoring call."); + return; + } + mData.dirW = normalize(dir); + } + + void DirectionalLight::updateFromAnimation(const glm::mat4& transform) + { + float3 fwd = float3(transform[2]); + setWorldDirection(fwd); + } + + // DistantLight + + DistantLight::SharedPtr DistantLight::create(const std::string& name) { - DistantLight* pLight = new DistantLight(); - return SharedPtr(pLight); + return SharedPtr(new DistantLight(name)); } - DistantLight::DistantLight() - : Light(LightType::Distant) + DistantLight::DistantLight(const std::string& name) + : Light(name, LightType::Distant) { mData.dirW = float3(0.f, -1.f, 0.f); setAngle(0.5f * 0.53f * (float)M_PI / 180.f); // Approximate sun half-angle update(); + mPrevData = mData; } - DistantLight::~DistantLight() = default; - void DistantLight::renderUI(Gui::Widgets& widget) { Light::renderUI(widget); @@ -352,15 +354,16 @@ namespace Falcor mData.transMatIT = glm::inverse(glm::transpose(mData.transMat)); } - // Code for analytic area lights. - AnalyticAreaLight::SharedPtr AnalyticAreaLight::create(LightType type) + void DistantLight::updateFromAnimation(const glm::mat4& transform) { - AnalyticAreaLight* pLight = new AnalyticAreaLight(type); - return SharedPtr(pLight); + float3 fwd = float3(transform[2]); + setWorldDirection(fwd); } - AnalyticAreaLight::AnalyticAreaLight(LightType type) - : Light(type) + // AnalyticAreaLight + + AnalyticAreaLight::AnalyticAreaLight(const std::string& name, LightType type) + : Light(name, type) { mData.tangent = float3(1, 0, 0); mData.bitangent = float3(0, 1, 0); @@ -368,10 +371,9 @@ namespace Falcor mScaling = float3(1, 1, 1); update(); + mPrevData = mData; } - AnalyticAreaLight::~AnalyticAreaLight() = default; - float AnalyticAreaLight::getPower() const { return luminance(mData.intensity) * (float)M_PI * mData.surfaceArea; @@ -382,64 +384,93 @@ namespace Falcor // Update matrix mData.transMat = mTransformMatrix * glm::scale(glm::mat4(), mScaling); mData.transMatIT = glm::inverse(glm::transpose(mData.transMat)); + } - switch ((LightType)mData.type) - { + // RectLight - case LightType::Rect: - { - float rx = glm::length(mData.transMat * float4(1.0f, 0.0f, 0.0f, 0.0f)); - float ry = glm::length(mData.transMat * float4(0.0f, 1.0f, 0.0f, 0.0f)); - mData.surfaceArea = 4.0f * rx * ry; - } - break; + RectLight::SharedPtr RectLight::create(const std::string& name) + { + return SharedPtr(new RectLight(name)); + } - case LightType::Sphere: - { - float rx = glm::length(mData.transMat * float4(1.0f, 0.0f, 0.0f, 0.0f)); - float ry = glm::length(mData.transMat * float4(0.0f, 1.0f, 0.0f, 0.0f)); - float rz = glm::length(mData.transMat * float4(0.0f, 0.0f, 1.0f, 0.0f)); + void RectLight::update() + { + AnalyticAreaLight::update(); - mData.surfaceArea = 4.0f * (float)M_PI * std::pow(std::pow(rx * ry, 1.6f) + std::pow(ry * rz, 1.6f) + std::pow(rx * rz, 1.6f) / 3.0f, 1.0f / 1.6f); - } - break; + float rx = glm::length(mData.transMat * float4(1.0f, 0.0f, 0.0f, 0.0f)); + float ry = glm::length(mData.transMat * float4(0.0f, 1.0f, 0.0f, 0.0f)); + mData.surfaceArea = 4.0f * rx * ry; + } - case LightType::Disc: - { - float rx = glm::length(mData.transMat * float4(1.0f, 0.0f, 0.0f, 0.0f)); - float ry = glm::length(mData.transMat * float4(0.0f, 1.0f, 0.0f, 0.0f)); + // DiscLight - mData.surfaceArea = (float)M_PI * rx * ry; - } - break; + DiscLight::SharedPtr DiscLight::create(const std::string& name) + { + return SharedPtr(new DiscLight(name)); + } - default: - break; - } + void DiscLight::update() + { + AnalyticAreaLight::update(); + + float rx = glm::length(mData.transMat * float4(1.0f, 0.0f, 0.0f, 0.0f)); + float ry = glm::length(mData.transMat * float4(0.0f, 1.0f, 0.0f, 0.0f)); + + mData.surfaceArea = (float)M_PI * rx * ry; + } + + // SphereLight + + SphereLight::SharedPtr SphereLight::create(const std::string& name) + { + return SharedPtr(new SphereLight(name)); } + void SphereLight::update() + { + AnalyticAreaLight::update(); + + float rx = glm::length(mData.transMat * float4(1.0f, 0.0f, 0.0f, 0.0f)); + float ry = glm::length(mData.transMat * float4(0.0f, 1.0f, 0.0f, 0.0f)); + float rz = glm::length(mData.transMat * float4(0.0f, 0.0f, 1.0f, 0.0f)); + + mData.surfaceArea = 4.0f * (float)M_PI * std::pow(std::pow(rx * ry, 1.6f) + std::pow(ry * rz, 1.6f) + std::pow(rx * rz, 1.6f) / 3.0f, 1.0f / 1.6f); + } + + SCRIPT_BINDING(Light) { pybind11::class_ light(m, "Light"); - light.def_property_readonly("name", &Light::getName); + light.def_property("name", &Light::getName, &Light::setName); light.def_property("active", &Light::isActive, &Light::setActive); light.def_property("animated", &Light::isAnimated, &Light::setIsAnimated); - light.def_property("intensity", &Light::getIntensityForScript, &Light::setIntensityFromScript); - light.def_property("color", &Light::getColorForScript, &Light::setColorFromScript); + light.def_property("intensity", &Light::getIntensity, &Light::setIntensity); + + pybind11::class_ pointLight(m, "PointLight"); + pointLight.def(pybind11::init(&PointLight::create), "name"_a = ""); + pointLight.def_property("position", &PointLight::getWorldPosition, &PointLight::setWorldPosition); + pointLight.def_property("direction", &PointLight::getWorldDirection, &PointLight::setWorldDirection); + pointLight.def_property("openingAngle", &PointLight::getOpeningAngle, &PointLight::setOpeningAngle); + pointLight.def_property("penumbraAngle", &PointLight::getPenumbraAngle, &PointLight::setPenumbraAngle); pybind11::class_ directionalLight(m, "DirectionalLight"); + directionalLight.def(pybind11::init(&DirectionalLight::create), "name"_a = ""); directionalLight.def_property("direction", &DirectionalLight::getWorldDirection, &DirectionalLight::setWorldDirection); pybind11::class_ distantLight(m, "DistantLight"); + distantLight.def(pybind11::init(&DistantLight::create), "name"_a = ""); distantLight.def_property("direction", &DistantLight::getWorldDirection, &DistantLight::setWorldDirection); distantLight.def_property("angle", &DistantLight::getAngle, &DistantLight::setAngle); - pybind11::class_ pointLight(m, "PointLight"); - pointLight.def_property("position", &PointLight::getWorldPosition, &PointLight::setWorldPosition); - pointLight.def_property("direction", &PointLight::getWorldDirection, &PointLight::setWorldDirection); - pointLight.def_property("openingAngle", &PointLight::getOpeningAngle, &PointLight::setOpeningAngle); - pointLight.def_property("penumbraAngle", &PointLight::getPenumbraAngle, &PointLight::setPenumbraAngle); - pybind11::class_ analyticLight(m, "AnalyticAreaLight"); - } + + pybind11::class_ rectLight(m, "RectLight"); + rectLight.def(pybind11::init(&RectLight::create), "name"_a = ""); + + pybind11::class_ discLight(m, "DiscLight"); + discLight.def(pybind11::init(&DiscLight::create), "name"_a = ""); + + pybind11::class_ sphereLight(m, "SphereLight"); + sphereLight.def(pybind11::init(&SphereLight::create), "name"_a = ""); + } } diff --git a/Source/Falcor/Scene/Lights/Light.h b/Source/Falcor/Scene/Lights/Light.h index a1f9737463..608cb515ed 100644 --- a/Source/Falcor/Scene/Lights/Light.h +++ b/Source/Falcor/Scene/Lights/Light.h @@ -87,6 +87,10 @@ namespace Falcor */ virtual void setIntensity(const float3& intensity); + /** Get the light intensity. + */ + const float3& getIntensity() const { return mData.intensity; } + enum class Changes { None = 0x0, @@ -105,17 +109,10 @@ namespace Falcor */ Changes getChanges() const { return mChanges; } - /** Scripting helper functions for getting/setting intensity and color. - */ - void setIntensityFromScript(float intensity) { setIntensityFromUI(intensity); } - void setColorFromScript(float3 color) { setColorFromUI(color); } - float getIntensityForScript() { return getIntensityForUI(); } - float3 getColorForScript() { return getColorForUI(); } - void updateFromAnimation(const glm::mat4& transform) override {} protected: - Light(LightType type); + Light(const std::string& name, LightType type); static const size_t kDataSize = sizeof(LightData); @@ -136,45 +133,8 @@ namespace Falcor Changes mChanges = Changes::None; }; - /** Directional light source. - */ - class dlldecl DirectionalLight : public Light - { - public: - using SharedPtr = std::shared_ptr; - using SharedConstPtr = std::shared_ptr; - - static SharedPtr create(); - ~DirectionalLight(); - - /** Render UI elements for this light. - */ - void renderUI(Gui::Widgets& widget) override; - - /** Set the light's world-space direction. - \param[in] dir Light direction. Does not have to be normalized. - */ - void setWorldDirection(const float3& dir); - - /** Set the scene parameters - */ - void setWorldParams(const float3& center, float radius); - - /** Get the light's world-space direction. - */ - const float3& getWorldDirection() const { return mData.dirW; } - - /** Get total light power (needed for light picking) - */ - float getPower() const override { return 0.f; } - - void updateFromAnimation(const glm::mat4& transform) override; - - private: - DirectionalLight(); - }; - - /** Simple infinitely-small point light with quadratic attenuation + /** Point light source. + Simple infinitely-small point light with quadratic attenuation. */ class dlldecl PointLight : public Light { @@ -182,8 +142,8 @@ namespace Falcor using SharedPtr = std::shared_ptr; using SharedConstPtr = std::shared_ptr; - static SharedPtr create(); - ~PointLight(); + static SharedPtr create(const std::string& name = ""); + ~PointLight() = default; /** Render UI elements for this light. */ @@ -215,10 +175,6 @@ namespace Falcor */ const float3& getWorldDirection() const { return mData.dirW; } - /** Get the light intensity. - */ - const float3& getIntensity() const { return mData.intensity; } - /** Get the penumbra half-angle */ float getPenumbraAngle() const { return mData.penumbraAngle; } @@ -235,63 +191,59 @@ namespace Falcor void updateFromAnimation(const glm::mat4& transform) override; private: - PointLight(); + PointLight(const std::string& name); }; - /** - Analytic area light source. + + /** Directional light source. */ - class dlldecl AnalyticAreaLight : public Light + class dlldecl DirectionalLight : public Light { public: - using SharedPtr = std::shared_ptr; - using SharedConstPtr = std::shared_ptr; - - /** Creates an analytic area light. - \param[in] type The type of analytic area light (rectangular, sphere, disc etc). See LightData.slang - */ - static SharedPtr create(LightType type); + using SharedPtr = std::shared_ptr; + using SharedConstPtr = std::shared_ptr; - ~AnalyticAreaLight(); + static SharedPtr create(const std::string& name = ""); + ~DirectionalLight() = default; - /** Set light source scaling - \param[in] scale x,y,z scaling factors + /** Render UI elements for this light. */ - void setScaling(float3 scale) { mScaling = scale; } + void renderUI(Gui::Widgets& widget) override; - /** Set light source scale - */ - float3 getScaling() const { return mScaling; } + /** Set the light's world-space direction. + \param[in] dir Light direction. Does not have to be normalized. + */ + void setWorldDirection(const float3& dir); - /** Get total light power (needed for light picking) + /** Set the scene parameters */ - float getPower() const override; + void setWorldParams(const float3& center, float radius); - /** Set transform matrix - \param[in] mtx object to world space transform matrix + /** Get the light's world-space direction. */ - void setTransformMatrix(const glm::mat4& mtx) { mTransformMatrix = mtx; update(); } + const float3& getWorldDirection() const { return mData.dirW; } - /** Get transform matrix + /** Get total light power (needed for light picking) */ - glm::mat4 getTransformMatrix() const { return mTransformMatrix; } + float getPower() const override { return 0.f; } - private: - AnalyticAreaLight(LightType type); - void update(); + void updateFromAnimation(const glm::mat4& transform) override; - float3 mScaling; ///< Scaling, controls the size of the light - glm::mat4 mTransformMatrix; ///< Transform matrix minus scaling component + private: + DirectionalLight(const std::string& name); }; + /** Distant light source. + Same as directional light source but subtending a non-zero solid angle. + */ class dlldecl DistantLight : public Light { public: using SharedPtr = std::shared_ptr; using SharedConstPtr = std::shared_ptr; - static SharedPtr create(); - ~DistantLight(); + static SharedPtr create(const std::string& name = ""); + ~DistantLight() = default; /** Render UI elements for this light. */ @@ -319,11 +271,105 @@ namespace Falcor */ float getPower() const override { return 0.f; } + void updateFromAnimation(const glm::mat4& transform) override; + private: - DistantLight(); + DistantLight(const std::string& name); void update(); float mAngle; ///<< Half-angle subtended by the source. }; + /** Analytic area light source. + */ + class dlldecl AnalyticAreaLight : public Light + { + public: + using SharedPtr = std::shared_ptr; + using SharedConstPtr = std::shared_ptr; + + ~AnalyticAreaLight() = default; + + /** Set light source scaling + \param[in] scale x,y,z scaling factors + */ + void setScaling(float3 scale) { mScaling = scale; update(); } + + /** Set light source scale + */ + float3 getScaling() const { return mScaling; } + + /** Get total light power (needed for light picking) + */ + float getPower() const override; + + /** Set transform matrix + \param[in] mtx object to world space transform matrix + */ + void setTransformMatrix(const glm::mat4& mtx) { mTransformMatrix = mtx; update(); } + + /** Get transform matrix + */ + glm::mat4 getTransformMatrix() const { return mTransformMatrix; } + + protected: + AnalyticAreaLight(const std::string& name, LightType type); + + virtual void update(); + + float3 mScaling; ///< Scaling, controls the size of the light + glm::mat4 mTransformMatrix; ///< Transform matrix minus scaling component + }; + + /** Rectangular area light source. + */ + class dlldecl RectLight : public AnalyticAreaLight + { + public: + using SharedPtr = std::shared_ptr; + using SharedConstPtr = std::shared_ptr; + + static SharedPtr create(const std::string& name = ""); + ~RectLight() = default; + + private: + RectLight(const std::string& name) : AnalyticAreaLight(name, LightType::Rect) {} + + virtual void update() override; + }; + + /** Disc area light source. + */ + class dlldecl DiscLight : public AnalyticAreaLight + { + public: + using SharedPtr = std::shared_ptr; + using SharedConstPtr = std::shared_ptr; + + static SharedPtr create(const std::string& name = ""); + ~DiscLight() = default; + + private: + DiscLight(const std::string& name) : AnalyticAreaLight(name, LightType::Disc) {} + + virtual void update() override; + }; + + /** Sphere area light source. + */ + class dlldecl SphereLight : public AnalyticAreaLight + { + public: + using SharedPtr = std::shared_ptr; + using SharedConstPtr = std::shared_ptr; + + static SharedPtr create(const std::string& name = ""); + ~SphereLight() = default; + + private: + SphereLight(const std::string& name) : AnalyticAreaLight(name, LightType::Sphere) {} + + virtual void update() override; + }; + enum_class_operators(Light::Changes); } diff --git a/Source/Falcor/Scene/Lights/LightData.slang b/Source/Falcor/Scene/Lights/LightData.slang index 87b7e917ba..bb06281cd5 100644 --- a/Source/Falcor/Scene/Lights/LightData.slang +++ b/Source/Falcor/Scene/Lights/LightData.slang @@ -41,10 +41,10 @@ enum class LightType { Point, ///< Point light source, can be a spot light if its opening angle is < 2pi Directional, ///< Directional light source + Distant, ///< Distant light that subtends a non-zero solid angle Rect, ///< Quad shaped area light source - Sphere, ///< Spherical area light source Disc, ///< Disc shaped area light source - Distant, ///< Distant light that subtends a non-zero solid angle + Sphere, ///< Spherical area light source }; /** This is a host/device structure that describes analytic light sources. diff --git a/Source/Falcor/Scene/Lights/LightProbe.cpp b/Source/Falcor/Scene/Lights/LightProbe.cpp deleted file mode 100644 index 94e35570ca..0000000000 --- a/Source/Falcor/Scene/Lights/LightProbe.cpp +++ /dev/null @@ -1,258 +0,0 @@ -/*************************************************************************** - # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. - # - # Redistribution and use in source and binary forms, with or without - # modification, are permitted provided that the following conditions - # are met: - # * Redistributions of source code must retain the above copyright - # notice, this list of conditions and the following disclaimer. - # * Redistributions in binary form must reproduce the above copyright - # notice, this list of conditions and the following disclaimer in the - # documentation and/or other materials provided with the distribution. - # * Neither the name of NVIDIA CORPORATION nor the names of its - # contributors may be used to endorse or promote products derived - # from this software without specific prior written permission. - # - # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY - # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - **************************************************************************/ -#include "stdafx.h" -#include "LightProbe.h" -#include "RenderGraph/BasePasses/FullScreenPass.h" -#include "Utils/UI/Gui.h" -#include "Core/API/RenderContext.h" -#include "Core/API/Device.h" - -namespace Falcor -{ - uint32_t LightProbe::sLightProbeCount = 0; - LightProbeSharedResources LightProbe::sSharedResources; - - class PreIntegration - { - public: - const char* kShader = "Scene/Lights/LightProbeIntegration.ps.slang"; - - bool isInitialized() const { return mInitialized; } - - void init() - { - mpDiffuseLDPass = FullScreenPass::create(std::string(kShader), Program::DefineList().add("_INTEGRATE_DIFFUSE_LD")); - mpSpecularLDPass = FullScreenPass::create(std::string(kShader), Program::DefineList().add("_INTEGRATE_SPECULAR_LD")); - mpDFGPass = FullScreenPass::create(std::string(kShader), Program::DefineList().add("_INTEGRATE_DFG")); - - // Shared - mpSampler = Sampler::create(Sampler::Desc().setFilterMode(Sampler::Filter::Linear, Sampler::Filter::Linear, Sampler::Filter::Linear)); - mpDiffuseLDPass["gSampler"] = mpSampler; - mpSpecularLDPass["gSampler"] = mpSampler; - mpDFGPass["gSampler"] = mpSampler; - - mInitialized = true; - } - - void release() - { - mpDiffuseLDPass = nullptr; - mpSpecularLDPass = nullptr; - mpDFGPass = nullptr; - mpSampler = nullptr; - - mInitialized = false; - } - - Texture::SharedPtr integrateDFG(RenderContext* pContext, const Texture::SharedPtr& pTexture, uint32_t size, ResourceFormat format, uint32_t sampleCount) - { - return executeSingleMip(pContext, mpDFGPass, pTexture, size, format, sampleCount); - } - - Texture::SharedPtr integrateDiffuseLD(RenderContext* pContext, const Texture::SharedPtr& pTexture, uint32_t size, ResourceFormat format, uint32_t sampleCount) - { - return executeSingleMip(pContext, mpDiffuseLDPass, pTexture, size, format, sampleCount); - } - - Texture::SharedPtr integrateSpecularLD(RenderContext* pContext, const Texture::SharedPtr& pTexture, uint32_t size, ResourceFormat format, uint32_t sampleCount) - { - mpSpecularLDPass["gInputTex"] = pTexture; - mpSpecularLDPass["DataCB"]["gSampleCount"] = sampleCount; - - Texture::SharedPtr pOutput = Texture::create2D(size, size, format, 1, Texture::kMaxPossible, nullptr, Resource::BindFlags::ShaderResource | Resource::BindFlags::RenderTarget); - - // Execute on each mip level - uint32_t mipCount = pOutput->getMipCount(); - for (uint32_t i = 0; i < mipCount; i++) - { - Fbo::SharedPtr pFbo = Fbo::create(); - pFbo->attachColorTarget(pOutput, 0, i); - - // Roughness to integrate for on current mip level - mpSpecularLDPass["DataCB"]["gRoughness"] = float(i) / float(mipCount - 1); - mpSpecularLDPass->execute(pContext, pFbo); - } - - return pOutput; - } - - private: - - Texture::SharedPtr executeSingleMip(RenderContext* pContext, const FullScreenPass::SharedPtr& pPass, const Texture::SharedPtr& pTexture, uint32_t size, ResourceFormat format, uint32_t sampleCount) - { - pPass["gInputTex"] = pTexture; - pPass["DataCB"]["gSampleCount"] = sampleCount; - - // Output texture - Fbo::SharedPtr pFbo = Fbo::create2D(size, size, Fbo::Desc().setColorTarget(0, format)); - - // Execute - pPass->execute(pContext, pFbo); - return pFbo->getColorTexture(0); - } - - - bool mInitialized = false; - FullScreenPass::SharedPtr mpDiffuseLDPass; - FullScreenPass::SharedPtr mpSpecularLDPass; - FullScreenPass::SharedPtr mpDFGPass; - Sampler::SharedPtr mpSampler; - }; - - static PreIntegration sIntegration; - - LightProbe::LightProbe(RenderContext* pContext, const Texture::SharedPtr& pTexture, uint32_t diffSamples, uint32_t specSamples, uint32_t diffSize, uint32_t specSize, ResourceFormat preFilteredFormat) - : mDiffSampleCount(diffSamples) - , mSpecSampleCount(specSamples) - { - if (sIntegration.isInitialized() == false) - { - assert(sLightProbeCount == 0); - sIntegration.init(); - sSharedResources.dfgTexture = sIntegration.integrateDFG(pContext, pTexture, 128, ResourceFormat::RGBA16Float, 128); - sSharedResources.dfgSampler = Sampler::create(Sampler::Desc().setFilterMode(Sampler::Filter::Point, Sampler::Filter::Point, Sampler::Filter::Point).setAddressingMode(Sampler::AddressMode::Clamp, Sampler::AddressMode::Clamp, Sampler::AddressMode::Clamp)); - } - - mData.resources.origTexture = pTexture; - mData.resources.diffuseTexture = sIntegration.integrateDiffuseLD(pContext, pTexture, diffSize, preFilteredFormat, diffSamples); - mData.resources.specularTexture = sIntegration.integrateSpecularLD(pContext, pTexture, specSize, preFilteredFormat, specSamples); - mData.sharedResources = sSharedResources; - sLightProbeCount++; - } - - LightProbe::~LightProbe() - { - sLightProbeCount--; - if (sLightProbeCount == 0) - { - sSharedResources.dfgTexture = nullptr; - sSharedResources.dfgSampler = nullptr; - sIntegration.release(); - } - } - - LightProbe::SharedPtr LightProbe::create(RenderContext* pContext, const std::string& filename, bool loadAsSrgb, ResourceFormat overrideFormat, uint32_t diffSampleCount, uint32_t specSampleCount, uint32_t diffSize, uint32_t specSize, ResourceFormat preFilteredFormat) - { - assert(gpDevice); - Texture::SharedPtr pTexture; - if (overrideFormat != ResourceFormat::Unknown) - { - Texture::SharedPtr pOrigTex = Texture::createFromFile(filename, false, loadAsSrgb); - if (pOrigTex) - { - pTexture = Texture::create2D(pOrigTex->getWidth(), pOrigTex->getHeight(), overrideFormat, 1, Texture::kMaxPossible, nullptr, Resource::BindFlags::RenderTarget | Resource::BindFlags::ShaderResource); - pTexture->setSourceFilename(pOrigTex->getSourceFilename()); - gpDevice->getRenderContext()->blit(pOrigTex->getSRV(0, 1, 0, 1), pTexture->getRTV(0, 0, 1)); - pTexture->generateMips(gpDevice->getRenderContext()); - } - } - else - { - pTexture = Texture::createFromFile(filename, true, loadAsSrgb); - } - - if (!pTexture) throw std::exception("Failed to create light probe"); - - return create(pContext, pTexture, diffSampleCount, specSampleCount, diffSize, specSize, preFilteredFormat); - } - - LightProbe::SharedPtr LightProbe::create(RenderContext* pContext, const Texture::SharedPtr& pTexture, uint32_t diffSampleCount, uint32_t specSampleCount, uint32_t diffSize, uint32_t specSize, ResourceFormat preFilteredFormat) - { - if (pTexture->getMipCount() == 1) - { - logWarning("Source textures used for generating light probes should have a valid mip chain."); - } - - return SharedPtr(new LightProbe(pContext, pTexture, diffSampleCount, specSampleCount, diffSize, specSize, preFilteredFormat)); - } - - void LightProbe::renderUI(Gui* pGui, const char* group) - { - Gui::Group g(pGui, group); - if (!group || g.open()) - { - g.var("World Position", mData.posW, -FLT_MAX, FLT_MAX); - - float intensity = mData.intensity.r; - if (g.var("Intensity", intensity, 0.0f)) - { - mData.intensity = float3(intensity); - } - - g.var("Radius", mData.radius, -1.0f); - - if (g.open()) g.release(); - } - } - - static bool checkOffset(UniformShaderVarOffset cbOffset, size_t cppOffset, const char* field) - { - if (cbOffset.getByteOffset() != cppOffset) - { - logError("LightProbe::setShaderData() = LightProbeData::" + std::string(field) + " CB offset mismatch. CB offset is " + std::to_string(cbOffset.getByteOffset()) + ", C++ data offset is " + std::to_string(cppOffset)); - return false; - } - return true; - } - -#if _LOG_ENABLED -#define check_offset(_a) {static bool b = true; if(b) {assert(checkOffset(var.getType()->getMemberOffset(#_a), offsetof(LightProbeData, _a), #_a));} b = false;} -#else -#define check_offset(_a) -#endif - - void LightProbe::setShaderData(const ShaderVar& var) - { - - // Set the data into the constant buffer - check_offset(posW); - check_offset(intensity); - static_assert(kDataSize % sizeof(float4) == 0, "LightProbeData size should be a multiple of 16"); - - if(!var.isValid()) return; - - // Set everything except for the resources - var.setBlob(&mData, kDataSize); - - // Bind the textures - auto resources = var["resources"]; - resources["origTexture"] = mData.resources.origTexture; - resources["diffuseTexture"] = mData.resources.diffuseTexture; - resources["specularTexture"] = mData.resources.specularTexture; - resources["sampler"] = mData.resources.sampler; - - auto sharedResources = var["sharedResources"]; - sharedResources["dfgTexture"] = mData.sharedResources.dfgTexture; - sharedResources["dfgSampler"] = mData.sharedResources.dfgSampler; - } - - SCRIPT_BINDING(LightProbe) - { - pybind11::class_(m, "LightProbe"); - } -} diff --git a/Source/Falcor/Scene/Lights/LightProbe.h b/Source/Falcor/Scene/Lights/LightProbe.h deleted file mode 100644 index 8f906d6222..0000000000 --- a/Source/Falcor/Scene/Lights/LightProbe.h +++ /dev/null @@ -1,153 +0,0 @@ -/*************************************************************************** - # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. - # - # Redistribution and use in source and binary forms, with or without - # modification, are permitted provided that the following conditions - # are met: - # * Redistributions of source code must retain the above copyright - # notice, this list of conditions and the following disclaimer. - # * Redistributions in binary form must reproduce the above copyright - # notice, this list of conditions and the following disclaimer in the - # documentation and/or other materials provided with the distribution. - # * Neither the name of NVIDIA CORPORATION nor the names of its - # contributors may be used to endorse or promote products derived - # from this software without specific prior written permission. - # - # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY - # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - **************************************************************************/ -#pragma once -#include "LightProbeData.slang" -#include "Core/API/Texture.h" -#include "Core/API/Sampler.h" - -namespace Falcor -{ - class RenderContext; - class Gui; - class ProgramVars; - class ParameterBlock; - - class dlldecl LightProbe - { - public: - using SharedPtr = std::shared_ptr; - using SharedConstPtr = std::shared_ptr; - - static const uint32_t kDataSize = sizeof(LightProbeData) - sizeof(LightProbeResources); - static const uint32_t kDefaultDiffSamples = 4096; - static const uint32_t kDefaultSpecSamples = 1024; - static const uint32_t kDefaultDiffSize = 128; - static const uint32_t kDefaultSpecSize = 1024; - - /** Create a light-probe from a file - \param[in] pContext The current render context to be used for pre-integration. - \param[in] filename Texture filename - \param[in] loadAsSrgb Indicates whether the source texture is in sRGB or linear color space - \param[in] overrideFormat Override the format of the original texture. ResourceFormat::Unknown means keep the original format. Useful in cases where generateMips is true, but the original format doesn't support automatic mip generation - \param[in] diffSampleCount How many times to sample when generating diffuse texture. - \param[in] specSampleCount How many times to sample when generating specular texture. - \param[in] diffSize The width and height of the pre-filtered diffuse texture. We always create a square texture. - \param[in] specSize The width and height of the pre-filtered specular texture. We always create a square texture. - \param[in] preFilteredFormat The format of the pre-filtered texture - */ - static SharedPtr create(RenderContext* pContext, const std::string& filename, bool loadAsSrgb, ResourceFormat overrideFormat = ResourceFormat::Unknown, uint32_t diffSampleCount = kDefaultDiffSamples, uint32_t specSampleCount = kDefaultSpecSamples, uint32_t diffSize = kDefaultDiffSize, uint32_t specSize = kDefaultSpecSize, ResourceFormat preFilteredFormat = ResourceFormat::RGBA16Float); - - /** Create a light-probe from a texture - \param[in] pContext The current render context to be used for pre-integration. - \param[in] pTexture The source texture - \param[in] diffSampleCount How many times to sample when generating diffuse texture. - \param[in] specSampleCount How many times to sample when generating specular texture. - \param[in] diffSize The width and height of the pre-filtered diffuse texture. We always create a square texture. - \param[in] specSize The width and height of the pre-filtered specular texture. We always create a square texture. - \param[in] preFilteredFormat The format of the pre-filtered texture - */ - static SharedPtr create(RenderContext* pContext, const Texture::SharedPtr& pTexture, uint32_t diffSampleCount = kDefaultDiffSamples, uint32_t specSampleCount = kDefaultSpecSamples, uint32_t diffSize = kDefaultDiffSize, uint32_t specSize = kDefaultSpecSize, ResourceFormat preFilteredFormat = ResourceFormat::RGBA16Float); - - ~LightProbe(); - - /** Render UI elements for this light. - \param[in] pGui The GUI to create the elements with - \param[in] group Optional. If specified, creates a UI group to display elements within - */ - void renderUI(Gui* pGui, const char* group = nullptr); - - /** Set the light probe's world-space position - */ - void setPosW(const float3& posW) { mData.posW = posW; } - - /** Get the light probe's world-space position - */ - const float3& getPosW() const { return mData.posW; } - - /** Set the spherical radius the light probe encompasses. Set radius to negative to sample as an infinite-distance global light probe. - */ - void setRadius(float radius) { mData.radius = radius; } - - /** Get the light probe's radius. - */ - float getRadius() const { return mData.radius; } - - /** Get the sample count used to generate the diffuse texture. - */ - uint32_t getDiffSampleCount() const { return mDiffSampleCount; } - - /** Get the sample count used to generate the specular texture. - */ - uint32_t getSpecSampleCount() const { return mSpecSampleCount; } - - /** Set the light probe's light intensity - */ - void setIntensity(const float3& intensity) { mData.intensity = intensity; } - - /** Get the light probe's light intensity - */ - const float3& getIntensity() const { return mData.intensity; } - - /** Attach a sampler to the light probe - */ - void setSampler(const Sampler::SharedPtr& pSampler) { mData.resources.sampler = pSampler; } - - /** Get the sampler state - */ - const Sampler::SharedPtr& getSampler() const { return mData.resources.sampler; } - - /** Get the light probe's source texture. - */ - const Texture::SharedPtr& getOrigTexture() const { return mData.resources.origTexture; } - - /** Get the light probe's diffuse texture. - */ - const Texture::SharedPtr& getDiffuseTexture() const { return mData.resources.diffuseTexture; } - - /** Get the light probe's specular texture. - */ - const Texture::SharedPtr& getSpecularTexture() const { return mData.resources.specularTexture; } - - /** Get the texture storing the pre-integrated DFG term shared by all light probes. - */ - static const Texture::SharedPtr& getDfgTexture() { return sSharedResources.dfgTexture; } - - /** Bind the light data into a shader var - */ - void setShaderData(const ShaderVar& var); - - private: - static uint32_t sLightProbeCount; - static LightProbeSharedResources sSharedResources; - - LightProbeData mData; - uint32_t mDiffSampleCount; - uint32_t mSpecSampleCount; - LightProbe(RenderContext* pContext, const Texture::SharedPtr& pTexture, uint32_t diffSamples, uint32_t specSamples, uint32_t diffSize, uint32_t specSize, ResourceFormat preFilteredFormat); - }; -} diff --git a/Source/Falcor/Scene/Lights/Lights.slang b/Source/Falcor/Scene/Lights/Lights.slang index bc64e458eb..e89eec4bde 100644 --- a/Source/Falcor/Scene/Lights/Lights.slang +++ b/Source/Falcor/Scene/Lights/Lights.slang @@ -35,7 +35,6 @@ import Utils.Helpers; import Scene.ShadingData; __exported import Scene.Lights.LightData; -__exported import Scene.Lights.LightProbeData; struct LightSample { @@ -127,11 +126,6 @@ LightSample evalPointLight(in LightData light, in float3 surfacePosW) return ls; } -float linearRoughnessToLod(float linearRoughness, float mipCount) -{ - return sqrt(linearRoughness) * (mipCount - 1); -} - /** Evaluate a light source intensity/direction at a shading point */ LightSample evalLight(LightData light, ShadingData sd) @@ -152,107 +146,3 @@ LightSample evalLight(LightData light, ShadingData sd) calcCommonLightProperties(sd, ls); return ls; }; - -float3 getDiffuseDominantDir(float3 N, float3 V, float ggxAlpha) -{ - float a = 1.02341 * ggxAlpha - 1.51174; - float b = -0.511705 * ggxAlpha + 0.755868; - float factor = saturate((saturate(dot(N, V)) * a + b) * ggxAlpha); - return normalize(lerp(N, V, factor)); -} - -float3 getSpecularDominantDir(float3 N, float3 R, float ggxAlpha) -{ - float smoothness = 1 - ggxAlpha; - float factor = smoothness * (sqrt(smoothness) + ggxAlpha); - return normalize(lerp(N, R, factor)); -} - -float3 evalLightProbeDiffuse(LightProbeData probe, ShadingData sd) -{ - float3 N = getDiffuseDominantDir(sd.N, sd.V, sd.ggxAlpha); - - // Interpret negative radius as global light probe with infinite distance - // Otherwise simulate the light probe as covering a finite spherical area - float2 uv; - if(probe.radius < 0.0f) - { - uv = dirToSphericalCrd(N); - } - else - { - float3 intersectPosW; - intersectRaySphere(sd.posW, N, probe.posW, probe.radius, intersectPosW); - uv = dirToSphericalCrd(normalize(intersectPosW - probe.posW)); - } - - float width, height, mipCount; - probe.resources.diffuseTexture.GetDimensions(0, width, height, mipCount); - - float3 diffuseLighting = probe.resources.diffuseTexture.SampleLevel(probe.resources.sampler, uv, 0).rgb; - float preintegratedDisneyBRDF = probe.sharedResources.dfgTexture.SampleLevel(probe.sharedResources.dfgSampler, float2(sd.NdotV, sd.ggxAlpha), 0).z; - - return diffuseLighting * preintegratedDisneyBRDF * sd.diffuse.rgb; -} - -float3 evalLightProbeSpecular(LightProbeData probe, ShadingData sd, float3 L) -{ - float dfgWidth, dfgHeight; - probe.sharedResources.dfgTexture.GetDimensions(dfgWidth, dfgHeight); - - float width, height, mipCount; - probe.resources.specularTexture.GetDimensions(0, width, height, mipCount); - - float3 dominantDir = getSpecularDominantDir(sd.N, L, sd.ggxAlpha); - float mipLevel = linearRoughnessToLod(sd.ggxAlpha, mipCount); - - float2 uv; - if (probe.radius < 0.0f) - { - uv = dirToSphericalCrd(dominantDir); - } - else - { - float3 intersectPosW; - intersectRaySphere(sd.posW, dominantDir, probe.posW, probe.radius, intersectPosW); - uv = dirToSphericalCrd(normalize(intersectPosW - probe.posW)); - } - - float3 ld = probe.resources.specularTexture.SampleLevel(probe.resources.sampler, uv, mipLevel).rgb; - - float2 dfg = probe.sharedResources.dfgTexture.SampleLevel(probe.sharedResources.dfgSampler, float2(sd.NdotV, sd.ggxAlpha), 0).xy; - - // ld * (f0 * Gv * (1 - Fc)) + (f90 * Gv * Fc) - return ld * (sd.specular * dfg.x + dfg.y); -} - -/** Evaluate a 2D light-probe filtered using linear-filtering -*/ -LightSample evalLightProbeLinear2D(LightProbeData probe, ShadingData sd) -{ - LightSample ls; - - // Calculate the reflection vector - ls.L = reflect(-sd.V, sd.N); - - // Evaluate diffuse component - ls.diffuse = evalLightProbeDiffuse(probe, sd); - - // Get the specular component - ls.specular = evalLightProbeSpecular(probe, sd, ls.L); - - ls.diffuse *= probe.intensity; - ls.specular *= probe.intensity; - ls.posW = probe.posW; - ls.distance = length(probe.posW - sd.posW); - calcCommonLightProperties(sd, ls); - - return ls; -} - -/** Evaluate the properties of a light-probe -*/ -LightSample evalLightProbe(LightProbeData probe, ShadingData sd) -{ - return evalLightProbeLinear2D(probe, sd); -} diff --git a/Source/Falcor/Scene/Material/Material.cpp b/Source/Falcor/Scene/Material/Material.cpp index ae896124fe..a8fc656714 100644 --- a/Source/Falcor/Scene/Material/Material.cpp +++ b/Source/Falcor/Scene/Material/Material.cpp @@ -59,6 +59,7 @@ namespace Falcor widget.text("Shading model:"); if (getShadingModel() == ShadingModelMetalRough) widget.text("MetalRough", true); else if (getShadingModel() == ShadingModelSpecGloss) widget.text("SpecGloss", true); + else if (getShadingModel() == ShadingModelHairChiang16) widget.text("HairChiang16", true); else should_not_get_here(); if (const auto& tex = getBaseColorTexture(); tex != nullptr) @@ -109,6 +110,14 @@ namespace Falcor if (widget.button("Remove texture##NormalMap")) setNormalMap(nullptr); } + if (const auto& tex = getDisplacementMap(); tex != nullptr) + { + widget.text("Displacement map: " + tex->getSourceFilename()); + widget.text("Texture info: " + std::to_string(tex->getWidth()) + "x" + std::to_string(tex->getHeight()) + " (" + to_string(tex->getFormat()) + ")"); + widget.image("Displacement map", tex, float2(100.f)); + if (widget.button("Remove texture##DisplacementMap")) setDisplacementMap(nullptr); + } + if (const auto& tex = getEmissiveTexture(); tex != nullptr) { widget.text("Emissive color: " + tex->getSourceFilename()); @@ -206,29 +215,44 @@ namespace Falcor void Material::setTexture(TextureSlot slot, Texture::SharedPtr pTexture) { + if (pTexture == getTexture(slot)) return; + switch (slot) { case TextureSlot::BaseColor: - setBaseColorTexture(pTexture); + mResources.baseColor = pTexture; + updateBaseColorType(); + updateAlphaMode(); break; case TextureSlot::Specular: - setSpecularTexture(pTexture); + mResources.specular = pTexture; + updateSpecularType(); break; case TextureSlot::Emissive: - setEmissiveTexture(pTexture); + mResources.emissive = pTexture; + updateEmissiveType(); break; case TextureSlot::Normal: - setNormalMap(pTexture); + mResources.normalMap = pTexture; + updateNormalMapMode(); break; case TextureSlot::Occlusion: - setOcclusionMap(pTexture); + mResources.occlusionMap = pTexture; + updateOcclusionFlag(); + break; + case TextureSlot::Displacement: + mResources.displacementMap = pTexture; + updateDisplacementFlag(); break; case TextureSlot::SpecularTransmission: - setSpecularTransmissionTexture(pTexture); + mResources.specularTransmission = pTexture; + updateSpecularTransmissionType(); break; default: should_not_get_here(); } + + markUpdates(UpdateFlags::ResourcesChanged); } Texture::SharedPtr Material::getTexture(TextureSlot slot) const @@ -236,17 +260,19 @@ namespace Falcor switch (slot) { case TextureSlot::BaseColor: - return getBaseColorTexture(); + return mResources.baseColor; case TextureSlot::Specular: - return getSpecularTexture(); + return mResources.specular; case TextureSlot::Emissive: - return getEmissiveTexture(); + return mResources.emissive; case TextureSlot::Normal: - return getNormalMap(); + return mResources.normalMap; case TextureSlot::Occlusion: - return getOcclusionMap(); + return mResources.occlusionMap; + case TextureSlot::Displacement: + return mResources.displacementMap; case TextureSlot::SpecularTransmission: - return getSpecularTransmissionTexture(); + return mResources.specularTransmission; default: should_not_get_here(); } @@ -264,6 +290,11 @@ namespace Falcor return dim; } + void Material::setTextureTransform(const Transform& textureTransform) + { + mTextureTransform = textureTransform; + } + void Material::loadTexture(TextureSlot slot, const std::string& filename, bool useSrgb) { std::string fullpath; @@ -299,6 +330,7 @@ namespace Falcor case TextureSlot::Occlusion: return true; case TextureSlot::Normal: + case TextureSlot::Displacement: return false; default: should_not_get_here(); @@ -306,49 +338,6 @@ namespace Falcor } } - void Material::setBaseColorTexture(Texture::SharedPtr pBaseColor) - { - if (mResources.baseColor != pBaseColor) - { - mResources.baseColor = pBaseColor; - markUpdates(UpdateFlags::ResourcesChanged); - updateBaseColorType(); - bool hasAlpha = pBaseColor && doesFormatHasAlpha(pBaseColor->getFormat()); - setAlphaMode(hasAlpha ? AlphaModeMask : AlphaModeOpaque); - } - } - - void Material::setSpecularTexture(Texture::SharedPtr pSpecular) - { - if (mResources.specular != pSpecular) - { - mResources.specular = pSpecular; - markUpdates(UpdateFlags::ResourcesChanged); - updateSpecularType(); - } - } - - void Material::setEmissiveTexture(const Texture::SharedPtr& pEmissive) - { - if (mResources.emissive != pEmissive) - { - mResources.emissive = pEmissive; - markUpdates(UpdateFlags::ResourcesChanged); - updateEmissiveType(); - } - } - - void Material::setSpecularTransmissionTexture(const Texture::SharedPtr& pSpecularTransmission) - { - if (mResources.specularTransmission != pSpecularTransmission) - { - mResources.specularTransmission = pSpecularTransmission; - markUpdates(UpdateFlags::ResourcesChanged); - updateSpecularTransmissionType(); - } - } - - void Material::setBaseColor(const float4& color) { if (mData.baseColor != color) @@ -440,42 +429,6 @@ namespace Falcor } } - void Material::setNormalMap(Texture::SharedPtr pNormalMap) - { - if (mResources.normalMap != pNormalMap) - { - mResources.normalMap = pNormalMap; - markUpdates(UpdateFlags::ResourcesChanged); - uint32_t normalMode = NormalMapUnused; - if (pNormalMap) - { - switch(getFormatChannelCount(pNormalMap->getFormat())) - { - case 2: - normalMode = NormalMapRG; - break; - case 3: - case 4: // Some texture formats don't support RGB, only RGBA. We have no use for the alpha channel in the normal map. - normalMode = NormalMapRGB; - break; - default: - should_not_get_here(); - logWarning("Unsupported normal map format for material " + mName); - } - } - setFlags(PACK_NORMAL_MAP_TYPE(mData.flags, normalMode)); - } - } - - void Material::setOcclusionMap(Texture::SharedPtr pOcclusionMap) - { - if (mResources.occlusionMap != pOcclusionMap) - { - mResources.occlusionMap = pOcclusionMap; - markUpdates(UpdateFlags::ResourcesChanged); - updateOcclusionFlag(); - } - } bool Material::operator==(const Material& other) const { @@ -498,8 +451,13 @@ namespace Falcor compare_texture(normalMap); compare_texture(occlusionMap); compare_texture(specularTransmission); + compare_texture(displacementMap); #undef compare_texture + if (mResources.samplerState != other.mResources.samplerState) return false; + if (mTextureTransform.getMatrix() != other.mTextureTransform.getMatrix()) return false; + if (mOcclusionMapEnabled != other.mOcclusionMapEnabled) return false; + return true; } @@ -546,6 +504,34 @@ namespace Falcor setFlags(PACK_SPEC_TRANS_TYPE(mData.flags, getChannelMode(mResources.specularTransmission != nullptr, mData.specularTransmission))); } + void Material::updateAlphaMode() + { + bool hasAlpha = mResources.baseColor && doesFormatHasAlpha(mResources.baseColor->getFormat()); + setAlphaMode(hasAlpha ? AlphaModeMask : AlphaModeOpaque); + } + + void Material::updateNormalMapMode() + { + uint32_t normalMode = NormalMapUnused; + if (mResources.normalMap) + { + switch(getFormatChannelCount(mResources.normalMap->getFormat())) + { + case 2: + normalMode = NormalMapRG; + break; + case 3: + case 4: // Some texture formats don't support RGB, only RGBA. We have no use for the alpha channel in the normal map. + normalMode = NormalMapRGB; + break; + default: + should_not_get_here(); + logWarning("Unsupported normal map format for material " + mName); + } + } + setFlags(PACK_NORMAL_MAP_TYPE(mData.flags, normalMode)); + } + void Material::updateOcclusionFlag() { bool hasMap = false; @@ -564,6 +550,12 @@ namespace Falcor setFlags(PACK_OCCLUSION_MAP(mData.flags, shouldEnable ? 1 : 0)); } + void Material::updateDisplacementFlag() + { + bool hasMap = (mResources.occlusionMap != nullptr); + setFlags(PACK_DISPLACEMENT_MAP(mData.flags, hasMap ? 1 : 0)); + } + SCRIPT_BINDING(Material) { pybind11::enum_ textureSlot(m, "MaterialTextureSlot"); @@ -573,9 +565,10 @@ namespace Falcor textureSlot.value("Normal", Material::TextureSlot::Normal); textureSlot.value("Occlusion", Material::TextureSlot::Occlusion); textureSlot.value("SpecularTransmission", Material::TextureSlot::SpecularTransmission); + textureSlot.value("Displacement", Material::TextureSlot::Displacement); pybind11::class_ material(m, "Material"); - material.def_property_readonly("name", &Material::getName); + material.def_property("name", &Material::getName, &Material::setName); material.def_property("baseColor", &Material::getBaseColor, &Material::setBaseColor); material.def_property("specularParams", &Material::getSpecularParams, &Material::setSpecularParams); material.def_property("roughness", &Material::getRoughness, &Material::setRoughness); @@ -589,7 +582,9 @@ namespace Falcor material.def_property("alphaThreshold", &Material::getAlphaThreshold, &Material::setAlphaThreshold); material.def_property("doubleSided", &Material::isDoubleSided, &Material::setDoubleSided); material.def_property("nestedPriority", &Material::getNestedPriority, &Material::setNestedPriority); + material.def_property("textureTransform", pybind11::overload_cast(&Material::getTextureTransform, pybind11::const_), &Material::setTextureTransform); + material.def(pybind11::init(&Material::create), "name"_a); material.def("loadTexture", &Material::loadTexture, "slot"_a, "filename"_a, "useSrgb"_a = true); material.def("clearTexture", &Material::clearTexture, "slot"_a); } diff --git a/Source/Falcor/Scene/Material/Material.h b/Source/Falcor/Scene/Material/Material.h index 7968c9c14a..a4c5d153e4 100644 --- a/Source/Falcor/Scene/Material/Material.h +++ b/Source/Falcor/Scene/Material/Material.h @@ -28,6 +28,7 @@ #pragma once #include "MaterialData.slang" #include "MaterialDefines.slangh" +#include "Scene/Transform.h" namespace Falcor { @@ -52,6 +53,16 @@ namespace Falcor - RGB - Specular Color - A - Gloss + ShadingModelHairChiang16 + BaseColor + - RGB - Absorption coefficient, sigmaA + - A - Unused + Specular + - R - Longitudinal roughness, betaM + - G - Azimuthal roughness, betaN + - B - The angle that the small scales on the surface of hair are offset from the base cylinder (in degrees). + - A - Unused + Common for all shading models Emissive - RGB - Emissive Color @@ -85,6 +96,7 @@ namespace Falcor Normal, Occlusion, SpecularTransmission, + Displacement, Count // Must be last }; @@ -152,35 +164,35 @@ namespace Falcor /** Set the base color texture */ - void setBaseColorTexture(Texture::SharedPtr pBaseColor); + void setBaseColorTexture(Texture::SharedPtr pBaseColor) { setTexture(TextureSlot::BaseColor, pBaseColor); } /** Get the base color texture */ - Texture::SharedPtr getBaseColorTexture() const { return mResources.baseColor; } + Texture::SharedPtr getBaseColorTexture() const { return getTexture(TextureSlot::BaseColor); } /** Set the specular texture */ - void setSpecularTexture(Texture::SharedPtr pSpecular); + void setSpecularTexture(Texture::SharedPtr pSpecular) { setTexture(TextureSlot::Specular, pSpecular); } /** Get the specular texture */ - Texture::SharedPtr getSpecularTexture() const { return mResources.specular; } + Texture::SharedPtr getSpecularTexture() const { return getTexture(TextureSlot::Specular); } /** Set the emissive texture */ - void setEmissiveTexture(const Texture::SharedPtr& pEmissive); + void setEmissiveTexture(const Texture::SharedPtr& pEmissive) { setTexture(TextureSlot::Emissive, pEmissive); } /** Get the emissive texture */ - Texture::SharedPtr getEmissiveTexture() const { return mResources.emissive; } + Texture::SharedPtr getEmissiveTexture() const { return getTexture(TextureSlot::Emissive); } /** Set the specular transmission texture */ - void setSpecularTransmissionTexture(const Texture::SharedPtr& pTransmission); + void setSpecularTransmissionTexture(const Texture::SharedPtr& pTransmission) { setTexture(TextureSlot::SpecularTransmission, pTransmission); } /** Get the specular transmission texture */ - Texture::SharedPtr getSpecularTransmissionTexture() const { return mResources.specularTransmission; } + Texture::SharedPtr getSpecularTransmissionTexture() const { return getTexture(TextureSlot::SpecularTransmission); } /** Set the shading model */ @@ -192,19 +204,27 @@ namespace Falcor /** Set the normal map */ - void setNormalMap(Texture::SharedPtr pNormalMap); + void setNormalMap(Texture::SharedPtr pNormalMap) { setTexture(TextureSlot::Normal, pNormalMap); } /** Get the normal map */ - Texture::SharedPtr getNormalMap() const { return mResources.normalMap; } + Texture::SharedPtr getNormalMap() const { return getTexture(TextureSlot::Normal); } /** Set the occlusion map */ - void setOcclusionMap(Texture::SharedPtr pOcclusionMap); + void setOcclusionMap(Texture::SharedPtr pOcclusionMap) { setTexture(TextureSlot::Occlusion, pOcclusionMap); } /** Get the occlusion map */ - Texture::SharedPtr getOcclusionMap() const { return mResources.occlusionMap; } + Texture::SharedPtr getOcclusionMap() const { return getTexture(TextureSlot::Occlusion); } + + /** Set the displacement map + */ + void setDisplacementMap(Texture::SharedPtr pDisplacementMap) { setTexture(TextureSlot::Displacement, pDisplacementMap); } + + /** Get the displacement map + */ + Texture::SharedPtr getDisplacementMap() const { return getTexture(TextureSlot::Displacement); } /** Set the base color */ @@ -314,7 +334,8 @@ namespace Falcor */ void setNestedPriority(uint32_t priority); - /** Get the nested priority used for nested dielectrics + /** Get the nested priority used for nested dielectrics. + \return Nested priority, with 0 reserved for the highest possible priority. */ uint32_t getNestedPriority() const { return EXTRACT_NESTED_PRIORITY(mData.flags); } @@ -322,7 +343,8 @@ namespace Falcor */ bool isEmissive() const { return EXTRACT_EMISSIVE_TYPE(mData.flags) != ChannelTypeUnused; } - /** Comparison operator + /** Comparison operator. + \return True if all materials properties *except* the name are identical. */ bool operator==(const Material& other) const; @@ -342,6 +364,18 @@ namespace Falcor */ const MaterialResources& getResources() const { return mResources; } + /** Set the material texture transform. + */ + void setTextureTransform(const Transform& texTransform); + + /** Get a reference to the material texture transform. + */ + Transform& getTextureTransform() { return mTextureTransform; } + + /** Get the material texture transform. + */ + const Transform& getTextureTransform() const { return mTextureTransform; } + private: void markUpdates(UpdateFlags updates); @@ -349,13 +383,17 @@ namespace Falcor void updateBaseColorType(); void updateSpecularType(); void updateEmissiveType(); - void updateOcclusionFlag(); void updateSpecularTransmissionType(); + void updateAlphaMode(); + void updateNormalMapMode(); + void updateOcclusionFlag(); + void updateDisplacementFlag(); Material(const std::string& name); std::string mName; MaterialData mData; MaterialResources mResources; + Transform mTextureTransform; bool mOcclusionMapEnabled = false; mutable UpdateFlags mUpdates = UpdateFlags::None; static UpdateFlags sGlobalUpdates; diff --git a/Source/Falcor/Scene/Material/MaterialData.slang b/Source/Falcor/Scene/Material/MaterialData.slang index 7012b6fb4b..d738944a03 100644 --- a/Source/Falcor/Scene/Material/MaterialData.slang +++ b/Source/Falcor/Scene/Material/MaterialData.slang @@ -46,6 +46,7 @@ struct MaterialResources Texture2D emissive; Texture2D normalMap; Texture2D specularTransmission; + Texture2D displacementMap; // The following maps are not yet used by the material system Texture2D occlusionMap; // Ambient occlusion map @@ -70,6 +71,9 @@ struct MaterialData float3 volumeAbsorption = float3(0, 0, 0); ///< Volume absorption coefficient. float _pad0 = 0.f; + /** Get the nested priority used for nested dielectrics. + \return Nested priority, with 0 reserved for the highest possible priority. + */ uint getNestedPriority() { return EXTRACT_NESTED_PRIORITY(flags); } }; diff --git a/Source/Falcor/Scene/Material/MaterialDefines.slangh b/Source/Falcor/Scene/Material/MaterialDefines.slangh index 9543ef2f27..521332594e 100644 --- a/Source/Falcor/Scene/Material/MaterialDefines.slangh +++ b/Source/Falcor/Scene/Material/MaterialDefines.slangh @@ -35,6 +35,7 @@ #define ShadingModelMetalRough 0 //#define ShadingModelMetalAnisoRough 1 Reserved for future use #define ShadingModelSpecGloss 2 +#define ShadingModelHairChiang16 3 // Channel type #define ChannelTypeUnused 0 @@ -51,51 +52,55 @@ #define AlphaModeMask 1 // Bit count -#define SHADING_MODEL_BITS (3) -#define DIFFUSE_TYPE_BITS (3) -#define SPECULAR_TYPE_BITS (3) -#define EMISSIVE_TYPE_BITS (3) -#define NORMAL_MAP_BITS (2) -#define OCCLUSION_MAP_BITS (1) -#define ALPHA_MODE_BITS (2) -#define DOUBLE_SIDED_BITS (1) -#define NESTED_PRIORITY_BITS (4) -#define SPEC_TRANS_TYPE_BITS (3) +#define SHADING_MODEL_BITS (3) +#define DIFFUSE_TYPE_BITS (3) +#define SPECULAR_TYPE_BITS (3) +#define EMISSIVE_TYPE_BITS (3) +#define NORMAL_MAP_BITS (2) +#define OCCLUSION_MAP_BITS (1) +#define ALPHA_MODE_BITS (2) +#define DOUBLE_SIDED_BITS (1) +#define NESTED_PRIORITY_BITS (4) +#define SPEC_TRANS_TYPE_BITS (3) +#define DISPLACEMENT_MAP_BITS (1) // Offsets -#define SHADING_MODEL_OFFSET (0) -#define DIFFUSE_TYPE_OFFSET (SHADING_MODEL_OFFSET + SHADING_MODEL_BITS) -#define SPECULAR_TYPE_OFFSET (DIFFUSE_TYPE_OFFSET + DIFFUSE_TYPE_BITS) -#define EMISSIVE_TYPE_OFFSET (SPECULAR_TYPE_OFFSET + SPECULAR_TYPE_BITS) -#define NORMAL_MAP_OFFSET (EMISSIVE_TYPE_OFFSET + EMISSIVE_TYPE_BITS) -#define OCCLUSION_MAP_OFFSET (NORMAL_MAP_OFFSET + NORMAL_MAP_BITS) -#define ALPHA_MODE_OFFSET (OCCLUSION_MAP_OFFSET + OCCLUSION_MAP_BITS) -#define DOUBLE_SIDED_OFFSET (ALPHA_MODE_OFFSET + ALPHA_MODE_BITS) -#define NESTED_PRIORITY_OFFSET (DOUBLE_SIDED_OFFSET + DOUBLE_SIDED_BITS) -#define SPEC_TRANS_TYPE_OFFSET (NESTED_PRIORITY_OFFSET + NESTED_PRIORITY_BITS) +#define SHADING_MODEL_OFFSET (0) +#define DIFFUSE_TYPE_OFFSET (SHADING_MODEL_OFFSET + SHADING_MODEL_BITS) +#define SPECULAR_TYPE_OFFSET (DIFFUSE_TYPE_OFFSET + DIFFUSE_TYPE_BITS) +#define EMISSIVE_TYPE_OFFSET (SPECULAR_TYPE_OFFSET + SPECULAR_TYPE_BITS) +#define NORMAL_MAP_OFFSET (EMISSIVE_TYPE_OFFSET + EMISSIVE_TYPE_BITS) +#define OCCLUSION_MAP_OFFSET (NORMAL_MAP_OFFSET + NORMAL_MAP_BITS) +#define ALPHA_MODE_OFFSET (OCCLUSION_MAP_OFFSET + OCCLUSION_MAP_BITS) +#define DOUBLE_SIDED_OFFSET (ALPHA_MODE_OFFSET + ALPHA_MODE_BITS) +#define NESTED_PRIORITY_OFFSET (DOUBLE_SIDED_OFFSET + DOUBLE_SIDED_BITS) +#define SPEC_TRANS_TYPE_OFFSET (NESTED_PRIORITY_OFFSET + NESTED_PRIORITY_BITS) +#define DISPLACEMENT_MAP_OFFSET (SPEC_TRANS_TYPE_OFFSET + SPEC_TRANS_TYPE_BITS) // Extract bits #define EXTRACT_BITS(bits, offset, value) ((value >> offset) & ((1 << bits) - 1)) -#define EXTRACT_SHADING_MODEL(value) EXTRACT_BITS(SHADING_MODEL_BITS, SHADING_MODEL_OFFSET, value) -#define EXTRACT_DIFFUSE_TYPE(value) EXTRACT_BITS(DIFFUSE_TYPE_BITS, DIFFUSE_TYPE_OFFSET, value) -#define EXTRACT_SPECULAR_TYPE(value) EXTRACT_BITS(SPECULAR_TYPE_BITS, SPECULAR_TYPE_OFFSET, value) -#define EXTRACT_EMISSIVE_TYPE(value) EXTRACT_BITS(EMISSIVE_TYPE_BITS, EMISSIVE_TYPE_OFFSET, value) -#define EXTRACT_NORMAL_MAP_TYPE(value) EXTRACT_BITS(NORMAL_MAP_BITS, NORMAL_MAP_OFFSET, value) -#define EXTRACT_OCCLUSION_MAP(value) EXTRACT_BITS(OCCLUSION_MAP_BITS, OCCLUSION_MAP_OFFSET, value) -#define EXTRACT_ALPHA_MODE(value) EXTRACT_BITS(ALPHA_MODE_BITS, ALPHA_MODE_OFFSET, value) -#define EXTRACT_DOUBLE_SIDED(value) EXTRACT_BITS(DOUBLE_SIDED_BITS, DOUBLE_SIDED_OFFSET, value) -#define EXTRACT_NESTED_PRIORITY(value) EXTRACT_BITS(NESTED_PRIORITY_BITS, NESTED_PRIORITY_OFFSET, value) -#define EXTRACT_SPEC_TRANS_TYPE(value) EXTRACT_BITS(SPEC_TRANS_TYPE_BITS, SPEC_TRANS_TYPE_OFFSET, value) +#define EXTRACT_SHADING_MODEL(value) EXTRACT_BITS(SHADING_MODEL_BITS, SHADING_MODEL_OFFSET, value) +#define EXTRACT_DIFFUSE_TYPE(value) EXTRACT_BITS(DIFFUSE_TYPE_BITS, DIFFUSE_TYPE_OFFSET, value) +#define EXTRACT_SPECULAR_TYPE(value) EXTRACT_BITS(SPECULAR_TYPE_BITS, SPECULAR_TYPE_OFFSET, value) +#define EXTRACT_EMISSIVE_TYPE(value) EXTRACT_BITS(EMISSIVE_TYPE_BITS, EMISSIVE_TYPE_OFFSET, value) +#define EXTRACT_NORMAL_MAP_TYPE(value) EXTRACT_BITS(NORMAL_MAP_BITS, NORMAL_MAP_OFFSET, value) +#define EXTRACT_OCCLUSION_MAP(value) EXTRACT_BITS(OCCLUSION_MAP_BITS, OCCLUSION_MAP_OFFSET, value) +#define EXTRACT_ALPHA_MODE(value) EXTRACT_BITS(ALPHA_MODE_BITS, ALPHA_MODE_OFFSET, value) +#define EXTRACT_DOUBLE_SIDED(value) EXTRACT_BITS(DOUBLE_SIDED_BITS, DOUBLE_SIDED_OFFSET, value) +#define EXTRACT_NESTED_PRIORITY(value) EXTRACT_BITS(NESTED_PRIORITY_BITS, NESTED_PRIORITY_OFFSET, value) +#define EXTRACT_SPEC_TRANS_TYPE(value) EXTRACT_BITS(SPEC_TRANS_TYPE_BITS, SPEC_TRANS_TYPE_OFFSET, value) +#define EXTRACT_DISPLACEMENT_MAP(value) EXTRACT_BITS(DISPLACEMENT_MAP_BITS, DISPLACEMENT_MAP_OFFSET, value) // Pack bits #define PACK_BITS(bits, offset, flags, value) (((value & ((1 << bits) - 1)) << offset) | (flags & (~(((1 << bits) - 1) << offset)))) -#define PACK_SHADING_MODEL(flags, value) PACK_BITS(SHADING_MODEL_BITS, SHADING_MODEL_OFFSET, flags, value) -#define PACK_DIFFUSE_TYPE(flags, value) PACK_BITS(DIFFUSE_TYPE_BITS, DIFFUSE_TYPE_OFFSET, flags, value) -#define PACK_SPECULAR_TYPE(flags, value) PACK_BITS(SPECULAR_TYPE_BITS, SPECULAR_TYPE_OFFSET, flags, value) -#define PACK_EMISSIVE_TYPE(flags, value) PACK_BITS(EMISSIVE_TYPE_BITS, EMISSIVE_TYPE_OFFSET, flags, value) -#define PACK_NORMAL_MAP_TYPE(flags, value) PACK_BITS(NORMAL_MAP_BITS, NORMAL_MAP_OFFSET, flags, value) -#define PACK_OCCLUSION_MAP(flags, value) PACK_BITS(OCCLUSION_MAP_BITS, OCCLUSION_MAP_OFFSET, flags, value) -#define PACK_ALPHA_MODE(flags, value) PACK_BITS(ALPHA_MODE_BITS, ALPHA_MODE_OFFSET, flags, value) -#define PACK_DOUBLE_SIDED(flags, value) PACK_BITS(DOUBLE_SIDED_BITS, DOUBLE_SIDED_OFFSET, flags, value) -#define PACK_NESTED_PRIORITY(flags, value) PACK_BITS(NESTED_PRIORITY_BITS, NESTED_PRIORITY_OFFSET, flags, value) -#define PACK_SPEC_TRANS_TYPE(flags, value) PACK_BITS(SPEC_TRANS_TYPE_BITS, SPEC_TRANS_TYPE_OFFSET, flags, value) +#define PACK_SHADING_MODEL(flags, value) PACK_BITS(SHADING_MODEL_BITS, SHADING_MODEL_OFFSET, flags, value) +#define PACK_DIFFUSE_TYPE(flags, value) PACK_BITS(DIFFUSE_TYPE_BITS, DIFFUSE_TYPE_OFFSET, flags, value) +#define PACK_SPECULAR_TYPE(flags, value) PACK_BITS(SPECULAR_TYPE_BITS, SPECULAR_TYPE_OFFSET, flags, value) +#define PACK_EMISSIVE_TYPE(flags, value) PACK_BITS(EMISSIVE_TYPE_BITS, EMISSIVE_TYPE_OFFSET, flags, value) +#define PACK_NORMAL_MAP_TYPE(flags, value) PACK_BITS(NORMAL_MAP_BITS, NORMAL_MAP_OFFSET, flags, value) +#define PACK_OCCLUSION_MAP(flags, value) PACK_BITS(OCCLUSION_MAP_BITS, OCCLUSION_MAP_OFFSET, flags, value) +#define PACK_ALPHA_MODE(flags, value) PACK_BITS(ALPHA_MODE_BITS, ALPHA_MODE_OFFSET, flags, value) +#define PACK_DOUBLE_SIDED(flags, value) PACK_BITS(DOUBLE_SIDED_BITS, DOUBLE_SIDED_OFFSET, flags, value) +#define PACK_NESTED_PRIORITY(flags, value) PACK_BITS(NESTED_PRIORITY_BITS, NESTED_PRIORITY_OFFSET, flags, value) +#define PACK_SPEC_TRANS_TYPE(flags, value) PACK_BITS(SPEC_TRANS_TYPE_BITS, SPEC_TRANS_TYPE_OFFSET, flags, value) +#define PACK_DISPLACEMENT_MAP(flags, value) PACK_BITS(DISPLACEMENT_MAP_BITS, DISPLACEMENT_MAP_OFFSET, flags, value) diff --git a/Source/Falcor/Scene/Material/MaterialTextureLoader.cpp b/Source/Falcor/Scene/Material/MaterialTextureLoader.cpp new file mode 100644 index 0000000000..edccf7e4b3 --- /dev/null +++ b/Source/Falcor/Scene/Material/MaterialTextureLoader.cpp @@ -0,0 +1,83 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#include "stdafx.h" +#include "MaterialTextureLoader.h" + +namespace Falcor +{ + MaterialTextureLoader::MaterialTextureLoader(bool useSrgb) + : mUseSrgb(useSrgb) + { + } + + MaterialTextureLoader::~MaterialTextureLoader() + { + assignTextures(); + } + + void MaterialTextureLoader::loadTexture(const Material::SharedPtr& pMaterial, Material::TextureSlot slot, const std::string& filename) + { + assert(pMaterial); + + bool srgb = mUseSrgb && pMaterial->isSrgbTextureRequired(slot); + + std::string fullPath; + if (!findFileInDataDirectories(filename, fullPath)) + { + logWarning("Can't find texture image file '" + filename + "'"); + return; + } + + TextureKey textureKey{fullPath, srgb}; + + // Load texture if not already requested before. + if (mRequestedTextures.find(textureKey) == mRequestedTextures.end()) + { + mRequestedTextures[textureKey] = mAsyncTextureLoader.loadFromFile(fullPath, true, srgb); + } + + // Store assignment to material for later. + mTextureAssignments.emplace_back(TextureAssignment{ pMaterial, slot, textureKey }); + } + + void MaterialTextureLoader::assignTextures() + { + // Wait for all textures to be loaded. + std::map loadedTextures; + for (auto &[key, texture] : mRequestedTextures) + { + loadedTextures[key] = texture.get(); + } + + // Assign textures to materials. + for (const auto& assignment : mTextureAssignments) + { + assignment.pMaterial->setTexture(assignment.textureSlot, loadedTextures[assignment.textureKey]); + } + } +} diff --git a/Source/Falcor/Scene/Material/MaterialTextureLoader.h b/Source/Falcor/Scene/Material/MaterialTextureLoader.h new file mode 100644 index 0000000000..330ec3cdb9 --- /dev/null +++ b/Source/Falcor/Scene/Material/MaterialTextureLoader.h @@ -0,0 +1,74 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#pragma once + +#include "Falcor.h" +#include "Utils/AsyncTextureLoader.h" + +namespace Falcor +{ + /** Helper class to load material textures using multiple threads. + + Calling `loadTexture` does not assign the texture to the material right away. + Instead, an asynchronous texture load request is issued and a reference for the + material assignment is stored. When the client destroys the instance of the + `MaterialTextureLoader`, it blocks until all textures are loaded and assigns + them to the materials. + */ + class MaterialTextureLoader + { + public: + MaterialTextureLoader(bool useSrgb); + ~MaterialTextureLoader(); + + /** Request loading a material texture. + \param[in] pMaterial Material to load texture into. + \param[in] slot Slot to load texture into. + \param[in] filename Texture filename. + */ + void loadTexture(const Material::SharedPtr& pMaterial, Material::TextureSlot slot, const std::string& filename); + + private: + void assignTextures(); + + bool mUseSrgb; + + using TextureKey = std::pair; // filename, srgb + + struct TextureAssignment + { + Material::SharedPtr pMaterial; + Material::TextureSlot textureSlot; + TextureKey textureKey; + }; + + std::map> mRequestedTextures; + std::vector mTextureAssignments; + AsyncTextureLoader mAsyncTextureLoader; + }; +} diff --git a/Source/Falcor/Scene/NullTrace.cs.slang b/Source/Falcor/Scene/NullTrace.cs.slang new file mode 100644 index 0000000000..49119c30ea --- /dev/null +++ b/Source/Falcor/Scene/NullTrace.cs.slang @@ -0,0 +1,46 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +import Utils.Attributes; + +[root] RaytracingAccelerationStructure gTlas; +RWTexture2D gOutput; + +[numthreads(16, 16, 1)] +void main(uint3 dispatchThreadId : SV_DispatchThreadID) +{ + RayDesc ray; + ray.Origin = float3(0, 0, 0); + ray.Direction = float3(1, 0, 0); + ray.TMin = 0.f; + ray.TMax = 1.f; + + RayQuery rayQuery; + rayQuery.TraceRayInline(gTlas, RAY_FLAG_NONE, 0xff, ray); + rayQuery.Proceed(); + gOutput[dispatchThreadId.xy] = (rayQuery.CommittedStatus() == COMMITTED_TRIANGLE_HIT) ? 1 : 0; +} diff --git a/Source/Falcor/Scene/Raster.slang b/Source/Falcor/Scene/Raster.slang index 32529c202b..eeefbe0c28 100644 --- a/Source/Falcor/Scene/Raster.slang +++ b/Source/Falcor/Scene/Raster.slang @@ -38,7 +38,9 @@ struct VSIn // Other vertex attributes uint meshInstanceID : DRAW_ID; - float3 prevPos : PREV_POSITION; + + // System values + uint vertexID : SV_VertexID; StaticVertexData unpack() { @@ -56,16 +58,15 @@ struct VSIn struct VSOut { - INTERPOLATION_MODE float3 normalW : NORMAL; - INTERPOLATION_MODE float4 tangentW : TANGENT; - INTERPOLATION_MODE float2 texC : TEXCRD; - INTERPOLATION_MODE float3 posW : POSW; - INTERPOLATION_MODE float3 colorV : COLOR; - INTERPOLATION_MODE float4 prevPosH : PREVPOSH; + INTERPOLATION_MODE float3 normalW : NORMAL; ///< Shading normal in world space (not normalized!). + INTERPOLATION_MODE float4 tangentW : TANGENT; ///< Shading tangent in world space (not normalized!). + INTERPOLATION_MODE float2 texC : TEXCRD; ///< Texture coordinate. + INTERPOLATION_MODE float3 posW : POSW; ///< Position in world space. + INTERPOLATION_MODE float4 prevPosH : PREVPOSH; ///< Position in clip space for the previous frame. // Per-triangle data - nointerpolation uint meshInstanceID : DRAW_ID; - nointerpolation uint materialID : MATERIAL_ID; + nointerpolation uint meshInstanceID : DRAW_ID; ///< Mesh instance ID. + nointerpolation uint materialID : MATERIAL_ID; ///< Material ID. float4 posH : SV_POSITION; }; @@ -86,10 +87,18 @@ VSOut defaultVS(VSIn vIn) float4 tangent = vIn.unpack().tangent; vOut.tangentW = float4(mul(tangent.xyz, (float3x3)gScene.getWorldMatrix(vIn.meshInstanceID)), tangent.w); - float4 prevPosW = mul(float4(vIn.prevPos, 1.f), gScene.getPrevWorldMatrix(vIn.meshInstanceID)); + // Compute the vertex position in the previous frame. + float3 prevPos = vIn.pos; + MeshInstanceData meshInstance = gScene.getMeshInstance(vIn.meshInstanceID); + if (meshInstance.hasDynamicData()) + { + uint dynamicVertexIndex = gScene.meshes[meshInstance.meshID].dynamicVbOffset + vIn.vertexID; + prevPos = gScene.prevVertices[dynamicVertexIndex].position; + } + float4 prevPosW = mul(float4(prevPos, 1.f), gScene.getPrevWorldMatrix(vIn.meshInstanceID)); vOut.prevPosH = mul(prevPosW, gScene.camera.data.prevViewProjMatNoJitter); - return vOut; + return vOut; } /** Setup vertex data based on interpolated vertex attributes. @@ -104,8 +113,7 @@ VertexData prepareVertexData(VSOut vsOut, float3 faceNormalW) v.texC = vsOut.texC; v.normalW = normalize(vsOut.normalW); v.faceNormalW = faceNormalW; - // Handle invalid tangents gracefully (avoid NaN from normalization). - v.tangentW.xyz = vsOut.tangentW.w != 0.f ? normalize(vsOut.tangentW.xyz) : float3(0, 0, 0); + v.tangentW.xyz = normalize(vsOut.tangentW.xyz); v.tangentW.w = sign(vsOut.tangentW.w); // Preserve zero to indicate invalid tangent. return v; } diff --git a/Source/Falcor/Scene/Raytracing.slang b/Source/Falcor/Scene/Raytracing.slang index 8908e9e56b..a4b8a51ab4 100644 --- a/Source/Falcor/Scene/Raytracing.slang +++ b/Source/Falcor/Scene/Raytracing.slang @@ -25,12 +25,13 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **************************************************************************/ +import Utils.Attributes; __exported import Scene.Scene; __exported import Scene.Shading; import Experimental.Scene.Material.TexLODHelpers; -RaytracingAccelerationStructure gRtScene; +[root] RaytracingAccelerationStructure gRtScene; cbuffer DxrPerFrame { diff --git a/Source/Falcor/Scene/RaytracingInline.slang b/Source/Falcor/Scene/RaytracingInline.slang new file mode 100644 index 0000000000..d2d81d2634 --- /dev/null +++ b/Source/Falcor/Scene/RaytracingInline.slang @@ -0,0 +1,124 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ + +/** Utilities for inline ray tracing using DXR 1.1. + + Import this module in your shader and call Scene::setRaytracingShaderData() + on the host to bind the necessary resources. +*/ +import Utils.Attributes; +import Utils.Helpers; +import Scene.Intersection; +__exported import Scene.Scene; +__exported import Scene.HitInfo; + +[root] RaytracingAccelerationStructure gRtScene; + +/** Returns the global mesh instance ID for a ray hit. + \param[in] instanceID The instance ID of the TLAS instance. + \param[in] geometryIndex The geometry index of the BLAS. + \return Global mesh instance ID. +*/ +uint getMeshInstanceID(uint instanceID, uint geometryIndex) +{ + return instanceID + geometryIndex; +} + +/** Create a HitInfo for a RayQuery committed hit on triangles. + \param[in] rayQuery RayQuery object. + \return Candidate hit info. +*/ +HitInfo getCommittedTriangleHit(RayQuery rayQuery) +{ + HitInfo hit; + hit.type = InstanceType::TriangleMesh; + hit.instanceID = getMeshInstanceID(rayQuery.CommittedInstanceID(), rayQuery.CommittedGeometryIndex()); + hit.primitiveIndex = rayQuery.CommittedPrimitiveIndex(); + hit.barycentrics = rayQuery.CommittedTriangleBarycentrics(); + return hit; +} + +/** Create a HitInfo for a RayQuery candidate hit on triangles. + \param[in] rayQuery RayQuery object. + \return Candidate hit info. +*/ +HitInfo getCandidateTriangleHit(RayQuery rayQuery) +{ + HitInfo hit; + hit.type = InstanceType::TriangleMesh; + hit.instanceID = getMeshInstanceID(rayQuery.CandidateInstanceID(), rayQuery.CandidateGeometryIndex()); + hit.primitiveIndex = rayQuery.CandidatePrimitiveIndex(); + hit.barycentrics = rayQuery.CandidateTriangleBarycentrics(); + return hit; +} + +/** Create a HitInfo for a RayQuery committed hit on curves. + \param[in] rayQuery RayQuery object. + \param[in] curveCommittedAttribs Attribution object. + \return Candidate hit info. +*/ +HitInfo getCommittedCurveHit(RayQuery rayQuery, float2 curveCommittedAttribs) +{ + HitInfo hit; + hit.type = InstanceType::Curve; + hit.instanceID = gScene.getCurveInstanceID(rayQuery.CommittedInstanceID(), rayQuery.CommittedGeometryIndex()); + hit.primitiveIndex = rayQuery.CommittedPrimitiveIndex(); + hit.barycentrics = curveCommittedAttribs; + return hit; +} + +/** Create a HitInfo for a RayQuery candidate hit on curves. + \param[in] rayQuery RayQuery object. + \param[in] ray Ray object. + \param[out] hitT Distance value t at the hit point. + \param[out] hit Candidate hit info. + \return True if finding a valid candidate curve hit. +*/ +bool getCandidateCurveHit(RayQuery rayQuery, const RayDesc ray, out float hitT, out HitInfo hit) +{ + float3 rayDir = ray.Direction; + const float rayLength = length(rayDir); + const float invRayLength = 1.f / rayLength; + rayDir *= invRayLength; + + hit.type = InstanceType::Curve; + hit.instanceID = gScene.getCurveInstanceID(rayQuery.CandidateInstanceID(), rayQuery.CandidateGeometryIndex()); + hit.primitiveIndex = rayQuery.CandidatePrimitiveIndex(); + + uint v0Index = gScene.getFirstCurveVertexIndex(hit.instanceID, hit.primitiveIndex); + StaticCurveVertexData v0 = gScene.getCurveVertex(v0Index); + StaticCurveVertexData v1 = gScene.getCurveVertex(v0Index + 1); + + float2 result; + bool isect = intersectLinearSweptSphere(ray.Origin, rayDir, float4(v0.position, v0.radius), float4(v1.position, v1.radius), result); + result.x *= invRayLength; + + hitT = result.x; + hit.barycentrics = float2(result.y, 0.f); + return (isect && hitT >= rayQuery.RayTMin() && hitT < rayQuery.CommittedRayT()); +} diff --git a/Source/Falcor/Scene/Scene.cpp b/Source/Falcor/Scene/Scene.cpp index b37a025f27..5612b7e976 100644 --- a/Source/Falcor/Scene/Scene.cpp +++ b/Source/Falcor/Scene/Scene.cpp @@ -27,47 +27,68 @@ **************************************************************************/ #include "stdafx.h" #include "Scene.h" -#include "HitInfo.h" #include "Raytracing/RtProgram/RtProgram.h" #include "Raytracing/RtProgramVars.h" #include +#include namespace Falcor { + static_assert(sizeof(MeshDesc) % 16 == 0, "MeshDesc size should be a multiple of 16"); static_assert(sizeof(PackedStaticVertexData) % 16 == 0, "PackedStaticVertexData size should be a multiple of 16"); static_assert(sizeof(PackedMeshInstanceData) % 16 == 0, "PackedMeshInstanceData size should be a multiple of 16"); - static_assert(PackedMeshInstanceData::kMatrixBits + PackedMeshInstanceData::kMeshBits + PackedMeshInstanceData::kFlagsBits <= 32); + static_assert(sizeof(ProceduralPrimitiveData) % 16 == 0, "ProceduralPrimitiveData size should be a multiple of 16"); + static_assert(PackedMeshInstanceData::kMatrixBits + PackedMeshInstanceData::kMeshBits + PackedMeshInstanceData::kFlagsBits + PackedMeshInstanceData::kMaterialBits <= 64); namespace { - // Checks if the transform flips the coordinate system handedness (its determinant is negative). - bool doesTransformFlip(const glm::mat4& m) - { - return glm::determinant((glm::mat3)m) < 0.f; - } + // Large scenes are split into multiple BLAS groups in order to reduce build memory usage. + // The target is max 0.5GB intermediate memory per BLAS group. Note that this is not a strict limit. + const size_t kMaxBLASBuildMemory = 1ull << 29; const std::string kParameterBlockName = "gScene"; const std::string kMeshBufferName = "meshes"; const std::string kMeshInstanceBufferName = "meshInstances"; - const std::string kIndexBufferName = "indices"; + const std::string kIndexBufferName = "indexData"; const std::string kVertexBufferName = "vertices"; const std::string kPrevVertexBufferName = "prevVertices"; + const std::string kProceduralPrimBufferName = "proceduralPrimitives"; + const std::string kProceduralPrimAABBBufferName = "proceduralPrimitiveAABBs"; + const std::string kCurveBufferName = "curves"; + const std::string kCurveInstanceBufferName = "curveInstances"; + const std::string kCurveIndexBufferName = "curveIndices"; + const std::string kCurveVertexBufferName = "curveVertices"; + const std::string kCurvePrevVertexBufferName = "curvePrevVertices"; const std::string kMaterialsBufferName = "materials"; const std::string kLightsBufferName = "lights"; + const std::string kVolumesBufferName = "volumes"; + const std::string kStats = "stats"; + const std::string kBounds = "bounds"; + const std::string kAnimations = "animations"; + const std::string kLoopAnimations = "loopAnimations"; const std::string kCamera = "camera"; const std::string kCameras = "cameras"; const std::string kCameraSpeed = "cameraSpeed"; + const std::string kLights = "lights"; const std::string kAnimated = "animated"; const std::string kRenderSettings = "renderSettings"; const std::string kEnvMap = "envMap"; const std::string kMaterials = "materials"; + const std::string kVolumes = "volumes"; const std::string kGetLight = "getLight"; const std::string kGetMaterial = "getMaterial"; + const std::string kGetVolume = "getVolume"; const std::string kSetEnvMap = "setEnvMap"; const std::string kAddViewpoint = "addViewpoint"; const std::string kRemoveViewpoint = "kRemoveViewpoint"; const std::string kSelectViewpoint = "selectViewpoint"; + + // Checks if the transform flips the coordinate system handedness (its determinant is negative). + bool doesTransformFlip(const glm::mat4& m) + { + return glm::determinant((glm::mat3)m) < 0.f; + } } const FileDialogFilterVec& Scene::getFileExtensionFilters() @@ -94,9 +115,12 @@ namespace Falcor Shader::DefineList Scene::getSceneDefines() const { Shader::DefineList defines; - defines.add("MATERIAL_COUNT", std::to_string(mMaterials.size())); - defines.add("INDEXED_VERTICES", hasIndexBuffer() ? "1" : "0"); - defines.add(HitInfo::getDefines(this)); + defines.add("SCENE_MATERIAL_COUNT", std::to_string(mMaterials.size())); + defines.add("SCENE_GRID_COUNT", std::to_string(mGrids.size())); + defines.add("SCENE_HAS_INDEXED_VERTICES", hasIndexBuffer() ? "1" : "0"); + defines.add("SCENE_HAS_16BIT_INDICES", mHas16BitIndices ? "1" : "0"); + defines.add("SCENE_HAS_32BIT_INDICES", mHas32BitIndices ? "1" : "0"); + defines.add(mHitInfo.getDefines()); return defines; } @@ -106,33 +130,44 @@ namespace Falcor { mpLightCollection = LightCollection::create(pContext, shared_from_this()); mpLightCollection->setShaderData(mpSceneBlock["lightCollection"]); + + mSceneStats.emissiveMemoryInBytes = mpLightCollection->getMemoryUsageInBytes(); } return mpLightCollection; } - void Scene::render(RenderContext* pContext, GraphicsState* pState, GraphicsVars* pVars, RenderFlags flags) + void Scene::rasterize(RenderContext* pContext, GraphicsState* pState, GraphicsVars* pVars, RenderFlags flags) { - PROFILE("renderScene"); + PROFILE("rasterizeScene"); - pState->setVao(mpVao); pVars->setParameterBlock("gScene", mpSceneBlock); bool overrideRS = !is_set(flags, RenderFlags::UserRasterizerState); auto pCurrentRS = pState->getRasterizerState(); bool isIndexed = hasIndexBuffer(); - if (mDrawCounterClockwiseMeshes.count) + for (const auto& draw : mDrawArgs) { - if (overrideRS) pState->setRasterizerState(nullptr); - if (isIndexed) pContext->drawIndexedIndirect(pState, pVars, mDrawCounterClockwiseMeshes.count, mDrawCounterClockwiseMeshes.pBuffer.get(), 0, nullptr, 0); - else pContext->drawIndirect(pState, pVars, mDrawCounterClockwiseMeshes.count, mDrawCounterClockwiseMeshes.pBuffer.get(), 0, nullptr, 0); - } + assert(draw.count > 0); - if (mDrawClockwiseMeshes.count) - { - if (overrideRS) pState->setRasterizerState(mpFrontClockwiseRS); - if (isIndexed) pContext->drawIndexedIndirect(pState, pVars, mDrawClockwiseMeshes.count, mDrawClockwiseMeshes.pBuffer.get(), 0, nullptr, 0); - else pContext->drawIndirect(pState, pVars, mDrawClockwiseMeshes.count, mDrawClockwiseMeshes.pBuffer.get(), 0, nullptr, 0); + // Set state. + pState->setVao(draw.ibFormat == ResourceFormat::R16Uint ? mpVao16Bit : mpVao); + + if (overrideRS) + { + if (draw.ccw) pState->setRasterizerState(nullptr); + else pState->setRasterizerState(mpFrontClockwiseRS); + } + + // Draw the primitives. + if (isIndexed) + { + pContext->drawIndexedIndirect(pState, pVars, draw.count, draw.pBuffer.get(), 0, nullptr, 0); + } + else + { + pContext->drawIndirect(pState, pVars, draw.count, draw.pBuffer.get(), 0, nullptr, 0); + } } if (overrideRS) pState->setRasterizerState(pCurrentRS); @@ -170,32 +205,66 @@ namespace Falcor mpMeshInstancesBuffer = Buffer::createStructured(mpSceneBlock[kMeshInstanceBufferName], (uint32_t)mMeshInstanceData.size(), Resource::BindFlags::ShaderResource, Buffer::CpuAccess::None, nullptr, false); mpMeshInstancesBuffer->setName("Scene::mpMeshInstancesBuffer"); + if (!mProceduralPrimData.empty()) + { + // Create buffer to be used in BLAS creation. This is also bound to the scene for lookup in shaders + // Requires unordered access and will be in Non-Pixel Shader Resource state. + mpRtAABBBuffer = Buffer::createStructured(sizeof(D3D12_RAYTRACING_AABB), (uint32_t)mRtAABBRaw.size()); + mpRtAABBBuffer->setName("Scene::mpRtAABBBuffer"); + + mpProceduralPrimitivesBuffer = Buffer::createStructured(mpSceneBlock[kProceduralPrimBufferName], (uint32_t)mProceduralPrimData.size(), Resource::BindFlags::ShaderResource, Buffer::CpuAccess::None, nullptr, false); + mpProceduralPrimitivesBuffer->setName("Scene::mpProceduralPrimBuffer"); + } + + if (!mCurveDesc.empty()) + { + mpCurvesBuffer = Buffer::createStructured(mpSceneBlock[kCurveBufferName], (uint32_t)mCurveDesc.size(), Resource::BindFlags::ShaderResource, Buffer::CpuAccess::None, nullptr, false); + mpCurvesBuffer->setName("Scene::mpCurvesBuffer"); + mpCurveInstancesBuffer = Buffer::createStructured(mpSceneBlock[kCurveInstanceBufferName], (uint32_t)mCurveInstanceData.size(), Resource::BindFlags::ShaderResource, Buffer::CpuAccess::None, nullptr, false); + mpCurveInstancesBuffer->setName("Scene::mpCurveInstancesBuffer"); + } + mpMaterialsBuffer = Buffer::createStructured(mpSceneBlock[kMaterialsBufferName], (uint32_t)mMaterials.size(), Resource::BindFlags::ShaderResource, Buffer::CpuAccess::None, nullptr, false); mpMaterialsBuffer->setName("Scene::mpMaterialsBuffer"); - if (mLights.size()) + if (!mLights.empty()) { mpLightsBuffer = Buffer::createStructured(mpSceneBlock[kLightsBufferName], (uint32_t)mLights.size(), Resource::BindFlags::ShaderResource, Buffer::CpuAccess::None, nullptr, false); mpLightsBuffer->setName("Scene::mpLightsBuffer"); } + + if (!mVolumes.empty()) + { + mpVolumesBuffer = Buffer::createStructured(mpSceneBlock[kVolumesBufferName], (uint32_t)mVolumes.size(), Resource::BindFlags::ShaderResource, Buffer::CpuAccess::None, nullptr, false); + mpVolumesBuffer->setName("Scene::mpVolumesBuffer"); + } } void Scene::uploadResources() { + assert(mpAnimationController); + // Upload geometry mpMeshesBuffer->setBlob(mMeshDesc.data(), 0, sizeof(MeshDesc) * mMeshDesc.size()); + if (!mCurveDesc.empty()) mpCurvesBuffer->setBlob(mCurveDesc.data(), 0, sizeof(CurveDesc) * mCurveDesc.size()); mpSceneBlock->setBuffer(kMeshInstanceBufferName, mpMeshInstancesBuffer); mpSceneBlock->setBuffer(kMeshBufferName, mpMeshesBuffer); + mpSceneBlock->setBuffer(kProceduralPrimBufferName, mpProceduralPrimitivesBuffer); + mpSceneBlock->setBuffer(kProceduralPrimAABBBufferName, mpRtAABBBuffer); + mpSceneBlock->setBuffer(kCurveInstanceBufferName, mpCurveInstancesBuffer); + mpSceneBlock->setBuffer(kCurveBufferName, mpCurvesBuffer); mpSceneBlock->setBuffer(kLightsBufferName, mpLightsBuffer); + mpSceneBlock->setBuffer(kVolumesBufferName, mpVolumesBuffer); mpSceneBlock->setBuffer(kMaterialsBufferName, mpMaterialsBuffer); if (hasIndexBuffer()) mpSceneBlock->setBuffer(kIndexBufferName, mpVao->getIndexBuffer()); mpSceneBlock->setBuffer(kVertexBufferName, mpVao->getVertexBuffer(Scene::kStaticDataBufferIndex)); - mpSceneBlock->setBuffer(kPrevVertexBufferName, mpVao->getVertexBuffer(Scene::kPrevVertexBufferIndex)); + mpSceneBlock->setBuffer(kPrevVertexBufferName, mpAnimationController->getPrevVertexData()); // Can be nullptr - if (mpLightProbe) + if (mpCurveVao != nullptr) { - mpLightProbe->setShaderData(mpSceneBlock["lightProbe"]); + mpSceneBlock->setBuffer(kCurveIndexBufferName, mpCurveVao->getIndexBuffer()); + mpSceneBlock->setBuffer(kCurveVertexBufferName, mpCurveVao->getVertexBuffer(Scene::kStaticDataBufferIndex)); } } @@ -219,6 +288,7 @@ namespace Falcor set_texture(emissive); set_texture(normalMap); set_texture(occlusionMap); + set_texture(displacementMap); #undef set_texture var["samplerState"] = resources.samplerState; @@ -232,20 +302,30 @@ namespace Falcor void Scene::updateBounds() { const auto& globalMatrices = mpAnimationController->getGlobalMatrices(); - std::vector instanceBBs; - instanceBBs.reserve(mMeshInstanceData.size()); + mSceneBB = AABB(); for (const auto& inst : mMeshInstanceData) { - const BoundingBox& meshBB = mMeshBBs[inst.meshID]; + const AABB& meshBB = mMeshBBs[inst.meshID]; + const glm::mat4& transform = globalMatrices[inst.globalMatrixID]; + mSceneBB |= meshBB.transform(transform); + } + + for (const auto& aabb : mCustomPrimitiveAABBs) + { + mSceneBB |= aabb; + } + + for (const auto& inst : mCurveInstanceData) + { + const AABB& curveBB = mCurveBBs[inst.curveID]; const glm::mat4& transform = globalMatrices[inst.globalMatrixID]; - instanceBBs.push_back(meshBB.transform(transform)); + mSceneBB |= curveBB.transform(transform); } - mSceneBB = instanceBBs.front(); - for (const BoundingBox& bb : instanceBBs) + for (const auto& volume : mVolumes) { - mSceneBB = BoundingBox::fromUnion(mSceneBB, bb); + mSceneBB |= volume->getBounds(); } } @@ -257,10 +337,20 @@ namespace Falcor for (auto& inst : mMeshInstanceData) { uint32_t prevFlags = inst.flags; - inst.flags = (uint32_t)MeshInstanceFlags::None; const glm::mat4& transform = globalMatrices[inst.globalMatrixID]; - if (doesTransformFlip(transform)) inst.flags |= (uint32_t)MeshInstanceFlags::Flipped; + bool isTransformFlipped = doesTransformFlip(transform); + bool isObjectFrontFaceCW = getMesh(inst.meshID).isFrontFaceCW(); + bool isWorldFrontFaceCW = isObjectFrontFaceCW ^ isTransformFlipped; + + if (isTransformFlipped) inst.flags |= (uint32_t)MeshInstanceFlags::TransformFlipped; + else inst.flags &= ~(uint32_t)MeshInstanceFlags::TransformFlipped; + + if (isObjectFrontFaceCW) inst.flags |= (uint32_t)MeshInstanceFlags::IsObjectFrontFaceCW; + else inst.flags &= ~(uint32_t)MeshInstanceFlags::IsObjectFrontFaceCW; + + if (isWorldFrontFaceCW) inst.flags |= (uint32_t)MeshInstanceFlags::IsWorldFrontFaceCW; + else inst.flags &= ~(uint32_t)MeshInstanceFlags::IsWorldFrontFaceCW; dataChanged |= (inst.flags != prevFlags); } @@ -268,9 +358,23 @@ namespace Falcor if (forceUpdate || dataChanged) { // Make sure the scene data fits in the packed format. - // TODO: If we run into the limits, use bits from the materialID field. - if (globalMatrices.size() >= (1 << PackedMeshInstanceData::kMatrixBits)) throw std::exception("Number of transform matrices exceed the maximum"); - if (getMeshCount() >= (1 << PackedMeshInstanceData::kMeshBits)) throw std::exception("Number of meshes exceed the maximum"); + size_t maxMatrices = 1 << PackedMeshInstanceData::kMatrixBits; + if (globalMatrices.size() > maxMatrices) + { + throw std::exception(("Number of transform matrices (" + std::to_string(globalMatrices.size()) + ") exceeds the maximum (" + std::to_string(maxMatrices) + ").").c_str()); + } + + size_t maxMeshes = 1 << PackedMeshInstanceData::kMeshBits; + if (getMeshCount() > maxMeshes) + { + throw std::exception(("Number of meshes (" + std::to_string(getMeshCount()) + ") exceeds the maximum (" + std::to_string(maxMeshes) + ").").c_str()); + } + + size_t maxMaterials = 1 << PackedMeshInstanceData::kMaterialBits; + if (mMaterials.size() > maxMaterials) + { + throw std::exception(("Number of materials (" + std::to_string(mMaterials.size()) + ") exceeds the maximum (" + std::to_string(maxMaterials) + ").").c_str()); + } // Prepare packed mesh instance data. assert(mMeshInstanceData.size() > 0); @@ -287,12 +391,40 @@ namespace Falcor } } + void Scene::updateProceduralPrimitives(bool forceUpdate) + { + if (mProceduralPrimData.empty()) return; + + if (forceUpdate) + { + size_t bytes = sizeof(ProceduralPrimitiveData) * mProceduralPrimData.size(); + assert(mpProceduralPrimitivesBuffer && mpProceduralPrimitivesBuffer->getSize() == bytes); + mpProceduralPrimitivesBuffer->setBlob(mProceduralPrimData.data(), 0, bytes); + mpRtAABBBuffer->setBlob(mRtAABBRaw.data(), 0, sizeof(D3D12_RAYTRACING_AABB) * mRtAABBRaw.size()); + } + } + + void Scene::updateCurveInstances(bool forceUpdate) + { + if (mCurveInstanceData.empty()) return; + + if (forceUpdate) + { + mpCurveInstancesBuffer->setBlob(mCurveInstanceData.data(), 0, sizeof(CurveInstanceData) * mCurveInstanceData.size()); + } + } + void Scene::finalize() { - sortMeshes(); + assert(mHas16BitIndices || mHas32BitIndices); + mHitInfo.init(*this); initResources(); mpAnimationController->animate(gpDevice->getRenderContext(), 0); // Requires Scene block to exist updateMeshInstances(true); + + updateCurveInstances(true); + updateProceduralPrimitives(true); + updateBounds(); createDrawList(); if (mCameras.size() == 0) @@ -306,11 +438,14 @@ namespace Falcor uploadSelectedCamera(); addViewpoint(); updateLights(true); + updateVolumes(true); updateEnvMap(true); updateMaterials(true); uploadResources(); // Upload data after initialization is complete updateGeometryStats(); + updateMaterialStats(); updateLightStats(); + updateVolumeStats(); prepareUI(); } @@ -329,6 +464,16 @@ namespace Falcor { mCameraList.push_back({ camId, mCameras[camId]->getName() }); } + + // Construct a vector of indices that sort the materials by case-insensitive name. + mSortedMaterialIndices.resize(mMaterials.size()); + std::iota(mSortedMaterialIndices.begin(), mSortedMaterialIndices.end(), 0); + std::sort(mSortedMaterialIndices.begin(), mSortedMaterialIndices.end(), [this](uint32_t a, uint32_t b) { + const std::string& astr = mMaterials[a]->getName(); + const std::string& bstr = mMaterials[b]->getName(); + const auto r = std::mismatch(astr.begin(), astr.end(), bstr.begin(), bstr.end(), [](uint8_t l, uint8_t r) { return tolower(l) == tolower(r); }); + return r.second != bstr.end() && (r.first == astr.end() || tolower(*r.first) < tolower(*r.second)); + }); } void Scene::updateGeometryStats() @@ -353,21 +498,133 @@ namespace Falcor s.instancedVertexCount += mesh.vertexCount; s.instancedTriangleCount += mesh.getTriangleCount(); } + + s.uniqueCurvePointCount = 0; + s.uniqueCurveSegmentCount = 0; + s.instancedCurvePointCount = 0; + s.instancedCurveSegmentCount = 0; + + for (uint32_t curveID = 0; curveID < getCurveCount(); curveID++) + { + const auto& curve = getCurve(curveID); + s.uniqueCurvePointCount += curve.vertexCount; + s.uniqueCurveSegmentCount += curve.getSegmentCount(); + } + for (uint32_t instanceID = 0; instanceID < getCurveInstanceCount(); instanceID++) + { + const auto& instance = getCurveInstance(instanceID); + const auto& curve = getCurve(instance.curveID); + s.instancedCurvePointCount += curve.vertexCount; + s.instancedCurveSegmentCount += curve.getSegmentCount(); + } + + // Calculate memory usage. + const auto& pIB = mpVao->getIndexBuffer(); + const auto& pVB = mpVao->getVertexBuffer(kStaticDataBufferIndex); + const auto& pDrawID = mpVao->getVertexBuffer(kDrawIdBufferIndex); + + s.indexMemoryInBytes = 0; + s.vertexMemoryInBytes = 0; + s.geometryMemoryInBytes = 0; + s.animationMemoryInBytes = 0; + + s.indexMemoryInBytes += pIB ? pIB->getSize() : 0; + s.vertexMemoryInBytes += pVB ? pVB->getSize() : 0; + + s.curveIndexMemoryInBytes = 0; + s.curveVertexMemoryInBytes = 0; + + if (mpCurveVao != nullptr) + { + const auto& pCurveIB = mpCurveVao->getIndexBuffer(); + const auto& pCurveVB = mpCurveVao->getVertexBuffer(kStaticDataBufferIndex); + + s.curveIndexMemoryInBytes += pCurveIB ? pCurveIB->getSize() : 0; + s.curveVertexMemoryInBytes += pCurveVB ? pCurveVB->getSize() : 0; + } + + s.geometryMemoryInBytes += mpMeshesBuffer ? mpMeshesBuffer->getSize() : 0; + s.geometryMemoryInBytes += mpMeshInstancesBuffer ? mpMeshInstancesBuffer->getSize() : 0; + s.geometryMemoryInBytes += mpRtAABBBuffer ? mpRtAABBBuffer->getSize() : 0; + s.geometryMemoryInBytes += mpProceduralPrimitivesBuffer ? mpProceduralPrimitivesBuffer->getSize() : 0; + s.geometryMemoryInBytes += pDrawID ? pDrawID->getSize() : 0; + for (const auto& draw : mDrawArgs) + { + assert(draw.pBuffer); + s.geometryMemoryInBytes += draw.pBuffer->getSize(); + } + s.geometryMemoryInBytes += mpCurvesBuffer ? mpCurvesBuffer->getSize() : 0; + s.geometryMemoryInBytes += mpCurveInstancesBuffer ? mpCurveInstancesBuffer->getSize() : 0; + + s.animationMemoryInBytes += getAnimationController()->getMemoryUsageInBytes(); + } + + void Scene::updateMaterialStats() + { + auto& s = mSceneStats; + + std::set textures; + for (const auto& m : mMaterials) + { + for (uint32_t i = 0; i < (uint32_t)Material::TextureSlot::Count; i++) + { + const auto& t = m->getTexture((Material::TextureSlot)i); + if (t) textures.insert(t); + } + } + + s.materialCount = mMaterials.size(); + s.materialMemoryInBytes = mpMaterialsBuffer ? mpMaterialsBuffer->getSize() : 0; + s.textureCount = textures.size(); + s.textureCompressedCount = 0; + s.textureTexelCount = 0; + s.textureMemoryInBytes = 0; + + for (const auto& t : textures) + { + s.textureTexelCount += t->getTexelCount(); + s.textureMemoryInBytes += t->getTextureSizeInBytes(); + if (isCompressedFormat(t->getFormat())) s.textureCompressedCount++; + } } - void Scene::updateRaytracingStats() + void Scene::updateRaytracingBLASStats() { auto& s = mSceneStats; + s.blasGroupCount = mBlasGroups.size(); s.blasCount = mBlasData.size(); s.blasCompactedCount = 0; s.blasMemoryInBytes = 0; + s.blasScratchMemoryInBytes = 0; for (const auto& blas : mBlasData) { if (blas.useCompaction) s.blasCompactedCount++; s.blasMemoryInBytes += blas.blasByteSize; } + if (mpBlasScratch) s.blasScratchMemoryInBytes += mpBlasScratch->getSize(); + if (mpBlasStaticWorldMatrices) s.blasScratchMemoryInBytes += mpBlasStaticWorldMatrices->getSize(); + } + + void Scene::updateRaytracingTLASStats() + { + auto& s = mSceneStats; + + s.tlasCount = 0; + s.tlasMemoryInBytes = 0; + s.tlasScratchMemoryInBytes = 0; + + for (const auto& [i, tlas] : mTlasCache) + { + if (tlas.pTlas) + { + s.tlasMemoryInBytes += tlas.pTlas->getSize(); + s.tlasCount++; + } + if (tlas.pInstanceDescs) s.tlasScratchMemoryInBytes += tlas.pInstanceDescs->getSize(); + } + if (mpTlasScratch) s.tlasScratchMemoryInBytes += mpTlasScratch->getSize(); } void Scene::updateLightStats() @@ -406,6 +663,26 @@ namespace Falcor break; } } + + s.lightsMemoryInBytes = mpLightsBuffer ? mpLightsBuffer->getSize() : 0; + } + + void Scene::updateVolumeStats() + { + auto& s = mSceneStats; + + s.volumeCount = mVolumes.size(); + s.volumeMemoryInBytes = mpVolumesBuffer ? mpVolumesBuffer->getSize() : 0; + + s.gridCount = mGrids.size(); + s.gridVoxelCount = 0; + s.gridMemoryInBytes = 0; + + for (const auto& g : mGrids) + { + s.gridVoxelCount += g->getVoxelCount(); + s.gridMemoryInBytes += g->getGridSizeInBytes(); + } } bool Scene::updateAnimatable(Animatable& animatable, const AnimationController& controller, bool force) @@ -419,7 +696,7 @@ namespace Falcor if (force || (animatable.hasAnimation() && animatable.isAnimated())) { - if (!controller.didMatrixChanged(nodeID) && !force) return false; + if (!controller.isMatrixChanged(nodeID) && !force) return false; glm::mat4 transform = controller.getGlobalMatrices()[nodeID]; animatable.updateFromAnimation(transform); @@ -501,6 +778,56 @@ namespace Falcor return flags; } + Scene::UpdateFlags Scene::updateVolumes(bool forceUpdate) + { + Volume::UpdateFlags combinedUpdates = Volume::UpdateFlags::None; + + // Update animations and get combined updates. + for (const auto& volume : mVolumes) + { + updateAnimatable(*volume, *mpAnimationController, forceUpdate); + combinedUpdates |= volume->getUpdates(); + } + + // Early out if no volumes have changed. + if (!forceUpdate && combinedUpdates == Volume::UpdateFlags::None) return UpdateFlags::None; + + // Upload grids. + if (forceUpdate) + { + auto var = mpSceneBlock["grids"]; + for (size_t i = 0; i < mGrids.size(); ++i) + { + mGrids[i]->setShaderData(var[i]); + } + } + + // Upload volumes and clear updates. + uint32_t volumeIndex = 0; + for (const auto& volume : mVolumes) + { + if (forceUpdate || volume->getUpdates() != Volume::UpdateFlags::None) + { + auto data = volume->getData(); + data.densityGrid = volume->getDensityGrid() ? mGridIDs.at(volume->getDensityGrid()) : kInvalidGrid; + data.emissionGrid = volume->getEmissionGrid() ? mGridIDs.at(volume->getEmissionGrid()) : kInvalidGrid; + mpVolumesBuffer->setElement(volumeIndex, data); + } + volume->clearUpdates(); + volumeIndex++; + } + + mpSceneBlock["volumeCount"] = (uint32_t)mVolumes.size(); + + UpdateFlags flags = UpdateFlags::None; + if (is_set(combinedUpdates, Volume::UpdateFlags::TransformChanged)) flags |= UpdateFlags::VolumesMoved; + if (is_set(combinedUpdates, Volume::UpdateFlags::PropertiesChanged)) flags |= UpdateFlags::VolumePropertiesChanged; + if (is_set(combinedUpdates, Volume::UpdateFlags::GridsChanged)) flags |= UpdateFlags::VolumeGridsChanged; + if (is_set(combinedUpdates, Volume::UpdateFlags::BoundsChanged)) flags |= UpdateFlags::VolumeBoundsChanged; + + return flags; + } + Scene::UpdateFlags Scene::updateEnvMap(bool forceUpdate) { UpdateFlags flags = UpdateFlags::None; @@ -508,12 +835,19 @@ namespace Falcor if (mpEnvMap) { auto envMapChanges = mpEnvMap->beginFrame(); - if (envMapChanges != EnvMap::Changes::None || forceUpdate) + if (envMapChanges != EnvMap::Changes::None || mEnvMapChanged || forceUpdate) { - if (envMapChanges != EnvMap::Changes::None) flags |= UpdateFlags::EnvMapChanged; + if (envMapChanges != EnvMap::Changes::None) flags |= UpdateFlags::EnvMapPropertiesChanged; mpEnvMap->setShaderData(mpSceneBlock[kEnvMap]); } } + mSceneStats.envMapMemoryInBytes = mpEnvMap ? mpEnvMap->getMemoryUsageInBytes() : 0; + + if (mEnvMapChanged) + { + flags |= UpdateFlags::EnvMapChanged; + mEnvMapChanged = false; + } return flags; } @@ -537,6 +871,7 @@ namespace Falcor } } + updateMaterialStats(); Material::clearGlobalUpdates(); return flags; @@ -550,7 +885,7 @@ namespace Falcor mUpdates |= UpdateFlags::SceneGraphChanged; for (const auto& inst : mMeshInstanceData) { - if (mpAnimationController->didMatrixChanged(inst.globalMatrixID)) + if (mpAnimationController->isMatrixChanged(inst.globalMatrixID)) { mUpdates |= UpdateFlags::MeshesMoved; } @@ -559,6 +894,7 @@ namespace Falcor mUpdates |= updateSelectedCamera(false); mUpdates |= updateLights(false); + mUpdates |= updateVolumes(false); mUpdates |= updateEnvMap(false); mUpdates |= updateMaterials(false); pContext->flush(); @@ -576,7 +912,15 @@ namespace Falcor } // Update light collection - if (mpLightCollection && mpLightCollection->update(pContext)) mUpdates |= UpdateFlags::LightCollectionChanged; + if (mpLightCollection && mpLightCollection->update(pContext)) + { + mUpdates |= UpdateFlags::LightCollectionChanged; + mSceneStats.emissiveMemoryInBytes = mpLightCollection->getMemoryUsageInBytes(); + } + else if (!mpLightCollection) + { + mSceneStats.emissiveMemoryInBytes = 0; + } if (mRenderSettings != mPrevRenderSettings) { @@ -593,6 +937,11 @@ namespace Falcor { bool isEnabled = mpAnimationController->isEnabled(); if (widget.checkbox("Animate Scene", isEnabled)) mpAnimationController->setEnabled(isEnabled); + + if (auto animGroup = widget.group("Animations")) + { + mpAnimationController->renderUI(animGroup); + } } auto camera = mCameras[mSelectedCamera]; @@ -634,24 +983,32 @@ namespace Falcor camera->renderUI(cameraGroup); } - if (auto lightingGroup = widget.group("Render Settings")) + if (auto renderSettingsGroup = widget.group("Render Settings")) { - lightingGroup.checkbox("Use environment light", mRenderSettings.useEnvLight); - lightingGroup.tooltip("This enables using the environment map as a distant light source.", true); + renderSettingsGroup.checkbox("Use environment light", mRenderSettings.useEnvLight); + renderSettingsGroup.tooltip("This enables using the environment map as a distant light source.", true); - lightingGroup.checkbox("Use analytic lights", mRenderSettings.useAnalyticLights); - lightingGroup.tooltip("This enables using analytic lights.", true); + renderSettingsGroup.checkbox("Use analytic lights", mRenderSettings.useAnalyticLights); + renderSettingsGroup.tooltip("This enables using analytic lights.", true); - lightingGroup.checkbox("Emissive", mRenderSettings.useEmissiveLights); - lightingGroup.tooltip("This enables using emissive triangles as lights.", true); + renderSettingsGroup.checkbox("Use emissive", mRenderSettings.useEmissiveLights); + renderSettingsGroup.tooltip("This enables using emissive triangles as lights.", true); + + renderSettingsGroup.checkbox("Use volumes", mRenderSettings.useVolumes); + renderSettingsGroup.tooltip("This enables rendering of heterogeneous volumes.", true); } - if (mpEnvMap) + if (auto envMapGroup = widget.group("EnvMap")) { - if (auto envMapGroup = widget.group("EnvMap")) + if (envMapGroup.button("Load")) { - mpEnvMap->renderUI(envMapGroup); + std::string filename; + if (openFileDialog(Bitmap::getFileDialogFilters(ResourceFormat::RGBA32Float), filename)) loadEnvMap(filename); } + + if (mpEnvMap && envMapGroup.button("Clear", true)) setEnvMap(nullptr); + + if (mpEnvMap) mpEnvMap->renderUI(envMapGroup); } if (auto lightsGroup = widget.group("Lights")) @@ -670,61 +1027,120 @@ namespace Falcor if (auto materialsGroup = widget.group("Materials")) { - uint32_t materialID = 0; - for (auto& material : mMaterials) - { - auto name = std::to_string(materialID) + ": " + material->getName(); - if (auto materialGroup = materialsGroup.group(name)) + materialsGroup.checkbox("Sort by name", mSortMaterialsByName); + auto showMaterial = [&](uint32_t materialID, const std::string& label) { + auto material = mMaterials[materialID]; + if (auto materialGroup = materialsGroup.group(label)) { if (material->renderUI(materialGroup)) uploadMaterial(materialID); } - materialID++; + }; + if (mSortMaterialsByName) + { + for (uint32_t materialID : mSortedMaterialIndices) + { + auto label = mMaterials[materialID]->getName() + " (#" + std::to_string(materialID) + ")"; + showMaterial(materialID, label); + } + } + else + { + uint32_t materialID = 0; + for (auto& material : mMaterials) + { + auto label = std::to_string(materialID) + ": " + material->getName(); + showMaterial(materialID, label); + materialID++; + } + } + } + + if (auto volumesGroup = widget.group("Volumes")) + { + uint32_t volumeID = 0; + for (auto& volume : mVolumes) + { + auto name = std::to_string(volumeID) + ": " + volume->getName(); + if (auto volumeGroup = volumesGroup.group(name)) + { + volume->renderUI(volumeGroup); + } + volumeID++; } } if (auto statsGroup = widget.group("Statistics")) { + const auto& s = mSceneStats; + const double bytesPerTexel = s.textureTexelCount > 0 ? (double)s.textureMemoryInBytes / s.textureTexelCount : 0.0; + std::ostringstream oss; + oss << "Total scene memory: " << formatByteSize(s.getTotalMemory()) << std::endl + << std::endl; // Geometry stats. oss << "Geometry stats:" << std::endl << " Mesh count: " << getMeshCount() << std::endl << " Mesh instance count: " << getMeshInstanceCount() << std::endl << " Transform matrix count: " << getAnimationController()->getGlobalMatrices().size() << std::endl - << " Unique triangle count: " << mSceneStats.uniqueTriangleCount << std::endl - << " Unique vertex count: " << mSceneStats.uniqueVertexCount << std::endl - << " Instanced triangle count: " << mSceneStats.instancedTriangleCount << std::endl - << " Instanced vertex count: " << mSceneStats.instancedVertexCount << std::endl + << " Unique triangle count: " << s.uniqueTriangleCount << std::endl + << " Unique vertex count: " << s.uniqueVertexCount << std::endl + << " Instanced triangle count: " << s.instancedTriangleCount << std::endl + << " Instanced vertex count: " << s.instancedVertexCount << std::endl + << " Index buffer memory: " << formatByteSize(s.indexMemoryInBytes) << std::endl + << " Vertex buffer memory: " << formatByteSize(s.vertexMemoryInBytes) << std::endl + << " Geometry data memory: " << formatByteSize(s.geometryMemoryInBytes) << std::endl + << " Animation data memory: " << formatByteSize(s.animationMemoryInBytes) << std::endl + << " Curve count: " << getCurveCount() << std::endl + << " Curve instance count: " << getCurveInstanceCount() << std::endl + << " Unique curve segment count: " << s.uniqueCurveSegmentCount << std::endl + << " Unique curve point count: " << s.uniqueCurvePointCount << std::endl + << " Instanced curve segment count: " << s.instancedCurveSegmentCount << std::endl + << " Instanced curve point count: " << s.instancedCurvePointCount << std::endl + << " Curve index buffer memory: " << formatByteSize(s.curveIndexMemoryInBytes) << std::endl + << " Curve vertex buffer memory: " << formatByteSize(s.curveVertexMemoryInBytes) << std::endl << std::endl; // Raytracing stats. oss << "Raytracing stats:" << std::endl - << " BLAS count (total): " << mSceneStats.blasCount << std::endl - << " BLAS count (compacted): " << mSceneStats.blasCompactedCount << std::endl - << " BLAS memory (bytes): " << mSceneStats.blasMemoryInBytes << std::endl + << " BLAS groups: " << s.blasGroupCount << std::endl + << " BLAS count (total): " << s.blasCount << std::endl + << " BLAS count (compacted): " << s.blasCompactedCount << std::endl + << " BLAS memory (final): " << formatByteSize(s.blasMemoryInBytes) << std::endl + << " BLAS memory (scratch): " << formatByteSize(s.blasScratchMemoryInBytes) << std::endl + << " TLAS count: " << s.tlasCount << std::endl + << " TLAS memory (final): " << formatByteSize(s.tlasMemoryInBytes) << std::endl + << " TLAS memory (scratch): " << formatByteSize(s.tlasScratchMemoryInBytes) << std::endl << std::endl; // Material stats. oss << "Materials stats:" << std::endl - << " Material count: " << getMaterialCount() << std::endl + << " Material count: " << s.materialCount << std::endl + << " Material memory: " << formatByteSize(s.materialMemoryInBytes) << std::endl + << " Texture count (total): " << s.textureCount << std::endl + << " Texture count (compressed): " << s.textureCompressedCount << std::endl + << " Texture texel count: " << s.textureTexelCount << std::endl + << " Texture memory: " << formatByteSize(s.textureMemoryInBytes) << std::endl + << " Bytes/texel (average): " << std::fixed << std::setprecision(2) << bytesPerTexel << std::endl << std::endl; // Analytic light stats. oss << "Analytic light stats:" << std::endl - << " Active light count: " << mSceneStats.activeLightCount << std::endl - << " Total light count: " << mSceneStats.totalLightCount << std::endl - << " Point light count: " << mSceneStats.pointLightCount << std::endl - << " Directional light count: " << mSceneStats.directionalLightCount << std::endl - << " Rect light count: " << mSceneStats.rectLightCount << std::endl - << " Sphere light count: " << mSceneStats.sphereLightCount << std::endl - << " Distant light count: " << mSceneStats.distantLightCount << std::endl + << " Active light count: " << s.activeLightCount << std::endl + << " Total light count: " << s.totalLightCount << std::endl + << " Point light count: " << s.pointLightCount << std::endl + << " Directional light count: " << s.directionalLightCount << std::endl + << " Rect light count: " << s.rectLightCount << std::endl + << " Sphere light count: " << s.sphereLightCount << std::endl + << " Distant light count: " << s.distantLightCount << std::endl + << " Analytic lights memory: " << formatByteSize(s.lightsMemoryInBytes) << std::endl << std::endl; // Emissive light stats. oss << "Emissive light stats:" << std::endl; if (mpLightCollection) { - auto stats = mpLightCollection->getStats(); + const auto& stats = mpLightCollection->getStats(); oss << " Active triangle count: " << stats.trianglesActive << std::endl << " Active uniform triangle count: " << stats.trianglesActiveUniform << std::endl << " Active textured triangle count: " << stats.trianglesActiveTextured << std::endl @@ -733,7 +1149,8 @@ namespace Falcor << " Textured mesh count: " << stats.meshesTextured << std::endl << " Total triangle count: " << stats.triangleCount << std::endl << " Texture triangle count: " << stats.trianglesTextured << std::endl - << " Culled triangle count: " << stats.trianglesCulled << std::endl; + << " Culled triangle count: " << stats.trianglesCulled << std::endl + << " Emissive lights memory: " << formatByteSize(s.emissiveMemoryInBytes) << std::endl; } else { @@ -745,8 +1162,9 @@ namespace Falcor oss << "Environment map:" << std::endl; if (mpEnvMap) { - oss << " Filename: " << mpEnvMap->getFilename() << std::endl; - oss << " Resolution: " << mpEnvMap->getEnvMap()->getWidth() << "x" << mpEnvMap->getEnvMap()->getHeight() << std::endl; + oss << " Filename: " << mpEnvMap->getFilename() << std::endl + << " Resolution: " << mpEnvMap->getEnvMap()->getWidth() << "x" << mpEnvMap->getEnvMap()->getHeight() << std::endl + << " Texture memory: " << formatByteSize(s.envMapMemoryInBytes) << std::endl; } else { @@ -754,6 +1172,21 @@ namespace Falcor } oss << std::endl; + // Volumes stats. + oss << "Volume stats:" << std::endl + << " Volume count: " << s.volumeCount << std::endl + << " Volume memory: " << formatByteSize(s.volumeMemoryInBytes) << std::endl + << std::endl; + + // Grid stats. + oss << "Grid stats:" << std::endl + << " Grid count: " << s.gridCount << std::endl + << " Grid voxel count: " << s.gridVoxelCount << std::endl + << " Grid memory: " << formatByteSize(s.gridMemoryInBytes) << std::endl + << std::endl; + + if (statsGroup.button("Print to log")) logInfo("\n" + oss.str()); + statsGroup.text(oss.str()); } @@ -781,19 +1214,22 @@ namespace Falcor return mRenderSettings.useEmissiveLights && mpLightCollection != nullptr && mpLightCollection->getActiveLightCount() > 0; } + bool Scene::useVolumes() const + { + return mRenderSettings.useVolumes && mVolumes.empty() == false; + } + void Scene::setCamera(const Camera::SharedPtr& pCamera) { - auto name = pCamera->getName(); - for (uint index = 0; index < mCameras.size(); index++) + auto it = std::find(mCameras.begin(), mCameras.end(), pCamera); + if (it != mCameras.end()) { - if (mCameras[index]->getName() == name) - { - selectCamera(index); - return; - } + selectCamera((uint32_t)std::distance(mCameras.begin(), it)); + } + else if (pCamera) + { + logWarning("Selected camera " + pCamera->getName() + " does not exist."); } - logWarning("Selected camera " + name + " does not exist."); - pybind11::print("Selected camera", name, "does not exist."); } void Scene::selectCamera(uint32_t index) @@ -802,22 +1238,20 @@ namespace Falcor if (index >= mCameras.size()) { logWarning("Selected camera index " + std::to_string(index) + " is invalid."); - pybind11::print("Selected camera index", index, "is invalid."); return; } mSelectedCamera = index; mCameraSwitched = true; setCameraController(mCamCtrlType); - updateSelectedCamera(false); } void Scene::resetCamera(bool resetDepthRange) { auto camera = getCamera(); - float radius = length(mSceneBB.extent); - camera->setPosition(mSceneBB.center); - camera->setTarget(mSceneBB.center + float3(0, 0, -1)); + float radius = mSceneBB.radius(); + camera->setPosition(mSceneBB.center()); + camera->setTarget(mSceneBB.center() + float3(0, 0, -1)); camera->setUpVector(float3(0, 1, 0)); if (resetDepthRange) @@ -885,6 +1319,16 @@ namespace Falcor return nullptr; } + Volume::SharedPtr Scene::getVolumeByName(const std::string& name) const + { + for (const auto& v : mVolumes) + { + if (v->getName() == name) return v; + } + + return nullptr; + } + Light::SharedPtr Scene::getLightByName(const std::string& name) const { for (const auto& l : mLights) @@ -910,54 +1354,58 @@ namespace Falcor void Scene::createDrawList() { + assert(mDrawArgs.empty()); auto pMatricesBuffer = mpSceneBlock->getBuffer("worldMatrices"); const glm::mat4* matrices = (glm::mat4*)pMatricesBuffer->map(Buffer::MapType::Read); // #SCENEV2 This will cause the pipeline to flush and sync, but it's probably not too bad as this only happens once - auto createBuffers = [&](const auto& drawClockwiseMeshes, const auto& drawCounterClockwiseMeshes) + // Helper to create the draw-indirect buffer. + auto createDrawBuffer = [this](const auto& drawMeshes, bool ccw, ResourceFormat ibFormat = ResourceFormat::Unknown) { - // Create the draw-indirect buffer - if (drawCounterClockwiseMeshes.size()) + if (drawMeshes.size() > 0) { - mDrawCounterClockwiseMeshes.pBuffer = Buffer::create(sizeof(drawCounterClockwiseMeshes[0]) * drawCounterClockwiseMeshes.size(), Resource::BindFlags::IndirectArg, Buffer::CpuAccess::None, drawCounterClockwiseMeshes.data()); - mDrawCounterClockwiseMeshes.pBuffer->setName("Scene::mDrawCounterClockwiseMeshes::pBuffer"); - mDrawCounterClockwiseMeshes.count = (uint32_t)drawCounterClockwiseMeshes.size(); + DrawArgs draw; + draw.pBuffer = Buffer::create(sizeof(drawMeshes[0]) * drawMeshes.size(), Resource::BindFlags::IndirectArg, Buffer::CpuAccess::None, drawMeshes.data()); + draw.pBuffer->setName("Scene draw buffer"); + assert(drawMeshes.size() <= std::numeric_limits::max()); + draw.count = (uint32_t)drawMeshes.size(); + draw.ccw = ccw; + draw.ibFormat = ibFormat; + mDrawArgs.push_back(draw); } - - if (drawClockwiseMeshes.size()) - { - mDrawClockwiseMeshes.pBuffer = Buffer::create(sizeof(drawClockwiseMeshes[0]) * drawClockwiseMeshes.size(), Resource::BindFlags::IndirectArg, Buffer::CpuAccess::None, drawClockwiseMeshes.data()); - mDrawClockwiseMeshes.pBuffer->setName("Scene::mDrawClockwiseMeshes::pBuffer"); - mDrawClockwiseMeshes.count = (uint32_t)drawClockwiseMeshes.size(); - } - - size_t drawCount = drawClockwiseMeshes.size() + drawCounterClockwiseMeshes.size(); - assert(drawCount <= std::numeric_limits::max()); }; if (hasIndexBuffer()) { - std::vector drawClockwiseMeshes, drawCounterClockwiseMeshes; + std::vector drawClockwiseMeshes[2], drawCounterClockwiseMeshes[2]; + uint32_t instanceID = 0; for (const auto& instance : mMeshInstanceData) { const auto& mesh = mMeshDesc[instance.meshID]; const auto& transform = matrices[instance.globalMatrixID]; + bool use16Bit = mesh.use16BitIndices(); D3D12_DRAW_INDEXED_ARGUMENTS draw; draw.IndexCountPerInstance = mesh.indexCount; draw.InstanceCount = 1; - draw.StartIndexLocation = mesh.ibOffset; + draw.StartIndexLocation = mesh.ibOffset * (use16Bit ? 2 : 1); draw.BaseVertexLocation = mesh.vbOffset; - draw.StartInstanceLocation = (uint32_t)(drawClockwiseMeshes.size() + drawCounterClockwiseMeshes.size()); + draw.StartInstanceLocation = instanceID++; - (doesTransformFlip(transform)) ? drawClockwiseMeshes.push_back(draw) : drawCounterClockwiseMeshes.push_back(draw); + int i = use16Bit ? 0 : 1; + (doesTransformFlip(transform)) ? drawClockwiseMeshes[i].push_back(draw) : drawCounterClockwiseMeshes[i].push_back(draw); } - createBuffers(drawClockwiseMeshes, drawCounterClockwiseMeshes); + + createDrawBuffer(drawClockwiseMeshes[0], false, ResourceFormat::R16Uint); + createDrawBuffer(drawClockwiseMeshes[1], false, ResourceFormat::R32Uint); + createDrawBuffer(drawCounterClockwiseMeshes[0], true, ResourceFormat::R16Uint); + createDrawBuffer(drawCounterClockwiseMeshes[1], true, ResourceFormat::R32Uint); } else { std::vector drawClockwiseMeshes, drawCounterClockwiseMeshes; + uint32_t instanceID = 0; for (const auto& instance : mMeshInstanceData) { const auto& mesh = mMeshDesc[instance.meshID]; @@ -968,126 +1416,88 @@ namespace Falcor draw.VertexCountPerInstance = mesh.vertexCount; draw.InstanceCount = 1; draw.StartVertexLocation = mesh.vbOffset; - draw.StartInstanceLocation = (uint32_t)(drawClockwiseMeshes.size() + drawCounterClockwiseMeshes.size()); + draw.StartInstanceLocation = instanceID++; (doesTransformFlip(transform)) ? drawClockwiseMeshes.push_back(draw) : drawCounterClockwiseMeshes.push_back(draw); } - createBuffers(drawClockwiseMeshes, drawCounterClockwiseMeshes); - } - } - - void Scene::sortMeshes() - { - // We first sort meshes into groups with the same transform. - // The mesh instances list is then reordered to match this order. - // - // For ray tracing, we create one BLAS per mesh group and the mesh instances - // can therefore be directly indexed by [InstanceID() + GeometryIndex()]. - // This avoids the need to have a lookup table from hit IDs to mesh instance. - - // Build a list of mesh instance indices per mesh. - std::vector> instanceLists(mMeshDesc.size()); - for (size_t i = 0; i < mMeshInstanceData.size(); i++) - { - assert(mMeshInstanceData[i].meshID < instanceLists.size()); - instanceLists[mMeshInstanceData[i].meshID].push_back(i); - } - - // The non-instanced meshes are grouped based on what global matrix ID their transform is. - std::unordered_map> nodeToMeshList; - for (uint32_t meshId = 0; meshId < (uint32_t)instanceLists.size(); meshId++) - { - const auto& instanceList = instanceLists[meshId]; - if (instanceList.size() > 1) continue; // Only processing non-instanced meshes here - - assert(instanceList.size() == 1); - uint32_t globalMatrixId = mMeshInstanceData[instanceList[0]].globalMatrixID; - nodeToMeshList[globalMatrixId].push_back(meshId); - } - // Build final result. Format is a list of Mesh ID's per mesh group. - - // This should currently only be run on scene initialization. - assert(mMeshGroups.empty()); - - // Non-instanced meshes were sorted above so just copy each list. - for (const auto& it : nodeToMeshList) mMeshGroups.push_back({ it.second }); - - // Meshes that have multiple instances go in their own groups. - for (uint32_t meshId = 0; meshId < (uint32_t)instanceLists.size(); meshId++) - { - const auto& instanceList = instanceLists[meshId]; - if (instanceList.size() == 1) continue; // Only processing instanced meshes here - mMeshGroups.push_back({ std::vector({ meshId }) }); - } - - // Calculate mapping from new mesh instance ID to existing instance index. - // Here, just append existing instance ID's in order they appear in the mesh groups. - std::vector instanceMapping; - for (const auto& meshGroup : mMeshGroups) - { - for (const uint32_t meshId : meshGroup.meshList) - { - const auto& instanceList = instanceLists[meshId]; - for (size_t idx : instanceList) - { - instanceMapping.push_back(idx); - } - } - } - assert(instanceMapping.size() == mMeshInstanceData.size()); - { - // Check that all indices exist - std::set instanceIndices(instanceMapping.begin(), instanceMapping.end()); - assert(instanceIndices.size() == mMeshInstanceData.size()); - } - - // Now reorder mMeshInstanceData based on the new mapping. - // We'll make a copy of the existing data first, and the populate the array. - std::vector prevInstanceData = mMeshInstanceData; - for (size_t i = 0; i < mMeshInstanceData.size(); i++) - { - assert(instanceMapping[i] < prevInstanceData.size()); - mMeshInstanceData[i] = prevInstanceData[instanceMapping[i]]; - } - - // Create mapping of meshes to their instances. - mMeshIdToInstanceIds.clear(); - mMeshIdToInstanceIds.resize(mMeshDesc.size()); - for (uint32_t instId = 0; instId < (uint32_t)mMeshInstanceData.size(); instId++) - { - mMeshIdToInstanceIds[mMeshInstanceData[instId].meshID].push_back(instId); + createDrawBuffer(drawClockwiseMeshes, false); + createDrawBuffer(drawCounterClockwiseMeshes, true); } } - void Scene::initGeomDesc() + void Scene::initGeomDesc(RenderContext* pContext) { assert(mBlasData.empty()); + assert(!mpBlasStaticWorldMatrices); const VertexBufferLayout::SharedConstPtr& pVbLayout = mpVao->getVertexLayout()->getBufferLayout(kStaticDataBufferIndex); const Buffer::SharedPtr& pVb = mpVao->getVertexBuffer(kStaticDataBufferIndex); const Buffer::SharedPtr& pIb = mpVao->getIndexBuffer(); + const auto& globalMatrices = mpAnimationController->getGlobalMatrices(); + + auto getStaticMatricesBuffer = [&]() + { + // If scene has static meshes we let the BLAS build transform them to world space if needed. + // For this we need the GPU address of the transform matrix of each mesh in row-major format. + // Since glm uses column-major format we create a buffer with the transposed matrices. + // Note that this is sufficient to do once only as the transforms for static meshes can't change. + // TODO: Use AnimationController's matrix buffer directly when we've switched to a row-major matrix library. + if (!mpBlasStaticWorldMatrices) + { + std::vector transposedMatrices; + transposedMatrices.reserve(globalMatrices.size()); + for (const auto& m : globalMatrices) transposedMatrices.push_back(glm::transpose(m)); + + uint32_t float4Count = (uint32_t)transposedMatrices.size() * 4; + mpBlasStaticWorldMatrices = Buffer::createStructured(sizeof(float4), float4Count, Resource::BindFlags::ShaderResource, Buffer::CpuAccess::None, transposedMatrices.data(), false); + mpBlasStaticWorldMatrices->setName("Scene::mpBlasStaticWorldMatrices"); + + // Transition the resource to non-pixel shader state as expected by DXR. + pContext->resourceBarrier(mpBlasStaticWorldMatrices.get(), Resource::State::NonPixelShader); + } + return mpBlasStaticWorldMatrices; + }; assert(mMeshGroups.size() > 0); - mBlasData.resize(mMeshGroups.size()); + uint32_t totalBlasCount = (uint32_t)mMeshGroups.size() + (mpRtAABBBuffer ? 1 : 0); // If there are custom primitives, they are all placed in one more BLAS + mBlasData.resize(totalBlasCount); mRebuildBlas = true; mHasSkinnedMesh = false; - for (size_t i = 0; i < mBlasData.size(); i++) + for (size_t i = 0; i < mMeshGroups.size(); i++) { const auto& meshList = mMeshGroups[i].meshList; + const bool isStatic = mMeshGroups[i].isStatic; auto& blas = mBlasData[i]; auto& geomDescs = blas.geomDescs; geomDescs.resize(meshList.size()); for (size_t j = 0; j < meshList.size(); j++) { - const MeshDesc& mesh = mMeshDesc[meshList[j]]; - blas.hasSkinnedMesh |= mMeshHasDynamicData[meshList[j]]; + const uint32_t meshID = meshList[j]; + const MeshDesc& mesh = mMeshDesc[meshID]; + blas.hasSkinnedMesh |= mMeshHasDynamicData[meshID]; D3D12_RAYTRACING_GEOMETRY_DESC& desc = geomDescs[j]; desc.Type = D3D12_RAYTRACING_GEOMETRY_TYPE_TRIANGLES; - desc.Triangles.Transform3x4 = 0; + desc.Triangles.Transform3x4 = 0; // The default is no transform + + if (isStatic) + { + // Static meshes will be pre-transformed when building the BLAS. + // Lookup the matrix ID here. If it is an identity matrix, no action is needed. + assert(mMeshIdToInstanceIds[meshID].size() == 1); + uint32_t instanceID = mMeshIdToInstanceIds[meshID][0]; + assert(instanceID < mMeshInstanceData.size()); + uint32_t matrixID = mMeshInstanceData[instanceID].globalMatrixID; + + if (globalMatrices[matrixID] != glm::identity()) + { + // Get the GPU address of the transform in row-major format. + desc.Triangles.Transform3x4 = getStaticMatricesBuffer()->getGpuAddress() + matrixID * 64ull; + } + } // If this is an opaque mesh, set the opaque flag const auto& material = mMaterials[mesh.materialID]; @@ -1103,9 +1513,12 @@ namespace Falcor // Set index data if (pIb) { - desc.Triangles.IndexBuffer = pIb->getGpuAddress() + (mesh.ibOffset * getFormatBytesPerBlock(mpVao->getIndexBufferFormat())); + // The global index data is stored in a dword array. + // Each mesh specifies whether its indices are in 16-bit or 32-bit format. + ResourceFormat ibFormat = mesh.use16BitIndices() ? ResourceFormat::R16Uint : ResourceFormat::R32Uint; + desc.Triangles.IndexBuffer = pIb->getGpuAddress() + mesh.ibOffset * sizeof(uint32_t); desc.Triangles.IndexCount = mesh.indexCount; - desc.Triangles.IndexFormat = getDxgiFormat(mpVao->getIndexBufferFormat()); + desc.Triangles.IndexFormat = getDxgiFormat(ibFormat); } else { @@ -1117,9 +1530,174 @@ namespace Falcor } mHasSkinnedMesh |= blas.hasSkinnedMesh; + assert(!(isStatic && mHasSkinnedMesh)); + } + + if (mpRtAABBBuffer) + { + auto& blas = mBlasData.back(); + blas.geomDescs.resize(mCustomPrimitiveAABBs.size() + mCurveDesc.size()); + + uint64_t bbAddressOffset = 0; + uint32_t geomIndexOffset = 0; + for (const auto& aabb : mCustomPrimitiveAABBs) + { + D3D12_RAYTRACING_GEOMETRY_DESC& desc = blas.geomDescs[geomIndexOffset++]; + desc.Type = D3D12_RAYTRACING_GEOMETRY_TYPE_PROCEDURAL_PRIMITIVE_AABBS; + desc.Flags = D3D12_RAYTRACING_GEOMETRY_FLAG_NONE; + + desc.AABBs.AABBCount = 1; // Currently only one AABB per user-defined prim supported + desc.AABBs.AABBs.StartAddress = mpRtAABBBuffer->getGpuAddress() + bbAddressOffset; + desc.AABBs.AABBs.StrideInBytes = sizeof(D3D12_RAYTRACING_AABB); + + bbAddressOffset += sizeof(D3D12_RAYTRACING_AABB); + } + + for (const auto& curve : mCurveDesc) + { + // One geometry desc per curve. + D3D12_RAYTRACING_GEOMETRY_DESC& desc = blas.geomDescs[geomIndexOffset++]; + + desc.Type = D3D12_RAYTRACING_GEOMETRY_TYPE_PROCEDURAL_PRIMITIVE_AABBS; + + // Curves are transparent or not depends on whether we use anyhit shaders for back-face culling. +#if CURVE_BACKFACE_CULLING_USING_ANYHIT + desc.Flags = D3D12_RAYTRACING_GEOMETRY_FLAG_NONE; +#else + desc.Flags = D3D12_RAYTRACING_GEOMETRY_FLAG_OPAQUE; +#endif + + desc.AABBs.AABBCount = curve.indexCount; + desc.AABBs.AABBs.StartAddress = mpRtAABBBuffer->getGpuAddress() + bbAddressOffset; + desc.AABBs.AABBs.StrideInBytes = sizeof(D3D12_RAYTRACING_AABB); + + bbAddressOffset += sizeof(D3D12_RAYTRACING_AABB) * curve.indexCount; + } + } + } + + void Scene::preparePrebuildInfo(RenderContext* pContext) + { + for (auto& blas : mBlasData) + { + // Determine how BLAS build/update should be done. + // The default choice is to compact all static BLASes and those that don't need to be rebuilt every frame. + // For all other BLASes, compaction just adds overhead. + // TODO: Add compaction on/off switch for profiling. + // TODO: Disable compaction for skinned meshes if update performance becomes a problem. + blas.updateMode = mBlasUpdateMode; + blas.useCompaction = !blas.hasSkinnedMesh || blas.updateMode != UpdateMode::Rebuild; + + // Setup build parameters. + D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_INPUTS& inputs = blas.buildInputs; + inputs.Type = D3D12_RAYTRACING_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL; + inputs.DescsLayout = D3D12_ELEMENTS_LAYOUT_ARRAY; + inputs.NumDescs = (uint32_t)blas.geomDescs.size(); + inputs.pGeometryDescs = blas.geomDescs.data(); + inputs.Flags = D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_NONE; + + // Add necessary flags depending on settings. + if (blas.useCompaction) + { + inputs.Flags |= D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_ALLOW_COMPACTION; + } + if (blas.hasSkinnedMesh && blas.updateMode == UpdateMode::Refit) + { + inputs.Flags |= D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_ALLOW_UPDATE; + } + + // Set optional performance hints. + // TODO: Set FAST_BUILD for skinned meshes if update/rebuild performance becomes a problem. + // TODO: Add FAST_TRACE on/off switch for profiling. It is disabled by default as it is scene-dependent. + //if (!blas.hasSkinnedMesh) + //{ + // inputs.Flags |= D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_PREFER_FAST_TRACE; + //} + + // Get prebuild info. + GET_COM_INTERFACE(gpDevice->getApiHandle(), ID3D12Device5, pDevice5); + pDevice5->GetRaytracingAccelerationStructurePrebuildInfo(&inputs, &blas.prebuildInfo); + + // Figure out the padded allocation sizes to have proper alignment. + assert(blas.prebuildInfo.ResultDataMaxSizeInBytes > 0); + blas.resultByteSize = align_to(D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BYTE_ALIGNMENT, blas.prebuildInfo.ResultDataMaxSizeInBytes); + + uint64_t scratchByteSize = std::max(blas.prebuildInfo.ScratchDataSizeInBytes, blas.prebuildInfo.UpdateScratchDataSizeInBytes); + blas.scratchByteSize = align_to(D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BYTE_ALIGNMENT, scratchByteSize); } } + void Scene::computeBlasGroups() + { + mBlasGroups.clear(); + uint64_t groupSize = 0; + + for (uint32_t blasId = 0; blasId < mBlasData.size(); blasId++) + { + auto& blas = mBlasData[blasId]; + size_t blasSize = blas.resultByteSize + blas.scratchByteSize; + + // Start new BLAS group on first iteration or if group size would exceed the target. + if (groupSize == 0 || groupSize + blasSize > kMaxBLASBuildMemory) + { + mBlasGroups.push_back({}); + groupSize = 0; + } + + // Add BLAS to current group. + assert(mBlasGroups.size() > 0); + auto& group = mBlasGroups.back(); + group.blasIndices.push_back(blasId); + blas.blasGroupIndex = (uint32_t)mBlasGroups.size() - 1; + + // Update data offsets and sizes. + blas.resultByteOffset = group.resultByteSize; + blas.scratchByteOffset = group.scratchByteSize; + group.resultByteSize += blas.resultByteSize; + group.scratchByteSize += blas.scratchByteSize; + + groupSize += blasSize; + } + + // Validation that all offsets and sizes are correct. + uint64_t totalResultSize = 0; + uint64_t totalScratchSize = 0; + std::set blasIDs; + + for (size_t blasGroupIndex = 0; blasGroupIndex < mBlasGroups.size(); blasGroupIndex++) + { + uint64_t resultSize = 0; + uint64_t scratchSize = 0; + + const auto& group = mBlasGroups[blasGroupIndex]; + assert(!group.blasIndices.empty()); + + for (auto blasId : group.blasIndices) + { + assert(blasId < mBlasData.size()); + const auto& blas = mBlasData[blasId]; + + assert(blasIDs.insert(blasId).second); + assert(blas.blasGroupIndex == blasGroupIndex); + + assert(blas.resultByteSize > 0); + assert(blas.resultByteOffset == resultSize); + resultSize += blas.resultByteSize; + + assert(blas.scratchByteSize > 0); + assert(blas.scratchByteOffset == scratchSize); + scratchSize += blas.scratchByteSize; + + assert(blas.blasByteOffset == 0); + assert(blas.blasByteSize == 0); + } + + assert(resultSize == group.resultByteSize); + assert(scratchSize == group.scratchByteSize); + } + assert(blasIDs.size() == mBlasData.size()); + } + void Scene::buildBlas(RenderContext* pContext) { PROFILE("buildBlas"); @@ -1129,250 +1707,281 @@ namespace Falcor const Buffer::SharedPtr& pIb = mpVao->getIndexBuffer(); pContext->resourceBarrier(pVb.get(), Resource::State::NonPixelShader); if (pIb) pContext->resourceBarrier(pIb.get(), Resource::State::NonPixelShader); + if (mpRtAABBBuffer) pContext->resourceBarrier(mpRtAABBBuffer.get(), Resource::State::NonPixelShader); + + if (mpCurveVao) + { + const Buffer::SharedPtr& pCurveVb = mpCurveVao->getVertexBuffer(kStaticDataBufferIndex); + const Buffer::SharedPtr& pCurveIb = mpCurveVao->getIndexBuffer(); + pContext->resourceBarrier(pCurveVb.get(), Resource::State::NonPixelShader); + pContext->resourceBarrier(pCurveIb.get(), Resource::State::NonPixelShader); + } // On the first time, or if a full rebuild is necessary we will: // - Update all build inputs and prebuild info + // - Compute BLAS groups // - Calculate total intermediate buffer sizes // - Build all BLASes into an intermediate buffer // - Calculate total compacted buffer size // - Compact/clone all BLASes to their final location + if (mRebuildBlas) { - uint64_t totalMaxBlasSize = 0; - uint64_t totalScratchSize = 0; + logInfo("Initiating BLAS build for " + std::to_string(mBlasData.size()) + " mesh groups"); - for (auto& blas : mBlasData) - { - // Determine how BLAS build/update should be done. - // The default choice is to compact all static BLASes and those that don't need to be rebuilt every frame. For those compaction just adds overhead. - // TODO: Add compaction on/off switch for profiling. - // TODO: Disable compaction for skinned meshes if update performance becomes a problem. - blas.updateMode = mBlasUpdateMode; - blas.useCompaction = !blas.hasSkinnedMesh || blas.updateMode != UpdateMode::Rebuild; - - // Setup build parameters. - D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_INPUTS& inputs = blas.buildInputs; - inputs.Type = D3D12_RAYTRACING_ACCELERATION_STRUCTURE_TYPE_BOTTOM_LEVEL; - inputs.DescsLayout = D3D12_ELEMENTS_LAYOUT_ARRAY; - inputs.NumDescs = (uint32_t)blas.geomDescs.size(); - inputs.pGeometryDescs = blas.geomDescs.data(); - inputs.Flags = D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_NONE; - - // Add necessary flags depending on settings. - if (blas.useCompaction) - { - inputs.Flags |= D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_ALLOW_COMPACTION; - } - if (blas.hasSkinnedMesh && blas.updateMode == UpdateMode::Refit) - { - inputs.Flags |= D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_ALLOW_UPDATE; - } + // Compute pre-build info per BLAS and organize the BLASes into groups + // in order to limit GPU memory usage during BLAS build. + preparePrebuildInfo(pContext); + computeBlasGroups(); + + logInfo("BLAS build split into " + std::to_string(mBlasGroups.size()) + " groups"); + + // Compute the required maximum size of the result and scratch buffers. + uint64_t resultByteSize = 0; + uint64_t scratchByteSize = 0; + size_t maxBlasCount = 0; - // Set optional performance hints. - // TODO: Set FAST_BUILD for skinned meshes if update/rebuild performance becomes a problem. - // TODO: Add FAST_TRACE on/off switch for profiling. It is disabled by default as it is scene-dependent. - //if (!blas.hasSkinnedMesh) - //{ - // inputs.Flags |= D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_PREFER_FAST_TRACE; - //} - - // Get prebuild info. - GET_COM_INTERFACE(gpDevice->getApiHandle(), ID3D12Device5, pDevice5); - pDevice5->GetRaytracingAccelerationStructurePrebuildInfo(&inputs, &blas.prebuildInfo); - - // Figure out the padded allocation sizes to have proper alignement. - uint64_t paddedMaxBlasSize = align_to(D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BYTE_ALIGNMENT, blas.prebuildInfo.ResultDataMaxSizeInBytes); - blas.blasByteOffset = totalMaxBlasSize; - totalMaxBlasSize += paddedMaxBlasSize; - - uint64_t scratchSize = std::max(blas.prebuildInfo.ScratchDataSizeInBytes, blas.prebuildInfo.UpdateScratchDataSizeInBytes); - uint64_t paddedScratchSize = align_to(D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BYTE_ALIGNMENT, scratchSize); - blas.scratchByteOffset = totalScratchSize; - totalScratchSize += paddedScratchSize; + for (const auto& group : mBlasGroups) + { + resultByteSize = std::max(resultByteSize, group.resultByteSize); + scratchByteSize = std::max(scratchByteSize, group.scratchByteSize); + maxBlasCount = std::max(maxBlasCount, group.blasIndices.size()); } + assert(resultByteSize > 0 && scratchByteSize > 0); + + logInfo("BLAS build result buffer size: " + formatByteSize(resultByteSize)); + logInfo("BLAS build scratch buffer size: " + formatByteSize(scratchByteSize)); - // Allocate intermediate buffers and scratch buffer. + // Allocate result and scratch buffers. // The scratch buffer we'll retain because it's needed for subsequent rebuilds and updates. // TODO: Save memory by reducing the scratch buffer to the minimum required for the dynamic objects. - if (mpBlasScratch == nullptr || mpBlasScratch->getSize() < totalScratchSize) + if (mpBlasScratch == nullptr || mpBlasScratch->getSize() < scratchByteSize) { - mpBlasScratch = Buffer::create(totalScratchSize, Buffer::BindFlags::UnorderedAccess, Buffer::CpuAccess::None); + mpBlasScratch = Buffer::create(scratchByteSize, Buffer::BindFlags::UnorderedAccess, Buffer::CpuAccess::None); mpBlasScratch->setName("Scene::mpBlasScratch"); } - else - { - // If we didn't need to reallocate, just insert a barrier so it's safe to use. - pContext->uavBarrier(mpBlasScratch.get()); - } - Buffer::SharedPtr pDestBuffer = Buffer::create(totalMaxBlasSize, Buffer::BindFlags::AccelerationStructure, Buffer::CpuAccess::None); + Buffer::SharedPtr pResultBuffer = Buffer::create(resultByteSize, Buffer::BindFlags::AccelerationStructure, Buffer::CpuAccess::None); + assert(pResultBuffer && mpBlasScratch); + // Allocate post-build info buffer and staging resource for readback. const size_t postBuildInfoSize = sizeof(D3D12_RAYTRACING_ACCELERATION_STRUCTURE_POSTBUILD_INFO_COMPACTED_SIZE_DESC); static_assert(postBuildInfoSize == sizeof(D3D12_RAYTRACING_ACCELERATION_STRUCTURE_POSTBUILD_INFO_CURRENT_SIZE_DESC)); - Buffer::SharedPtr pPostbuildInfoBuffer = Buffer::create(mBlasData.size() * postBuildInfoSize, Buffer::BindFlags::None, Buffer::CpuAccess::Read); + Buffer::SharedPtr pPostbuildInfoBuffer = Buffer::create(maxBlasCount * postBuildInfoSize, Buffer::BindFlags::UnorderedAccess, Buffer::CpuAccess::None); + Buffer::SharedPtr pPostbuildInfoStagingBuffer = Buffer::create(maxBlasCount * postBuildInfoSize, Buffer::BindFlags::None, Buffer::CpuAccess::Read); - // Build the BLASes into the intermediate destination buffer. - // We output postbuild info to a separate buffer to find out the final size requirements. - assert(pDestBuffer && pPostbuildInfoBuffer && mpBlasScratch); - uint64_t postBuildInfoOffset = 0; + assert(pPostbuildInfoBuffer->getGpuAddress() % postBuildInfoSize == 0); // Check alignment expected by DXR - for (const auto& blas : mBlasData) + // Iterate over BLAS groups. For each group build and compact all BLASes. + for (size_t blasGroupIndex = 0; blasGroupIndex < mBlasGroups.size(); blasGroupIndex++) { - D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_DESC asDesc = {}; - asDesc.Inputs = blas.buildInputs; - asDesc.ScratchAccelerationStructureData = mpBlasScratch->getGpuAddress() + blas.scratchByteOffset; - asDesc.DestAccelerationStructureData = pDestBuffer->getGpuAddress() + blas.blasByteOffset; + auto& group = mBlasGroups[blasGroupIndex]; - // Need to find out the the postbuild compacted BLAS size to know the final allocation size. - D3D12_RAYTRACING_ACCELERATION_STRUCTURE_POSTBUILD_INFO_DESC postbuildInfoDesc = {}; - postbuildInfoDesc.InfoType = blas.useCompaction ? D3D12_RAYTRACING_ACCELERATION_STRUCTURE_POSTBUILD_INFO_COMPACTED_SIZE : D3D12_RAYTRACING_ACCELERATION_STRUCTURE_POSTBUILD_INFO_CURRENT_SIZE; - postbuildInfoDesc.DestBuffer = pPostbuildInfoBuffer->getGpuAddress() + postBuildInfoOffset; - postBuildInfoOffset += postBuildInfoSize; + // Insert barriers. The buffers are now ready to be written. + pContext->uavBarrier(pResultBuffer.get()); + pContext->uavBarrier(mpBlasScratch.get()); - GET_COM_INTERFACE(pContext->getLowLevelData()->getCommandList(), ID3D12GraphicsCommandList4, pList4); - pList4->BuildRaytracingAccelerationStructure(&asDesc, 1, &postbuildInfoDesc); - } + // Transition the post-build info buffer to unoredered access state as expected by DXR. + pContext->resourceBarrier(pPostbuildInfoBuffer.get(), Resource::State::UnorderedAccess); - // Release scratch buffer if there is no animated content. We will not need it. - if (!mHasSkinnedMesh) mpBlasScratch.reset(); + // Build the BLASes into the intermediate result buffer. + // We output post-build info in order to find out the final size requirements. + uint64_t postBuildInfoOffset = 0; + for (uint32_t blasId : group.blasIndices) + { + const auto& blas = mBlasData[blasId]; - // Read back the calculated final size requirements for each BLAS. - // For this purpose we have to flush and map the postbuild info buffer for readback. - // TODO: We could copy to a staging buffer first and wait on a GPU fence for when it's ready. - // But there is no other work to do inbetween so it probably wouldn't help. This is only done once at startup anyway. - pContext->flush(true); - const D3D12_RAYTRACING_ACCELERATION_STRUCTURE_POSTBUILD_INFO_COMPACTED_SIZE_DESC* postBuildInfo = - (const D3D12_RAYTRACING_ACCELERATION_STRUCTURE_POSTBUILD_INFO_COMPACTED_SIZE_DESC*) pPostbuildInfoBuffer->map(Buffer::MapType::Read); + D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_DESC asDesc = {}; + asDesc.Inputs = blas.buildInputs; + asDesc.ScratchAccelerationStructureData = mpBlasScratch->getGpuAddress() + blas.scratchByteOffset; + asDesc.DestAccelerationStructureData = pResultBuffer->getGpuAddress() + blas.resultByteOffset; - uint64_t totalBlasSize = 0; - for (size_t i = 0; i < mBlasData.size(); i++) - { - auto& blas = mBlasData[i]; - blas.blasByteSize = postBuildInfo[i].CompactedSizeInBytes; - assert(blas.blasByteSize <= blas.prebuildInfo.ResultDataMaxSizeInBytes); - uint64_t paddedBlasSize = align_to(D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BYTE_ALIGNMENT, blas.blasByteSize); - totalBlasSize += paddedBlasSize; - } - pPostbuildInfoBuffer->unmap(); + // Need to find out the post-build compacted BLAS size to know the final allocation size. + D3D12_RAYTRACING_ACCELERATION_STRUCTURE_POSTBUILD_INFO_DESC postbuildInfoDesc = {}; + postbuildInfoDesc.InfoType = blas.useCompaction ? D3D12_RAYTRACING_ACCELERATION_STRUCTURE_POSTBUILD_INFO_COMPACTED_SIZE : D3D12_RAYTRACING_ACCELERATION_STRUCTURE_POSTBUILD_INFO_CURRENT_SIZE; + postbuildInfoDesc.DestBuffer = pPostbuildInfoBuffer->getGpuAddress() + postBuildInfoOffset; + postBuildInfoOffset += postBuildInfoSize; - // Allocate final BLAS buffer. - if (mpBlas == nullptr || mpBlas->getSize() < totalBlasSize) - { - mpBlas = Buffer::create(totalBlasSize, Buffer::BindFlags::AccelerationStructure, Buffer::CpuAccess::None); - mpBlas->setName("Scene::mpBlas"); - } - else - { - // If we didn't need to reallocate, just insert a barrier so it's safe to use. - pContext->uavBarrier(mpBlas.get()); - } + GET_COM_INTERFACE(pContext->getLowLevelData()->getCommandList(), ID3D12GraphicsCommandList4, pList4); + pList4->BuildRaytracingAccelerationStructure(&asDesc, 1, &postbuildInfoDesc); + } - // Insert barriers for the intermediate buffer. This is probably not necessary since we flushed above, but it's not going to hurt. - pContext->uavBarrier(pDestBuffer.get()); + // Copy post-build info to staging buffer and flush. + // TODO: Wait on a GPU fence for when it's ready instead of doing a full flush. + pContext->copyResource(pPostbuildInfoStagingBuffer.get(), pPostbuildInfoBuffer.get()); + pContext->flush(true); - // Compact/clone all BLASes to their final location. - uint64_t blasOffset = 0; - for (auto& blas : mBlasData) - { - GET_COM_INTERFACE(pContext->getLowLevelData()->getCommandList(), ID3D12GraphicsCommandList4, pList4); - pList4->CopyRaytracingAccelerationStructure( - mpBlas->getGpuAddress() + blasOffset, - pDestBuffer->getGpuAddress() + blas.blasByteOffset, - blas.useCompaction ? D3D12_RAYTRACING_ACCELERATION_STRUCTURE_COPY_MODE_COMPACT : D3D12_RAYTRACING_ACCELERATION_STRUCTURE_COPY_MODE_CLONE); - - uint64_t paddedBlasSize = align_to(D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BYTE_ALIGNMENT, blas.blasByteSize); - blas.blasByteOffset = blasOffset; - blasOffset += paddedBlasSize; + // Read back the calculated final size requirements for each BLAS. + // The byte offset of each final BLAS is computed here. + const D3D12_RAYTRACING_ACCELERATION_STRUCTURE_POSTBUILD_INFO_COMPACTED_SIZE_DESC* postBuildInfo = + (const D3D12_RAYTRACING_ACCELERATION_STRUCTURE_POSTBUILD_INFO_COMPACTED_SIZE_DESC*)pPostbuildInfoStagingBuffer->map(Buffer::MapType::Read); + + group.finalByteSize = 0; + for (size_t i = 0; i < group.blasIndices.size(); i++) + { + const uint32_t blasId = group.blasIndices[i]; + auto& blas = mBlasData[blasId]; + + // Check the size. Upon failure a zero size may be reported. + const uint64_t byteSize = postBuildInfo[i].CompactedSizeInBytes; + assert(byteSize <= blas.prebuildInfo.ResultDataMaxSizeInBytes); + if (byteSize == 0) throw std::runtime_error("Acceleration structure build failed for BLAS index " + std::to_string(blasId)); + + blas.blasByteSize = align_to(D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BYTE_ALIGNMENT, byteSize); + blas.blasByteOffset = group.finalByteSize; + group.finalByteSize += blas.blasByteSize; + } + assert(group.finalByteSize > 0); + pPostbuildInfoBuffer->unmap(); + + logInfo("BLAS group " + std::to_string(blasGroupIndex) + " final size: " + formatByteSize(group.finalByteSize)); + + // Allocate final BLAS buffer. + auto& pBlas = group.pBlas; + if (pBlas == nullptr || pBlas->getSize() < group.finalByteSize) + { + pBlas = Buffer::create(group.finalByteSize, Buffer::BindFlags::AccelerationStructure, Buffer::CpuAccess::None); + pBlas->setName("Scene::mBlasGroups[" + std::to_string(blasGroupIndex) + "].pBlas"); + } + else + { + // If we didn't need to reallocate, just insert a barrier so it's safe to use. + pContext->uavBarrier(pBlas.get()); + } + + // Insert barrier. The result buffer is now ready to be consumed. + // TOOD: This is probably not necessary since we flushed above, but it's not going to hurt. + pContext->uavBarrier(pResultBuffer.get()); + + // Compact/clone all BLASes to their final location. + for (uint32_t blasId : group.blasIndices) + { + auto& blas = mBlasData[blasId]; + + GET_COM_INTERFACE(pContext->getLowLevelData()->getCommandList(), ID3D12GraphicsCommandList4, pList4); + pList4->CopyRaytracingAccelerationStructure( + pBlas->getGpuAddress() + blas.blasByteOffset, + pResultBuffer->getGpuAddress() + blas.resultByteOffset, + blas.useCompaction ? D3D12_RAYTRACING_ACCELERATION_STRUCTURE_COPY_MODE_COMPACT : D3D12_RAYTRACING_ACCELERATION_STRUCTURE_COPY_MODE_CLONE); + } + + // Insert barrier. The BLAS buffer is now ready for use. + pContext->uavBarrier(pBlas.get()); } - assert(blasOffset == totalBlasSize); - // Insert barrier. The BLAS buffer is now ready for use. - pContext->uavBarrier(mpBlas.get()); + // Release scratch buffer if there is no animated content. We will not need it. + if (!mHasSkinnedMesh) mpBlasScratch.reset(); - updateRaytracingStats(); + updateRaytracingBLASStats(); mRebuildBlas = false; - - return; } // If we get here, all BLASes have previously been built and compacted. We will: // - Early out if there are no animated meshes. // - Update or rebuild in-place the ones that are animated. + assert(!mRebuildBlas); if (mHasSkinnedMesh == false) return; - // Insert barriers. The buffers are now ready to be written to. - assert(mpBlas && mpBlasScratch); - pContext->uavBarrier(mpBlas.get()); - pContext->uavBarrier(mpBlasScratch.get()); - - for (const auto& blas : mBlasData) + for (const auto& group : mBlasGroups) { - // Skip updating BLASes not containing skinned meshes. - if (!blas.hasSkinnedMesh) continue; + // Insert barriers. The buffers are now ready to be written. + auto& pBlas = group.pBlas; + assert(pBlas && mpBlasScratch); + pContext->uavBarrier(pBlas.get()); + pContext->uavBarrier(mpBlasScratch.get()); - // Build/update BLAS. - D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_DESC asDesc = {}; - asDesc.Inputs = blas.buildInputs; - asDesc.ScratchAccelerationStructureData = mpBlasScratch->getGpuAddress() + blas.scratchByteOffset; - asDesc.DestAccelerationStructureData = mpBlas->getGpuAddress() + blas.blasByteOffset; - - if (blas.updateMode == UpdateMode::Refit) - { - // Set source address to destination address to update in place. - asDesc.SourceAccelerationStructureData = asDesc.DestAccelerationStructureData; - asDesc.Inputs.Flags |= D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_PERFORM_UPDATE; - } - else + // Iterate over all BLASes in group. + for (uint32_t blasId : group.blasIndices) { - // We'll rebuild in place. The BLAS should not be compacted, check that size matches prebuild info. - assert(blas.blasByteSize == blas.prebuildInfo.ResultDataMaxSizeInBytes); + const auto& blas = mBlasData[blasId]; + + // Skip BLASes not containing skinned meshes. + if (!blas.hasSkinnedMesh) continue; + + // Rebuild/update BLAS. + D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_DESC asDesc = {}; + asDesc.Inputs = blas.buildInputs; + asDesc.ScratchAccelerationStructureData = mpBlasScratch->getGpuAddress() + blas.scratchByteOffset; + asDesc.DestAccelerationStructureData = pBlas->getGpuAddress() + blas.blasByteOffset; + + if (blas.updateMode == UpdateMode::Refit) + { + // Set source address to destination address to update in place. + asDesc.SourceAccelerationStructureData = asDesc.DestAccelerationStructureData; + asDesc.Inputs.Flags |= D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_PERFORM_UPDATE; + } + else + { + // We'll rebuild in place. The BLAS should not be compacted, check that size matches prebuild info. + assert(blas.blasByteSize == blas.prebuildInfo.ResultDataMaxSizeInBytes); + } + + GET_COM_INTERFACE(pContext->getLowLevelData()->getCommandList(), ID3D12GraphicsCommandList4, pList4); + pList4->BuildRaytracingAccelerationStructure(&asDesc, 0, nullptr); } - GET_COM_INTERFACE(pContext->getLowLevelData()->getCommandList(), ID3D12GraphicsCommandList4, pList4); - pList4->BuildRaytracingAccelerationStructure(&asDesc, 0, nullptr); + // Insert barrier. The BLAS buffer is now ready for use. + pContext->uavBarrier(pBlas.get()); } - - // Insert barrier. The BLAS buffer is now ready for use. - pContext->uavBarrier(mpBlas.get()); } void Scene::fillInstanceDesc(std::vector& instanceDescs, uint32_t rayCount, bool perMeshHitEntry) const { - assert(mpBlas); instanceDescs.clear(); uint32_t instanceContributionToHitGroupIndex = 0; uint32_t instanceId = 0; - for (size_t i = 0; i < mBlasData.size(); i++) + for (size_t i = 0; i < mMeshGroups.size(); i++) { const auto& meshList = mMeshGroups[i].meshList; + const bool isStatic = mMeshGroups[i].isStatic; + + assert(mBlasData[i].blasGroupIndex < mBlasGroups.size()); + const auto& pBlas = mBlasGroups[mBlasData[i].blasGroupIndex].pBlas; + assert(pBlas); D3D12_RAYTRACING_INSTANCE_DESC desc = {}; - desc.AccelerationStructure = mpBlas->getGpuAddress() + mBlasData[i].blasByteOffset; + desc.AccelerationStructure = pBlas->getGpuAddress() + mBlasData[i].blasByteOffset; desc.InstanceMask = 0xFF; desc.InstanceContributionToHitGroupIndex = perMeshHitEntry ? instanceContributionToHitGroupIndex : 0; + instanceContributionToHitGroupIndex += rayCount * (uint32_t)meshList.size(); - // If multiple meshes are in a BLAS: - // - Their global matrix is the same. - // - From sortMeshes(), each mesh in the BLAS is guaranteed to be non-instanced, so only one INSTANCE_DESC is needed - if (meshList.size() > 1) + // From the scene builder we can expect the following: + // + // If BLAS is marked as static: + // - The meshes are pre-transformed to world-space. + // - The meshes are guaranteed to be non-instanced, so only one INSTANCE_DESC with an identity transform is needed. + // + // If there are multiple meshes in a BLAS not marked as static: + // - Their global matrix is the same, + // - The meshes are guaranteed to be non-instanced, so only one INSTANCE_DESC is needed. + // + if (isStatic || meshList.size() > 1) { - assert(mMeshIdToInstanceIds[meshList[0]].size() == 1); - assert(mMeshIdToInstanceIds[meshList[0]][0] == instanceId); // Mesh instances are sorted by instanceId + for (size_t j = 0; j < meshList.size(); j++) + { + assert(mMeshIdToInstanceIds[meshList[j]].size() == 1); + assert(mMeshIdToInstanceIds[meshList[j]][0] == instanceId + (uint32_t)j); // Mesh instances are sorted by instanceId + } desc.InstanceID = instanceId; instanceId += (uint32_t)meshList.size(); - // Any instances of the mesh will get you the correct matrix, so just pick the first mesh then the first instance. - uint32_t matrixId = mMeshInstanceData[desc.InstanceID].globalMatrixID; - glm::mat4 transform4x4 = transpose(mpAnimationController->getGlobalMatrices()[matrixId]); + glm::mat4 transform4x4 = glm::identity(); + if (!isStatic) + { + // Dynamic meshes. + // Any instances of the mesh will get you the correct matrix, so just pick the first mesh then the first instance. + uint32_t matrixId = mMeshInstanceData[desc.InstanceID].globalMatrixID; + transform4x4 = transpose(mpAnimationController->getGlobalMatrices()[matrixId]); + } std::memcpy(desc.Transform, &transform4x4, sizeof(desc.Transform)); instanceDescs.push_back(desc); } // If only one mesh is in the BLAS, there CAN be multiple instances of it. It is either: - // - A non-instanced mesh that was unable to be merged with others - // - A mesh with multiple instances + // - A non-instanced mesh that was unable to be merged with others, or + // - A mesh with multiple instances. else { assert(meshList.size() == 1); @@ -1390,6 +1999,29 @@ namespace Falcor } } } + + // One instance with identity transform for AABBs. + if (mpRtAABBBuffer) + { + // Last BLAS should be all AABBs. + assert(mBlasData.size() == mMeshGroups.size() + 1); + + assert(mBlasData.back().blasGroupIndex < mBlasGroups.size()); + const auto& pBlas = mBlasGroups[mBlasData.back().blasGroupIndex].pBlas; + assert(pBlas); + + D3D12_RAYTRACING_INSTANCE_DESC desc = {}; + desc.AccelerationStructure = pBlas->getGpuAddress() + mBlasData.back().blasByteOffset; + desc.InstanceMask = 0xFF; + desc.InstanceID = 0; + + // Start AABB hitgroup lookup after the triangle hitgroups + desc.InstanceContributionToHitGroupIndex = perMeshHitEntry ? instanceContributionToHitGroupIndex : rayCount; + + glm::mat4 identityMat = glm::identity(); + std::memcpy(desc.Transform, &identityMat, sizeof(desc.Transform)); + instanceDescs.push_back(desc); + } } void Scene::buildTlas(RenderContext* pContext, uint32_t rayCount, bool perMeshHitEntry) @@ -1441,7 +2073,9 @@ namespace Falcor { assert(tlas.pInstanceDescs == nullptr); // Instance desc should also be null if no TLAS tlas.pTlas = Buffer::create(mTlasPrebuildInfo.ResultDataMaxSizeInBytes, Buffer::BindFlags::AccelerationStructure, Buffer::CpuAccess::None); + tlas.pTlas->setName("Scene TLAS buffer"); tlas.pInstanceDescs = Buffer::create((uint32_t)mInstanceDescs.size() * sizeof(D3D12_RAYTRACING_INSTANCE_DESC), Buffer::BindFlags::None, Buffer::CpuAccess::Write, mInstanceDescs.data()); + tlas.pInstanceDescs->setName("Scene instance descs buffer"); } // Else update instance descs and barrier TLAS buffers else @@ -1471,21 +2105,11 @@ namespace Falcor // Create TLAS SRV if (tlas.pSrv == nullptr) { - D3D12_SHADER_RESOURCE_VIEW_DESC srvDesc = {}; - srvDesc.ViewDimension = D3D12_SRV_DIMENSION_RAYTRACING_ACCELERATION_STRUCTURE; - srvDesc.Shader4ComponentMapping = D3D12_DEFAULT_SHADER_4_COMPONENT_MAPPING; - srvDesc.RaytracingAccelerationStructure.Location = tlas.pTlas->getGpuAddress(); - - DescriptorSet::Layout layout; - layout.addRange(DescriptorSet::Type::TextureSrv, 0, 1); - DescriptorSet::SharedPtr pSet = DescriptorSet::create(gpDevice->getCpuDescriptorPool(), layout); - gpDevice->getApiHandle()->CreateShaderResourceView(nullptr, &srvDesc, pSet->getCpuHandle(0)); - - ResourceWeakPtr pWeak = tlas.pTlas; - tlas.pSrv = std::make_shared(pWeak, pSet, 0, 1, 0, 1); + tlas.pSrv = ShaderResourceView::createViewForAccelerationStructure(tlas.pTlas); } mTlasCache[rayCount] = tlas; + updateRaytracingTLASStats(); } void Scene::setGeometryIndexIntoRtVars(const std::shared_ptr& pVars) @@ -1494,7 +2118,16 @@ namespace Falcor // This is the local index of which mesh in the BLAS was hit. // In DXR 1.0 we have to pass it via a constant buffer to the shader, // in DXR 1.1 it is available through the GeometryIndex() system value. - // + + auto setIntoVars = [](const EntryPointGroupVars::SharedPtr& pVars, uint32_t geometryIndex) + { + auto var = pVars->findMember(0).findMember("geometryIndex"); + if (var.isValid()) + { + var = geometryIndex; + } + }; + assert(!mBlasData.empty()); uint32_t meshCount = getMeshCount(); uint32_t descHitCount = pVars->getDescHitGroupCount(); @@ -1505,12 +2138,7 @@ namespace Falcor { for (uint32_t hit = 0; hit < descHitCount; hit++) { - auto pHitVars = pVars->getHitVars(hit, meshId); - auto var = pHitVars->findMember(0).findMember("geometryIndex"); - if (var.isValid()) - { - var = geometryIndex; - } + setIntoVars(pVars->getHitVars(hit, meshId), geometryIndex); } geometryIndex++; @@ -1523,6 +2151,18 @@ namespace Falcor blasIndex++; } } + + // For custom primitives, there is one geometry per primitive, all in one BLAS + geometryIndex = 0; // Reset counter + for (uint32_t i = 0; i < getProceduralPrimitiveCount(); i++) + { + for (uint32_t hit = 0; hit < descHitCount; hit++) + { + setIntoVars(pVars->getAABBHitVars(hit, i), geometryIndex); + } + + geometryIndex++; + } } void Scene::setRaytracingShaderData(RenderContext* pContext, const ShaderVar& var, uint32_t rayTypeCount) @@ -1530,7 +2170,7 @@ namespace Falcor // On first execution, create BLAS for each mesh. if (mBlasData.empty()) { - initGeomDesc(); + initGeomDesc(pContext); buildBlas(pContext); } @@ -1565,11 +2205,70 @@ namespace Falcor var["gRtScene"].setSrv(tlasIt->second.pSrv); } + std::vector Scene::getMeshBlasIDs() const + { + const uint32_t invalidID = uint32_t(-1); + std::vector blasIDs(mMeshDesc.size(), invalidID); + + for (uint32_t blasID = 0; blasID < (uint32_t)mMeshGroups.size(); blasID++) + { + for (auto meshID : mMeshGroups[blasID].meshList) + { + assert(meshID < blasIDs.size()); + blasIDs[meshID] = blasID; + } + } + + for (auto blasID : blasIDs) assert(blasID != invalidID); + return blasIDs; + } + + void Scene::nullTracePass(RenderContext* pContext, const uint2& dim) + { + if (!gpDevice->isFeatureSupported(Device::SupportedFeatures::RaytracingTier1_1)) + { + logWarning("Scene::nullTracePass() - Raytracing Tier 1.1 is not supported by the current device"); + return; + } + + D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_INPUTS inputs = {}; + inputs.Type = D3D12_RAYTRACING_ACCELERATION_STRUCTURE_TYPE_TOP_LEVEL; + inputs.DescsLayout = D3D12_ELEMENTS_LAYOUT_ARRAY; + inputs.NumDescs = 0; + inputs.Flags = D3D12_RAYTRACING_ACCELERATION_STRUCTURE_BUILD_FLAG_NONE; + + GET_COM_INTERFACE(gpDevice->getApiHandle(), ID3D12Device5, pDevice5); + D3D12_RAYTRACING_ACCELERATION_STRUCTURE_PREBUILD_INFO prebuildInfo = {}; + pDevice5->GetRaytracingAccelerationStructurePrebuildInfo(&inputs, &prebuildInfo); + auto pScratch = Buffer::create(prebuildInfo.ScratchDataSizeInBytes, Buffer::BindFlags::UnorderedAccess, Buffer::CpuAccess::None); + auto pTlas = Buffer::create(prebuildInfo.ResultDataMaxSizeInBytes, Buffer::BindFlags::AccelerationStructure, Buffer::CpuAccess::None); + + D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_DESC asDesc = {}; + asDesc.Inputs = inputs; + asDesc.ScratchAccelerationStructureData = pScratch->getGpuAddress(); + asDesc.DestAccelerationStructureData = pTlas->getGpuAddress(); + + GET_COM_INTERFACE(pContext->getLowLevelData()->getCommandList(), ID3D12GraphicsCommandList4, pList4); + pList4->BuildRaytracingAccelerationStructure(&asDesc, 0, nullptr); + pContext->uavBarrier(pTlas.get()); + + Program::Desc desc; + desc.addShaderLibrary("Scene/NullTrace.cs.slang").csEntry("main").setShaderModel("6_5"); + auto pass = ComputePass::create(desc); + pass["gOutput"] = Texture::create2D(dim.x, dim.y, ResourceFormat::R8Uint, 1, 1, nullptr, ResourceBindFlags::UnorderedAccess); + pass["gTlas"].setSrv(ShaderResourceView::createViewForAccelerationStructure(pTlas)); + + for (size_t i = 0; i < 100; i++) + { + pass->execute(pContext, uint3(dim, 1)); + } + } + void Scene::setEnvMap(EnvMap::SharedPtr pEnvMap) { if (mpEnvMap == pEnvMap) return; mpEnvMap = pEnvMap; - if (mpEnvMap) mpEnvMap->setShaderData(mpSceneBlock[kEnvMap]); + mEnvMapChanged = true; } void Scene::loadEnvMap(const std::string& filename) @@ -1603,7 +2302,7 @@ namespace Falcor break; case CameraControllerType::Orbiter: mpCamCtrl = OrbiterCameraController::create(camera); - ((OrbiterCameraController*)mpCamCtrl.get())->setModelParams(mSceneBB.center, length(mSceneBB.extent), 3.5f); + ((OrbiterCameraController*)mpCamCtrl.get())->setModelParams(mSceneBB.center(), mSceneBB.radius(), 3.5f); break; case CameraControllerType::SixDOF: mpCamCtrl = SixDoFCameraController::create(camera); @@ -1640,19 +2339,19 @@ namespace Falcor std::string c; // Render settings. - c += Scripting::makeSetProperty(sceneVar, kRenderSettings, mRenderSettings); + c += ScriptWriter::makeSetProperty(sceneVar, kRenderSettings, mRenderSettings); // Animations. if (hasAnimation() && !isAnimated()) { - c += Scripting::makeSetProperty(sceneVar, kAnimated, false); + c += ScriptWriter::makeSetProperty(sceneVar, kAnimated, false); } for (size_t i = 0; i < mLights.size(); ++i) { const auto& light = mLights[i]; if (light->hasAnimation() && !light->isAnimated()) { - c += Scripting::makeSetProperty(sceneVar + "." + kGetLight + "(" + std::to_string(i) + ").", kAnimated, false); + c += ScriptWriter::makeSetProperty(sceneVar + "." + kGetLight + "(" + std::to_string(i) + ").", kAnimated, false); } } @@ -1664,7 +2363,7 @@ namespace Falcor c += getCamera()->getScript(sceneVar + "." + kCamera); // Camera speed. - c += Scripting::makeSetProperty(sceneVar, kCameraSpeed, mCameraSpeed); + c += ScriptWriter::makeSetProperty(sceneVar, kCameraSpeed, mCameraSpeed); // Viewpoints. if (hasSavedViewpoints()) @@ -1672,39 +2371,101 @@ namespace Falcor for (size_t i = 1; i < mViewpoints.size(); i++) { auto v = mViewpoints[i]; - c += Scripting::makeMemberFunc(sceneVar, kAddViewpoint, v.position, v.target, v.up, v.index); + c += ScriptWriter::makeMemberFunc(sceneVar, kAddViewpoint, v.position, v.target, v.up, v.index); } } return c; } + pybind11::dict Scene::SceneStats::toPython() const + { + pybind11::dict d; + + // Geometry stats + d["uniqueTriangleCount"] = uniqueTriangleCount; + d["uniqueVertexCount"] = uniqueVertexCount; + d["instancedTriangleCount"] = instancedTriangleCount; + d["instancedVertexCount"] = instancedVertexCount; + d["indexMemoryInBytes"] = indexMemoryInBytes; + d["vertexMemoryInBytes"] = vertexMemoryInBytes; + d["geometryMemoryInBytes"] = geometryMemoryInBytes; + d["animationMemoryInBytes"] = animationMemoryInBytes; + + // Curve stats + d["uniqueCurveSegmentCount"] = uniqueCurveSegmentCount; + d["uniqueCurvePointCount"] = uniqueCurvePointCount; + d["instancedCurveSegmentCount"] = instancedCurveSegmentCount; + d["instancedCurvePointCount"] = instancedCurvePointCount; + d["curveIndexMemoryInBytes"] = curveIndexMemoryInBytes; + d["curveVertexMemoryInBytes"] = curveVertexMemoryInBytes; + + // Material stats + d["materialCount"] = materialCount; + d["materialMemoryInBytes"] = materialMemoryInBytes; + d["textureCount"] = textureCount; + d["textureCompressedCount"] = textureCompressedCount; + d["textureTexelCount"] = textureTexelCount; + d["textureMemoryInBytes"] = textureMemoryInBytes; + + // Raytracing stats + d["blasGroupCount"] = blasGroupCount; + d["blasCount"] = blasCount; + d["blasCompactedCount"] = blasCompactedCount; + d["blasMemoryInBytes"] = blasMemoryInBytes; + d["blasScratchMemoryInBytes"] = blasScratchMemoryInBytes; + d["tlasCount"] = tlasCount; + d["tlasMemoryInBytes"] = tlasMemoryInBytes; + d["tlasScratchMemoryInBytes"] = tlasScratchMemoryInBytes; + + // Light stats + d["activeLightCount"] = activeLightCount; + d["totalLightCount"] = totalLightCount; + d["pointLightCount"] = pointLightCount; + d["directionalLightCount"] = directionalLightCount; + d["rectLightCount"] = rectLightCount; + d["sphereLightCount"] = sphereLightCount; + d["distantLightCount"] = distantLightCount; + d["lightsMemoryInBytes"] = lightsMemoryInBytes; + d["envMapMemoryInBytes"] = envMapMemoryInBytes; + d["emissiveMemoryInBytes"] = emissiveMemoryInBytes; + + // Volume stats + d["volumeCount"] = volumeCount; + d["volumeMemoryInBytes"] = volumeMemoryInBytes; + + // Grid stats + d["gridCount"] = gridCount; + d["gridVoxelCount"] = gridVoxelCount; + d["gridMemoryInBytes"] = gridMemoryInBytes; + + return d; + } + SCRIPT_BINDING(Scene) { pybind11::class_ scene(m, "Scene"); + scene.def_property_readonly(kStats.c_str(), [] (const Scene* pScene) { return pScene->getSceneStats().toPython(); }); + scene.def_property_readonly(kBounds.c_str(), &Scene::getSceneBounds, pybind11::return_value_policy::copy); scene.def_property(kCamera.c_str(), &Scene::getCamera, &Scene::setCamera); + scene.def_property(kEnvMap.c_str(), &Scene::getEnvMap, &Scene::setEnvMap); + scene.def_property_readonly(kAnimations.c_str(), &Scene::getAnimations); scene.def_property_readonly(kCameras.c_str(), &Scene::getCameras); - scene.def_property_readonly(kEnvMap.c_str(), &Scene::getEnvMap); + scene.def_property_readonly(kLights.c_str(), &Scene::getLights); scene.def_property_readonly(kMaterials.c_str(), &Scene::getMaterials); + scene.def_property_readonly(kVolumes.c_str(), &Scene::getVolumes); scene.def_property(kCameraSpeed.c_str(), &Scene::getCameraSpeed, &Scene::setCameraSpeed); scene.def_property(kAnimated.c_str(), &Scene::isAnimated, &Scene::setIsAnimated); + scene.def_property(kLoopAnimations.c_str(), &Scene::isLooped, &Scene::setIsLooped); scene.def_property(kRenderSettings.c_str(), pybind11::overload_cast(&Scene::getRenderSettings, pybind11::const_), &Scene::setRenderSettings); - scene.def("animate", &Scene::toggleAnimations, "animate"_a); // PYTHONDEPRECATED - auto animateCamera = [](Scene* pScene, bool animate) { pScene->getCamera()->setIsAnimated(animate); }; - scene.def("animateCamera", animateCamera, "animate"_a); // PYTHONDEPRECATED - auto animateLight = [](Scene* pScene, uint32_t index, bool animate) { pScene->getLight(index)->setIsAnimated(animate); }; - scene.def("animateLight", animateLight, "index"_a, "animate"_a); // PYTHONDEPRECATED - scene.def(kSetEnvMap.c_str(), &Scene::loadEnvMap, "filename"_a); scene.def(kGetLight.c_str(), &Scene::getLight, "index"_a); scene.def(kGetLight.c_str(), &Scene::getLightByName, "name"_a); - scene.def("light", &Scene::getLight); // PYTHONDEPRECATED - scene.def("light", &Scene::getLightByName); // PYTHONDEPRECATED scene.def(kGetMaterial.c_str(), &Scene::getMaterial, "index"_a); scene.def(kGetMaterial.c_str(), &Scene::getMaterialByName, "name"_a); - scene.def("material", &Scene::getMaterial); // PYTHONDEPRECATED - scene.def("material", &Scene::getMaterialByName); // PYTHONDEPRECATED + scene.def(kGetVolume.c_str(), &Scene::getVolume, "index"_a); + scene.def(kGetVolume.c_str(), &Scene::getVolumeByName, "name"_a); // Viewpoints scene.def(kAddViewpoint.c_str(), pybind11::overload_cast<>(&Scene::addViewpoint)); // add current camera as viewpoint @@ -1712,15 +2473,13 @@ namespace Falcor scene.def(kRemoveViewpoint.c_str(), &Scene::removeViewpoint); // remove the selected viewpoint scene.def(kSelectViewpoint.c_str(), &Scene::selectViewpoint, "index"_a); // select a viewpoint by index - scene.def("viewpoint", pybind11::overload_cast<>(&Scene::addViewpoint)); // PYTHONDEPRECATED save the current camera position etc. - scene.def("viewpoint", pybind11::overload_cast(&Scene::selectViewpoint)); // PYTHONDEPRECATED select a previously saved camera viewpoint - // RenderSettings ScriptBindings::SerializableStruct renderSettings(m, "SceneRenderSettings"); #define field(f_) field(#f_, &Scene::RenderSettings::f_) renderSettings.field(useEnvLight); renderSettings.field(useAnalyticLights); renderSettings.field(useEmissiveLights); + renderSettings.field(useVolumes); #undef field } } diff --git a/Source/Falcor/Scene/Scene.h b/Source/Falcor/Scene/Scene.h index fecc695687..258305ac03 100644 --- a/Source/Falcor/Scene/Scene.h +++ b/Source/Falcor/Scene/Scene.h @@ -29,25 +29,35 @@ #include "Core/API/VAO.h" #include "Animation/Animation.h" #include "Lights/Light.h" -#include "Lights/LightProbe.h" #include "Camera/Camera.h" #include "Material/Material.h" +#include "Volume/Volume.h" +#include "Volume/Grid.h" #include "Utils/Math/AABB.h" #include "Animation/AnimationController.h" #include "Camera/CameraController.h" #include "Experimental/Scene/Lights/LightCollection.h" #include "Experimental/Scene/Lights/EnvMap.h" #include "SceneTypes.slang" +#include "HitInfo.h" + +// Indicating the implementation of curve back-face culling is in anyhit shaders or intersection shaders. +// Currently, the performance numbers on BabyCheetah scene with 20 indirect bounces are 77ms (with anyhit) and 73ms (without anyhit). +// It will be removed once we have conclusions on performance. +#define CURVE_BACKFACE_CULLING_USING_ANYHIT 0 namespace Falcor { class RtProgramVars; /** DXR Scene and Resources Layout: - - BLAS creation logic is similar to Falcor 3.0, and are grouped in the following order: - 1) For non-instanced meshes, group them if they use the same scene graph transform matrix. One BLAS is created per group. - a) It is possible a non-instanced mesh has no other meshes to merge with. In that case, the mesh goes in its own BLAS. - 2) For instanced meshes, one BLAS is created per mesh. + - BLAS creation logic: + 1) For static meshes, pre-transform and group them into single BLAS. + a) This can be overridden by the 'RTDontMergeStatic' scene build flag. + 2) For dynamic non-instanced meshes, group them if they use the same scene graph transform matrix. One BLAS is created per group. + a) This can be overridden by the 'RTDontMergeDynamic' scene build flag. + b) It is possible a non-instanced mesh has no other meshes to merge with. In that case, the mesh goes in its own BLAS. + 3) For instanced meshes, one BLAS is created per mesh. - TLAS Construction: - Hit shaders use InstanceID() and GeometryIndex() to identify what was hit. @@ -77,12 +87,24 @@ namespace Falcor using SharedPtr = std::shared_ptr; using LightList = std::vector; static const uint32_t kMaxBonesPerVertex = 4; + static const uint32_t kInvalidBone = -1; + static const uint32_t kInvalidGrid = -1; + + static const uint32_t kCurveIntersectionTypeID = 0; static const FileDialogFilterVec& getFileExtensionFilters(); + /** Create scene from file. + \param[in] filename Import the scene from this file. + \return Scene object, or nullptr if an error occured. + */ static SharedPtr create(const std::string& filename); - // #SCENE: we should get rid of this. We can't right now because we can't create a structured-buffer of materials (MaterialData contains textures) + /** Get scene defines. + These defines must be set on all programs that access the scene. + The defines are static and it's sufficient to set them once after loading. + \return List of shader defines. + */ Shader::DefineList getSceneDefines() const; /** Render settings determining how the scene is rendered. @@ -90,15 +112,17 @@ namespace Falcor */ struct RenderSettings { - bool useEnvLight = true; ///< Enable distant lighting from environment map. + bool useEnvLight = true; ///< Enable lighting from environment map. bool useAnalyticLights = true; ///< Enable lighting from analytic lights. bool useEmissiveLights = true; ///< Enable lighting from emissive lights. + bool useVolumes = true; ///< Enable rendering of heterogeneous volumes. bool operator==(const RenderSettings& other) const { return (useEnvLight == other.useEnvLight) && (useAnalyticLights == other.useAnalyticLights) && - (useEmissiveLights == other.useEmissiveLights); + (useEmissiveLights == other.useEmissiveLights) && + (useVolumes == other.useVolumes); } bool operator!=(const RenderSettings& other) const { return !(*this == other); } @@ -106,40 +130,45 @@ namespace Falcor enum class RenderFlags { - None = 0x0, - UserRasterizerState = 0x1, ///< Use the rasterizer state currently bound to `pState`. If this flag is not set, the default rasterizer state will be used. - ///< Note that we need to change the rasterizer state during rendering because some meshes have a negative scale factor, and hence the triangles will have a different winding order. - ///< If such meshes exist, overriding the state may result in incorrect rendering output + None = 0x0, + UserRasterizerState = 0x1, ///< Use the rasterizer state currently bound to `pState`. If this flag is not set, the default rasterizer state will be used. + ///< Note that we need to change the rasterizer state during rendering because some meshes have a negative scale factor, and hence the triangles will have a different winding order. + ///< If such meshes exist, overriding the state may result in incorrect rendering output }; /** Flags indicating if and what was updated in the scene */ enum class UpdateFlags { - None = 0x0, ///< Nothing happened - MeshesMoved = 0x1, ///< Meshes moved - CameraMoved = 0x2, ///< The camera moved - CameraPropertiesChanged = 0x4, ///< Some camera properties changed, excluding position - CameraSwitched = 0x8, ///< Selected a different camera - LightsMoved = 0x10, ///< Lights were moved - LightIntensityChanged = 0x20, ///< Light intensity changed - LightPropertiesChanged = 0x40, ///< Other light changes not included in LightIntensityChanged and LightsMoved - SceneGraphChanged = 0x80, ///< Any transform in the scene graph changed. - LightCollectionChanged = 0x100, ///< Light collection changed (mesh lights) - MaterialsChanged = 0x200, ///< Materials changed - EnvMapChanged = 0x400, ///< Environment map changed (check EnvMap::getChanges() for more specific information) - LightCountChanged = 0x800, ///< Number of active lights changed - RenderSettingsChanged = 0x1000,///< Render settings changed + None = 0x0, ///< Nothing happened + MeshesMoved = 0x1, ///< Meshes moved + CameraMoved = 0x2, ///< The camera moved + CameraPropertiesChanged = 0x4, ///< Some camera properties changed, excluding position + CameraSwitched = 0x8, ///< Selected a different camera + LightsMoved = 0x10, ///< Lights were moved + LightIntensityChanged = 0x20, ///< Light intensity changed + LightPropertiesChanged = 0x40, ///< Other light changes not included in LightIntensityChanged and LightsMoved + SceneGraphChanged = 0x80, ///< Any transform in the scene graph changed. + LightCollectionChanged = 0x100, ///< Light collection changed (mesh lights) + MaterialsChanged = 0x200, ///< Materials changed + EnvMapChanged = 0x400, ///< Environment map changed + EnvMapPropertiesChanged = 0x800, ///< Environment map properties changed (check EnvMap::getChanges() for more specific information) + LightCountChanged = 0x1000, ///< Number of active lights changed + RenderSettingsChanged = 0x2000, ///< Render settings changed + VolumesMoved = 0x4000, ///< Volumes were moved + VolumePropertiesChanged = 0x8000, ///< Volume properties changed + VolumeGridsChanged = 0x10000, ///< Volume grids changed + VolumeBoundsChanged = 0x20000, ///< Volume bounds changed All = -1 }; - /** Settings for how the scene is updated + /** Settings for how the scene ray tracing acceleration structures are updated. */ enum class UpdateMode { - Rebuild, ///< Recreate acceleration structure when updates are needed - Refit ///< Update acceleration structure when updates are needed + Rebuild, ///< Recreate acceleration structure when updates are needed. + Refit ///< Update acceleration structure when updates are needed. }; enum class CameraControllerType @@ -149,6 +178,85 @@ namespace Falcor SixDOF }; + /** Statistics. + */ + struct SceneStats + { + // Geometry stats + uint64_t uniqueTriangleCount = 0; ///< Number of unique triangles. A triangle can exist in multiple instances. + uint64_t uniqueVertexCount = 0; ///< Number of unique vertices. A vertex can be referenced by multiple triangles/instances. + uint64_t instancedTriangleCount = 0; ///< Number of instanced triangles. This is the total number of rendered triangles. + uint64_t instancedVertexCount = 0; ///< Number of instanced vertices. This is the total number of vertices in the rendered triangles. + uint64_t indexMemoryInBytes = 0; ///< Total memory in bytes used by the index buffer. + uint64_t vertexMemoryInBytes = 0; ///< Total memory in bytes used by the vertex buffer. + uint64_t geometryMemoryInBytes = 0; ///< Total memory in bytes used by the geometry data (meshes, curves, instances). + uint64_t animationMemoryInBytes = 0; ///< Total memory in bytes used by the animation system (transforms, skinning buffers). + + // Curve stats + uint64_t uniqueCurveSegmentCount = 0; ///< Number of unique curve segments (linear tube segments by default). A segment can exist in multiple instances. + uint64_t uniqueCurvePointCount = 0; ///< Number of unique curve points. A point can be referenced by multiple segments/instances. + uint64_t instancedCurveSegmentCount = 0; ///< Number of instanced curve segments (linear tube segments by default). This is the total number of rendered segments. + uint64_t instancedCurvePointCount = 0; ///< Number of instanced curve points. This is the total number of end points in the rendered segments. + uint64_t curveIndexMemoryInBytes = 0; ///< Total memory in bytes used by the curve index buffer. + uint64_t curveVertexMemoryInBytes = 0; ///< Total memory in bytes used by the curve vertex buffer. + + // Material stats + uint64_t materialCount = 0; ///< Number of materials. + uint64_t materialMemoryInBytes = 0; ///< Total memory in bytes used by the material data. + uint64_t textureCount = 0; ///< Number of unique textures. A texture can be referenced by multiple materials. + uint64_t textureCompressedCount = 0; ///< Number of unique compressed textures. + uint64_t textureTexelCount = 0; ///< Total number of texels in all textures. + uint64_t textureMemoryInBytes = 0; ///< Total memory in bytes used by the textures. + + // Raytracing stats + uint64_t blasGroupCount = 0; ///< Number of BLAS groups. There is one BLAS buffer per group. + uint64_t blasCount = 0; ///< Number of BLASes. + uint64_t blasCompactedCount = 0; ///< Number of compacted BLASes. + uint64_t blasMemoryInBytes = 0; ///< Total memory in bytes used by the BLASes. + uint64_t blasScratchMemoryInBytes = 0; ///< Additional memory in bytes kept around for BLAS updates etc. + uint64_t tlasCount = 0; ///< Number of TLASes. + uint64_t tlasMemoryInBytes = 0; ///< Total memory in bytes used by the TLASes. + uint64_t tlasScratchMemoryInBytes = 0; ///< Additional memory in bytes kept around for TLAS updates etc. + + // Light stats + uint64_t activeLightCount = 0; ///< Number of active lights. + uint64_t totalLightCount = 0; ///< Number of lights in the scene. + uint64_t pointLightCount = 0; ///< Number of point lights. + uint64_t directionalLightCount = 0; ///< Number of directional lights. + uint64_t rectLightCount = 0; ///< Number of rect lights. + uint64_t sphereLightCount = 0; ///< Number of sphere lights. + uint64_t distantLightCount = 0; ///< Number of distant lights. + uint64_t lightsMemoryInBytes = 0; ///< Total memory in bytes used by the analytic lights. + uint64_t envMapMemoryInBytes = 0; ///< Total memory in bytes used by the environment map. + uint64_t emissiveMemoryInBytes = 0; ///< Total memory in bytes used by the emissive lights. + + // Volume stats + uint64_t volumeCount = 0; ///< Number of volumes. + uint64_t volumeMemoryInBytes = 0; ///< Total memory in bytes used by the volumes. + + // Grid stats + uint64_t gridCount = 0; ///< Number of grids. + uint64_t gridVoxelCount = 0; ///< Total number of voxels in all grids. + uint64_t gridMemoryInBytes = 0; ///< Total memory in bytes used by the grids. + + /** Get the total memory usage. + */ + uint64_t getTotalMemory() const + { + return indexMemoryInBytes + vertexMemoryInBytes + geometryMemoryInBytes + animationMemoryInBytes + + curveIndexMemoryInBytes + curveVertexMemoryInBytes + materialMemoryInBytes + textureMemoryInBytes + + blasMemoryInBytes + blasScratchMemoryInBytes + tlasMemoryInBytes + tlasScratchMemoryInBytes + + lightsMemoryInBytes + envMapMemoryInBytes + emissiveMemoryInBytes + + volumeMemoryInBytes + gridMemoryInBytes; + } + + /** Convert to python dict. + */ + pybind11::dict toPython() const; + }; + + const SceneStats& getSceneStats() const { return mSceneStats; } + /** Get the render settings. */ const RenderSettings& getRenderSettings() const { return mRenderSettings; } @@ -177,6 +285,10 @@ namespace Falcor */ bool useEmissiveLights() const; + /** Returns true if there are active volumes and they should be rendererd. + */ + bool useVolumes() const; + /** Access the scene's currently selected camera to change properties or to use elsewhere. */ const Camera::SharedPtr& getCamera() { return mCameras[mSelectedCamera]; } @@ -189,33 +301,28 @@ namespace Falcor */ void setCamera(const Camera::SharedPtr& pCamera); - /** Set the currently selected camera's aspect ratio + /** Set the currently selected camera's aspect ratio. */ void setCameraAspectRatio(float ratio); - /** Set the camera controller type + /** Set the camera controller type. */ void setCameraController(CameraControllerType type); - /** Get the camera controller type + /** Get the camera controller type. */ CameraControllerType getCameraControllerType() const { return mCamCtrlType; } - /** Toggle whether the currently selected camera is animated. - */ - deprecate("4.0.2", "Use Camera::setIsAnimated() instead.") - void toggleCameraAnimation(bool active) { mCameras[mSelectedCamera]->setIsAnimated(active); } - /** Reset the currently selected camera. This function will place the camera at the center of scene and optionally set the depth range to some reasonable pre-determined values */ void resetCamera(bool resetDepthRange = true); - /** Set the camera's speed + /** Set the camera's speed. */ void setCameraSpeed(float speed); - /** Get the camera's speed + /** Get the camera's speed. */ float getCameraSpeed() const { return mCameraSpeed; } @@ -231,9 +338,6 @@ namespace Falcor */ void selectCamera(std::string name); - deprecate("4.0.1", "Use addViewpoint() instead.") - void saveNewViewpoint() { addViewpoint(); } - /** Add a new viewpoint to the list of viewpoints. */ void addViewpoint(const float3& position, const float3& target, const float3& up, uint32_t cameraIndex = 0); @@ -246,62 +350,107 @@ namespace Falcor */ void selectViewpoint(uint32_t index); - deprecate("4.0.1", "Use selectViewpoint() instead.") - void gotoViewpoint(uint32_t index) { selectViewpoint(index); } - - /** Returns true if there are saved viewpoints (used for dumping to config) + /** Returns true if there are saved viewpoints (used for dumping to config). */ bool hasSavedViewpoints() { return mViewpoints.size() > 1; } - /** Get the number of meshes + /** Get the number of meshes. */ uint32_t getMeshCount() const { return (uint32_t)mMeshDesc.size(); } - /** Get a mesh desc + /** Get the number of procedural primitives. + */ + uint32_t getProceduralPrimitiveCount() const { return (uint32_t)mProceduralPrimData.size(); } + + /** Get the number of curves. + */ + uint32_t getCurveCount() const { return (uint32_t)mCurveDesc.size(); } + + /** Get procedural primitives by raw index (the order they were defined, also the geometry order in the BLAS). + */ + const ProceduralPrimitiveData& getProceduralPrimitive(uint32_t index) const { return mProceduralPrimData[index]; } + + /** Get a mesh desc. */ const MeshDesc& getMesh(uint32_t meshID) const { return mMeshDesc[meshID]; } - /** Get the number of mesh instances + /** Get the number of mesh instances. */ uint32_t getMeshInstanceCount() const { return (uint32_t)mMeshInstanceData.size(); } - /** Get a mesh instance desc + /** Get a mesh instance desc. */ const MeshInstanceData& getMeshInstance(uint32_t instanceID) const { return mMeshInstanceData[instanceID]; } + /** Get a curve desc. + */ + const CurveDesc& getCurve(uint32_t curveID) const { return mCurveDesc[curveID]; } + + /** Get the number of curve instances. + */ + uint32_t getCurveInstanceCount() const { return (uint32_t)mCurveInstanceData.size(); } + + /** Get a curve instance desc. + */ + const CurveInstanceData& getCurveInstance(uint32_t instanceID) const { return mCurveInstanceData[instanceID]; } + /** Get a list of all materials in the scene. */ const std::vector& getMaterials() const { return mMaterials; } - /** Get the number of materials in the scene + /** Get the number of materials in the scene. */ uint32_t getMaterialCount() const { return (uint32_t)mMaterials.size(); } - /** Get a material + /** Get a material. */ const Material::SharedPtr& getMaterial(uint32_t materialID) const { return mMaterials[materialID]; } - /** Get a material by name + /** Get a material by name. */ Material::SharedPtr getMaterialByName(const std::string& name) const; - /** Get the scene bounds + /** Get a list of all volumes in the scene. + */ + const std::vector& getVolumes() const { return mVolumes; } + + /** Get a volume. */ - const BoundingBox& getSceneBounds() const { return mSceneBB; } + const Volume::SharedPtr& getVolume(uint32_t volumeID) const { return mVolumes[volumeID]; } - /** Get a mesh's bounds + /** Get a volume by name. */ - const BoundingBox& getMeshBounds(uint32_t meshID) const { return mMeshBBs[meshID]; } + Volume::SharedPtr getVolumeByName(const std::string& name) const; - /** Get the number of lights in the scene + /** Get the hit info requirements. + */ + const HitInfo& getHitInfo() const { return mHitInfo; } + + /** Get the scene bounds in world space. + */ + const AABB& getSceneBounds() const { return mSceneBB; } + + /** Get a mesh's bounds in object space. + */ + const AABB& getMeshBounds(uint32_t meshID) const { return mMeshBBs[meshID]; } + + /** Get a curve's bounds in object space. + */ + const AABB& getCurveBounds(uint32_t curveID) const { return mCurveBBs[curveID]; } + + /** Get a list of all lights in the scene. + */ + const std::vector& getLights() { return mLights; }; + + /** Get the number of lights in the scene. */ uint32_t getLightCount() const { return (uint32_t)mLights.size(); } - /** Get a light + /** Get a light. */ const Light::SharedPtr& getLight(uint32_t lightID) const { return mLights[lightID]; } - /** Get a light by name + /** Get a light by name. */ Light::SharedPtr getLightByName(const std::string& name) const; @@ -317,17 +466,8 @@ namespace Falcor */ const EnvMap::SharedPtr& getEnvMap() const { return mpEnvMap; } - /** Get the light probe or nullptr if it doesn't exist. - */ - const LightProbe::SharedPtr& getLightProbe() const { return mpLightProbe; } - - /** Toggle whether the specified light is animated. - */ - deprecate("4.0.2", "Use Light::setIsAnimated() instead.") - void toggleLightAnimation(int index, bool active) { mLights[index]->setIsAnimated(active); } - /** Set how the scene's TLASes are updated when raytracing. - TLASes are REBUILT by default + TLASes are REBUILT by default. */ void setTlasUpdateMode(UpdateMode mode) { mTlasUpdateMode = mode; } @@ -336,7 +476,7 @@ namespace Falcor UpdateMode getTlasUpdateMode() { return mTlasUpdateMode; } /** Set how the scene's BLASes are updated when raytracing. - BLASes are REFIT by default + BLASes are REFIT by default. */ void setBlasUpdateMode(UpdateMode mode); @@ -350,31 +490,40 @@ namespace Falcor */ UpdateFlags update(RenderContext* pContext, double currentTime); - /** Get the changes that happened during the last update + /** Get the changes that happened during the last update. The flags only change during an `update()` call, if something changed between calling `update()` and `getUpdates()`, the returned result will not reflect it */ UpdateFlags getUpdates() const { return mUpdates; } - /** Render the scene using the rasterizer + /** Render the scene using the rasterizer. */ - void render(RenderContext* pContext, GraphicsState* pState, GraphicsVars* pVars, RenderFlags flags = RenderFlags::None); + void rasterize(RenderContext* pContext, GraphicsState* pState, GraphicsVars* pVars, RenderFlags flags = RenderFlags::None); - /** Render the scene using raytracing + /** Render the scene using raytracing. */ void raytrace(RenderContext* pContext, RtProgram* pProgram, const std::shared_ptr& pVars, uint3 dispatchDims); - /** Render the UI + /** Render the UI. */ void renderUI(Gui::Widgets& widget); - /** Bind a sampler to the materials + /** Bind a sampler to the materials. */ void bindSamplerToMaterials(const Sampler::SharedPtr& pSampler); - /** Get the scene's VAO + /** Get the scene's VAO. + The default VAO uses 32-bit vertex indices. For meshes with 16-bit indices, use getVao16() instead. */ const Vao::SharedPtr& getVao() const { return mpVao; } + /** Get the scene's VAO for 16-bit vertex indices. + */ + const Vao::SharedPtr& getVao16() const { return mpVao16Bit; } + + /** Get the scene's VAO for curves. + */ + const Vao::SharedPtr& getCurveVao() const { return mpCurveVao; } + /** Set an environment map. \param[in] pEnvMap Environment map. Can be nullptr. */ @@ -385,15 +534,15 @@ namespace Falcor */ void loadEnvMap(const std::string& filename); - /** Handle mouse events + /** Handle mouse events. */ bool onMouseEvent(const MouseEvent& mouseEvent); - /** Handle keyboard events + /** Handle keyboard events. */ bool onKeyEvent(const KeyboardEvent& keyEvent); - /** Get the filename that the scene was loaded from + /** Get the filename that the scene was loaded from. */ const std::string& getFilename() const { return mFilename; } @@ -401,6 +550,10 @@ namespace Falcor */ const AnimationController* getAnimationController() const { return mpAnimationController.get(); } + /** Get the scene's animations. + */ + std::vector& getAnimations() { return mpAnimationController->getAnimations(); } + /** Returns true if scene has animation data. */ bool hasAnimation() const { return mpAnimationController->hasAnimations(); } @@ -413,6 +566,14 @@ namespace Falcor */ bool isAnimated() const { return mpAnimationController->isEnabled(); }; + /** Enable/disable global animation looping. + */ + void setIsLooped(bool looped) { mpAnimationController->setIsLooped(looped); } + + /** Returns true if scene animations are looped globally. + */ + bool isLooped() { return mpAnimationController->isLooped(); } + /** Toggle all animations on or off. */ void toggleAnimations(bool animate); @@ -435,6 +596,20 @@ namespace Falcor */ void setRaytracingShaderData(RenderContext* pContext, const ShaderVar& var, uint32_t rayTypeCount = 1); + /** Get the name of the mesh with the given ID. + */ + std::string getMeshName(uint32_t meshID) const { assert(meshID < mMeshNames.size()); return mMeshNames[meshID]; } + + /** Return true if the given mesh ID is valid, false otherwise. + */ + bool hasMesh(uint32_t meshID) const { return meshID < mMeshNames.size(); } + + /** Get a list of raytracing BLAS IDs for all meshes. The list is arranged by mesh ID. + */ + std::vector getMeshBlasIDs() const; + + static void nullTracePass(RenderContext* pContext, const uint2& dim); + std::string getScript(const std::string& sceneVar); private: @@ -442,17 +617,16 @@ namespace Falcor friend class AnimationController; static constexpr uint32_t kStaticDataBufferIndex = 0; - static constexpr uint32_t kPrevVertexBufferIndex = kStaticDataBufferIndex + 1; - static constexpr uint32_t kDrawIdBufferIndex = kPrevVertexBufferIndex + 1; + static constexpr uint32_t kDrawIdBufferIndex = kStaticDataBufferIndex + 1; static constexpr uint32_t kVertexBufferCount = kDrawIdBufferIndex + 1; static SharedPtr create(); - /** Create scene parameter block and retrieve pointers to buffers + /** Create scene parameter block and retrieve pointers to buffers. */ void initResources(); - /** Uploads scene data to parameter block + /** Uploads scene data to parameter block. */ void uploadResources(); @@ -472,33 +646,45 @@ namespace Falcor */ void updateMeshInstances(bool forceUpdate); + /** Update procedural primitives. + */ + void updateProceduralPrimitives(bool forceUpdate); + + /** Update curve instances. + */ + void updateCurveInstances(bool forceUpdate); + /** Do any additional initialization required after scene data is set and draw lists are determined. */ void finalize(); - /** Create the draw list for rasterization + /** Create the draw list for rasterization. */ void createDrawList(); - /** Sort meshes into groups by transform. Updates mMeshInstances and mMeshGroups. + /** Initialize geometry descs for each BLAS. + */ + void initGeomDesc(RenderContext* pContext); + + /** Initialize pre-build information for each BLAS. */ - void sortMeshes(); + void preparePrebuildInfo(RenderContext* pContext); - /** Initialize geometry descs for each BLAS + /** Compute BLAS groups. */ - void initGeomDesc(); + void computeBlasGroups(); - /** Generate bottom level acceleration structures for all meshes + /** Generate bottom level acceleration structures for all meshes. */ void buildBlas(RenderContext* pContext); /** Generate data for creating a TLAS. - #SCENE TODO: Add argument to build descs based off a draw list + #SCENE TODO: Add argument to build descs based off a draw list. */ void fillInstanceDesc(std::vector& instanceDescs, uint32_t rayCount, bool perMeshHitEntry) const; /** Generate top level acceleration structure for the scene. Automatically determines whether to build or refit. - \param[in] rayCount Number of ray types in the shader. Required to setup how instances index into the Shader Table + \param[in] rayCount Number of ray types in the shader. Required to setup how instances index into the Shader Table. */ void buildTlas(RenderContext* pContext, uint32_t rayCount, bool perMeshHitEntry); @@ -520,45 +706,28 @@ namespace Falcor UpdateFlags updateSelectedCamera(bool forceUpdate); UpdateFlags updateLights(bool forceUpdate); + UpdateFlags updateVolumes(bool forceUpdate); UpdateFlags updateEnvMap(bool forceUpdate); UpdateFlags updateMaterials(bool forceUpdate); void updateGeometryStats(); - void updateRaytracingStats(); + void updateMaterialStats(); + void updateRaytracingBLASStats(); + void updateRaytracingTLASStats(); void updateLightStats(); - - struct SceneStats - { - // Geometry stats - size_t uniqueTriangleCount = 0; ///< Number of unique triangles. A triangle can exist in multiple instances. - size_t uniqueVertexCount = 0; ///< Number of unique vertices. A vertex can be referenced by multiple triangles/instances. - size_t instancedTriangleCount = 0; ///< Number of instanced triangles. This is the total number of rendered triangles. - size_t instancedVertexCount = 0; ///< Number of instanced vertices. This is the total number of vertices in the rendered triangles. - - // Raytracing stats - size_t blasCount = 0; ///< Number of BLASes. - size_t blasCompactedCount = 0; ///< Number of compacted BLASes. - size_t blasMemoryInBytes = 0; ///< Total memory in bytes used by the BLASes. - - // Light stats - size_t activeLightCount = 0; ///< Number of active lights. - size_t totalLightCount = 0; ///< Number of lights in the scene. - size_t pointLightCount = 0; ///< Number of point lights. - size_t directionalLightCount = 0; ///< Number of directional lights. - size_t rectLightCount = 0; ///< Number of rect lights. - size_t sphereLightCount = 0; ///< Number of sphere lights. - size_t distantLightCount = 0; ///< Number of distant lights. - }; + void updateVolumeStats(); Scene(); // Scene Geometry - Vao::SharedPtr mpVao; + struct DrawArgs { - Buffer::SharedPtr pBuffer; - uint32_t count = 0; - } mDrawClockwiseMeshes, mDrawCounterClockwiseMeshes; + Buffer::SharedPtr pBuffer; ///< Buffer holding the draw-indirect arguments. + uint32_t count = 0; ///< Number of draws. + bool ccw = true; ///< True if counterclockwise triangle winding. + ResourceFormat ibFormat = ResourceFormat::Unknown; ///< Index buffer format. + }; static const uint32_t kInvalidNode = -1; @@ -568,42 +737,81 @@ namespace Falcor Node(const std::string& n, uint32_t p, const glm::mat4& t, const glm::mat4& l2b) : parent(p), name(n), transform(t), localToBindSpace(l2b) {}; std::string name; uint32_t parent = kInvalidNode; - glm::mat4 transform; // The node's transformation matrix - glm::mat4 localToBindSpace; // Local to bind space transformation + glm::mat4 transform; ///< The node's transformation matrix. + glm::mat4 localToBindSpace; ///< Local to bind space transformation. }; + /** Represents a group of meshes. + The meshes are geometries in the same ray tracing bottom-level acceleration structure (BLAS). + */ struct MeshGroup { std::vector meshList; ///< List of meshId's that are part of the group. + bool isStatic = false; ///< True if group represents static non-instanced geometry. }; - // #SCENE We don't need those vectors on the host - std::vector mMeshDesc; ///< Copy of mesh data GPU buffer (mpMeshes) - std::vector mMeshInstanceData; ///< Mesh instance data. - std::vector mPackedMeshInstanceData;///< Copy of packed mesh instance data GPU buffer (mpMeshInstances) - std::vector mMeshGroups; ///< Groups of meshes with identical transforms. Each group maps to a BLAS for ray tracing. - std::vector mSceneGraph; ///< For each index i, the array element indicates the parent node. Indices are in relation to mLocalToWorldMatrices + Vao::SharedPtr mpVao; ///< Vertex array object for the global vertex/index buffers. + Vao::SharedPtr mpVao16Bit; ///< VAO for drawing meshes with 16-bit vertex indices. + std::vector mDrawArgs; ///< List of draw arguments for rasterizing the scene. + + Vao::SharedPtr mpCurveVao; ///< Vertex array object for the global curve vertex/index buffers. - std::vector mMaterials; ///< Bound to parameter block - std::vector mLights; ///< Bound to parameter block - LightCollection::SharedPtr mpLightCollection; ///< Bound to parameter block - LightProbe::SharedPtr mpLightProbe; ///< Bound to parameter block - EnvMap::SharedPtr mpEnvMap; ///< Bound to parameter block + std::vector mMeshDesc; ///< Copy of mesh data GPU buffer (mpMeshes). + std::vector mMeshInstanceData; ///< Mesh instance data. + std::vector mPackedMeshInstanceData;///< Copy of packed mesh instance data GPU buffer (mpMeshInstances). + std::vector mProceduralPrimData; ///< Procedural intersection AABB index data (offset, count) including all primitive types (custom primitives, curves, etc.). + std::vector mCustomPrimitiveAABBs; ///< User-defined custom primitive AABBs. + std::vector mCurveDesc; ///< Copy of curve data GPU buffer (mpCurves). + std::vector mCurveInstanceData; ///< Curve instance data. + std::vector mMeshGroups; ///< Groups of meshes. Each group maps to a BLAS for ray tracing. + std::vector mMeshNames; ///< Mesh names, indxed by mesh ID + std::vector mSceneGraph; ///< For each index i, the array element indicates the parent node. Indices are in relation to mLocalToWorldMatrices. + + /** The following array and buffer records the AABBs of all procedural primitives, including custom primitives, curves, etc. + There is an implicit type conversion from D3D12_RAYTRACING_AABB to AABB (defined in Utils.Math.AABB). + It is fine because both structs have the same data layout. + */ + std::vector mRtAABBRaw; ///< Raw AABB data (min, max). + Buffer::SharedPtr mpRtAABBBuffer; ///< GPU Buffer of raw AABB data. Used for acceleration structure creation, and bound to the Scene for access in shaders. + + bool mHas16BitIndices = false; ///< True if any meshes use 16-bit indices. + bool mHas32BitIndices = false; ///< True if any meshes use 32-bit indices. + + // Materials + std::vector mMaterials; ///< Bound to parameter block. + std::vector mSortedMaterialIndices; ///< Indices of materials, sorted alphabetically by case-insensitive name + bool mSortMaterialsByName = false; ///< If true, display materials sorted by name, rather than by ID + + // Lights + std::vector mLights; ///< Bound to parameter block. + std::vector mVolumes; ///< Bound to parameter block. + std::vector mGrids; ///< Bound to parameter block. + std::unordered_map mGridIDs; + LightCollection::SharedPtr mpLightCollection; ///< Bound to parameter block. + EnvMap::SharedPtr mpEnvMap; ///< Bound to parameter block. + bool mEnvMapChanged = false; // Scene Metadata (CPU Only) - std::vector mMeshBBs; ///< Bounding boxes for meshes (not instances) - std::vector> mMeshIdToInstanceIds; ///< Mapping of what instances belong to which mesh - BoundingBox mSceneBB; ///< Bounding boxes of the entire scene - std::vector mMeshHasDynamicData; ///< Whether a Mesh has dynamic data, meaning it is skinned + std::vector mMeshBBs; ///< Bounding boxes for meshes (not instances) in object space. + std::vector> mMeshIdToInstanceIds; ///< Mapping of what instances belong to which mesh. + std::vector mCurveBBs; ///< Bounding boxes for curves (not instances) in object space. + std::vector> mCurveIdToInstanceIds; ///< Mapping of what instances belong to which curve. + HitInfo mHitInfo; ///< Geometry hit info requirements. + AABB mSceneBB; ///< Bounding boxes of the entire scene in world space. + std::vector mMeshHasDynamicData; ///< Whether a Mesh has dynamic data, meaning it is skinned. SceneStats mSceneStats; ///< Scene statistics. RenderSettings mRenderSettings; ///< Render settings. RenderSettings mPrevRenderSettings; - // Resources + // Scene Block Resources Buffer::SharedPtr mpMeshesBuffer; Buffer::SharedPtr mpMeshInstancesBuffer; + Buffer::SharedPtr mpProceduralPrimitivesBuffer; + Buffer::SharedPtr mpCurvesBuffer; + Buffer::SharedPtr mpCurveInstancesBuffer; Buffer::SharedPtr mpMaterialsBuffer; Buffer::SharedPtr mpLightsBuffer; + Buffer::SharedPtr mpVolumesBuffer; ParameterBlock::SharedPtr mpSceneBlock; // Camera @@ -633,44 +841,67 @@ namespace Falcor AnimationController::UniquePtr mpAnimationController; // Raytracing Data - UpdateMode mTlasUpdateMode = UpdateMode::Rebuild; ///< How the TLAS should be updated when there are changes in the scene - UpdateMode mBlasUpdateMode = UpdateMode::Refit; ///< How the BLAS should be updated when there are changes to meshes + UpdateMode mTlasUpdateMode = UpdateMode::Rebuild; ///< How the TLAS should be updated when there are changes in the scene. + UpdateMode mBlasUpdateMode = UpdateMode::Refit; ///< How the BLAS should be updated when there are changes to meshes. - std::vector mInstanceDescs; ///< Shared between TLAS builds to avoid reallocating CPU memory + std::vector mInstanceDescs; ///< Shared between TLAS builds to avoid reallocating CPU memory. struct TlasData { Buffer::SharedPtr pTlas; - ShaderResourceView::SharedPtr pSrv; ///< Shader Resource View for binding the TLAS - Buffer::SharedPtr pInstanceDescs; ///< Buffer holding instance descs for the TLAS + ShaderResourceView::SharedPtr pSrv; ///< Shader Resource View for binding the TLAS. + Buffer::SharedPtr pInstanceDescs; ///< Buffer holding instance descs for the TLAS. UpdateMode updateMode = UpdateMode::Rebuild; ///< Update mode this TLAS was created with. }; - std::unordered_map mTlasCache; ///< Top Level Acceleration Structure for scene data cached per shader ray count - ///< Number of ray types in program affects Shader Table indexing + std::unordered_map mTlasCache; ///< Top Level Acceleration Structure for scene data cached per shader ray count. + ///< Number of ray types in program affects Shader Table indexing. Buffer::SharedPtr mpTlasScratch; ///< Scratch buffer used for TLAS builds. Can be shared as long as instance desc count is the same, which for now it is. D3D12_RAYTRACING_ACCELERATION_STRUCTURE_PREBUILD_INFO mTlasPrebuildInfo; ///< This can be reused as long as the number of instance descs doesn't change. + /** Describes one BLAS. + */ struct BlasData { D3D12_RAYTRACING_ACCELERATION_STRUCTURE_PREBUILD_INFO prebuildInfo; D3D12_BUILD_RAYTRACING_ACCELERATION_STRUCTURE_INPUTS buildInputs; std::vector geomDescs; - uint64_t blasByteSize = 0; ///< Size of the final BLAS. - uint64_t blasByteOffset = 0; ///< Offset into the BLAS buffer to where it is stored. - uint64_t scratchByteOffset = 0; ///< Offset into the scratch buffer to use for updates/rebuilds. + uint32_t blasGroupIndex = 0; ///< Index of the BLAS group that contains this BLAS. + + uint64_t resultByteSize = 0; ///< Maximum result data size for the BLAS build, including padding. + uint64_t resultByteOffset = 0; ///< Offset into the BLAS result buffer. + uint64_t scratchByteSize = 0; ///< Maximum scratch data size for the BLAS build, including padding. + uint64_t scratchByteOffset = 0; ///< Offset into the BLAS scratch buffer. + + uint64_t blasByteSize = 0; ///< Size of the final BLAS post-compaction, including padding. + uint64_t blasByteOffset = 0; ///< Offset into the final BLAS buffer. bool hasSkinnedMesh = false; ///< Whether the BLAS contains a skinned mesh, which means the BLAS may need to be updated. bool useCompaction = false; ///< Whether the BLAS should be compacted after build. UpdateMode updateMode = UpdateMode::Refit; ///< Update mode this BLAS was created with. }; - std::vector mBlasData; ///< All data related to the scene's BLASes. - Buffer::SharedPtr mpBlas; ///< Buffer containing all BLASes. - Buffer::SharedPtr mpBlasScratch; ///< Scratch buffer used for BLAS builds. - bool mRebuildBlas = true; ///< Flag to indicate BLASes need to be rebuilt. - bool mHasSkinnedMesh = false; ///< Whether the scene has a skinned mesh at all. + /** Describes a group of BLASes. + */ + struct BlasGroup + { + std::vector blasIndices; ///< Indices of all BLASes in the group. + + uint64_t resultByteSize = 0; ///< Maximum result data size for all BLASes in the group, including padding. + uint64_t scratchByteSize = 0; ///< Maximum scratch data size for all BLASes in the group, including padding. + uint64_t finalByteSize = 0; ///< Size of the final BLASes in the group post-compaction, including padding. + + Buffer::SharedPtr pBlas; ///< Buffer containing all final BLASes in the group. + }; + + // BLAS Data is ordered as all mesh BLAS's first, followed by one BLAS containing all AABBs. + std::vector mBlasData; ///< All data related to the scene's BLASes. + std::vector mBlasGroups; ///< BLAS group data. + Buffer::SharedPtr mpBlasScratch; ///< Scratch buffer used for BLAS builds. + Buffer::SharedPtr mpBlasStaticWorldMatrices; ///< Object-to-world transform matrices in row-major format. Only valid for static meshes. + bool mRebuildBlas = true; ///< Flag to indicate BLASes need to be rebuilt. + bool mHasSkinnedMesh = false; ///< Whether the scene has a skinned mesh at all. std::string mFilename; }; diff --git a/Source/Falcor/Scene/Scene.slang b/Source/Falcor/Scene/Scene.slang index cb748942cb..e5395e1e3c 100644 --- a/Source/Falcor/Scene/Scene.slang +++ b/Source/Falcor/Scene/Scene.slang @@ -30,20 +30,22 @@ __exported import Scene.SceneTypes; __exported import Scene.Camera.Camera; __exported import Scene.Lights.LightData; -__exported import Scene.Lights.LightProbeData; __exported import Scene.Material.MaterialData; +__exported import Scene.Volume.Volume; +__exported import Scene.Volume.Grid; import HitInfo; import TextureSampler; import Utils.Attributes; +import Utils.Math.MathHelpers; import Experimental.Scene.Lights.LightCollection; import Experimental.Scene.Lights.EnvMap; import Experimental.Scene.Material.TexLODHelpers; -#ifndef MATERIAL_COUNT +#ifndef SCENE_MATERIAL_COUNT // This error occurs when a shader imports Scene.slang without setting the defines // returned by Scene::getSceneDefines(). -#error "MATERIAL_COUNT not defined!" +#error "SCENE_MATERIAL_COUNT not defined!" #endif /** Data required for rendering @@ -59,23 +61,46 @@ struct Scene StructuredBuffer previousFrameWorldMatrices; [root] StructuredBuffer vertices; ///< Vertex data for this frame. - StructuredBuffer prevVertices; ///< Vertex data for the previous frame, to handle skinned meshes. -#if INDEXED_VERTICES - [root] ByteAddressBuffer indices; ///< Vertex indices, three 32-bit indices per triangle packed tightly. + StructuredBuffer prevVertices; ///< Vertex data for the previous frame, for dynamic meshes only. +#if SCENE_HAS_INDEXED_VERTICES + [root] ByteAddressBuffer indexData; ///< Vertex indices, three indices per triangle packed tightly. The format is specified per mesh. #endif + // Custom primitives + StructuredBuffer proceduralPrimitives; ///< Metadata for procedural primtive definitions. Each can refer to multiple AABBs. + StructuredBuffer proceduralPrimitiveAABBs; ///< Global AABBs for procedural primitives. + + // Curves + StructuredBuffer curveInstances; + StructuredBuffer curves; + + StructuredBuffer curveVertices; + ByteAddressBuffer curveIndices; + StructuredBuffer curvePrevVertices; + // Materials StructuredBuffer materials; - MaterialResources materialResources[MATERIAL_COUNT]; + MaterialResources materialResources[SCENE_MATERIAL_COUNT]; // Lights and camera uint lightCount; StructuredBuffer lights; LightCollection lightCollection; - LightProbeData lightProbe; EnvMap envMap; Camera camera; + // Volumes + uint volumeCount; + StructuredBuffer volumes; +#if SCENE_GRID_COUNT > 0 + Grid grids[SCENE_GRID_COUNT]; +#else + Grid grids[1]; // Zero-length arrays are not supported. +#endif + + + // Mesh and instance data access + float4x4 loadWorldMatrix(uint matrixID) { float4x4 m = @@ -125,19 +150,19 @@ struct Scene return m; }; - uint getMaterialID(uint meshInstanceID) + bool isWorldMatrixFlippedWinding(uint meshInstanceID) { - return meshInstances[meshInstanceID].materialID; - }; + return (meshInstances[meshInstanceID].unpack().flags & uint(MeshInstanceFlags::TransformFlipped)) != 0; + } - uint getMaterialCount() + bool isObjectFrontFaceCW(uint meshInstanceID) { - return MATERIAL_COUNT; + return (meshInstances[meshInstanceID].unpack().flags & uint(MeshInstanceFlags::IsObjectFrontFaceCW)) != 0; } - MaterialData getMaterial(uint materialID) + bool isWorldFrontFaceCW(uint meshInstanceID) { - return materials[materialID]; + return (meshInstances[meshInstanceID].unpack().flags & uint(MeshInstanceFlags::IsWorldFrontFaceCW)) != 0; } MeshInstanceData getMeshInstance(uint meshInstanceID) @@ -150,6 +175,28 @@ struct Scene return meshes[meshInstances[meshInstanceID].unpack().meshID]; } + // Materials and lights access + + uint getMaterialID(uint meshInstanceID) + { + return meshInstances[meshInstanceID].unpack().materialID; + }; + + uint getCurveMaterialID(uint curveInstanceID) + { + return curveInstances[curveInstanceID].materialID; + } + + uint getMaterialCount() + { + return SCENE_MATERIAL_COUNT; + } + + MaterialData getMaterial(uint materialID) + { + return materials[materialID]; + } + uint getLightCount() { return lightCount; @@ -160,29 +207,77 @@ struct Scene return lights[lightIndex]; } - bool isWorldMatrixFlippedWinding(uint meshInstanceID) + // Volume access + + uint getGridCount() { - return (meshInstances[meshInstanceID].unpack().flags & uint(MeshInstanceFlags::Flipped)) != 0; + return SCENE_GRID_COUNT; } + void getGrid(uint gridIndex, out Grid grid) + { + grid = grids[gridIndex]; + } + + uint getVolumeCount() + { + return volumeCount; + } + + Volume getVolume(uint volumeIndex) + { + return volumes[volumeIndex]; + } // Geometry access - /** Returns the global vertex indices for a given triangle. + /** Returns the local vertex indices for a given triangle. \param[in] meshInstanceID The mesh instance ID. \param[in] triangleIndex Index of the triangle in the given mesh. - \return Vertex indices into the global vertex buffer. + \return Vertex indices local to the current mesh. */ - uint3 getIndices(uint meshInstanceID, uint triangleIndex) - { -#if INDEXED_VERTICES - uint baseIndex = meshInstances[meshInstanceID].ibOffset + (triangleIndex * 3); - uint3 vtxIndices = indices.Load3(baseIndex * 4); + uint3 getLocalIndices(uint meshInstanceID, uint triangleIndex) + { + const MeshInstanceData meshInstance = getMeshInstance(meshInstanceID); +#if SCENE_HAS_INDEXED_VERTICES + // Determine what format of the indices. + // It's only if the scene has mixed formats that we incur the cost of checking the flag bit. +#if SCENE_HAS_16BIT_INDICES && SCENE_HAS_32BIT_INDICES + bool use16Bit = meshInstance.flags & uint(MeshInstanceFlags::Use16BitIndices); +#elif SCENE_HAS_16BIT_INDICES + bool use16Bit = true; #else + bool use16Bit = false; +#endif + // Load the vertex indices. + uint baseIndex = meshInstance.ibOffset * 4; + uint3 vtxIndices; + if (use16Bit) + { + baseIndex += triangleIndex * 6; + vtxIndices = (uint3)indexData.Load(baseIndex); + } + else + { + baseIndex += triangleIndex * 12; + vtxIndices = indexData.Load3(baseIndex); + } +#else // !SCENE_HAS_INDEXED_VERTICES uint baseIndex = triangleIndex * 3; uint3 vtxIndices = { baseIndex, baseIndex + 1, baseIndex + 2 }; #endif - vtxIndices += meshInstances[meshInstanceID].vbOffset; + return vtxIndices; + } + + /** Returns the global vertex indices for a given triangle. + \param[in] meshInstanceID The mesh instance ID. + \param[in] triangleIndex Index of the triangle in the given mesh. + \return Vertex indices into the global vertex buffer. + */ + uint3 getIndices(uint meshInstanceID, uint triangleIndex) + { + uint3 vtxIndices = getLocalIndices(meshInstanceID, triangleIndex); + vtxIndices += getMeshInstance(meshInstanceID).vbOffset; return vtxIndices; } @@ -197,14 +292,16 @@ struct Scene /** Returns a triangle's face normal in object space. \param[in] vtxIndices Indices into the scene's global vertex buffer. - \param[out] Face normal in object space (normalized). Front facing for counter-clockwise winding. + \param[in] isFrontFaceCW True if front-facing side has clockwise winding in object space. + \param[out] Face normal in object space (normalized). */ - float3 getFaceNormalInObjectSpace(uint3 vtxIndices) + float3 getFaceNormalInObjectSpace(uint3 vtxIndices, bool isFrontFaceCW) { float3 p0 = vertices[vtxIndices[0]].position; float3 p1 = vertices[vtxIndices[1]].position; float3 p2 = vertices[vtxIndices[2]].position; - return normalize(cross(p1 - p0, p2 - p0)); + float3 N = normalize(cross(p1 - p0, p2 - p0)); + return isFrontFaceCW ? -N : N; } /** Returns a triangle's face normal in world space. @@ -219,6 +316,7 @@ struct Scene float3 p1 = vertices[vtxIndices[1]].position; float3 p2 = vertices[vtxIndices[2]].position; float3 N = cross(p1 - p0, p2 - p0); + if (isObjectFrontFaceCW(meshInstanceID)) N = -N; float3x3 worldInvTransposeMat = getInverseTransposeWorldMatrix(meshInstanceID); return normalize(mul(N, worldInvTransposeMat)); } @@ -252,8 +350,8 @@ struct Scene float3 N = cross(e[0], e[1]); triangleArea = 0.5f * length(N); - // Flip the normal if the instance transform changed the handedness of the coordinate system. - if (isWorldMatrixFlippedWinding(meshInstanceID)) N = -N; + // Flip the normal depending on final winding order in world space. + if (isWorldFrontFaceCW(meshInstanceID)) N = -N; return normalize(N); } @@ -312,7 +410,7 @@ struct Scene v.texC += vertices[1].texCrd * barycentrics[1]; v.texC += vertices[2].texCrd * barycentrics[2]; - v.faceNormalW = getFaceNormalInObjectSpace(vtxIndices); + v.faceNormalW = getFaceNormalInObjectSpace(vtxIndices, isObjectFrontFaceCW(meshInstanceID)); float4x4 worldMat = getWorldMatrix(meshInstanceID); float3x3 worldInvTransposeMat = getInverseTransposeWorldMatrix(meshInstanceID); @@ -324,8 +422,7 @@ struct Scene v.normalW = normalize(v.normalW); v.faceNormalW = normalize(v.faceNormalW); - // Handle invalid tangents gracefully (avoid NaN from normalization). - v.tangentW.xyz = v.tangentW.w != 0.f ? normalize(v.tangentW.xyz) : float3(0, 0, 0); + v.tangentW.xyz = normalize(v.tangentW.xyz); return v; } @@ -347,7 +444,7 @@ struct Scene */ VertexData getVertexData(HitInfo hit) { - return getVertexData(hit.meshInstanceID, hit.primitiveIndex, hit.getBarycentricWeights()); + return getVertexData(hit.instanceID, hit.primitiveIndex, hit.getBarycentricWeights()); } /** Returns the interpolated vertex attributes for a given hitpoint. @@ -357,7 +454,7 @@ struct Scene */ VertexData getVertexData(HitInfo hit, out StaticVertexData vertices[3]) { - return getVertexData(hit.meshInstanceID, hit.primitiveIndex, hit.getBarycentricWeights(), vertices); + return getVertexData(hit.instanceID, hit.primitiveIndex, hit.getBarycentricWeights(), vertices); } /** Returns interpolated vertex attributes in a ray tracing hit program when ray cones are used for texture LOD. @@ -382,13 +479,27 @@ struct Scene */ float3 getPrevPosW(uint meshInstanceID, uint triangleIndex, float3 barycentrics) { - const uint3 vtxIndices = getIndices(meshInstanceID, triangleIndex); float3 prevPos = float3(0, 0, 0); - [unroll] - for (int i = 0; i < 3; i++) + MeshInstanceData meshInstance = getMeshInstance(meshInstanceID); + if (meshInstance.hasDynamicData()) { - prevPos += prevVertices[vtxIndices[i]].position * barycentrics[i]; + // For dynamic meshes, the previous position is stored in a separate buffer. + uint3 vtxIndices = getLocalIndices(meshInstanceID, triangleIndex); + vtxIndices += meshes[meshInstance.meshID].dynamicVbOffset; + + prevPos += prevVertices[vtxIndices[0]].position * barycentrics[0]; + prevPos += prevVertices[vtxIndices[1]].position * barycentrics[1]; + prevPos += prevVertices[vtxIndices[2]].position * barycentrics[2]; + } + else + { + // For non-dynamic meshes, the previous positions are the same as the current. + uint3 vtxIndices = getIndices(meshInstanceID, triangleIndex); + + prevPos += vertices[vtxIndices[0]].position * barycentrics[0]; + prevPos += vertices[vtxIndices[1]].position * barycentrics[1]; + prevPos += vertices[vtxIndices[2]].position * barycentrics[2]; } float4x4 prevWorldMat = getPrevWorldMatrix(meshInstanceID); @@ -429,6 +540,112 @@ struct Scene } } + // Procedural Primitive AABB access + + AABB getProceduralPrimitiveAABB(uint instanceID, uint geometryIndex) + { + uint globalHitID = instanceID + geometryIndex; + ProceduralPrimitiveData primitive = proceduralPrimitives[globalHitID]; + return proceduralPrimitiveAABBs[primitive.AABBOffset]; + } + + // Curve access + + uint getCurveInstanceID(uint instanceID, uint geometryIndex) + { + uint globalHitID = instanceID + geometryIndex; + return proceduralPrimitives[globalHitID].instanceIdx; + } + + CurveInstanceData getCurveInstance(uint curveInstanceID) + { + return curveInstances[curveInstanceID]; + } + + CurveDesc getCurveDesc(uint curveInstanceID) + { + return curves[curveInstances[curveInstanceID].curveID]; + } + + float4x4 getWorldMatrixForCurves(uint curveInstanceID) + { + uint matrixID = curveInstances[curveInstanceID].globalMatrixID; + return loadWorldMatrix(matrixID); + }; + + /** Returns the global curve vertex indices for the first control point of a curve segment. + \param[in] curveInstanceID The curve instance ID. + \param[in] curgeSegIndex Index of the curve segment in the given curve. + \return Index of the first control point into the global curve vertex buffer. + */ + uint getFirstCurveVertexIndex(uint curveInstanceID, uint curveSegIndex) + { + uint baseIndex = curveInstances[curveInstanceID].ibOffset + curveSegIndex; + uint vertexIndex = curveIndices.Load(baseIndex * 4); + vertexIndex += curveInstances[curveInstanceID].vbOffset; + return vertexIndex; + } + + /** Returns curve vertex data. + \param[in] index Global curve vertex index. + \return Curve vertex data. + */ + StaticCurveVertexData getCurveVertex(uint index) + { + return curveVertices[index]; + } + + /** Returns the interpolated vertex attributes for a curve segment. + \param[in] curveInstanceID The curve instance ID. + \param[in] curveSegIndex Index of the curve segment in the given curve. + \param[in] u Parameter u (between 0 and 1) interpolating the end points. + \param[in] posW Intersection position in the world space. + \param[out] radius Sphere radius at the intersection. + \return Interpolated vertex attributes. + */ + VertexData getVertexDataFromCurve(uint curveInstanceID, uint curveSegIndex, float u, float3 posW, out float radius) + { + const uint v0Index = getFirstCurveVertexIndex(curveInstanceID, curveSegIndex); + VertexData v = {}; + + StaticCurveVertexData vertices[2] = { getCurveVertex(v0Index), getCurveVertex(v0Index + 1) }; + + // Note that worldMat should not have a scale component. + // Otherwise the curve endcaps might not be spheres. + const float4x4 worldMat = getWorldMatrixForCurves(curveInstanceID); + + const float3 center = vertices[0].position * (1.f - u) + vertices[1].position * u; + const float3 centerW = mul(float4(center, 1.f), worldMat).xyz; + + radius = vertices[0].radius * (1.f - u) + vertices[1].radius * u; + + v.normalW = normalize(posW - centerW); + v.faceNormalW = v.normalW; + + // To avoid numerical issues, reprojecting from posW = rayOrigin + t * rayDir. + v.posW = centerW + radius * v.normalW; + + const float3 dir01 = vertices[1].position - vertices[0].position; + const float3 dir01W = mul(dir01, (float3x3)worldMat); + v.tangentW = float4(normalize(dir01W), 1.f); + + // All curve segments in a strand share the same texture coordinates (i.e., determined at the root of the strand). + v.texC = vertices[0].texCrd * (1.f - u) + vertices[1].texCrd * u; + + return v; + } + + /** Returns the interpolated vertex attributes for a given hitpoint on curves. + \param[in] hit Hit info. + \param[in] posW Intersection position in the world space. + \param[out] radius Sphere radius at the intersection. + \return Interpolated vertex attributes. + */ + VertexData getVertexDataFromCurve(HitInfo hit, float3 posW, out float radius) + { + return getVertexDataFromCurve(hit.instanceID, hit.primitiveIndex, hit.barycentrics.x, posW, radius); + } + // Emissive access /** Check if a material has an emissive texture. diff --git a/Source/Falcor/Scene/SceneBuilder.cpp b/Source/Falcor/Scene/SceneBuilder.cpp index 6ffb8502b1..2dd2b4e134 100644 --- a/Source/Falcor/Scene/SceneBuilder.cpp +++ b/Source/Falcor/Scene/SceneBuilder.cpp @@ -30,17 +30,28 @@ #include "Importer.h" #include "Utils/Math/MathConstants.slangh" #include "Utils/Timing/TimeReport.h" -#include "../Externals/mikktspace/mikktspace.h" +#include #include namespace Falcor { namespace { + // Large mesh groups are split in order to reduce the size of the largest BLAS. + // The target is max 16M triangles per BLAS (= approx 0.5GB post-compaction). Note that this is not a strict limit. + const size_t kMaxTrianglesPerBLAS = 1ull << 24; + // Texture coordinates for textured emissive materials are quantized for performance reasons. // We'll log a warning if the maximum quantization error exceeds this value. const float kMaxTexelError = 0.5f; + int largestAxis(const float3& v) + { + if (v.x >= v.y && v.x >= v.z) return 0; + else if (v.y >= v.z) return 1; + else return 2; + } + class MikkTSpaceWrapper { public: @@ -123,9 +134,23 @@ namespace Falcor if (any(greaterThan(abs(lhs.boneWeights - rhs.boneWeights), float4(threshold)))) return false; return true; } + + std::vector compact16BitIndices(const std::vector& indices) + { + if (indices.empty()) return {}; + size_t sz = div_round_up(indices.size(), 2ull); // Storing two 16-bit indices per dword. + std::vector indexData(sz); + uint16_t* pIndices = reinterpret_cast(indexData.data()); + for (size_t i = 0; i < indices.size(); i++) + { + assert(indices[i] < (1u << 16)); + pIndices[i] = static_cast(indices[i]); + } + return indexData; + } } - SceneBuilder::SceneBuilder(Flags flags) : mFlags(flags) {}; + SceneBuilder::SceneBuilder(Flags flags) : mFlags(flags) {} SceneBuilder::SharedPtr SceneBuilder::create(Flags flags) { @@ -145,65 +170,141 @@ namespace Falcor return success; } - uint32_t SceneBuilder::addNode(const Node& node) - { - assert(node.parent == kInvalidNode || node.parent < mSceneGraph.size()); - - assert(mSceneGraph.size() <= std::numeric_limits::max()); - uint32_t newNodeID = (uint32_t)mSceneGraph.size(); - mSceneGraph.push_back(InternalNode(node)); - if(node.parent != kInvalidNode) mSceneGraph[node.parent].children.push_back(newNodeID); - mDirty = true; - return newNodeID; - } - bool SceneBuilder::isNodeAnimated(uint32_t nodeID) const + Scene::SharedPtr SceneBuilder::getScene() { - assert(nodeID < mSceneGraph.size()); + if (mpScene) return mpScene; - while (nodeID != kInvalidNode) + // Finish loading textures. This blocks until all textures are loaded and assigned. + mpMaterialTextureLoader.reset(); + + // If no meshes were added, we create a dummy mesh to keep the scene generation working. + // Scenes with no meshes can be useful for example when using volumes in isolation. + if (mMeshes.empty()) { - for (const auto& animation : mAnimations) - { - if (animation->getChannel(nodeID) != Animation::kInvalidChannel) return true; - } - nodeID = mSceneGraph[nodeID].parent; + logWarning("Scene contains no meshes. Creating a dummy mesh."); + // Add a dummy (degenerate) mesh. + auto dummyMesh = TriangleMesh::createDummy(); + auto dummyMaterial = Material::create("Dummy"); + auto meshID = addTriangleMesh(dummyMesh, dummyMaterial); + Node dummyNode = { "Dummy", glm::identity(), glm::identity() }; + auto nodeID = addNode(dummyNode); + addMeshInstance(nodeID, meshID); } - return false; - } + // Post-process the scene data. + TimeReport timeReport; - void SceneBuilder::setNodeInterpolationMode(uint32_t nodeID, Animation::InterpolationMode interpolationMode, bool enableWarping) - { - assert(nodeID < mSceneGraph.size()); + removeUnusedMeshes(); + pretransformStaticMeshes(); + calculateMeshBoundingBoxes(); + createMeshGroups(); + optimizeGeometry(); + createGlobalBuffers(); + createCurveGlobalBuffers(); + removeDuplicateMaterials(); + collectVolumeGrids(); + quantizeTexCoords(); - while (nodeID != kInvalidNode) + timeReport.measure("Post processing meshes"); + + // Create the scene object and assign resources. + mpScene = Scene::create(); + mpScene->mRenderSettings = mRenderSettings; + mpScene->mCameras = mCameras; + mpScene->mSelectedCamera = (uint32_t)(mpSelectedCamera ? std::distance(mCameras.begin(), std::find(mCameras.begin(), mCameras.end(), mpSelectedCamera)) : 0); + mpScene->mCameraSpeed = mCameraSpeed; + mpScene->mLights = mLights; + mpScene->mMaterials = mMaterials; + mpScene->mVolumes = mVolumes; + mpScene->mCustomPrimitiveAABBs = mCustomPrimitiveAABBs; + mpScene->mGrids = mGrids; + mpScene->mGridIDs = mGridIDs; + mpScene->mpEnvMap = mpEnvMap; + mpScene->mFilename = mFilename; + + // Prepare scene resources. + createNodeList(); + + uint32_t drawCount = createMeshData(); + createMeshVao(drawCount); + createMeshBoundingBoxes(); + + if (!mCurves.empty()) { - for (const auto& animation : mAnimations) - { - if (uint32_t channelID = animation->getChannel(nodeID); channelID != Animation::kInvalidChannel) - { - animation->setInterpolationMode(channelID, interpolationMode, enableWarping); - } - } - nodeID = mSceneGraph[nodeID].parent; + createCurveData(); + createCurveVao(); + calculateCurveBoundingBoxes(); + mapCurvesToProceduralPrimitives(Scene::kCurveIntersectionTypeID); } + + createRaytracingAABBData(); + + mpScene->mpAnimationController = AnimationController::create(mpScene.get(), mBuffersData.staticData, mBuffersData.dynamicData, mAnimations); + + // Finalize the scene object. This is where the final setup is done. + mpScene->finalize(); + + timeReport.measure("Creating resources"); + timeReport.printToLog(); + + return mpScene; } - void SceneBuilder::addMeshInstance(uint32_t nodeID, uint32_t meshID) + // Meshes + + uint32_t SceneBuilder::addMesh(const Mesh& mesh) { - assert(meshID < mMeshes.size()); - mSceneGraph.at(nodeID).meshes.push_back(meshID); - mMeshes.at(meshID).instances.push_back(nodeID); - mDirty = true; + return addProcessedMesh(processMesh(mesh)); + } + + uint32_t SceneBuilder::addTriangleMesh(const TriangleMesh::SharedPtr& pTriangleMesh, const Material::SharedPtr& pMaterial) + { + Mesh mesh; + + const auto& indices = pTriangleMesh->getIndices(); + const auto& vertices = pTriangleMesh->getVertices(); + + mesh.name = pTriangleMesh->getName(); + mesh.faceCount = (uint32_t)(indices.size() / 3); + mesh.vertexCount = (uint32_t)vertices.size(); + mesh.indexCount = (uint32_t)indices.size(); + mesh.pIndices = indices.data(); + mesh.topology = Vao::Topology::TriangleList; + mesh.pMaterial = pMaterial; + + std::vector positions(vertices.size()); + std::vector normals(vertices.size()); + std::vector texCoords(vertices.size()); + std::transform(vertices.begin(), vertices.end(), positions.begin(), [] (const auto& v) { return v.position; }); + std::transform(vertices.begin(), vertices.end(), normals.begin(), [] (const auto& v) { return v.normal; }); + std::transform(vertices.begin(), vertices.end(), texCoords.begin(), [] (const auto& v) { return v.texCoord; }); + + mesh.positions = { positions.data(), SceneBuilder::Mesh::AttributeFrequency::Vertex }; + mesh.normals = { normals.data(), SceneBuilder::Mesh::AttributeFrequency::Vertex }; + mesh.texCrds = { texCoords.data(), SceneBuilder::Mesh::AttributeFrequency::Vertex }; + + return addMesh(mesh); } - uint32_t SceneBuilder::addMesh(const Mesh& meshDesc) + SceneBuilder::ProcessedMesh SceneBuilder::processMesh(const Mesh& mesh_) const { - logInfo("Adding mesh with name '" + meshDesc.name + "'"); + // This function preprocesses a mesh into the final runtime representation. + // Note the function needs to be thread safe. The following steps are performed: + // - Error checking + // - Compute tangent space if needed + // - Merge identical vertices, compute new indices + // - Validate final vertex data + // - Compact vertices/indices into runtime format // Copy the mesh desc so we can update it. The caller retains the ownership of the data. - Mesh mesh = meshDesc; + Mesh mesh = mesh_; + ProcessedMesh processedMesh; + + processedMesh.name = mesh.name; + processedMesh.topology = mesh.topology; + processedMesh.pMaterial = mesh.pMaterial; + processedMesh.isFrontFaceCW = mesh.isFrontFaceCW; // Error checking. auto throw_on_missing_element = [&](const std::string& element) @@ -236,7 +337,7 @@ namespace Falcor // Generate tangent space if that's required. std::vector tangents; - if (!is_set(mFlags, Flags::UseOriginalTangentSpace) || !mesh.tangents.pData) + if (!(is_set(mFlags, Flags::UseOriginalTangentSpace) || mesh.useOriginalTangentSpace) || !mesh.tangents.pData) { tangents = MikkTSpaceWrapper::generateTangents(mesh); if (!tangents.empty()) @@ -252,6 +353,32 @@ namespace Falcor } } + // Pretransform the texture coordinates, rather than tranforming them at runtime. + std::vector transformedTexCoords; + if (mesh.texCrds.pData != nullptr) + { + const glm::mat4 xform = mesh.pMaterial->getTextureTransform().getMatrix(); + if (xform != glm::identity()) + { + size_t texCoordCount = mesh.getAttributeCount(mesh.texCrds); + transformedTexCoords.resize(texCoordCount); + // The given matrix transforms the texture (e.g., scaling > 1 enlarges the texture). + // Because we're transforming the input coordinates, apply the inverse. + const float4x4 invXform = glm::inverse(xform); + // Because texture transforms are 2D and affine, we only need apply the corresponding 3x2 matrix + glm::mat3x2 coordTransform; + coordTransform[0] = invXform[0].xy; + coordTransform[1] = invXform[1].xy; + coordTransform[2] = invXform[3].xy; + + for (size_t i = 0; i < texCoordCount; ++i) + { + transformedTexCoords[i] = coordTransform * float3(mesh.texCrds.pData[i], 1.f); + } + mesh.texCrds.pData = transformedTexCoords.data(); + } + } + // Build new vertex/index buffers by merging identical vertices. // The search is based on the topology defined by the original index buffer. // @@ -307,7 +434,7 @@ namespace Falcor assert(indices.size() == mesh.indexCount); if (vertices.size() != mesh.vertexCount) { - logInfo("Mesh with name '" + mesh.name + "' had original vertex count " + std::to_string(mesh.vertexCount) + ", new vertex count " + std::to_string(vertices.size())); + logDebug("Mesh with name '" + mesh.name + "' had original vertex count " + std::to_string(mesh.vertexCount) + ", new vertex count " + std::to_string(vertices.size())); } // Validate vertex data to check for invalid numbers and missing tangent frame. @@ -320,82 +447,25 @@ namespace Falcor if (invalidCount > 0) logWarning("The mesh '" + mesh.name + "' has inf/nan vertex attributes at " + std::to_string(invalidCount) + " vertices. Please fix the asset."); if (zeroCount > 0) logWarning("The mesh '" + mesh.name + "' has zero-length normals/tangents at " + std::to_string(zeroCount) + " vertices. Please fix the asset."); - // Match texture coordinate quantization for textured emissives to match PackedEmissiveTriangle. - // This is to avoid mismatch when sampling and evaluating emissive triangles. - if (mesh.pMaterial->getEmissiveTexture() != nullptr) - { - float2 minTexCrd = float2(std::numeric_limits::infinity()); - float2 maxTexCrd = float2(-std::numeric_limits::infinity()); - float2 maxError = float2(0); - - for (auto& v : vertices) - { - float2 texCrd = v.first.texCrd; - minTexCrd = min(minTexCrd, texCrd); - maxTexCrd = max(maxTexCrd, texCrd); - v.first.texCrd = f16tof32(f32tof16(texCrd)); - maxError = max(maxError, abs(v.first.texCrd - texCrd)); - } - - // Issue warning if quantization errors are too large. - float2 maxAbsCrd = max(abs(minTexCrd), abs(maxTexCrd)); - if (maxAbsCrd.x > HLF_MAX || maxAbsCrd.y > HLF_MAX) - { - logWarning("Texture coordinates for emissive textured mesh '" + mesh.name + "' are outside the representable range, expect rendering errors."); - } - else - { - // Compute maximum quantization error in texels. - // The texcoords are used for all texture channels so taking the maximum dimensions. - uint2 maxTexDim = mesh.pMaterial->getMaxTextureDimensions(); - maxError *= maxTexDim; - float maxTexelError = std::max(maxError.x, maxError.y); - - if (maxTexelError > kMaxTexelError) - { - std::ostringstream oss; - oss << "Texture coordinates for emissive textured mesh '" << mesh.name << "' have a large quantization error of " << maxTexelError << " texels. " - << "The coordinate range is [" << minTexCrd.x << ", " << maxTexCrd.x << "] x [" << minTexCrd.y << ", " << maxTexCrd.y << "] for maximum texture dimensions (" - << maxTexDim.x << ", " << maxTexDim.y << ")."; - logWarning(oss.str()); - } - } - } - - // Add the mesh to the scene. // If the non-indexed vertices build flag is set, we will de-index the data below. const bool isIndexed = !is_set(mFlags, Flags::NonIndexedVertices); - const uint32_t outputVertexCount = isIndexed ? (uint32_t)vertices.size() : mesh.indexCount; - - mMeshes.push_back({}); - MeshSpec& spec = mMeshes.back(); - assert(mBuffersData.staticData.size() <= std::numeric_limits::max() && mBuffersData.dynamicData.size() <= std::numeric_limits::max() && mBuffersData.indices.size() <= std::numeric_limits::max()); - spec.staticVertexOffset = (uint32_t)mBuffersData.staticData.size(); - spec.dynamicVertexOffset = (uint32_t)mBuffersData.dynamicData.size(); + const uint32_t vertexCount = isIndexed ? (uint32_t)vertices.size() : mesh.indexCount; + // Copy indices into processed mesh. if (isIndexed) { - spec.indexOffset = (uint32_t)mBuffersData.indices.size(); - spec.indexCount = mesh.indexCount; - } - - spec.vertexCount = outputVertexCount; - spec.topology = mesh.topology; - spec.materialId = addMaterial(mesh.pMaterial, is_set(mFlags, Flags::RemoveDuplicateMaterials)); + processedMesh.indexCount = indices.size(); + processedMesh.use16BitIndices = (vertices.size() <= (1u << 16)) && !(is_set(mFlags, Flags::Force32BitIndices)); - if (mesh.hasBones()) - { - spec.hasDynamicData = true; + if (!processedMesh.use16BitIndices) processedMesh.indexData = std::move(indices); + else processedMesh.indexData = compact16BitIndices(indices); } - // Copy indices into global index array. - if (isIndexed) - { - mBuffersData.indices.insert(mBuffersData.indices.end(), indices.begin(), indices.end()); - } + // Copy vertices into processed mesh. + processedMesh.staticData.reserve(vertexCount); + if (mesh.hasBones()) processedMesh.dynamicData.reserve(vertexCount); - // Copy vertices into global vertex arrays. - for (uint32_t i = 0; i < outputVertexCount; i++) + for (uint32_t i = 0; i < vertexCount; i++) { uint32_t index = isIndexed ? i : indices[i]; assert(index < vertices.size()); @@ -406,293 +476,1574 @@ namespace Falcor s.normal = v.normal; s.texCrd = v.texCrd; s.tangent = v.tangent; - mBuffersData.staticData.push_back(PackedStaticVertexData(s)); + processedMesh.staticData.push_back(s); if (mesh.hasBones()) { DynamicVertexData d; d.boneWeight = v.boneWeights; d.boneID = v.boneIDs; - d.staticIndex = (uint32_t)mBuffersData.staticData.size() - 1; - d.globalMatrixID = 0; // This will be initialized in createMeshData() - mBuffersData.dynamicData.push_back(d); + d.staticIndex = i; // This references the local vertex here and gets updated in addProcessedMesh(). + d.globalMatrixID = 0; // This will be initialized in createMeshData(). + processedMesh.dynamicData.push_back(d); } } - mDirty = true; - - assert(mMeshes.size() <= std::numeric_limits::max()); - return (uint32_t)mMeshes.size() - 1; + return processedMesh; } - uint32_t SceneBuilder::addMaterial(const Material::SharedPtr& pMaterial, bool removeDuplicate) + uint32_t SceneBuilder::addProcessedMesh(const ProcessedMesh& mesh) { - assert(pMaterial); + const bool isIndexed = !is_set(mFlags, Flags::NonIndexedVertices); - // Reuse previously added materials - if (auto it = std::find(mMaterials.begin(), mMaterials.end(), pMaterial); it != mMaterials.end()) + MeshSpec spec; + + // Add the mesh to the scene. + spec.name = mesh.name; + spec.topology = mesh.topology; + spec.materialId = addMaterial(mesh.pMaterial); + spec.isFrontFaceCW = mesh.isFrontFaceCW; + + spec.vertexCount = (uint32_t)mesh.staticData.size(); + spec.staticVertexCount = (uint32_t)mesh.staticData.size(); + spec.dynamicVertexCount = (uint32_t)mesh.dynamicData.size(); + + spec.indexData = std::move(mesh.indexData); + spec.staticData = std::move(mesh.staticData); + spec.dynamicData = std::move(mesh.dynamicData); + + if (isIndexed) { - return (uint32_t)std::distance(mMaterials.begin(), it); + spec.indexCount = (uint32_t)mesh.indexCount; + spec.use16BitIndices = mesh.use16BitIndices; } - // Try to find previously added material with equal properties (duplicate) - if (auto it = std::find_if(mMaterials.begin(), mMaterials.end(), [&pMaterial] (const auto& m) { return *m == *pMaterial; }); it != mMaterials.end()) + if (!spec.dynamicData.empty()) { - const auto& equalMaterial = *it; - - // ASSIMP sometimes creates internal copies of a material: Always de-duplicate if name and properties are equal. - if (removeDuplicate || pMaterial->getName() == equalMaterial->getName()) - { - return (uint32_t)std::distance(mMaterials.begin(), it); - } - else - { - logInfo("Material '" + pMaterial->getName() + "' is a duplicate (has equal properties) of material '" + equalMaterial->getName() + "'."); - } + spec.hasDynamicData = true; } - mDirty = true; - mMaterials.push_back(pMaterial); - assert(mMaterials.size() <= std::numeric_limits::max()); - return (uint32_t)mMaterials.size() - 1; - } + mMeshes.push_back(spec); - uint32_t SceneBuilder::addCamera(const Camera::SharedPtr& pCamera) - { - assert(pCamera); - mCameras.push_back(pCamera); - mDirty = true; - assert(mCameras.size() <= std::numeric_limits::max()); - return (uint32_t)mCameras.size() - 1; - } + if (mMeshes.size() > std::numeric_limits::max()) + { + throw std::exception("Trying to build a scene that exceeds supported number of meshes"); + } - uint32_t SceneBuilder::addLight(const Light::SharedPtr& pLight) - { - assert(pLight); - mLights.push_back(pLight); - mDirty = true; - assert(mLights.size() <= std::numeric_limits::max()); - return (uint32_t)mLights.size() - 1; + return (uint32_t)(mMeshes.size() - 1); } - void SceneBuilder::setCamera(const std::string name) + void SceneBuilder::addCustomPrimitive(uint32_t typeID, const AABB& aabb) { - for (uint i = 0; i < mCameras.size(); i++) + uint32_t instanceIdx = 0; + auto it = mProceduralPrimInstanceCount.find(typeID); + if (it != mProceduralPrimInstanceCount.end()) { - if (mCameras[i]->getName() == name) - { - mSelectedCamera = i; - return; - } + instanceIdx = it->second++; } - } - - Vao::SharedPtr SceneBuilder::createVao(uint16_t drawCount) - { - for (auto& mesh : mMeshes) assert(mesh.topology == mMeshes[0].topology); - const size_t vertexCount = (uint32_t)mBuffersData.staticData.size(); - size_t ibSize = sizeof(uint32_t) * mBuffersData.indices.size(); - size_t staticVbSize = sizeof(PackedStaticVertexData) * vertexCount; - size_t prevVbSize = sizeof(PrevVertexData) * vertexCount; - assert(ibSize <= std::numeric_limits::max() && staticVbSize <= std::numeric_limits::max() && prevVbSize <= std::numeric_limits::max()); - - // Create the index buffer - Buffer::SharedPtr pIB = nullptr; - if (ibSize > 0) + else { - ResourceBindFlags ibBindFlags = Resource::BindFlags::Index | ResourceBindFlags::ShaderResource; - pIB = Buffer::create((uint32_t)ibSize, ibBindFlags, Buffer::CpuAccess::None, mBuffersData.indices.data()); + mProceduralPrimInstanceCount[typeID] = 1; } - // Create the vertex data as structured buffers - ResourceBindFlags vbBindFlags = ResourceBindFlags::ShaderResource | ResourceBindFlags::UnorderedAccess | ResourceBindFlags::Vertex; - Buffer::SharedPtr pStaticBuffer = Buffer::createStructured(sizeof(PackedStaticVertexData), (uint32_t)vertexCount, vbBindFlags, Buffer::CpuAccess::None, nullptr, false); - Buffer::SharedPtr pPrevBuffer = Buffer::createStructured(sizeof(PrevVertexData), (uint32_t)vertexCount, vbBindFlags, Buffer::CpuAccess::None, nullptr, false); + pushProceduralPrimitive(typeID, instanceIdx, (uint32_t)mCustomPrimitiveAABBs.size(), 1); + mCustomPrimitiveAABBs.push_back(aabb); + } - Vao::BufferVec pVBs(Scene::kVertexBufferCount); - pVBs[Scene::kStaticDataBufferIndex] = pStaticBuffer; - pVBs[Scene::kPrevVertexBufferIndex] = pPrevBuffer; - std::vector drawIDs(drawCount); - for (uint32_t i = 0; i < drawCount; i++) drawIDs[i] = i; - pVBs[Scene::kDrawIdBufferIndex] = Buffer::create(drawCount * sizeof(uint16_t), ResourceBindFlags::Vertex, Buffer::CpuAccess::None, drawIDs.data()); + // Curves - // The layout only initializes the vertex data and draw ID layout. The skinning data doesn't get passed into the vertex shader. - VertexLayout::SharedPtr pLayout = VertexLayout::create(); + uint32_t SceneBuilder::addCurve(const Curve& curve) + { + return addProcessedCurve(processCurve(curve)); + } - // Add the packed static vertex data layout - VertexBufferLayout::SharedPtr pStaticLayout = VertexBufferLayout::create(); - pStaticLayout->addElement(VERTEX_POSITION_NAME, offsetof(PackedStaticVertexData, position), ResourceFormat::RGB32Float, 1, VERTEX_POSITION_LOC); - pStaticLayout->addElement(VERTEX_PACKED_NORMAL_TANGENT_NAME, offsetof(PackedStaticVertexData, packedNormalTangent), ResourceFormat::RGB32Float, 1, VERTEX_PACKED_NORMAL_TANGENT_LOC); - pStaticLayout->addElement(VERTEX_TEXCOORD_NAME, offsetof(PackedStaticVertexData, texCrd), ResourceFormat::RG32Float, 1, VERTEX_TEXCOORD_LOC); - pLayout->addBufferLayout(Scene::kStaticDataBufferIndex, pStaticLayout); + SceneBuilder::ProcessedCurve SceneBuilder::processCurve(const Curve& curve) const + { + ProcessedCurve processedCurve; - // Add the previous vertex data layout - VertexBufferLayout::SharedPtr pPrevLayout = VertexBufferLayout::create(); - pPrevLayout->addElement(VERTEX_PREV_POSITION_NAME, offsetof(PrevVertexData, position), ResourceFormat::RGB32Float, 1, VERTEX_PREV_POSITION_LOC); - pLayout->addBufferLayout(Scene::kPrevVertexBufferIndex, pPrevLayout); + processedCurve.name = curve.name; + processedCurve.topology = Vao::Topology::LineStrip; + processedCurve.pMaterial = curve.pMaterial; - // Add the draw ID layout - VertexBufferLayout::SharedPtr pInstLayout = VertexBufferLayout::create(); - pInstLayout->addElement(INSTANCE_DRAW_ID_NAME, 0, ResourceFormat::R16Uint, 1, INSTANCE_DRAW_ID_LOC); - pInstLayout->setInputClass(VertexBufferLayout::InputClass::PerInstanceData, 1); - pLayout->addBufferLayout(Scene::kDrawIdBufferIndex, pInstLayout); + // Error checking. + auto throw_on_missing_element = [&](const std::string& element) + { + throw std::runtime_error("Error when adding the curve '" + curve.name + "' to the scene.\nThe curve is missing " + element + "."); + }; - Vao::SharedPtr pVao = Vao::create(mMeshes[0].topology, pLayout, pVBs, pIB, ResourceFormat::R32Uint); - return pVao; - } + auto missing_element_warning = [&](const std::string& element) + { + logWarning("The curve '" + curve.name + "' is missing the element " + element + ". This is not an error, the element will be filled with zeros which may result in incorrect rendering."); + }; - void SceneBuilder::createGlobalMatricesBuffer(Scene* pScene) - { - pScene->mSceneGraph.resize(mSceneGraph.size()); + if (curve.pMaterial == nullptr) throw_on_missing_element("material"); - for (size_t i = 0; i < mSceneGraph.size(); i++) - { - assert(mSceneGraph[i].parent <= std::numeric_limits::max()); - pScene->mSceneGraph[i] = Scene::Node(mSceneGraph[i].name, (uint32_t)mSceneGraph[i].parent, mSceneGraph[i].transform, mSceneGraph[i].localToBindPose); - } - } + if (curve.vertexCount == 0) throw_on_missing_element("vertices"); + if (curve.indexCount == 0) throw_on_missing_element("indices"); - uint32_t SceneBuilder::createMeshData(Scene* pScene) - { - auto& meshData = pScene->mMeshDesc; - auto& instanceData = pScene->mMeshInstanceData; - meshData.resize(mMeshes.size()); - pScene->mMeshHasDynamicData.resize(mMeshes.size()); + if (curve.positions.pData == nullptr) throw_on_missing_element("positions"); + if (curve.radius.pData == nullptr) throw_on_missing_element("radius"); + if (curve.tangents.pData == nullptr) throw_on_missing_element("tangents"); + if (curve.normals.pData == nullptr) missing_element_warning("normals"); + if (curve.texCrds.pData == nullptr) missing_element_warning("texture coordinates"); - size_t drawCount = 0; - for (uint32_t meshID = 0; meshID < mMeshes.size(); meshID++) - { - // Mesh data - const auto& mesh = mMeshes[meshID]; - meshData[meshID].materialID = mesh.materialId; - meshData[meshID].vbOffset = mesh.staticVertexOffset; - meshData[meshID].ibOffset = mesh.indexOffset; - meshData[meshID].vertexCount = mesh.vertexCount; - meshData[meshID].indexCount = mesh.indexCount; + // Copy indices and vertices into processed curve. + processedCurve.indexData.assign(curve.pIndices, curve.pIndices + curve.indexCount); - drawCount += mesh.instances.size(); + processedCurve.staticData.reserve(curve.vertexCount); + for (uint32_t i = 0; i < curve.vertexCount; i++) + { + StaticCurveVertexData s; + s.position = curve.positions.pData[i]; + s.radius = curve.radius.pData[i]; + s.tangent = curve.tangents.pData[i]; - // Mesh instance data - for (const auto& instance : mesh.instances) + if (curve.normals.pData != nullptr) { - instanceData.push_back({}); - auto& meshInstance = instanceData.back(); - meshInstance.globalMatrixID = instance; - meshInstance.materialID = mesh.materialId; - meshInstance.meshID = meshID; - meshInstance.vbOffset = mesh.staticVertexOffset; - meshInstance.ibOffset = mesh.indexOffset; + s.normal = curve.normals.pData[i]; } - - if (mesh.hasDynamicData) + else { - assert(mesh.instances.size() == 1); - pScene->mMeshHasDynamicData[meshID] = true; + s.normal = float3(0.f); + } - for (uint32_t i = 0; i < mesh.vertexCount; i++) - { - mBuffersData.dynamicData[mesh.dynamicVertexOffset + i].globalMatrixID = (uint32_t)mesh.instances[0]; - } + if (curve.texCrds.pData != nullptr) + { + s.texCrd = curve.texCrds.pData[i]; + } + else + { + s.texCrd = float2(0.f); } + + processedCurve.staticData.push_back(s); } - assert(drawCount <= std::numeric_limits::max()); - return (uint32_t)drawCount; + + return processedCurve; } - Scene::SharedPtr SceneBuilder::getScene() + uint32_t SceneBuilder::addProcessedCurve(const ProcessedCurve& curve) { - // We cache the scene because creating it is not cheap. - // With the PythonImporter, the scene is fetched twice, once for running - // the scene script and another time when the scene has finished loading. - if (mpScene && !mDirty) - { - // PythonImporter sets the filename after loading the nested scene, - // so we need to set it to the correct value here. - mpScene->mFilename = mFilename; - return mpScene; - } + CurveSpec spec; - if (mMeshes.size() == 0) - { - logError("Can't build scene. No meshes were loaded"); - return nullptr; - } + // Add the curve to the scene. + spec.name = curve.name; + spec.topology = curve.topology; + spec.materialId = addMaterial(curve.pMaterial); - TimeReport timeReport; + spec.vertexCount = (uint32_t)curve.staticData.size(); + spec.staticVertexCount = (uint32_t)curve.staticData.size(); - mpScene = Scene::create(); - mpScene->mCameras = mCameras; - mpScene->mSelectedCamera = mSelectedCamera; - mpScene->mCameraSpeed = mCameraSpeed; - mpScene->mLights = mLights; - mpScene->mMaterials = mMaterials; - mpScene->mpLightProbe = mpLightProbe; - mpScene->mpEnvMap = mpEnvMap; - mpScene->mFilename = mFilename; + spec.indexData = std::move(curve.indexData); + spec.staticData = std::move(curve.staticData); - createGlobalMatricesBuffer(mpScene.get()); - uint32_t drawCount = createMeshData(mpScene.get()); - assert(drawCount <= std::numeric_limits::max()); - mpScene->mpVao = createVao(drawCount); - calculateMeshBoundingBoxes(mpScene.get()); - createAnimationController(mpScene.get()); - mpScene->finalize(); - mDirty = false; + spec.indexCount = (uint32_t)curve.indexData.size(); - timeReport.measure("Creating resources"); - timeReport.printToLog(); + mCurves.push_back(spec); - return mpScene; + if (mCurves.size() > std::numeric_limits::max()) + { + throw std::exception("Trying to build a scene that exceeds supported number of curves."); + } + + return (uint32_t)(mCurves.size() - 1); } - void SceneBuilder::calculateMeshBoundingBoxes(Scene* pScene) + // Materials + + uint32_t SceneBuilder::addMaterial(const Material::SharedPtr& pMaterial) { - // Calculate mesh bounding boxes - pScene->mMeshBBs.resize(mMeshes.size()); - for (size_t i = 0; i < mMeshes.size(); i++) + assert(pMaterial); + + // Reuse previously added materials + if (auto it = std::find(mMaterials.begin(), mMaterials.end(), pMaterial); it != mMaterials.end()) { - const auto& mesh = mMeshes[i]; - float3 boxMin(FLT_MAX); - float3 boxMax(-FLT_MAX); + return (uint32_t)std::distance(mMaterials.begin(), it); + } - const auto* staticData = &mBuffersData.staticData[mesh.staticVertexOffset]; - for (uint32_t v = 0; v < mesh.vertexCount; v++) - { - boxMin = glm::min(boxMin, staticData[v].position); - boxMax = glm::max(boxMax, staticData[v].position); - } + mMaterials.push_back(pMaterial); + assert(mMaterials.size() <= std::numeric_limits::max()); + return (uint32_t)mMaterials.size() - 1; + } - pScene->mMeshBBs[i] = BoundingBox::fromMinMax(boxMin, boxMax); + Material::SharedPtr SceneBuilder::getMaterial(const std::string& name) const + { + for (const auto& pMaterial : mMaterials) + { + if (pMaterial->getName() == name) return pMaterial; } + return nullptr; } - void SceneBuilder::addAnimation(const Animation::SharedPtr& pAnimation) + void SceneBuilder::loadMaterialTexture(const Material::SharedPtr& pMaterial, Material::TextureSlot slot, const std::string& filename) { - mAnimations.push_back(pAnimation); - mDirty = true; + if (!mpMaterialTextureLoader) mpMaterialTextureLoader.reset(new MaterialTextureLoader(!is_set(mFlags, Flags::AssumeLinearSpaceTextures))); + mpMaterialTextureLoader->loadTexture(pMaterial, slot, filename); } - void SceneBuilder::createAnimationController(Scene* pScene) + // Volumes + + Volume::SharedPtr SceneBuilder::getVolume(const std::string& name) const { - pScene->mpAnimationController = AnimationController::create(pScene, mBuffersData.staticData, mBuffersData.dynamicData); - for (const auto& pAnim : mAnimations) + for (const auto& pVolume : mVolumes) { - pScene->mpAnimationController->addAnimation(pAnim); + if (pVolume->getName() == name) return pVolume; } + return nullptr; } - SCRIPT_BINDING(SceneBuilder) + uint32_t SceneBuilder::addVolume(const Volume::SharedPtr& pVolume) { - pybind11::enum_ flags(m, "SceneBuilderFlags"); - flags.value("Default", SceneBuilder::Flags::Default); - flags.value("RemoveDuplicateMaterials", SceneBuilder::Flags::RemoveDuplicateMaterials); + assert(pVolume); + + mVolumes.push_back(pVolume); + assert(mVolumes.size() <= std::numeric_limits::max()); + return (uint32_t)mVolumes.size() - 1; + } + + // Lights + + uint32_t SceneBuilder::addLight(const Light::SharedPtr& pLight) + { + assert(pLight); + mLights.push_back(pLight); + assert(mLights.size() <= std::numeric_limits::max()); + return (uint32_t)mLights.size() - 1; + } + + // Cameras + + uint32_t SceneBuilder::addCamera(const Camera::SharedPtr& pCamera) + { + assert(pCamera); + mCameras.push_back(pCamera); + assert(mCameras.size() <= std::numeric_limits::max()); + return (uint32_t)mCameras.size() - 1; + } + + void SceneBuilder::setSelectedCamera(const Camera::SharedPtr& pCamera) + { + auto it = std::find(mCameras.begin(), mCameras.end(), pCamera); + if (it != mCameras.end()) mpSelectedCamera = pCamera; + } + + // Animations + + void SceneBuilder::addAnimation(const Animation::SharedPtr& pAnimation) + { + mAnimations.push_back(pAnimation); + } + + Animation::SharedPtr SceneBuilder::createAnimation(Animatable::SharedPtr pAnimatable, const std::string& name, double duration) + { + assert(pAnimatable); + + uint32_t nodeID = pAnimatable->getNodeID(); + + if (nodeID != kInvalidNode && isNodeAnimated(nodeID)) + { + logWarning("Animatable object is already animated."); + return nullptr; + } + if (nodeID == kInvalidNode) nodeID = addNode(Node{ name, glm::identity(), glm::identity() }); + + pAnimatable->setNodeID(nodeID); + pAnimatable->setHasAnimation(true); + pAnimatable->setIsAnimated(true); + + auto animation = Animation::create(name, nodeID, duration); + addAnimation(animation); + return animation; + } + + // Scene graph + + uint32_t SceneBuilder::addNode(const Node& node) + { + assert(node.parent == kInvalidNode || node.parent < mSceneGraph.size()); + + assert(mSceneGraph.size() <= std::numeric_limits::max()); + uint32_t newNodeID = (uint32_t)mSceneGraph.size(); + mSceneGraph.push_back(InternalNode(node)); + if (node.parent != kInvalidNode) mSceneGraph[node.parent].children.push_back(newNodeID); + return newNodeID; + } + + void SceneBuilder::addMeshInstance(uint32_t nodeID, uint32_t meshID) + { + if (nodeID >= mSceneGraph.size()) throw std::runtime_error("SceneBuilder::addMeshInstance() - nodeID " + std::to_string(nodeID) + " is out of range"); + if (meshID >= mMeshes.size()) throw std::runtime_error("SceneBuilder::addMeshInstance() - meshID " + std::to_string(meshID) + " is out of range"); + + mSceneGraph[nodeID].meshes.push_back(meshID); + mMeshes[meshID].instances.push_back(nodeID); + } + + void SceneBuilder::addCurveInstance(uint32_t nodeID, uint32_t curveID) + { + if (nodeID >= mSceneGraph.size()) throw std::runtime_error("SceneBuilder::addCurveInstance() - nodeID " + std::to_string(nodeID) + " is out of range"); + if (curveID >= mCurves.size()) throw std::runtime_error("SceneBuilder::addCurveInstance() - curveID " + std::to_string(curveID) + " is out of range"); + + mSceneGraph[nodeID].curves.push_back(curveID); + mCurves[curveID].instances.push_back(nodeID); + } + + bool SceneBuilder::isNodeAnimated(uint32_t nodeID) const + { + assert(nodeID < mSceneGraph.size()); + + while (nodeID != kInvalidNode) + { + for (const auto& pAnimation : mAnimations) + { + if (pAnimation->getNodeID() == nodeID) return true; + } + nodeID = mSceneGraph[nodeID].parent; + } + + return false; + } + + void SceneBuilder::setNodeInterpolationMode(uint32_t nodeID, Animation::InterpolationMode interpolationMode, bool enableWarping) + { + assert(nodeID < mSceneGraph.size()); + + while (nodeID != kInvalidNode) + { + for (const auto& pAnimation : mAnimations) + { + if (pAnimation->getNodeID() == nodeID) + { + pAnimation->setInterpolationMode(interpolationMode); + pAnimation->setEnableWarping(enableWarping); + } + } + nodeID = mSceneGraph[nodeID].parent; + } + } + + // Internal + + void SceneBuilder::removeUnusedMeshes() + { + // If the scene contained meshes that are not referenced by the scene graph, + // those will be removed here and warnings logged. + + size_t unusedCount = 0; + for (uint32_t meshID = 0; meshID < (uint32_t)mMeshes.size(); meshID++) + { + auto& mesh = mMeshes[meshID]; + if (mesh.instances.empty()) + { + logWarning("Mesh with ID " + std::to_string(meshID) + " named '" + mesh.name + "' is not referenced by any scene graph nodes."); + unusedCount++; + } + } + + if (unusedCount > 0) + { + logWarning("Scene has " + std::to_string(unusedCount) + " unused meshes that will be removed."); + + const size_t meshCount = mMeshes.size(); + MeshList meshes; + meshes.reserve(meshCount); + + for (uint32_t meshID = 0; meshID < (uint32_t)meshCount; meshID++) + { + auto& mesh = mMeshes[meshID]; + if (mesh.instances.empty()) continue; // Skip unused meshes + + // Get new mesh ID. + const uint32_t newMeshID = (uint32_t)meshes.size(); + + // Update scene graph nodes meshIDs. + for (const auto nodeID : mesh.instances) + { + assert(nodeID < mSceneGraph.size()); + auto& node = mSceneGraph[nodeID]; + std::replace(node.meshes.begin(), node.meshes.end(), meshID, newMeshID); + } + + meshes.push_back(std::move(mesh)); + } + + mMeshes = std::move(meshes); + + // Validate scene graph. + assert(mMeshes.size() == meshCount - unusedCount); + for (const auto& node : mSceneGraph) + { + for (uint32_t meshID : node.meshes) assert(meshID < mMeshes.size()); + } + } + } + + void SceneBuilder::pretransformStaticMeshes() + { + // Add an identity transform node. + uint32_t identityNodeID = addNode(Node{ "Identity", glm::identity(), glm::identity() }); + auto& identityNode = mSceneGraph[identityNodeID]; + + size_t transformedMeshCount = 0; + for (uint32_t meshID = 0; meshID < (uint32_t)mMeshes.size(); meshID++) + { + auto& mesh = mMeshes[meshID]; + + // Skip instanced/animated/skinned meshes. + assert(!mesh.instances.empty()); + if (mesh.instances.size() > 1 || isNodeAnimated(mesh.instances[0]) || mesh.hasDynamicData) continue; + + assert(mesh.dynamicData.empty() && mesh.dynamicVertexCount == 0); + mesh.isStatic = true; + + // Compute the object->world transform for the node. + auto nodeID = mesh.instances[0]; + assert(nodeID != kInvalidNode); + + glm::mat4 transform = glm::identity(); + while (nodeID != kInvalidNode) + { + assert(nodeID < mSceneGraph.size()); + transform = mSceneGraph[nodeID].transform * transform; + + nodeID = mSceneGraph[nodeID].parent; + } + + // Flip triangle winding if the transform flips the coordinate system handedness (negative determinant). + bool flippedWinding = glm::determinant((glm::mat3)transform) < 0.f; + if (flippedWinding) mesh.isFrontFaceCW = !mesh.isFrontFaceCW; + + // Transform vertices to world space if not already identity transform. + if (transform != glm::identity()) + { + assert(!mesh.staticData.empty()); + assert((size_t)mesh.vertexCount == mesh.staticData.size()); + + glm::mat3 invTranspose3x3 = (glm::mat3)glm::transpose(glm::inverse(transform)); + glm::mat3 transform3x3 = (glm::mat3)transform; + + for (auto& v : mesh.staticData) + { + float4 p = transform * float4(v.position, 1.f); + v.position = p.xyz; + v.normal = glm::normalize(invTranspose3x3 * v.normal); + v.tangent.xyz = glm::normalize(transform3x3 * v.tangent.xyz); + // TODO: We should flip the sign of v.tangent.w if flippedWinding is true. + // Leaving that out for now for consistency with the shader code that needs the same fix. + } + + transformedMeshCount++; + } + + // Unlink mesh from its previous transform node. + // TODO: This will leave some nodes unused. We could run a separate pass to compact the node list. + assert(mesh.instances.size() == 1); + auto& prevNode = mSceneGraph[mesh.instances[0]]; + auto it = std::find(prevNode.meshes.begin(), prevNode.meshes.end(), meshID); + assert(it != prevNode.meshes.end()); + prevNode.meshes.erase(it); + + // Link mesh to the identity transform node. + identityNode.meshes.push_back(meshID); + mesh.instances[0] = identityNodeID; + } + + logDebug("Pre-transformed " + std::to_string(transformedMeshCount) + " static meshes to world space"); + } + + void SceneBuilder::calculateMeshBoundingBoxes() + { + for (auto& mesh : mMeshes) + { + assert(!mesh.staticData.empty()); + assert((size_t)mesh.vertexCount == mesh.staticData.size()); + + AABB meshBB; + for (auto& v : mesh.staticData) + { + meshBB.include(v.position); + } + + mesh.boundingBox = meshBB; + } + } + + void SceneBuilder::createMeshGroups() + { + assert(mMeshGroups.empty()); + + // This function sorts meshes into groups based on their properties. + // The scene will build one BLAS per mesh group for raytracing. + // Note that a BLAS may be referenced by multiple TLAS instances. + // + // The sorting criteria are: + // - Instanced meshes are placed in unique groups (BLASes). + // The TLAS instances will apply different transforms but all refer to the same BLAS. + // - Non-instanced dynamic meshes (skinned and/or animated) are sorted into groups with the same transform. + // The idea is that all parts of a dynamic object that move together go in the same BLAS and the TLAS instance applies the transform. + // - Non-instanced static meshes are all placed in the same group. + // The vertices are pre-transformed in the BLAS and the TLAS instance has an identity transform. + // This ensures fast traversal for the static parts of a scene independent of the scene hierarchy. + // TODO: Add build flag to turn off pre-transformation to world space. + + // Classify non-instanced meshes. + // The non-instanced dynamic meshes are grouped based on what global matrix ID their transform is. + // The non-instanced static meshes are placed in the same group. + std::unordered_map> nodeToMeshList; + std::vector staticMeshes; + + for (uint32_t meshID = 0; meshID < (uint32_t)mMeshes.size(); meshID++) + { + const auto& mesh = mMeshes[meshID]; + if (mesh.instances.size() > 1) continue; // Only processing non-instanced meshes here + + assert(mesh.instances.size() == 1); + uint32_t nodeID = mesh.instances[0]; + + if (mesh.isStatic) staticMeshes.push_back(meshID); + else nodeToMeshList[nodeID].push_back(meshID); + } + + // Build final result. Format is a list of Mesh ID's per mesh group. + + // All static non-instanced meshes go in a single group or individual groups depending on config. + if (!staticMeshes.empty()) + { + if (!is_set(mFlags, Flags::RTDontMergeStatic)) + { + mMeshGroups.push_back({ staticMeshes, true }); + } + else + { + for (const auto& meshID : staticMeshes) mMeshGroups.push_back(MeshGroup{ std::vector({ meshID }), true }); + } + } + + // Non-instanced dynamic meshes were sorted above so just copy each list. + for (const auto& it : nodeToMeshList) + { + if (!is_set(mFlags, Flags::RTDontMergeDynamic)) + { + mMeshGroups.push_back({ it.second, false }); + } + else + { + for (const auto& meshID : it.second) mMeshGroups.push_back(MeshGroup{ std::vector({ meshID }), false }); + } + } + + // Instanced static and dynamic meshes always go in their own groups. + for (uint32_t meshID = 0; meshID < (uint32_t)mMeshes.size(); meshID++) + { + const auto& mesh = mMeshes[meshID]; + if (mesh.instances.size() == 1) continue; // Only processing instanced meshes here + mMeshGroups.push_back({ std::vector({ meshID }), false }); + } + } + + std::pair, std::optional> SceneBuilder::splitMesh(const uint32_t meshID, const int axis, const float pos) + { + // Splits a mesh by an axis-aligned plane. + // Each triangle is placed on either the left or right side of the plane with respect to its centroid. + // Individual triangles are not split, so the resulting meshes will in general have overlapping bounding boxes. + // If all triangles are already on either side, no split is necessary and the original mesh is retained. + + assert(meshID < mMeshes.size()); + assert(axis >= 0 && axis <= 2); + const auto& mesh = mMeshes[meshID]; + + // Check if mesh is supported. + if (mesh.dynamicVertexCount > 0 || !mesh.dynamicData.empty()) + { + throw std::exception(("Cannot split mesh '" + mesh.name + "', only non-dynamic meshes supported").c_str()); + } + if (mesh.topology != Vao::Topology::TriangleList) + { + throw std::exception(("Cannot split mesh '" + mesh.name + "', only triangle list topology supported").c_str()); + } + + // Early out if mesh is fully on either side of the splitting plane. + if (mesh.boundingBox.maxPoint[axis] < pos) return { meshID, std::nullopt }; + else if (mesh.boundingBox.minPoint[axis] >= pos) return { std::nullopt, meshID }; + + // Setup mesh specs. + auto createSpec = [](const MeshSpec& mesh, const std::string& name) + { + MeshSpec spec; + spec.name = name; + spec.topology = mesh.topology; + spec.materialId = mesh.materialId; + spec.isStatic = mesh.isStatic; + spec.isFrontFaceCW = mesh.isFrontFaceCW; + spec.instances = mesh.instances; + assert(mesh.hasDynamicData == false); + assert(mesh.dynamicVertexCount == 0); + return spec; + }; + + MeshSpec leftMesh = createSpec(mesh, mesh.name + ".0"); + MeshSpec rightMesh = createSpec(mesh, mesh.name + ".1"); + + if (mesh.indexCount > 0) splitIndexedMesh(mesh, leftMesh, rightMesh, axis, pos); + else splitNonIndexedMesh(mesh, leftMesh, rightMesh, axis, pos); + + // Check that no triangles were added or removed. + assert(leftMesh.getTriangleCount() + rightMesh.getTriangleCount() == mesh.getTriangleCount()); + + // It is possible all triangles ended up on either side of the splitting plane. + // In that case, there is no need to modify the original mesh and we'll just return. + if (leftMesh.getTriangleCount() == 0) return { std::nullopt, meshID }; + else if (rightMesh.getTriangleCount() == 0) return { meshID, std::nullopt }; + + logDebug("Mesh '" + mesh.name + "' with " + std::to_string(mesh.getTriangleCount()) + " triangles was split into two meshes with " + std::to_string(leftMesh.getTriangleCount()) + " and " + std::to_string(rightMesh.getTriangleCount()) + " triangles, respectively."); + + // Store new meshes. + // The left mesh replaces the existing mesh. + // The right mesh is appended at the end of the mesh list and linked to the instances. + assert(leftMesh.vertexCount > 0 && rightMesh.vertexCount > 0); + mMeshes[meshID] = std::move(leftMesh); + + uint32_t rightMeshID = (uint32_t)mMeshes.size(); + for (auto nodeID : mesh.instances) + { + mSceneGraph.at(nodeID).meshes.push_back(rightMeshID); + } + mMeshes.push_back(std::move(rightMesh)); + + return { meshID, rightMeshID }; + } + + void SceneBuilder::splitIndexedMesh(const MeshSpec& mesh, MeshSpec& leftMesh, MeshSpec& rightMesh, const int axis, const float pos) + { + assert(mesh.indexCount > 0 && !mesh.indexData.empty()); + + const uint32_t invalidIdx = uint32_t(-1); + std::vector leftIndexMap(mesh.indexCount, invalidIdx); + std::vector rightIndexMap(mesh.indexCount, invalidIdx); + + // Iterate over the triangles. + const size_t triangleCount = mesh.getTriangleCount(); + for (size_t i = 0; i < triangleCount * 3; i += 3) + { + const uint32_t indices[3] = { mesh.getIndex(i + 0), mesh.getIndex(i + 1), mesh.getIndex(i + 2) }; + + auto addVertex = [&](const uint32_t vtxIndex, MeshSpec& dstMesh, std::vector& indexMap) + { + if (indexMap[vtxIndex] != invalidIdx) return indexMap[vtxIndex]; + + uint32_t dstIndex = (uint32_t)dstMesh.staticData.size(); + dstMesh.staticData.push_back(mesh.staticData[vtxIndex]); + indexMap[vtxIndex] = dstIndex; + return dstIndex; + }; + auto addTriangleToMesh = [&](MeshSpec& dstMesh, std::vector& indexMap) + { + for (size_t j = 0; j < 3; j++) + { + uint32_t dstIdx = addVertex(indices[j], dstMesh, indexMap); + dstMesh.indexData.push_back(dstIdx); + } + }; + + // Compute the centroid and add the triangle to the left or right side. + float centroid = 0.f; + for (size_t j = 0; j < 3; j++) + { + centroid += mesh.staticData[indices[j]].position[axis]; + }; + centroid /= 3.f; + + if (centroid < pos) addTriangleToMesh(leftMesh, leftIndexMap); + else addTriangleToMesh(rightMesh, rightIndexMap); + } + + auto finalizeMesh = [this](MeshSpec& m) + { + m.indexCount = (uint32_t)m.indexData.size(); + m.vertexCount = (uint32_t)m.staticData.size(); + m.staticVertexCount = m.vertexCount; + + m.use16BitIndices = (m.vertexCount <= (1u << 16)) && !(is_set(mFlags, Flags::Force32BitIndices)); + if (m.use16BitIndices) m.indexData = compact16BitIndices(m.indexData); + + m.boundingBox = AABB(); + for (auto& v : m.staticData) m.boundingBox.include(v.position); + }; + + finalizeMesh(leftMesh); + finalizeMesh(rightMesh); + } + + void SceneBuilder::splitNonIndexedMesh(const MeshSpec& mesh, MeshSpec& leftMesh, MeshSpec& rightMesh, const int axis, const float pos) + { + assert(mesh.indexCount == 0 && mesh.indexData.empty()); + throw std::exception("SceneBuilder::splitNonIndexedMesh() not implemented"); + } + + size_t SceneBuilder::countTriangles(const MeshGroup& meshGroup) const + { + size_t triangleCount = 0; + for (auto meshID : meshGroup.meshList) + { + triangleCount += mMeshes[meshID].getTriangleCount(); + } + return triangleCount; + } + + AABB SceneBuilder::calculateBoundingBox(const MeshGroup& meshGroup) const + { + AABB bb; + for (auto meshID : meshGroup.meshList) + { + bb.include(mMeshes[meshID].boundingBox); + } + return bb; + } + + bool SceneBuilder::needsSplit(const MeshGroup& meshGroup, size_t& triangleCount) const + { + assert(!meshGroup.meshList.empty()); + triangleCount = countTriangles(meshGroup); + + if (triangleCount <= kMaxTrianglesPerBLAS) + { + return false; + } + else if (meshGroup.meshList.size() == 1) + { + // Issue warning if single mesh exceeds the triangle count limit. + // TODO: Implement mesh splitting to handle this case. + const auto& mesh = mMeshes[meshGroup.meshList[0]]; + assert(mesh.getTriangleCount() == triangleCount); + logWarning("Mesh '" + mesh.name + "' has " + std::to_string(triangleCount) + " triangles, expect extraneous GPU memory usage."); + + return false; + } + assert(meshGroup.meshList.size() > 1); + assert(triangleCount > kMaxTrianglesPerBLAS); + + return true; + } + + SceneBuilder::MeshGroupList SceneBuilder::splitMeshGroupSimple(MeshGroup& meshGroup) const + { + // This function partitions a mesh group into smaller groups based on triangle count. + // Note that the meshes are *not* reordered and individual meshes are not split, + // so it is still possible to get large spatial overlaps between groups. + + // Early out if splitting is not needed or possible. + size_t triangleCount = 0; + if (!needsSplit(meshGroup, triangleCount)) return MeshGroupList{ std::move(meshGroup) }; + + // Each new group holds at least one mesh, or if multiple, up to the target number of triangles. + assert(triangleCount > 0); + size_t targetGroupCount = div_round_up(triangleCount, kMaxTrianglesPerBLAS); + size_t targetTrianglesPerGroup = triangleCount / targetGroupCount; + + triangleCount = 0; + MeshGroupList groups; + + for (auto meshID : meshGroup.meshList) + { + // Start new group on first iteration or if triangle count would exceed the target. + size_t meshTris = mMeshes[meshID].getTriangleCount(); + if (triangleCount == 0 || triangleCount + meshTris > targetTrianglesPerGroup) + { + groups.push_back({ std::vector(), meshGroup.isStatic }); + triangleCount = 0; + } + + // Add mesh to group. + groups.back().meshList.push_back(meshID); + triangleCount += meshTris; + } + + assert(!groups.empty()); + return groups; + } + + SceneBuilder::MeshGroupList SceneBuilder::splitMeshGroupMedian(MeshGroup& meshGroup) const + { + // This function implements a recursive top-down BVH builder to partition a mesh group + // into smaller groups by splitting at the median in terms of triangle count. + // Note that individual meshes are not split, so it is still possible to get large spatial overlaps between groups. + + // Early out if splitting is not needed or possible. + size_t triangleCount = 0; + if (!needsSplit(meshGroup, triangleCount)) return MeshGroupList{ std::move(meshGroup) }; + + // Sort the meshes by centroid along the largest axis. + AABB bb = calculateBoundingBox(meshGroup); + const int axis = largestAxis(bb.extent()); + auto compareCentroids = [this, axis](uint32_t leftMeshID, uint32_t rightMeshID) + { + return mMeshes[leftMeshID].boundingBox.center()[axis] < mMeshes[rightMeshID].boundingBox.center()[axis]; + }; + + std::vector meshes = std::move(meshGroup.meshList); + std::sort(meshes.begin(), meshes.end(), compareCentroids); + + // Find the median mesh in terms of triangle count. + size_t triangles = 0; + auto countTriangles = [&](uint32_t meshID) + { + triangles += mMeshes[meshID].getTriangleCount(); + return triangles > triangleCount / 2; + }; + + auto splitIter = std::find_if(meshes.begin(), meshes.end(), countTriangles); + + // If all meshes ended up on either side, fall back on splitting at the middle mesh. + if (splitIter == meshes.begin() || splitIter == meshes.end()) + { + assert(meshes.size() >= 2); + splitIter = meshes.begin() + meshes.size() / 2; + } + assert(splitIter != meshes.begin() && splitIter != meshes.end()); + + // Recursively split the left and right mesh groups. + MeshGroup leftGroup{ std::vector(meshes.begin(), splitIter), meshGroup.isStatic }; + MeshGroup rightGroup{ std::vector(splitIter, meshes.end()), meshGroup.isStatic }; + assert(!leftGroup.meshList.empty() && !rightGroup.meshList.empty()); + + MeshGroupList leftList = splitMeshGroupMedian(leftGroup); + MeshGroupList rightList = splitMeshGroupMedian(rightGroup); + + // Move elements into a single list and return. + leftList.insert( + leftList.end(), + std::make_move_iterator(rightList.begin()), + std::make_move_iterator(rightList.end())); + + return leftList; + } + + SceneBuilder::MeshGroupList SceneBuilder::splitMeshGroupMidpointMeshes(MeshGroup& meshGroup) + { + // This function recursively splits a mesh group at the midpoint along the largest axis. + // Individual meshes that straddle the splitting plane are split into two halves. + // This will ensure minimal spatial overlaps between groups. + + // Early out if splitting is not needed or possible. + size_t triangleCount = 0; + if (!needsSplit(meshGroup, triangleCount)) return MeshGroupList{ std::move(meshGroup) }; + + // Find the midpoint along the largest axis. + AABB bb = calculateBoundingBox(meshGroup); + const int axis = largestAxis(bb.extent()); + const float pos = bb.center()[axis]; + + // Partition all meshes by the splitting plane. + std::vector leftMeshes, rightMeshes; + + for (auto meshID : meshGroup.meshList) + { + auto result = splitMesh(meshID, axis, pos); + if (auto leftMeshID = result.first) leftMeshes.push_back(*leftMeshID); + if (auto rightMeshID = result.second) rightMeshes.push_back(*rightMeshID); + } + + // If either side contains all meshes, do not split further. + if (leftMeshes.empty() || rightMeshes.empty()) return MeshGroupList{ std::move(meshGroup) }; + + // Recursively split the left and right mesh groups. + MeshGroup leftGroup{ std::move(leftMeshes), meshGroup.isStatic }; + MeshGroup rightGroup{ std::move(rightMeshes), meshGroup.isStatic }; + + MeshGroupList leftList = splitMeshGroupMidpointMeshes(leftGroup); + MeshGroupList rightList = splitMeshGroupMidpointMeshes(rightGroup); + + // Move elements into a single list and return. + leftList.insert( + leftList.end(), + std::make_move_iterator(rightList.begin()), + std::make_move_iterator(rightList.end())); + + return leftList; + } + + void SceneBuilder::optimizeGeometry() + { + // This function optimizes the geometry for raytracing performance and memory usage. + // + // There is a max triangles per group limit to reduce the worst-case memory requirements for BLAS builds. + // If the limit is exceeded, the geometry is split into multiple groups (BLASes). + // Splitting has performance implications for the traversal due to spatial overlap between the BLASes. + // + // To reduce the perf impact we may perform these steps: + // - Split large mesh groups (BLASes) into multiple smaller ones. + // - Split large meshes into smaller to reduce spatial overlap between BLASes. + // - Sort meshes into BLASes based on spatial locality. + + MeshGroupList optimizedGroups; + + for (auto& meshGroup : mMeshGroups) + { + //auto groups = splitMeshGroupSimple(meshGroup); + //auto groups = splitMeshGroupMedian(meshGroup); + auto groups = splitMeshGroupMidpointMeshes(meshGroup); + + if (groups.size() > 1) logWarning("SceneBuilder::optimizeGeometry() performance warning - Mesh group was split into " + std::to_string(groups.size()) + " groups"); + + optimizedGroups.insert( + optimizedGroups.end(), + std::make_move_iterator(groups.begin()), + std::make_move_iterator(groups.end())); + } + + mMeshGroups = std::move(optimizedGroups); + } + + void SceneBuilder::createGlobalBuffers() + { + assert(mBuffersData.indexData.empty()); + assert(mBuffersData.staticData.empty()); + assert(mBuffersData.dynamicData.empty()); + + const bool isIndexed = !is_set(mFlags, Flags::NonIndexedVertices); + + // Count total number of vertex and index data elements. + size_t totalIndexDataCount = 0; + size_t totalStaticVertexCount = 0; + size_t totalDynamicVertexCount = 0; + + for (const auto& mesh : mMeshes) + { + totalIndexDataCount += mesh.indexData.size(); + totalStaticVertexCount += mesh.staticData.size(); + totalDynamicVertexCount += mesh.dynamicData.size(); + } + + // Check the range. We currently use 32-bit offsets. + if (totalIndexDataCount > std::numeric_limits::max() || + totalStaticVertexCount > std::numeric_limits::max() || + totalDynamicVertexCount > std::numeric_limits::max()) + { + throw std::exception("Trying to build a scene that exceeds supported mesh data size."); + } + + mBuffersData.indexData.reserve(totalIndexDataCount); + mBuffersData.staticData.reserve(totalStaticVertexCount); + mBuffersData.dynamicData.reserve(totalDynamicVertexCount); + + // Copy all vertex and index data into the global buffers. + for (auto& mesh : mMeshes) + { + mesh.staticVertexOffset = (uint32_t)mBuffersData.staticData.size(); + mesh.dynamicVertexOffset = (uint32_t)mBuffersData.dynamicData.size(); + + // Insert the static vertex data in the global array. + // The vertices are automatically converted to their packed format in this step. + mBuffersData.staticData.insert(mBuffersData.staticData.end(), mesh.staticData.begin(), mesh.staticData.end()); + + if (isIndexed) + { + mesh.indexOffset = (uint32_t)mBuffersData.indexData.size(); + mBuffersData.indexData.insert(mBuffersData.indexData.end(), mesh.indexData.begin(), mesh.indexData.end()); + } + + if (!mesh.dynamicData.empty()) + { + mBuffersData.dynamicData.insert(mBuffersData.dynamicData.end(), mesh.dynamicData.begin(), mesh.dynamicData.end()); + + // Patch vertex index references. + for (uint32_t i = 0; i < mesh.dynamicData.size(); ++i) + { + mBuffersData.dynamicData[mesh.dynamicVertexOffset + i].staticIndex += mesh.staticVertexOffset; + } + } + + // Free the mesh local data. + mesh.indexData.clear(); + mesh.staticData.clear(); + mesh.dynamicData.clear(); + } + } + + void SceneBuilder::createCurveGlobalBuffers() + { + assert(mCurveBuffersData.indexData.empty()); + assert(mCurveBuffersData.staticData.empty()); + + // Count total number of curve vertex and index data elements. + size_t totalIndexDataCount = 0; + size_t totalStaticCurveVertexCount = 0; + + for (const auto& curve : mCurves) + { + totalIndexDataCount += curve.indexData.size(); + totalStaticCurveVertexCount += curve.staticData.size(); + } + + // Check the range. We currently use 32-bit offsets. + if (totalIndexDataCount > std::numeric_limits::max() || + totalStaticCurveVertexCount > std::numeric_limits::max()) + { + throw std::exception("Trying to build a scene that exceeds supported curve data size."); + } + + mCurveBuffersData.indexData.reserve(totalIndexDataCount); + mCurveBuffersData.staticData.reserve(totalStaticCurveVertexCount); + + // Copy all curve vertex and index data into the curve global buffers. + for (auto& curve : mCurves) + { + curve.staticVertexOffset = (uint32_t)mCurveBuffersData.staticData.size(); + mCurveBuffersData.staticData.insert(mCurveBuffersData.staticData.end(), curve.staticData.begin(), curve.staticData.end()); + + curve.indexOffset = (uint32_t)mCurveBuffersData.indexData.size(); + mCurveBuffersData.indexData.insert(mCurveBuffersData.indexData.end(), curve.indexData.begin(), curve.indexData.end()); + + // Free the curve local data. + curve.indexData.clear(); + curve.staticData.clear(); + } + } + + void SceneBuilder::removeDuplicateMaterials() + { + if (is_set(mFlags, Flags::DontMergeMaterials)) return; + + std::vector uniqueMaterials; + std::vector idMap(mMaterials.size()); + + // Find unique set of materials. + for (uint32_t id = 0; id < mMaterials.size(); ++id) + { + const auto& pMaterial = mMaterials[id]; + auto it = std::find_if(uniqueMaterials.begin(), uniqueMaterials.end(), [&pMaterial] (const auto& m) { return *m == *pMaterial; }); + if (it == uniqueMaterials.end()) + { + idMap[id] = (uint32_t)uniqueMaterials.size(); + uniqueMaterials.push_back(pMaterial); + } + else + { + logInfo("Removing duplicate material '" + pMaterial->getName() + "' (duplicate of '" + (*it)->getName() + "')"); + idMap[id] = (uint32_t)std::distance(uniqueMaterials.begin(), it); + } + } + + // Reassign material IDs. + for (auto& mesh : mMeshes) + { + mesh.materialId = idMap[mesh.materialId]; + } + + mMaterials = uniqueMaterials; + } + + void SceneBuilder::collectVolumeGrids() + { + // Collect grids from volumes. + std::set uniqueGrids; + for (auto& volume : mVolumes) + { + auto grids = volume->getAllGrids(); + uniqueGrids.insert(grids.begin(), grids.end()); + } + mGrids = GridList(uniqueGrids.begin(), uniqueGrids.end()); + + // Setup grid -> id map. + for (size_t i = 0; i < mGrids.size(); ++i) + { + mGridIDs.emplace(mGrids[i], (uint32_t)i); + } + } + + void SceneBuilder::quantizeTexCoords() + { + // Match texture coordinate quantization for textured emissives to format of PackedEmissiveTriangle. + // This is to avoid mismatch when sampling and evaluating emissive triangles. + // Note that non-emissive meshes are unmodified and use full precision texcoords. + for (auto& mesh : mMeshes) + { + const auto& pMaterial = mMaterials[mesh.materialId]; + if (pMaterial->getEmissiveTexture() != nullptr) + { + // Quantize texture coordinates to fp16. Also track the bounds and max error. + float2 minTexCrd = float2(std::numeric_limits::infinity()); + float2 maxTexCrd = float2(-std::numeric_limits::infinity()); + float2 maxError = float2(0); + + for (uint32_t i = 0; i < mesh.staticVertexCount; ++i) + { + auto& v = mBuffersData.staticData[mesh.staticVertexOffset + i]; + float2 texCrd = v.texCrd; + minTexCrd = min(minTexCrd, texCrd); + maxTexCrd = max(maxTexCrd, texCrd); + v.texCrd = f16tof32(f32tof16(texCrd)); + maxError = max(maxError, abs(v.texCrd - texCrd)); + } + + // Issue warning if quantization errors are too large. + float2 maxAbsCrd = max(abs(minTexCrd), abs(maxTexCrd)); + if (maxAbsCrd.x > HLF_MAX || maxAbsCrd.y > HLF_MAX) + { + logWarning("Texture coordinates for emissive textured mesh '" + mesh.name + "' are outside the representable range, expect rendering errors."); + } + else + { + // Compute maximum quantization error in texels. + // The texcoords are used for all texture channels so taking the maximum dimensions. + uint2 maxTexDim = pMaterial->getMaxTextureDimensions(); + maxError *= maxTexDim; + float maxTexelError = std::max(maxError.x, maxError.y); + + if (maxTexelError > kMaxTexelError) + { + std::ostringstream oss; + oss << "Texture coordinates for emissive textured mesh '" << mesh.name << "' have a large quantization error of " << maxTexelError << " texels. " + << "The coordinate range is [" << minTexCrd.x << ", " << maxTexCrd.x << "] x [" << minTexCrd.y << ", " << maxTexCrd.y << "] for maximum texture dimensions (" + << maxTexDim.x << ", " << maxTexDim.y << ")."; + logWarning(oss.str()); + } + } + } + } + } + + void SceneBuilder::createMeshVao(uint32_t drawCount) + { + for (auto& mesh : mMeshes) assert(mesh.topology == mMeshes[0].topology); + + // Create the index buffer. + size_t ibSize = sizeof(uint32_t) * mBuffersData.indexData.size(); + if (ibSize > std::numeric_limits::max()) + { + throw std::exception("Index buffer size exceeds 4GB"); + } + + Buffer::SharedPtr pIB = nullptr; + if (ibSize > 0) + { + ResourceBindFlags ibBindFlags = Resource::BindFlags::Index | ResourceBindFlags::ShaderResource; + pIB = Buffer::create(ibSize, ibBindFlags, Buffer::CpuAccess::None, mBuffersData.indexData.data()); + } + + // Create the vertex data structured buffer. + const size_t vertexCount = (uint32_t)mBuffersData.staticData.size(); + size_t staticVbSize = sizeof(PackedStaticVertexData) * vertexCount; + if (staticVbSize > std::numeric_limits::max()) + { + throw std::exception("Vertex buffer size exceeds 4GB"); + } + + ResourceBindFlags vbBindFlags = ResourceBindFlags::ShaderResource | ResourceBindFlags::UnorderedAccess | ResourceBindFlags::Vertex; + Buffer::SharedPtr pStaticBuffer = Buffer::createStructured(sizeof(PackedStaticVertexData), (uint32_t)vertexCount, vbBindFlags, Buffer::CpuAccess::None, nullptr, false); + + Vao::BufferVec pVBs(Scene::kVertexBufferCount); + pVBs[Scene::kStaticDataBufferIndex] = pStaticBuffer; + + // Create the draw ID buffer. + // This is only needed when rasterizing the scene. + ResourceFormat drawIDFormat = drawCount <= (1 << 16) ? ResourceFormat::R16Uint : ResourceFormat::R32Uint; + + Buffer::SharedPtr pDrawIDBuffer = nullptr; + if (drawIDFormat == ResourceFormat::R16Uint) + { + assert(drawCount <= (1 << 16)); + std::vector drawIDs(drawCount); + for (uint32_t i = 0; i < drawCount; i++) drawIDs[i] = i; + pDrawIDBuffer = Buffer::create(drawCount * sizeof(uint16_t), ResourceBindFlags::Vertex, Buffer::CpuAccess::None, drawIDs.data()); + } + else if (drawIDFormat == ResourceFormat::R32Uint) + { + std::vector drawIDs(drawCount); + for (uint32_t i = 0; i < drawCount; i++) drawIDs[i] = i; + pDrawIDBuffer = Buffer::create(drawCount * sizeof(uint32_t), ResourceBindFlags::Vertex, Buffer::CpuAccess::None, drawIDs.data()); + } + else should_not_get_here(); + + assert(pDrawIDBuffer); + pVBs[Scene::kDrawIdBufferIndex] = pDrawIDBuffer; + + // Create vertex layout. + // The layout only initializes the vertex data and draw ID layout. The skinning data doesn't get passed into the vertex shader. + VertexLayout::SharedPtr pLayout = VertexLayout::create(); + + // Add the packed static vertex data layout. + VertexBufferLayout::SharedPtr pStaticLayout = VertexBufferLayout::create(); + pStaticLayout->addElement(VERTEX_POSITION_NAME, offsetof(PackedStaticVertexData, position), ResourceFormat::RGB32Float, 1, VERTEX_POSITION_LOC); + pStaticLayout->addElement(VERTEX_PACKED_NORMAL_TANGENT_NAME, offsetof(PackedStaticVertexData, packedNormalTangent), ResourceFormat::RGB32Float, 1, VERTEX_PACKED_NORMAL_TANGENT_LOC); + pStaticLayout->addElement(VERTEX_TEXCOORD_NAME, offsetof(PackedStaticVertexData, texCrd), ResourceFormat::RG32Float, 1, VERTEX_TEXCOORD_LOC); + pLayout->addBufferLayout(Scene::kStaticDataBufferIndex, pStaticLayout); + + // Add the draw ID layout. + VertexBufferLayout::SharedPtr pInstLayout = VertexBufferLayout::create(); + pInstLayout->addElement(INSTANCE_DRAW_ID_NAME, 0, drawIDFormat, 1, INSTANCE_DRAW_ID_LOC); + pInstLayout->setInputClass(VertexBufferLayout::InputClass::PerInstanceData, 1); + pLayout->addBufferLayout(Scene::kDrawIdBufferIndex, pInstLayout); + + // Create the VAO objects. + // Note that the global index buffer can be mixed 16/32-bit format. + // For drawing the meshes we need separate VAOs for these cases. + assert(mpScene && mpScene->mpVao == nullptr); + mpScene->mpVao = Vao::create(mMeshes[0].topology, pLayout, pVBs, pIB, ResourceFormat::R32Uint); + mpScene->mpVao16Bit = Vao::create(mMeshes[0].topology, pLayout, pVBs, pIB, ResourceFormat::R16Uint); + } + + uint32_t SceneBuilder::createMeshData() + { + assert(mpScene->mMeshDesc.empty()); + assert(mpScene->mMeshInstanceData.empty()); + assert(mpScene->mMeshHasDynamicData.empty()); + assert(mpScene->mMeshIdToInstanceIds.empty()); + assert(mpScene->mMeshGroups.empty()); + + auto& meshData = mpScene->mMeshDesc; + auto& instanceData = mpScene->mMeshInstanceData; + meshData.resize(mMeshes.size()); + mpScene->mMeshHasDynamicData.resize(mMeshes.size()); + size_t drawCount = 0; + + // Setup all mesh data. + for (uint32_t meshID = 0; meshID < mMeshes.size(); meshID++) + { + const auto& mesh = mMeshes[meshID]; + meshData[meshID].materialID = mesh.materialId; + meshData[meshID].vbOffset = mesh.staticVertexOffset; + meshData[meshID].ibOffset = mesh.indexOffset; + meshData[meshID].vertexCount = mesh.vertexCount; + meshData[meshID].indexCount = mesh.indexCount; + meshData[meshID].dynamicVbOffset = mesh.hasDynamicData ? mesh.dynamicVertexOffset : 0; + assert(mesh.dynamicVertexCount == 0 || mesh.dynamicVertexCount == mesh.staticVertexCount); + + mpScene->mMeshNames.push_back(mesh.name); + + uint32_t meshFlags = 0; + meshFlags |= mesh.use16BitIndices ? (uint32_t)MeshFlags::Use16BitIndices : 0; + meshFlags |= mesh.hasDynamicData ? (uint32_t)MeshFlags::HasDynamicData : 0; + meshFlags |= mesh.isFrontFaceCW ? (uint32_t)MeshFlags::IsFrontFaceCW : 0; + meshData[meshID].flags = meshFlags; + + if (mesh.use16BitIndices) mpScene->mHas16BitIndices = true; + else mpScene->mHas32BitIndices = true; + + if (mesh.hasDynamicData) + { + assert(mesh.instances.size() == 1); + mpScene->mMeshHasDynamicData[meshID] = true; + + for (uint32_t i = 0; i < mesh.vertexCount; i++) + { + mBuffersData.dynamicData[mesh.dynamicVertexOffset + i].globalMatrixID = (uint32_t)mesh.instances[0]; + } + } + } + + // Setup all mesh instances. + // Mesh instances are added in the order they appear in the mesh groups. + // For ray tracing, one BLAS per mesh group is created and the mesh instances + // can therefore be directly indexed by [InstanceID() + GeometryIndex()]. + // This avoids the need to have a lookup table from hit IDs to mesh instance. + for (const auto& meshGroup : mMeshGroups) + { + assert(!meshGroup.meshList.empty()); + for (const uint32_t meshID : meshGroup.meshList) + { + const auto& mesh = mMeshes[meshID]; + drawCount += mesh.instances.size(); + + for (const auto& nodeID : mesh.instances) + { + instanceData.push_back({}); + auto& meshInstance = instanceData.back(); + meshInstance.globalMatrixID = nodeID; + meshInstance.materialID = mesh.materialId; + meshInstance.meshID = meshID; + meshInstance.vbOffset = mesh.staticVertexOffset; + meshInstance.ibOffset = mesh.indexOffset; + + uint32_t instanceFlags = 0; + instanceFlags |= mesh.use16BitIndices ? (uint32_t)MeshInstanceFlags::Use16BitIndices : 0; + instanceFlags |= mesh.hasDynamicData ? (uint32_t)MeshInstanceFlags::HasDynamicData : 0; + meshInstance.flags = instanceFlags; + } + } + } + + // Create mapping of mesh IDs to their instanc IDs. + mpScene->mMeshIdToInstanceIds.resize(mMeshes.size()); + for (uint32_t instanceID = 0; instanceID < (uint32_t)instanceData.size(); instanceID++) + { + const auto& instance = instanceData[instanceID]; + mpScene->mMeshIdToInstanceIds[instance.meshID].push_back(instanceID); + } + + // Setup mesh groups. This just copies our final list. + mpScene->mMeshGroups = mMeshGroups; + + assert(drawCount <= std::numeric_limits::max()); + return (uint32_t)drawCount; + } + + void SceneBuilder::createCurveVao() + { + // Create the index buffer. + size_t ibSize = sizeof(uint32_t) * mCurveBuffersData.indexData.size(); + if (ibSize > std::numeric_limits::max()) + { + throw std::exception("Curve index buffer size exceeds 4GB"); + } + + Buffer::SharedPtr pIB = nullptr; + if (ibSize > 0) + { + ResourceBindFlags ibBindFlags = Resource::BindFlags::Index | ResourceBindFlags::ShaderResource; + pIB = Buffer::create(ibSize, ibBindFlags, Buffer::CpuAccess::None, mCurveBuffersData.indexData.data()); + } + + // Create the vertex data as structured buffers. + const size_t vertexCount = (uint32_t)mCurveBuffersData.staticData.size(); + size_t staticVbSize = sizeof(StaticCurveVertexData) * vertexCount; + if (staticVbSize > std::numeric_limits::max()) + { + throw std::exception("Curve vertex buffer exceeds 4GB"); + } + + ResourceBindFlags vbBindFlags = ResourceBindFlags::ShaderResource | ResourceBindFlags::UnorderedAccess | ResourceBindFlags::Vertex; + // Also upload the curve vertex data. + Buffer::SharedPtr pStaticBuffer = Buffer::createStructured(sizeof(StaticCurveVertexData), (uint32_t)vertexCount, vbBindFlags, Buffer::CpuAccess::None, mCurveBuffersData.staticData.data(), false); + + // Curves do not need DrawIDBuffer. + Vao::BufferVec pVBs(Scene::kVertexBufferCount - 1); + pVBs[Scene::kStaticDataBufferIndex] = pStaticBuffer; + + // Create vertex layout. + // The layout only initializes the vertex data layout. The skinning data doesn't get passed into the vertex shader. + VertexLayout::SharedPtr pLayout = VertexLayout::create(); + + // Add the packed static vertex data layout. + VertexBufferLayout::SharedPtr pStaticLayout = VertexBufferLayout::create(); + pStaticLayout->addElement(CURVE_VERTEX_POSITION_NAME, offsetof(StaticCurveVertexData, position), ResourceFormat::RGB32Float, 1, CURVE_VERTEX_POSITION_LOC); + pStaticLayout->addElement(CURVE_VERTEX_RADIUS_NAME, offsetof(StaticCurveVertexData, radius), ResourceFormat::R32Float, 1, CURVE_VERTEX_RADIUS_LOC); + pStaticLayout->addElement(CURVE_VERTEX_TANGENT_NAME, offsetof(StaticCurveVertexData, tangent), ResourceFormat::RGB32Float, 1, CURVE_VERTEX_TANGENT_LOC); + pStaticLayout->addElement(CURVE_VERTEX_NORMAL_NAME, offsetof(StaticCurveVertexData, normal), ResourceFormat::RGB32Float, 1, CURVE_VERTEX_NORMAL_LOC); + pStaticLayout->addElement(CURVE_VERTEX_TEXCOORD_NAME, offsetof(StaticCurveVertexData, texCrd), ResourceFormat::RG32Float, 1, CURVE_VERTEX_TEXCOORD_LOC); + pLayout->addBufferLayout(Scene::kStaticDataBufferIndex, pStaticLayout); + + // Create the VAO objects. + assert(mpScene && mpScene->mpCurveVao == nullptr); + mpScene->mpCurveVao = Vao::create(Vao::Topology::LineStrip, pLayout, pVBs, pIB, ResourceFormat::R32Uint); + } + + void SceneBuilder::createCurveData() + { + auto& curveData = mpScene->mCurveDesc; + auto& instanceData = mpScene->mCurveInstanceData; + curveData.resize(mCurves.size()); + + for (uint32_t curveID = 0; curveID < mCurves.size(); curveID++) + { + // Curve data. + const auto& curve = mCurves[curveID]; + curveData[curveID].materialID = curve.materialId; + curveData[curveID].degree = curve.degree; + curveData[curveID].vbOffset = curve.staticVertexOffset; + curveData[curveID].ibOffset = curve.indexOffset; + curveData[curveID].vertexCount = curve.vertexCount; + curveData[curveID].indexCount = curve.indexCount; + + // Curve instance data. + for (const auto& instance : curve.instances) + { + instanceData.push_back({}); + auto& curveInstance = instanceData.back(); + curveInstance.globalMatrixID = instance; + curveInstance.materialID = curve.materialId; + curveInstance.curveID = curveID; + curveInstance.vbOffset = curve.staticVertexOffset; + curveInstance.ibOffset = curve.indexOffset; + } + } + } + + void SceneBuilder::mapCurvesToProceduralPrimitives(uint32_t typeID) + { + // Clear any previously mapped curves. + mProceduralPrimitives.resize(mCustomPrimitiveAABBs.size()); + + uint32_t offset = (uint32_t)mCustomPrimitiveAABBs.size(); // Start curve AABBs at end of user defined AABBs. + + // Add curves to mProceduralPrimitives. + for (uint32_t curveID = 0; curveID < mCurves.size(); curveID++) + { + const auto& curve = mCurves[curveID]; + assert(curve.instances.size() == 1); // Assume static curves. + for (const auto& instID : curve.instances) + { + pushProceduralPrimitive(typeID, curveID, offset, curve.indexCount); + } + offset += curve.indexCount; + } + } + + void SceneBuilder::createRaytracingAABBData() + { + if (mProceduralPrimitives.empty()) return; + + uint32_t totalAABBCount = (uint32_t)mCustomPrimitiveAABBs.size(); + for (uint32_t i = 0; i < mCurves.size(); i++) totalAABBCount += mCurves[i].indexCount; + + mpScene->mRtAABBRaw.resize(totalAABBCount); + uint32_t offset = 0; + + // Add all user-defined AABBs + for (auto& aabb : mCustomPrimitiveAABBs) + { + D3D12_RAYTRACING_AABB& rtAabb = mpScene->mRtAABBRaw[offset++]; + rtAabb.MinX = aabb.minPoint.x; + rtAabb.MinY = aabb.minPoint.y; + rtAabb.MinZ = aabb.minPoint.z; + rtAabb.MaxX = aabb.maxPoint.x; + rtAabb.MaxY = aabb.maxPoint.y; + rtAabb.MaxZ = aabb.maxPoint.z; + } + + // Compute AABBs of curve segments. + for (const auto& curve : mCurves) + { + const auto* indexData = &mCurveBuffersData.indexData[curve.indexOffset]; + const auto* staticData = &mCurveBuffersData.staticData[curve.staticVertexOffset]; + for (uint32_t j = 0; j < curve.indexCount; j++) + { + AABB curveSegBB; + uint32_t v = indexData[j]; + + for (uint32_t k = 0; k <= curve.degree; k++) + { + curveSegBB.include(staticData[v + k].position - float3(staticData[v + k].radius)); + curveSegBB.include(staticData[v + k].position + float3(staticData[v + k].radius)); + } + + D3D12_RAYTRACING_AABB& aabb = mpScene->mRtAABBRaw[offset++]; + aabb.MinX = curveSegBB.minPoint.x; + aabb.MinY = curveSegBB.minPoint.y; + aabb.MinZ = curveSegBB.minPoint.z; + aabb.MaxX = curveSegBB.maxPoint.x; + aabb.MaxY = curveSegBB.maxPoint.y; + aabb.MaxZ = curveSegBB.maxPoint.z; + } + } + + // Set custom prim metadata + mpScene->mProceduralPrimData = mProceduralPrimitives; + } + + void SceneBuilder::createNodeList() + { + mpScene->mSceneGraph.resize(mSceneGraph.size()); + + for (size_t i = 0; i < mSceneGraph.size(); i++) + { + assert(mSceneGraph[i].parent <= std::numeric_limits::max()); + mpScene->mSceneGraph[i] = Scene::Node(mSceneGraph[i].name, (uint32_t)mSceneGraph[i].parent, mSceneGraph[i].transform, mSceneGraph[i].localToBindPose); + } + } + + void SceneBuilder::createMeshBoundingBoxes() + { + mpScene->mMeshBBs.resize(mMeshes.size()); + + for (size_t i = 0; i < mMeshes.size(); i++) + { + const auto& mesh = mMeshes[i]; + mpScene->mMeshBBs[i] = mesh.boundingBox; + } + } + + void SceneBuilder::calculateCurveBoundingBoxes() + { + // Calculate curve bounding boxes. + mpScene->mCurveBBs.resize(mCurves.size()); + for (size_t i = 0; i < mCurves.size(); i++) + { + const auto& curve = mCurves[i]; + AABB curveBB; + + const auto* staticData = &mCurveBuffersData.staticData[curve.staticVertexOffset]; + for (uint32_t v = 0; v < curve.vertexCount; v++) + { + float radius = staticData[v].radius; + curveBB.include(staticData[v].position - float3(radius)); + curveBB.include(staticData[v].position + float3(radius)); + } + + mpScene->mCurveBBs[i] = curveBB; + } + } + + void SceneBuilder::pushProceduralPrimitive(uint32_t typeID, uint32_t instanceIdx, uint32_t AABBOffset, uint32_t AABBCount) + { + ProceduralPrimitiveData data; + data.typeID = typeID; + data.instanceIdx = instanceIdx; + data.AABBOffset = AABBOffset; + data.AABBCount = AABBCount; + + mProceduralPrimitives.push_back(data); + } + + SCRIPT_BINDING(SceneBuilder) + { + SCRIPT_BINDING_DEPENDENCY(Scene) + SCRIPT_BINDING_DEPENDENCY(TriangleMesh) + SCRIPT_BINDING_DEPENDENCY(Material) + SCRIPT_BINDING_DEPENDENCY(Light) + SCRIPT_BINDING_DEPENDENCY(Transform) + SCRIPT_BINDING_DEPENDENCY(EnvMap) + SCRIPT_BINDING_DEPENDENCY(Animation) + + pybind11::enum_ flags(m, "SceneBuilderFlags"); + flags.value("Default", SceneBuilder::Flags::Default); + flags.value("DontMergeMaterials", SceneBuilder::Flags::DontMergeMaterials); flags.value("UseOriginalTangentSpace", SceneBuilder::Flags::UseOriginalTangentSpace); flags.value("AssumeLinearSpaceTextures", SceneBuilder::Flags::AssumeLinearSpaceTextures); flags.value("DontMergeMeshes", SceneBuilder::Flags::DontMergeMeshes); - flags.value("BuffersAsShaderResource", SceneBuilder::Flags::BuffersAsShaderResource); flags.value("UseSpecGlossMaterials", SceneBuilder::Flags::UseSpecGlossMaterials); flags.value("UseMetalRoughMaterials", SceneBuilder::Flags::UseMetalRoughMaterials); flags.value("NonIndexedVertices", SceneBuilder::Flags::NonIndexedVertices); + flags.value("Force32BitIndices", SceneBuilder::Flags::Force32BitIndices); + flags.value("RTDontMergeStatic", SceneBuilder::Flags::RTDontMergeStatic); + flags.value("RTDontMergeDynamic", SceneBuilder::Flags::RTDontMergeDynamic); ScriptBindings::addEnumBinaryOperators(flags); + + pybind11::class_ sceneBuilder(m, "SceneBuilder"); + sceneBuilder.def_property_readonly("flags", &SceneBuilder::getFlags); + sceneBuilder.def_property_readonly("materials", &SceneBuilder::getMaterials); + sceneBuilder.def_property_readonly("volumes", &SceneBuilder::getVolumes); + sceneBuilder.def_property_readonly("lights", &SceneBuilder::getLights); + sceneBuilder.def_property_readonly("cameras", &SceneBuilder::getCameras); + sceneBuilder.def_property_readonly("animations", &SceneBuilder::getAnimations); + sceneBuilder.def_property("renderSettings", pybind11::overload_cast(&SceneBuilder::getRenderSettings, pybind11::const_), &SceneBuilder::setRenderSettings); + sceneBuilder.def_property("envMap", &SceneBuilder::getEnvMap, &SceneBuilder::setEnvMap); + sceneBuilder.def_property("selectedCamera", &SceneBuilder::getSelectedCamera, &SceneBuilder::setSelectedCamera); + sceneBuilder.def_property("cameraSpeed", &SceneBuilder::getCameraSpeed, &SceneBuilder::setCameraSpeed); + sceneBuilder.def("importScene", [] (SceneBuilder* pSceneBuilder, const std::string& filename, const pybind11::dict& dict, const std::vector& instances) { + SceneBuilder::InstanceMatrices instanceMatrices; + for (const auto& instance : instances) + { + instanceMatrices.push_back(instance.getMatrix()); + } + return pSceneBuilder->import(filename, instanceMatrices, Dictionary(dict)); + }, "filename"_a, "dict"_a = pybind11::dict(), "instances"_a = std::vector()); + sceneBuilder.def("addTriangleMesh", &SceneBuilder::addTriangleMesh, "triangleMesh"_a, "material"_a); + sceneBuilder.def("addMaterial", &SceneBuilder::addMaterial, "material"_a); + sceneBuilder.def("getMaterial", &SceneBuilder::getMaterial, "name"_a); + sceneBuilder.def("loadMaterialTexture", &SceneBuilder::loadMaterialTexture, "material"_a, "slot"_a, "filename"_a); + sceneBuilder.def("addVolume", &SceneBuilder::addVolume, "volume"_a); + sceneBuilder.def("getVolume", &SceneBuilder::getVolume, "name"_a); + sceneBuilder.def("addLight", &SceneBuilder::addLight, "light"_a); + sceneBuilder.def("addCamera", &SceneBuilder::addCamera, "camera"_a); + sceneBuilder.def("addAnimation", &SceneBuilder::addAnimation, "animation"_a); + sceneBuilder.def("createAnimation", &SceneBuilder::createAnimation, "animatable"_a, "name"_a, "duration"_a); + sceneBuilder.def("addNode", [] (SceneBuilder* pSceneBuilder, const std::string& name, const Transform& transform, uint32_t parent) { + SceneBuilder::Node node; + node.name = name; + node.transform = transform.getMatrix(); + node.parent = parent; + return pSceneBuilder->addNode(node); + }, "name"_a, "transform"_a = Transform(), "parent"_a = SceneBuilder::kInvalidNode); + sceneBuilder.def("addMeshInstance", &SceneBuilder::addMeshInstance); } } diff --git a/Source/Falcor/Scene/SceneBuilder.h b/Source/Falcor/Scene/SceneBuilder.h index 1bb270e1fa..9732311387 100644 --- a/Source/Falcor/Scene/SceneBuilder.h +++ b/Source/Falcor/Scene/SceneBuilder.h @@ -27,6 +27,9 @@ **************************************************************************/ #pragma once #include "Scene.h" +#include "Transform.h" +#include "TriangleMesh.h" +#include "Material/MaterialTextureLoader.h" #include "VertexAttrib.slangh" namespace Falcor @@ -36,24 +39,36 @@ namespace Falcor public: using SharedPtr = std::shared_ptr; + using MaterialList = std::vector; + using VolumeList = std::vector; + using GridList = std::vector; + using CameraList = std::vector; + using LightList = std::vector; + using AnimationList = std::vector; + /** Flags that control how the scene will be built. They can be combined together. */ enum class Flags { None = 0x0, ///< None - RemoveDuplicateMaterials = 0x1, ///< Deduplicate materials that have the same properties. The material name is ignored during the search. + DontMergeMaterials = 0x1, ///< Don't merge materials that have the same properties. Use this option to preserve the original material names. UseOriginalTangentSpace = 0x2, ///< Use the original tangent space that was loaded with the mesh. By default, we will ignore it and use MikkTSpace to generate the tangent space. We will always generate tangent space if it is missing. AssumeLinearSpaceTextures = 0x4, ///< By default, textures representing colors (diffuse/specular) are interpreted as sRGB data. Use this flag to force linear space for color textures. - DontMergeMeshes = 0x8, ///< Preserve the original list of meshes in the scene, don't merge meshes with the same material. - BuffersAsShaderResource = 0x10, ///< Generate the VBs and IB with the shader-resource-view bind flag. - UseSpecGlossMaterials = 0x20, ///< Set materials to use Spec-Gloss shading model. Otherwise default is Spec-Gloss for OBJ, Metal-Rough for everything else. - UseMetalRoughMaterials = 0x40, ///< Set materials to use Metal-Rough shading model. Otherwise default is Spec-Gloss for OBJ, Metal-Rough for everything else. - NonIndexedVertices = 0x80, ///< Convert meshes to use non-indexed vertices. This requires more memory but may increase performance. + DontMergeMeshes = 0x8, ///< Preserve the original list of meshes in the scene, don't merge meshes with the same material. This flag only applies to scenes imported by 'AssimpImporter'. + UseSpecGlossMaterials = 0x10, ///< Set materials to use Spec-Gloss shading model. Otherwise default is Spec-Gloss for OBJ, Metal-Rough for everything else. + UseMetalRoughMaterials = 0x20, ///< Set materials to use Metal-Rough shading model. Otherwise default is Spec-Gloss for OBJ, Metal-Rough for everything else. + NonIndexedVertices = 0x40, ///< Convert meshes to use non-indexed vertices. This requires more memory but may increase performance. + Force32BitIndices = 0x80, ///< Force 32-bit indices for all meshes. By default, 16-bit indices are used for small meshes. + RTDontMergeStatic = 0x100, ///< For raytracing, don't merge all static meshes into single pre-transformed BLAS. + RTDontMergeDynamic = 0x200, ///< For raytracing, don't merge all dynamic meshes with identical transforms into single BLAS. Default = None }; - /** Mesh description + /** Mesh description. + This struct is used by the importers to add new meshes. + The description is then processed by the scene builder into an optimized runtime format. + The frequency of each vertex attribute is specified individually, but note that an index list is always required. */ struct Mesh { @@ -88,6 +103,9 @@ namespace Falcor Attribute boneIDs; ///< Array of bone IDs. This field is optional. If it's set, that means that the mesh is animated, in which case boneWeights is required. Attribute boneWeights; ///< Array of bone weights. This field is optional. If it's set, that means that the mesh is animated, in which case boneIDs is required. + bool isFrontFaceCW = false; ///< Indicate whether front-facing side has clockwise winding in object space. + bool useOriginalTangentSpace = false; ///< Indicate whether to use the original tangent space that was loaded with the mesh. By default, we will ignore it and use MikkTSpace to generate the tangent space. + template T get(const Attribute& attribute, uint32_t face, uint32_t vert) const { @@ -110,6 +128,25 @@ namespace Falcor return T{}; } + template + size_t getAttributeCount(const Attribute& attribute) + { + switch (attribute.frequency) + { + case AttributeFrequency::Constant: + return 1; + case AttributeFrequency::Uniform: + return faceCount; + case AttributeFrequency::Vertex: + return vertexCount; + case AttributeFrequency::FaceVarying: + return 3 * faceCount; + default: + should_not_get_here(); + } + return 0; + } + float3 getPosition(uint32_t face, uint32_t vert) const { return get(positions, face, vert); } float3 getNormal(uint32_t face, uint32_t vert) const { return get(normals, face, vert); } float4 getTangent(uint32_t face, uint32_t vert) const { return get(tangents, face, vert); } @@ -143,6 +180,62 @@ namespace Falcor } }; + /** Pre-processed mesh data. + This data is formatted such that it can directly be copied + to the global scene buffers. + */ + struct ProcessedMesh + { + std::string name; + Vao::Topology topology = Vao::Topology::Undefined; + Material::SharedPtr pMaterial; + + uint64_t indexCount = 0; ///< Number of indices, or zero if non-indexed. + bool use16BitIndices = false; ///< True if the indices are in 16-bit format. + bool isFrontFaceCW = false; ///< Indicate whether front-facing side has clockwise winding in object space. + std::vector indexData; ///< Vertex indices in either 32-bit or 16-bit format packed tightly, or empty if non-indexed. + std::vector staticData; + std::vector dynamicData; + }; + + /** Curve description. + */ + struct Curve + { + template + struct Attribute + { + const T* pData = nullptr; + }; + + std::string name; ///< The curve's name. + uint32_t degree = 1; ///< Polynomial degree of the curve; linear (1) by default. + uint32_t vertexCount = 0; ///< The number of vertices. + uint32_t indexCount = 0; ///< The number of indices (i.e., tube segments). + const uint32_t* pIndices = nullptr; ///< Array of indices. The element count must match `indexCount`. This field is required. + Material::SharedPtr pMaterial; ///< The curve's material. Can't be nullptr. + + Attribute positions; ///< Array of vertex positions. This field is required. + Attribute radius; ///< Array of sphere radius. This field is required. + Attribute tangents; ///< Array of vertex tangents. This field is required. + Attribute normals; ///< Array of vertex normals. This field is optional. + Attribute texCrds; ///< Array of vertex texture coordinates. This field is optional. If set to nullptr, all texCrds will be set to (0,0). + }; + + /** Pre-processed curve data. + This data is formatted such that it can directly be copied + to the global scene buffers. + */ + struct ProcessedCurve + { + std::string name; + Vao::Topology topology = Vao::Topology::LineStrip; + Material::SharedPtr pMaterial; + + std::vector indexData; + std::vector staticData; + }; + static const uint32_t kInvalidNode = Scene::kInvalidNode; struct Node @@ -179,30 +272,133 @@ namespace Falcor */ Scene::SharedPtr getScene(); - /** Adds a node to the graph. - Note that if the node contains data other then the transform matrix (such as meshes or lights), you'll need to add those objects before adding the node. - \return The node ID. + /** Get the build flags */ - uint32_t addNode(const Node& node); + Flags getFlags() const { return mFlags; } - /** Check if a scene node is animated. This check is done recursively through parent nodes. - \return Returns true if node is animated. + /** Set the render settings. */ - bool isNodeAnimated(uint32_t nodeID) const; + void setRenderSettings(const Scene::RenderSettings& renderSettings) { mRenderSettings = renderSettings; } - /** Set the animation interpolation mode for a given scene node. This sets the mode recursively for all parent nodes. + /** Get the render settings. */ - void setNodeInterpolationMode(uint32_t nodeID, Animation::InterpolationMode interpolationMode, bool enableWarping); + Scene::RenderSettings& getRenderSettings() { return mRenderSettings; } - /** Add a mesh instance to a node + /** Get the render settings. */ - void addMeshInstance(uint32_t nodeID, uint32_t meshID); + const Scene::RenderSettings& getRenderSettings() const { return mRenderSettings; } + + // Meshes - /** Add a mesh. This function will throw an exception if something went wrong. - \param meshDesc The mesh's description. + /** Add a mesh. + Throws an exception if something went wrong. + \param mesh The mesh to add. \return The ID of the mesh in the scene. Note that all of the instances share the same mesh ID. */ - uint32_t addMesh(const Mesh& meshDesc); + uint32_t addMesh(const Mesh& mesh); + + /** Add a triangle mesh. + \param The triangle mesh to add. + \param pMaterial The material to use for the mesh. + \return The ID of the mesh in the scene. + */ + uint32_t addTriangleMesh(const TriangleMesh::SharedPtr& pTriangleMesh, const Material::SharedPtr& pMaterial); + + /** Pre-process a mesh into the data format that is used in the global scene buffers. + Throws an exception if something went wrong. + \param mesh The mesh to pre-process. + \return The pre-processed mesh. + */ + ProcessedMesh processMesh(const Mesh& mesh) const; + + /** Add a pre-processed mesh. + \param mesh The pre-processed mesh. + \return The ID of the mesh in the scene. Note that all of the instances share the same mesh ID. + */ + uint32_t addProcessedMesh(const ProcessedMesh& mesh); + + // Procedural primitives, including custom primitives, curves, etc. + + // Custom primitives + + /** Add an AABB defining a custom primitive. + \param[in] typeID The intersection shader ID that will be run on this primitive. + \param[in] aabb An AABB describing the bounds of the primitive. + */ + void addCustomPrimitive(uint32_t typeID, const AABB& aabb); + + // Curves + + /** Add a curve. + Throws an exception if something went wrong. + \param curve The curve to add. + \return The ID of the curve in the scene. Note that all of the instances share the same curve ID. + */ + uint32_t addCurve(const Curve& curve); + + /** Pre-process a curve into the data format that is used in the global scene buffers. + Throws an exception if something went wrong. + \param curve The curve to pre-process. + \return The pre-processed curve. + */ + ProcessedCurve processCurve(const Curve& curve) const; + + /** Add a pre-processed curve. + \param curve The pre-processed curve. + \return The ID of the curve in the scene. Note that all of the instances share the same curve ID. + */ + uint32_t addProcessedCurve(const ProcessedCurve& curve); + + // Materials + + /** Get the list of materials. + */ + const MaterialList& getMaterials() const { return mMaterials; } + + /** Get a material by name. + Note: This returns the first material found with a matching name. + \param name Material name. + \return Returns the first material with a matching name or nullptr if none was found. + */ + Material::SharedPtr getMaterial(const std::string& name) const; + + /** Add a material. + \param pMaterial The material. + \return The ID of the material in the scene. + */ + uint32_t addMaterial(const Material::SharedPtr& pMaterial); + + /** Request loading a material texture. + \param[in] pMaterial Material to load texture into. + \param[in] slot Slot to load texture into. + \param[in] filename Texture filename. + */ + void loadMaterialTexture(const Material::SharedPtr& pMaterial, Material::TextureSlot slot, const std::string& filename); + + // Volumes + + /** Get the list of volumes. + */ + const VolumeList& getVolumes() const { return mVolumes; } + + /** Get a volume by name. + Note: This returns the first volume found with a matching name. + \param name Volume name. + \return Returns the first volume with a matching name or nullptr if none was found. + */ + Volume::SharedPtr getVolume(const std::string& name) const; + + /** Add a volume. + \param pMaterial The volume. + \return The ID of the volume in the scene. + */ + uint32_t addVolume(const Volume::SharedPtr& pVolume); + + // Lights + + /** Get the list of lights. + */ + const LightList& getLights() const { return mLights; } /** Add a light source \param pLight The light object. @@ -210,47 +406,88 @@ namespace Falcor */ uint32_t addLight(const Light::SharedPtr& pLight); - /** Get the number of attached lights - */ - size_t getLightCount() const { return mLights.size(); } + // Environment map - /** Set a light-probe - \param pProbe The environment map. You can set it to null to disable environment mapping + /** Get the environment map. */ - void setLightProbe(const LightProbe::SharedPtr& pProbe) { mpLightProbe = pProbe; } + const EnvMap::SharedPtr& getEnvMap() const { return mpEnvMap; } - /** Set an environment map. + /** Set the environment map. \param[in] pEnvMap Environment map. Can be nullptr. */ void setEnvMap(EnvMap::SharedPtr pEnvMap) { mpEnvMap = pEnvMap; } + // Cameras + + /** Get the list of cameras. + */ + const CameraList& getCameras() const { return mCameras; } + /** Add a camera. \param pCamera Camera to be added. \return The camera ID */ uint32_t addCamera(const Camera::SharedPtr& pCamera); - /** Get the number of attached cameras + /** Get the selected camera. */ - size_t getCameraCount() const { return mCameras.size(); } + const Camera::SharedPtr& getSelectedCamera() const { return mpSelectedCamera; } - /** Select a camera. - \param name The name of the camera to select. + /** Set the selected camera. + \param pCamera Camera to use as selected camera (needs to be added first). */ - void setCamera(const std::string name); + void setSelectedCamera(const Camera::SharedPtr& pCamera); - /** Get the build flags + /** Get the camera speed. */ - Flags getFlags() const { return mFlags; } + float getCameraSpeed() const { return mCameraSpeed; } + + /** Set the camera speed. + */ + void setCameraSpeed(float speed) { mCameraSpeed = speed; } + + // Animations + + /** Get the list of animations. + */ + const AnimationList& getAnimations() const { return mAnimations; } /** Add an animation - \param animation The animation + \param pAnimation The animation */ void addAnimation(const Animation::SharedPtr& pAnimation); - /** Set the camera's speed + /** Create an animation for an animatable object. + \param pAnimatable Animatable object. + \param name Name of the animation. + \param duration Duration of the animation in seconds. + \return Returns a new animation or nullptr if an animation already exists. */ - void setCameraSpeed(float speed) { mCameraSpeed = speed; } + Animation::SharedPtr createAnimation(Animatable::SharedPtr pAnimatable, const std::string& name, double duration); + + // Scene graph + + /** Adds a node to the graph. + \return The node ID. + */ + uint32_t addNode(const Node& node); + + /** Add a mesh instance to a node + */ + void addMeshInstance(uint32_t nodeID, uint32_t meshID); + + /** Add a curve instance to a node. + */ + void addCurveInstance(uint32_t nodeID, uint32_t curveID); + + /** Check if a scene node is animated. This check is done recursively through parent nodes. + \return Returns true if node is animated. + */ + bool isNodeAnimated(uint32_t nodeID) const; + + /** Set the animation interpolation mode for a given scene node. This sets the mode recursively for all parent nodes. + */ + void setNodeInterpolationMode(uint32_t nodeID, Animation::InterpolationMode interpolationMode, bool enableWarping); private: SceneBuilder(Flags buildFlags); @@ -259,61 +496,159 @@ namespace Falcor { InternalNode() = default; InternalNode(const Node& n) : Node(n) {} - std::vector children; - std::vector meshes; + std::vector children; ///< Node IDs of all child nodes. + std::vector meshes; ///< Mesh IDs of all meshes this node transforms. + std::vector curves; ///< Curve IDs of all curves this node transforms. }; struct MeshSpec { - MeshSpec() = default; + std::string name; + Vao::Topology topology = Vao::Topology::Undefined; + uint32_t materialId = 0; ///< Global material ID. + uint32_t staticVertexOffset = 0; ///< Offset into the shared 'staticData' array. This is calculated in createGlobalBuffers(). + uint32_t staticVertexCount = 0; ///< Number of static vertices. + uint32_t dynamicVertexOffset = 0; ///< Offset into the shared 'dynamicData' array. This is calculated in createGlobalBuffers(). + uint32_t dynamicVertexCount = 0; ///< Number of dynamic vertices. + uint32_t indexOffset = 0; ///< Offset into the shared 'indexData' array. This is calculated in createGlobalBuffers(). + uint32_t indexCount = 0; ///< Number of indices, or zero if non-indexed. + uint32_t vertexCount = 0; ///< Number of vertices. + bool use16BitIndices = false; ///< True if the indices are in 16-bit format. + bool hasDynamicData = false; ///< True if mesh has dynamic vertices. + bool isStatic = false; ///< True if mesh is non-instanced and static (not dynamic or animated). + bool isFrontFaceCW = false; ///< Indicate whether front-facing side has clockwise winding in object space. + AABB boundingBox; ///< Mesh bounding-box in object space. + std::vector instances; ///< Node IDs of all instances of this mesh. + + // Pre-processed vertex data. + std::vector indexData; ///< Vertex indices in either 32-bit or 16-bit format packed tightly, or empty if non-indexed. + std::vector staticData; + std::vector dynamicData; + + uint32_t getTriangleCount() const + { + assert(topology == Vao::Topology::TriangleList); + return (indexCount > 0 ? indexCount : vertexCount) / 3; + } + + uint32_t getIndex(const size_t i) const + { + assert(i < indexCount); + return use16BitIndices ? reinterpret_cast(indexData.data())[i] : indexData[i]; + } + }; + + // TODO: Add support for dynamic curves + struct CurveSpec + { + std::string name; Vao::Topology topology; - uint32_t materialId = 0; - uint32_t indexOffset = 0; - uint32_t staticVertexOffset = 0; - uint32_t dynamicVertexOffset = 0; - uint32_t indexCount = 0; - uint32_t vertexCount = 0; - bool hasDynamicData = false; - std::vector instances; // Node IDs + uint32_t materialId = 0; ///< Global material ID. + uint32_t staticVertexOffset = 0; ///< Offset into the shared 'staticData' array. This is calculated in createCurveGlobalBuffers(). + uint32_t staticVertexCount = 0; ///< Number of static curve vertices. + uint32_t indexOffset = 0; ///< Offset into the shared 'indexData' array. This is calculated in createCurveGlobalBuffers(). + uint32_t indexCount = 0; ///< Number of indices. + uint32_t vertexCount = 0; ///< Number of vertices. + uint32_t degree = 1; ///< Polynomial degree of curve; linear (1) by default. + std::vector instances; ///< Node IDs of all instances of this curve. + + // Pre-processed curve vertex data. + std::vector indexData; ///< Vertex indices in 32-bit. + std::vector staticData; }; // Geometry data struct BuffersData { - std::vector indices; - std::vector staticData; - std::vector dynamicData; + std::vector indexData; ///< Vertex indices for all meshes in either 32-bit or 16-bit format packed tightly, decided per mesh. + std::vector staticData; ///< Vertex attributes for all meshes in packed format. + std::vector dynamicData; ///< Additional vertex attributes for dynamic (skinned) meshes. } mBuffersData; + struct CurveBuffersData + { + std::vector indexData; ///< Vertex indices for all curves in 32-bit. + std::vector staticData; ///< Vertex attributes for all curves. + } mCurveBuffersData; + using SceneGraph = std::vector; using MeshList = std::vector; + using MeshGroup = Scene::MeshGroup; + using MeshGroupList = std::vector; + using CurveList = std::vector; - bool mDirty = true; Scene::SharedPtr mpScene; SceneGraph mSceneGraph; const Flags mFlags; + std::string mFilename; + + Scene::RenderSettings mRenderSettings; MeshList mMeshes; - std::vector mMaterials; - std::unordered_map mMaterialToId; + MeshGroupList mMeshGroups; ///< Groups of meshes. Each group represents all the geometries in a BLAS for ray tracing. + + std::vector mProceduralPrimitives; ///< GPU Data struct of procedural primitive metadata. + std::unordered_map mProceduralPrimInstanceCount; ///< Map typeId to instance count. + std::vector mCustomPrimitiveAABBs; ///< User-defined custom primitive AABBs. + CurveList mCurves; - std::vector mCameras; - std::vector mLights; - LightProbe::SharedPtr mpLightProbe; + MaterialList mMaterials; + std::unique_ptr mpMaterialTextureLoader; + + VolumeList mVolumes; + GridList mGrids; + std::unordered_map mGridIDs; + + CameraList mCameras; + Camera::SharedPtr mpSelectedCamera; + LightList mLights; EnvMap::SharedPtr mpEnvMap; std::vector mAnimations; - uint32_t mSelectedCamera = 0; float mCameraSpeed = 1.0f; - uint32_t addMaterial(const Material::SharedPtr& pMaterial, bool removeDuplicate); - Vao::SharedPtr createVao(uint16_t drawCount); + // Mesh helpers - uint32_t createMeshData(Scene* pScene); - void createGlobalMatricesBuffer(Scene* pScene); - void calculateMeshBoundingBoxes(Scene* pScene); - void createAnimationController(Scene* pScene); - std::string mFilename; + /** Split a mesh by the given axis-aligned splitting plane. + \return Pair of optional mesh IDs for the meshes on the left and right side, respectively. + */ + std::pair, std::optional> splitMesh(uint32_t meshID, const int axis, const float pos); + + void splitIndexedMesh(const MeshSpec& mesh, MeshSpec& leftMesh, MeshSpec& rightMesh, const int axis, const float pos); + void splitNonIndexedMesh(const MeshSpec& mesh, MeshSpec& leftMesh, MeshSpec& rightMesh, const int axis, const float pos); + + // Mesh group helpers + size_t countTriangles(const MeshGroup& meshGroup) const; + AABB calculateBoundingBox(const MeshGroup& meshGroup) const; + bool needsSplit(const MeshGroup& meshGroup, size_t& triangleCount) const; + MeshGroupList splitMeshGroupSimple(MeshGroup& meshGroup) const; + MeshGroupList splitMeshGroupMedian(MeshGroup& meshGroup) const; + MeshGroupList splitMeshGroupMidpointMeshes(MeshGroup& meshGroup); + + // Post processing + void removeUnusedMeshes(); + void pretransformStaticMeshes(); + void calculateMeshBoundingBoxes(); + void createMeshGroups(); + void optimizeGeometry(); + void createGlobalBuffers(); + void createCurveGlobalBuffers(); + void removeDuplicateMaterials(); + void collectVolumeGrids(); + void quantizeTexCoords(); + + // Scene setup + uint32_t createMeshData(); + void createMeshVao(uint32_t drawCount); + void createCurveData(); + void createCurveVao(); + void mapCurvesToProceduralPrimitives(uint32_t typeID); + void createRaytracingAABBData(); + void createNodeList(); + void createMeshBoundingBoxes(); + void calculateCurveBoundingBoxes(); + + void pushProceduralPrimitive(uint32_t typeID, uint32_t instanceIdx, uint32_t AABBOffset, uint32_t AABBCount); }; enum_class_operators(SceneBuilder::Flags); diff --git a/Source/Falcor/Scene/SceneTypes.slang b/Source/Falcor/Scene/SceneTypes.slang index 5d5c6903e0..05525340e2 100644 --- a/Source/Falcor/Scene/SceneTypes.slang +++ b/Source/Falcor/Scene/SceneTypes.slang @@ -36,18 +36,41 @@ import Utils.Math.PackedFormats; BEGIN_NAMESPACE_FALCOR +enum class MeshFlags : uint32_t +{ + None = 0x0, + Use16BitIndices = 0x1, ///< Indices are in 16-bit format. The default is 32-bit. + HasDynamicData = 0x2, ///< Mesh has dynamic vertex data. + IsFrontFaceCW = 0x4, ///< Front-facing side has clockwise winding in object space. Note that the winding in world space may be flipped due to the instance transform. +}; + +/** Mesh data stored in 64B. +*/ struct MeshDesc { - uint vbOffset; ///< Offset into global vertex buffer. - uint ibOffset; ///< Offset into global index buffer, or zero if non-indexed. - uint vertexCount; ///< Vertex count. - uint indexCount; ///< Index count, or zero if non-indexed. - uint materialID; + uint vbOffset; ///< Offset into global vertex buffer. + uint ibOffset; ///< Offset into global index buffer, or zero if non-indexed. + uint vertexCount; ///< Vertex count. + uint indexCount; ///< Index count, or zero if non-indexed. + uint dynamicVbOffset; ///< Offset into dynamic vertex buffer, or zero if no dynamic data. + uint materialID; ///< Material ID. + uint flags; ///< See MeshFlags. + uint _pad; uint getTriangleCount() CONST_FUNCTION { return (indexCount > 0 ? indexCount : vertexCount) / 3; } + + bool use16BitIndices() CONST_FUNCTION + { + return (flags & (uint)MeshFlags::Use16BitIndices) != 0; + } + + bool isFrontFaceCW() CONST_FUNCTION + { + return (flags & (uint)MeshFlags::IsFrontFaceCW) != 0; + } }; enum class MeshInstanceFlags @@ -57,7 +80,11 @@ enum class MeshInstanceFlags #endif { None = 0x0, - Flipped = 0x1 + Use16BitIndices = 0x1, ///< Indices are in 16-bit format. The default is 32-bit. + HasDynamicData = 0x2, ///< Mesh has dynamic vertex data. + TransformFlipped = 0x4, ///< Instance transform flips the coordinate system handedness. TODO: Deprecate this flag if we need an extra bit. + IsObjectFrontFaceCW = 0x8, ///< Front-facing side has clockwise winding in object space. Note that the winding in world space may be flipped due to the instance transform. + IsWorldFrontFaceCW = 0x10, ///< Front-facing side has clockwise winding in world space. This is the combination of the mesh winding and instance transform handedness. }; struct MeshInstanceData @@ -65,54 +92,69 @@ struct MeshInstanceData uint globalMatrixID; uint materialID; uint meshID; - uint flags; ///< MeshInstanceFlags. + uint flags; ///< See MeshInstanceFlags. uint vbOffset; ///< Offset into global vertex buffer. uint ibOffset; ///< Offset into global index buffer, or zero if non-indexed. + + bool hasDynamicData() CONST_FUNCTION + { + return (flags & (uint)MeshInstanceFlags::HasDynamicData) != 0; + } }; /** Mesh instance data packed into 16B. */ struct PackedMeshInstanceData { - uint materialID; - uint packedIDs; ///< Packed meshID, globalMatrixID, and flags packed into 32 bits. + uint packedIDs[2]; ///< Packed materialID, meshID, globalMatrixID, and flags packed into 64 bits. uint vbOffset; ///< Offset into global vertex buffer. uint ibOffset; ///< Offset into global index buffer, or zero if non-indexed. // Packed representation + // The pack/unpack logic assumes that meshID representation is split between packedIds[0] and packedIds[1] + // (i.e., kFlagBigs + kMatrixBits < 32, kFlagsBits + kMatrixBits + kMeshBits > 32) + // bit position 31.......................................0 + // packedIds[1] | kMaterialBits | kMeshBitsHi | + // packedIDs[0] | kMeshBitsLo | kMatrixBits | kFlagBits | - static const uint kMatrixBits = 16; - static const uint kMeshBits = 15; - static const uint kFlagsBits = 1; + static const uint kMaterialBits = 17; + static const uint kMatrixBits = 21; + static const uint kMeshBits = 21; + static const uint kFlagsBits = 5; - static const uint kMatrixOffset = 0; - static const uint kMeshOffset = kMatrixOffset + kMatrixBits; - static const uint kFlagsOffset = kMeshOffset + kMeshBits; + static const uint kFlagsOffset = 0; + static const uint kMatrixOffset = kFlagsOffset + kFlagsBits; + static const uint kMeshOffsetLo = kMatrixOffset + kMatrixBits; + static const uint kMeshBitsLo = 32 - kMeshOffsetLo; + static const uint kMeshOffsetHi = 0; + static const uint kMeshBitsHi = kMeshBits - kMeshBitsLo; + static const uint kMaterialOffset = kMeshOffsetHi + kMeshBitsHi; #ifdef HOST_CODE void pack(const MeshInstanceData& d) { - materialID = d.materialID; vbOffset = d.vbOffset; ibOffset = d.ibOffset; assert(d.flags < (1 << kFlagsBits)); assert(d.meshID < (1 << kMeshBits)); assert(d.globalMatrixID < (1 << kMatrixBits)); - packedIDs = (d.flags << kFlagsOffset) | (d.meshID << kMeshOffset) | (d.globalMatrixID << kMatrixOffset); + assert(d.materialID < (1 << kMaterialBits)); + packedIDs[0] = (d.flags << kFlagsOffset) | (d.globalMatrixID << kMatrixOffset) | ((d.meshID & ((1 << kMeshBitsLo) -1)) << kMeshOffsetLo); + packedIDs[1] = (d.meshID >> kMeshBitsLo) | (d.materialID << kMaterialOffset); } #endif MeshInstanceData unpack() { MeshInstanceData d; - d.materialID = materialID; d.vbOffset = vbOffset; d.ibOffset = ibOffset; - d.globalMatrixID = (packedIDs >> kMatrixOffset) & ((1 << kMatrixBits) - 1); - d.meshID = (packedIDs >> kMeshOffset) & ((1 << kMeshBits) - 1); - d.flags = (packedIDs >> kFlagsOffset) & ((1 << kFlagsBits) - 1); + d.materialID = (packedIDs[1] >> kMaterialOffset) & ((1 << kMaterialBits) - 1); + d.meshID = ((packedIDs[0] >> kMeshOffsetLo) & ((1 << kMeshBitsLo) - 1)) | (((packedIDs[1] >> kMeshOffsetHi) & ((1 << kMeshBitsHi) -1)) << kMeshBitsLo); + d.globalMatrixID = (packedIDs[0] >> kMatrixOffset) & ((1 << kMatrixBits) - 1); + d.flags = (packedIDs[0] >> kFlagsOffset) & ((1 << kFlagsBits) - 1); return d; } @@ -122,7 +164,7 @@ struct StaticVertexData { float3 position; ///< Position. float3 normal; ///< Shading normal. - float4 tangent; ///< Shading tangent. The bitangent is computed: cross(normal, tangent.xyz) * tangent.w. + float4 tangent; ///< Shading tangent. The bitangent is computed: cross(normal, tangent.xyz) * tangent.w. NOTE: The tangent is *only* valid when tangent.w != 0. float2 texCrd; ///< Texture coordinates. }; @@ -207,4 +249,46 @@ struct VertexData float coneTexLODValue; ///< Texture LOD data for cone tracing. This is zero, unless getVertexDataRayCones() is used. }; +struct ProceduralPrimitiveData +{ + uint typeID; ///< Type ID corresponding to how it was defined during Scene creation. + uint instanceIdx; ///< Of custom primitives of this type, what instance index it is, in the order of addition to the scene. + + uint AABBOffset; ///< Offset into global AABB buffer. + uint AABBCount; ///< Number of AABBs within a custom primitive. +}; + +struct CurveDesc +{ + uint vbOffset; ///< Offset into global curve vertex buffer. + uint ibOffset; ///< Offset into global curve index buffer. + uint vertexCount; ///< Vertex count. + uint indexCount; ///< Index count. + uint degree; ///< Polynomial degree of curve; linear (1) by default. + uint materialID; ///< Material ID. + + uint getSegmentCount() CONST_FUNCTION + { + return indexCount; + } +}; + +struct CurveInstanceData +{ + uint globalMatrixID; + uint materialID; + uint curveID; + uint vbOffset; ///< Offset into global curve vertex buffer. + uint ibOffset; ///< Offset into global curve index buffer. +}; + +struct StaticCurveVertexData +{ + float3 position; ///< Position. + float radius; ///< Radius of the sphere at curve ends. + float3 tangent; ///< Shading tangent. + float3 normal; ///< Shading normal. + float2 texCrd; ///< Texture coordinates. +}; + END_NAMESPACE_FALCOR diff --git a/Source/Falcor/Scene/Shading.slang b/Source/Falcor/Scene/Shading.slang index f762cb32c7..be0c220858 100644 --- a/Source/Falcor/Scene/Shading.slang +++ b/Source/Falcor/Scene/Shading.slang @@ -32,6 +32,8 @@ import Scene.SceneTypes; import Scene.TextureSampler; import Scene.Material.MaterialData; import Experimental.Scene.Material.BxDFTypes; +import Experimental.Scene.Lights.EnvMapLighting; +import Utils.Math.MathHelpers; import Utils.Helpers; /** Convert RGB to normal (unnormalized). @@ -77,6 +79,10 @@ void applyNormalMap(MaterialData md, MaterialResources mr, in return; } + // Note if the normal ends up being parallel to the tangent, the tangent frame cannot be orthonormalized. + // That case is rare enough that it is probably not worth the runtime cost to check for it here. + // If it occurs we should foremost fix the asset, or if problems persist add a check here. + // Apply the transformation. sd.N = normalize(sd.T * mapN.x + sd.B * mapN.y + sd.N * mapN.z); sd.T = normalize(tangentW.xyz - sd.N * dot(tangentW.xyz, sd.N)); @@ -147,7 +153,7 @@ ShadingData _prepareShadingData(VertexData v, uint materialID md.flags = _MS_STATIC_MATERIAL_FLAGS; #endif - // Sample the diffuse texture and apply the alpha test + // Sample the diffuse texture and apply the alpha test. float4 baseColor = sampleTexture(mr.baseColor, mr.samplerState, v.texC, md.baseColor, EXTRACT_DIFFUSE_TYPE(md.flags), lod); sd.opacity = baseColor.a; applyAlphaTest(md.flags, baseColor.a, md.alphaThreshold, v.posW); @@ -160,25 +166,22 @@ ShadingData _prepareShadingData(VertexData v, uint materialID sd.faceN = v.faceNormalW; sd.frontFacing = dot(sd.V, sd.faceN) >= 0.f; sd.doubleSided = EXTRACT_DOUBLE_SIDED(md.flags); - - // Check that tangent exists, otherwise leave the vectors at zero to avoid NaNs. - const bool validTangentSpace = v.tangentW.w != 0.f; - if (validTangentSpace) - { - sd.T = normalize(v.tangentW.xyz - sd.N * dot(v.tangentW.xyz, sd.N)); - sd.B = cross(sd.N, sd.T) * v.tangentW.w; - } - + sd.activeLobes = (uint)LobeType::All; sd.materialID = materialID; sd.IoR = md.IoR; sd.specularTransmission = sampleTexture(mr.specularTransmission, mr.samplerState, v.texC, md.specularTransmission, EXTRACT_SPEC_TRANS_TYPE(md.flags), lod).r; sd.eta = sd.frontFacing ? (1 / sd.IoR) : sd.IoR; - // Sample the spec texture + // Setup tangent space. + bool validTangentSpace = computeTangentSpace(sd, v.tangentW); + + // Sample the specular texture. + // Depending on the shading model this encodes the material parameters differently. sd.occlusion = 1.0f; bool sampleOcclusion = EXTRACT_OCCLUSION_MAP(md.flags) > 0; float4 spec = sampleTexture(mr.specular, mr.samplerState, v.texC, md.specular, EXTRACT_SPECULAR_TYPE(md.flags), lod); + if (EXTRACT_SHADING_MODEL(md.flags) == ShadingModelMetalRough) { // R - Occlusion; G - Roughness; B - Metallic @@ -218,6 +221,7 @@ ShadingData _prepareShadingData(VertexData v, uint materialID sd.emissive = sampleTexture(mr.emissive, mr.samplerState, v.texC, float4(md.emissive, 1), EXTRACT_EMISSIVE_TYPE(md.flags), lod).rgb * md.emissiveFactor; } + // Apply normal mapping only if we have a valid tangent space. if (useNormalMap && validTangentSpace) applyNormalMap(md, mr, sd, v.tangentW, lod); sd.NdotV = dot(sd.N, sd.V); @@ -228,8 +232,6 @@ ShadingData _prepareShadingData(VertexData v, uint materialID sd.NdotV = -sd.NdotV; } - sd.activeLobes = (uint)LobeType::All; - return sd; } @@ -293,6 +295,61 @@ ShadingData prepareShadingData(VertexData v, uint materialID, MaterialData md, M return _prepareShadingData(v, materialID, md, mr, viewDir, lod, true); } +/** Computes an orthonormal tangent space based on the normal and given tangent. + \param[in,out] sd ShadingData struct that is updated. + \param[in] tangent Interpolated tangent in world space (xyz) and bitangent sign (w). The tangent is *only* valid when w is != 0. + \return True if a valid tangent space was computed based on the supplied tangent. +*/ +bool computeTangentSpace(inout ShadingData sd, const float4 tangentW) +{ + // Check that tangent space exists and can be safely orthonormalized. + // Otherwise invent a tanget frame based on the normal. + // We check that: + // - Tangent exists, this is indicated by a nonzero sign (w). + // - It has nonzero length. Zeros can occur due to interpolation or bad assets. + // - It is not parallel to the normal. This can occur due to normal mapping or bad assets. + // - It does not have NaNs. These will propagate and trigger the fallback. + + float NdotT = dot(tangentW.xyz, sd.N); + bool nonParallel = abs(NdotT) < 0.9999f; + bool nonZero = dot(tangentW.xyz, tangentW.xyz) > 0.f; + + bool valid = tangentW.w != 0.f && nonZero && nonParallel; + if (valid) + { + sd.T = normalize(tangentW.xyz - sd.N * NdotT); + sd.B = cross(sd.N, sd.T) * tangentW.w; + } + else + { + sd.T = perp_stark(sd.N); + sd.B = cross(sd.N, sd.T); + } + + return valid; +} + +/** Helper function to adjust the shading normal for avoding black pixels due to back-facing view direction. + Note: This breaks the reciprocity of the BSDF! +*/ +void adjustShadingNormal(inout ShadingData sd, VertexData v) +{ + float3 Ng = sd.frontFacing ? v.faceNormalW : -v.faceNormalW; + float3 Ns = sd.N; + + // Blend the shading normal towards the geometric normal at grazing angles. + // This is to avoid the view vector from becoming back-facing. + const float kCosThetaThreshold = 0.1f; + float cosTheta = dot(sd.V, Ns); + if (cosTheta <= kCosThetaThreshold) + { + float t = saturate(cosTheta * (1.f / kCosThetaThreshold)); + sd.N = normalize(lerp(Ng, Ns, t)); + sd.NdotV = dot(sd.N, sd.V); + computeTangentSpace(sd, v.tangentW); + } +} + // ---------------------------------------------------------------------------- // Legacy raster shading code // ---------------------------------------------------------------------------- @@ -336,14 +393,16 @@ ShadingResult evalMaterial(ShadingData sd, LightData light, float shadowFactor) return sr; }; -ShadingResult evalMaterial(ShadingData sd, LightProbeData probe) +ShadingResult evalMaterial(ShadingData sd, EnvMapLighting envMapLighting) { ShadingResult sr = {}; - LightSample ls = evalLightProbe(probe, sd); - sr.diffuse = ls.diffuse; - sr.color = sr.diffuse; - sr.specular = ls.specular; - sr.color += sr.specular; + // Calculate the reflection vector + float3 L = reflect(-sd.V, sd.N); + + sr.diffuse = envMapLighting.evalDiffuse(sd); + sr.specular = envMapLighting.evalSpecular(sd, L); + sr.color = sr.diffuse + sr.specular; + return sr; } diff --git a/Source/Falcor/Scene/Transform.cpp b/Source/Falcor/Scene/Transform.cpp new file mode 100644 index 0000000000..6c704f0c9e --- /dev/null +++ b/Source/Falcor/Scene/Transform.cpp @@ -0,0 +1,169 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#pragma once +#include "stdafx.h" +#include "Transform.h" +#include "glm/gtc/quaternion.hpp" +#include "glm/gtx/transform.hpp" + +namespace Falcor +{ + Transform::Transform() {} + + void Transform::setTranslation(const float3& translation) + { + mTranslation = translation; + mDirty = true; + } + + void Transform::setScaling(const float3& scaling) + { + mScaling = scaling; + mDirty = true; + } + + void Transform::setRotation(const glm::quat& rotation) + { + mRotation = rotation; + mDirty = true; + } + + float3 Transform::getRotationEuler() const + { + return glm::eulerAngles(mRotation); + } + + void Transform::setRotationEuler(const float3& angles) + { + setRotation(glm::quat(angles)); + } + + float3 Transform::getRotationEulerDeg() const + { + return glm::degrees(getRotationEuler()); + } + + void Transform::setRotationEulerDeg(const float3& angles) + { + setRotationEuler(glm::radians(angles)); + } + + void Transform::lookAt(const float3& position, const float3& target, const float3& up) + { + mTranslation = position; + float3 dir = normalize(target - position); + mRotation = glm::quatLookAtLH(dir, up); + } + + const glm::float4x4& Transform::getMatrix() const + { + if (mDirty) + { + glm::mat4 T = translate(mTranslation); + glm::mat4 R = mat4_cast(mRotation); + glm::mat4 S = scale(mScaling); + mMatrix = T * R * S; + mDirty = false; + } + + return mMatrix; + } + + SCRIPT_BINDING(Transform) + { + auto init = [](const pybind11::kwargs& args) + { + Transform transform; + + std::optional position; + std::optional target; + std::optional up; + + for (auto a : args) + { + auto key = a.first.cast(); + const auto& value = a.second; + + float3 float3Value; + float floatValue; + + bool isFloat3 = pybind11::isinstance(value); + bool isNumber = pybind11::isinstance(value) || pybind11::isinstance(value); + + if (isFloat3) float3Value = pybind11::cast(value); + if (isNumber) floatValue = pybind11::cast(value); + + if (key == "translation") + { + if (isFloat3) transform.setTranslation(float3Value); + } + else if (key == "scaling") + { + if (isFloat3) transform.setScaling(float3Value); + if (isNumber) transform.setScaling(float3(floatValue)); + } + else if (key == "rotationEuler") + { + if (isFloat3) transform.setRotationEuler(float3Value); + } + else if (key == "rotationEulerDeg") + { + if (isFloat3) transform.setRotationEulerDeg(float3Value); + } + else if (key == "position") + { + if (isFloat3) position = float3Value; + } + else if (key == "target") + { + if (isFloat3) target = float3Value; + } + else if (key == "up") + { + if (isFloat3) up = float3Value; + } + } + + if (position && target && up) + { + transform.lookAt(*position, *target, *up); + } + + return transform; + }; + + pybind11::class_ transform(m, "Transform"); + transform.def(pybind11::init(init)); + transform.def_property("translation", &Transform::getTranslation, &Transform::setTranslation); + transform.def_property("rotationEuler", &Transform::getRotationEuler, &Transform::setRotationEuler); + transform.def_property("rotationEulerDeg", &Transform::getRotationEulerDeg, &Transform::setRotationEulerDeg); + transform.def_property("scaling", &Transform::getScaling, &Transform::setScaling); + transform.def_property_readonly("matrix", &Transform::getMatrix); + transform.def("lookAt", &Transform::lookAt, "position"_a, "target"_a, "up"_a); + } +} diff --git a/Source/Falcor/Scene/Transform.h b/Source/Falcor/Scene/Transform.h new file mode 100644 index 0000000000..9546121c52 --- /dev/null +++ b/Source/Falcor/Scene/Transform.h @@ -0,0 +1,68 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#pragma once +#include "Falcor.h" + +namespace Falcor +{ + /** Helper to create transformation matrices based on translation, + rotation and scaling. + */ + class dlldecl Transform + { + public: + Transform(); + + const float3& getTranslation() const { return mTranslation; } + void setTranslation(const float3& translation); + + const float3& getScaling() const { return mScaling; } + void setScaling(const float3& scaling); + + const glm::quat& getRotation() const { return mRotation; } + void setRotation(const glm::quat& rotation); + + float3 getRotationEuler() const; + void setRotationEuler(const float3& angles); + + float3 getRotationEulerDeg() const; + void setRotationEulerDeg(const float3& angles); + + void lookAt(const float3& position, const float3& target, const float3& up); + + const glm::float4x4& getMatrix() const; + + private: + float3 mTranslation = float3(0.f); + float3 mScaling = float3(1.f); + glm::quat mRotation = glm::identity(); + + mutable bool mDirty = true; + mutable glm::float4x4 mMatrix; + }; +} diff --git a/Source/Falcor/Scene/TriangleMesh.cpp b/Source/Falcor/Scene/TriangleMesh.cpp new file mode 100644 index 0000000000..9275ddde8a --- /dev/null +++ b/Source/Falcor/Scene/TriangleMesh.cpp @@ -0,0 +1,268 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#include "stdafx.h" +#include "TriangleMesh.h" +#include +#include +#include + +namespace Falcor +{ + TriangleMesh::SharedPtr TriangleMesh::create() + { + return SharedPtr(new TriangleMesh()); + } + + TriangleMesh::SharedPtr TriangleMesh::create(const VertexList& vertices, const IndexList& indices) + { + return SharedPtr(new TriangleMesh(vertices, indices)); + } + + TriangleMesh::SharedPtr TriangleMesh::createDummy() + { + VertexList vertices = {{{0.f, 0.f, 0.f}, {0.f, 1.f, 0.f}, {0.f, 0.f}}}; + IndexList indices = {0, 0, 0}; + return create(vertices, indices); + } + + TriangleMesh::SharedPtr TriangleMesh::createQuad(float size) + { + float hsize = 0.5f * size; + float3 normal{0.f, 1.f, 0.f}; + + VertexList vertices{ + {{ -hsize, 0.f, -hsize }, normal, { 0.f, 0.f }}, + {{ hsize, 0.f, -hsize }, normal, { 1.f, 0.f }}, + {{ -hsize, 0.f, hsize }, normal, { 0.f, 1.f }}, + {{ hsize, 0.f, hsize }, normal, { 1.f, 1.f }}, + }; + + IndexList indices{ + 2, 1, 0, + 1, 2, 3, + }; + + return create(vertices, indices); + } + + TriangleMesh::SharedPtr TriangleMesh::createCube(float size) + { + const float3 positions[6][4] = + { + {{ -0.5f, -0.5f, -0.5f }, { -0.5f, -0.5f, 0.5f }, { 0.5f, -0.5f, 0.5f }, { 0.5f, -0.5f, -0.5f }}, + {{ -0.5f, 0.5f, 0.5f }, { -0.5f, 0.5f, -0.5f }, { 0.5f, 0.5f, -0.5f }, { 0.5f, 0.5f, 0.5f }}, + {{ -0.5f, 0.5f, -0.5f }, { -0.5f, -0.5f, -0.5f }, { 0.5f, -0.5f, -0.5f }, { 0.5f, 0.5f, -0.5f }}, + {{ 0.5f, 0.5f, 0.5f }, { 0.5f, -0.5f, 0.5f }, {-0.5f, -0.5f, 0.5f }, {-0.5f, 0.5f, 0.5f }}, + {{ -0.5f, 0.5f, 0.5f }, { -0.5f, -0.5f, 0.5f }, {-0.5f, -0.5f, -0.5f }, {-0.5f, 0.5f, -0.5f }}, + {{ 0.5f, 0.5f, -0.5f }, { 0.5f, -0.5f, -0.5f }, { 0.5f, -0.5f, 0.5f }, { 0.5f, 0.5f, 0.5f }}, + }; + + const float3 normals[6] = + { + { 0.f, -1.f, 0.f }, + { 0.f, 1.f, 0.f }, + { 0.f, 0.f, -1.f }, + { 0.f, 0.f, 1.f }, + { -1.f, 0.f, 0.f }, + { 1.f, 0.f, 0.f }, + }; + + const float2 texCoords[4] = {{ 0.f, 0.f }, { 1.f, 0.f }, { 1.f, 1.f }, { 0.f, 1.f }}; + + VertexList vertices; + IndexList indices; + + for (size_t i = 0; i < 6; ++i) + { + uint32_t idx = (uint32_t)vertices.size(); + indices.emplace_back(idx); + indices.emplace_back(idx + 2); + indices.emplace_back(idx + 1); + indices.emplace_back(idx); + indices.emplace_back(idx + 3); + indices.emplace_back(idx + 2); + + for (size_t j = 0; j < 4; ++j) + { + vertices.emplace_back(Vertex{ positions[i][j] * size, normals[i], texCoords[j] }); + } + } + + return create(vertices, indices); + } + + TriangleMesh::SharedPtr TriangleMesh::createSphere(float radius, uint32_t segmentsU, uint32_t segmentsV) + { + VertexList vertices; + IndexList indices; + + // Create vertices. + for (uint32_t v = 0; v <= segmentsV; ++v) + { + for (uint32_t u = 0; u <= segmentsU; ++u) + { + float2 uv = float2(u / float(segmentsU), v / float(segmentsV)); + float theta = uv.x * 2.f * (float)M_PI; + float phi = uv.y * (float)M_PI; + float3 dir = float3( + std::cos(theta) * std::sin(phi), + std::cos(phi), + std::sin(theta) * std::sin(phi) + ); + vertices.emplace_back(Vertex{ dir * radius, dir, uv }); + } + } + + // Create indices. + for (uint32_t v = 0; v < segmentsV; ++v) + { + for (uint32_t u = 0; u < segmentsU; ++u) + { + uint32_t i0 = v * (segmentsU + 1) + u; + uint32_t i1 = v * (segmentsU + 1) + (u + 1) % (segmentsU + 1); + uint32_t i2 = (v + 1) * (segmentsU + 1) + u; + uint32_t i3 = (v + 1) * (segmentsU + 1) + (u + 1) % (segmentsU + 1); + + indices.emplace_back(i0); + indices.emplace_back(i1); + indices.emplace_back(i2); + + indices.emplace_back(i2); + indices.emplace_back(i1); + indices.emplace_back(i3); + } + } + + return create(vertices, indices); + } + + TriangleMesh::SharedPtr TriangleMesh::createFromFile(const std::string& filename, bool smoothNormals) + { + std::string fullPath; + if (!findFileInDataDirectories(filename, fullPath)) + { + logWarning("Error when loading triangle mesh. Can't find mesh file '" + filename + "'"); + return nullptr; + } + + Assimp::Importer importer; + + unsigned int flags = + aiProcess_Triangulate | + (smoothNormals ? aiProcess_GenSmoothNormals : aiProcess_GenNormals) | + aiProcess_PreTransformVertices; + + auto scene = importer.ReadFile(fullPath.c_str(), flags); + if (!scene) + { + logWarning("Failed to load triangle mesh from '" + fullPath + "' (" + importer.GetErrorString() + ")"); + return nullptr; + } + + VertexList vertices; + IndexList indices; + + size_t vertexCount = 0; + size_t indexCount = 0; + + for (size_t meshIdx = 0; meshIdx < scene->mNumMeshes; ++meshIdx) + { + vertexCount += scene->mMeshes[meshIdx]->mNumVertices; + indexCount += scene->mMeshes[meshIdx]->mNumFaces * 3; + } + + vertices.reserve(vertexCount); + indices.reserve(indexCount); + + for (size_t meshIdx = 0; meshIdx < scene->mNumMeshes; ++meshIdx) + { + size_t indexBase = vertices.size(); + auto mesh = scene->mMeshes[meshIdx]; + for (size_t vertexIdx = 0; vertexIdx < mesh->mNumVertices; ++vertexIdx) + { + const auto& vertex = mesh->mVertices[vertexIdx]; + const auto& normal = mesh->mNormals[vertexIdx]; + const auto& texCoord = mesh->mTextureCoords[0] ? mesh->mTextureCoords[0][vertexIdx] : aiVector3D(0.f); + vertices.emplace_back(Vertex{ + float3(vertex.x, vertex.y, vertex.z), + float3(normal.x, normal.y, normal.z), + float2(texCoord.x, texCoord.y) + }); + } + for (size_t faceIdx = 0; faceIdx < mesh->mNumFaces; ++faceIdx) + { + const auto& face = mesh->mFaces[faceIdx]; + for (size_t i = 0; i < 3; ++i) indices.emplace_back((uint32_t)(indexBase + face.mIndices[i])); + } + } + + return create(vertices, indices); + } + + uint32_t TriangleMesh::addVertex(float3 position, float3 normal, float2 texCoord) + { + mVertices.emplace_back(Vertex{position, normal, texCoord}); + assert(mVertices.size() < std::numeric_limits::max()); + return (uint32_t)(mVertices.size() - 1); + } + + void TriangleMesh::addTriangle(uint32_t i0, uint32_t i1, uint32_t i2) + { + mIndices.emplace_back(i0); + mIndices.emplace_back(i1); + mIndices.emplace_back(i2); + } + + TriangleMesh::TriangleMesh() + {} + + TriangleMesh::TriangleMesh(const VertexList& vertices, const IndexList& indices) + : mVertices(vertices) + , mIndices(indices) + {} + + SCRIPT_BINDING(TriangleMesh) + { + pybind11::class_ triangleMesh(m, "TriangleMesh"); + triangleMesh.def_property("name", &TriangleMesh::getName, &TriangleMesh::setName); + triangleMesh.def_property_readonly("vertices", &TriangleMesh::getVertices); + triangleMesh.def_property_readonly("indices", &TriangleMesh::getIndices); + triangleMesh.def(pybind11::init(pybind11::overload_cast(&TriangleMesh::create))); + triangleMesh.def("addVertex", &TriangleMesh::addVertex, "position"_a, "normal"_a, "texCoord"_a); + triangleMesh.def("addTriangle", &TriangleMesh::addTriangle, "i0"_a, "i1"_a, "i2"_a); + triangleMesh.def_static("createQuad", &TriangleMesh::createQuad, "size"_a = 1.f); + triangleMesh.def_static("createCube", &TriangleMesh::createCube, "size"_a = 1.f); + triangleMesh.def_static("createSphere", &TriangleMesh::createSphere, "radius"_a = 1.f, "segmentsU"_a = 32, "segmentsV"_a = 32); + triangleMesh.def_static("createFromFile", &TriangleMesh::createFromFile, "filename"_a, "smoothNormals"_a = false); + + pybind11::class_ vertex(triangleMesh, "Vertex"); + vertex.def_readwrite("position", &TriangleMesh::Vertex::position); + vertex.def_readwrite("normal", &TriangleMesh::Vertex::normal); + vertex.def_readwrite("texCoord", &TriangleMesh::Vertex::texCoord); + } +} diff --git a/Source/Falcor/Scene/TriangleMesh.h b/Source/Falcor/Scene/TriangleMesh.h new file mode 100644 index 0000000000..90e45290ec --- /dev/null +++ b/Source/Falcor/Scene/TriangleMesh.h @@ -0,0 +1,147 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#pragma once +#include "Falcor.h" + +namespace Falcor +{ + /** Simple indexed triangle mesh. + Vertices have position, normal and texture coordinate attributes. + This class is used as a utility to pass simple geometry to the SceneBuilder. + */ + class dlldecl TriangleMesh + { + public: + using SharedPtr = std::shared_ptr; + + struct Vertex + { + float3 position; + float3 normal; + float2 texCoord; + }; + + using VertexList = std::vector; + using IndexList = std::vector; + + /** Creates a triangle mesh. + \return Returns the triangle mesh. + */ + static SharedPtr create(); + + /** Creates a triangle mesh. + \param[in] vertices Vertex list. + \param[in] indices Index list. + \return Returns the triangle mesh. + */ + static SharedPtr create(const VertexList& vertices, const IndexList& indices); + + /** Creates a dummy mesh (single degenerate triangle). + \return Returns the triangle mesh. + */ + static SharedPtr createDummy(); + + /** Creates a quad mesh, centered at the origin with normal pointing in positive Y direction. + \param[in] size Size of the quad. + \return Returns the triangle mesh. + */ + static SharedPtr createQuad(float size = 1.f); + + /** Creates a cube mesh, centered at the origin. + \param[in] size Size of the cube. + \return Returns the triangle mesh. + */ + static SharedPtr createCube(float size = 1.f); + + /** Creates a UV sphere mesh, centered at the origin with poles in positive/negative Y direction. + \param[in] radius Radius of the sphere. + \param[in] segmentsU Number of segments along parallels. + \param[in] segmentsV Number of segments along meridians. + \return Returns the triangle mesh. + */ + static SharedPtr createSphere(float radius = 0.5f, uint32_t segmentsU = 32, uint32_t segmentsV = 16); + + /** Creates a triangle mesh from a file. + This is using ASSIMP to support a wide variety of asset formats. + All geometry found in the asset is pre-transformed and merged into the same triangle mesh. + \param[in] filename File to load mesh from. + \param[in] smoothNormals If no normals are defined in the model, generate smooth instead of facet normals. + \return Returns the triangle mesh or nullptr if the mesh failed to load. + */ + static SharedPtr createFromFile(const std::string& filename, bool smoothNormals = false); + + /** Get the name of the triangle mesh. + \return Returns the name. + */ + const std::string getName() const { return mName; } + + /** Set the name of the triangle mesh. + \param[in] name Name to set. + */ + void setName(const std::string& name) { mName = name; } + + /** Adds a vertex to the vertex list. + \param[in] position Vertex position. + \param[in] normal Vertex normal. + \param[in] texCoord Vertex texture coordinate. + \return Returns the vertex index. + */ + uint32_t addVertex(float3 position, float3 normal, float2 texCoord); + + /** Adds a triangle to the index list. + \param[in] i0 First index. + \param[in] i1 Second index. + \param[in] i2 Third index. + */ + void addTriangle(uint32_t i0, uint32_t i1, uint32_t i2); + + /** Get the vertex list. + */ + const VertexList& getVertices() const { return mVertices; } + + /** Set the vertex list. + */ + void setVertices(const VertexList& vertices) { mVertices = vertices; } + + /** Get the index list. + */ + const IndexList& getIndices() const { return mIndices; } + + /** Set the index list. + */ + void setIndices(const IndexList& indices) { mIndices = indices; } + + private: + TriangleMesh(); + TriangleMesh(const VertexList& vertices, const IndexList& indices); + + std::string mName; + std::vector mVertices; + std::vector mIndices; + }; +} diff --git a/Source/Falcor/Scene/VertexAttrib.slangh b/Source/Falcor/Scene/VertexAttrib.slangh index fe89f4397b..bb55fe75f9 100644 --- a/Source/Falcor/Scene/VertexAttrib.slangh +++ b/Source/Falcor/Scene/VertexAttrib.slangh @@ -33,10 +33,9 @@ BEGIN_NAMESPACE_FALCOR #define VERTEX_POSITION_LOC 0 #define VERTEX_PACKED_NORMAL_TANGENT_LOC 1 #define VERTEX_TEXCOORD_LOC 2 -#define VERTEX_PREV_POSITION_LOC 3 -#define INSTANCE_DRAW_ID_LOC 4 +#define INSTANCE_DRAW_ID_LOC 3 -#define VERTEX_LOCATION_COUNT 5 +#define VERTEX_LOCATION_COUNT 4 #define VERTEX_USER_ELEM_COUNT 4 #define VERTEX_USER0_LOC (VERTEX_LOCATION_COUNT) @@ -44,7 +43,20 @@ BEGIN_NAMESPACE_FALCOR #define VERTEX_POSITION_NAME "POSITION" #define VERTEX_PACKED_NORMAL_TANGENT_NAME "PACKED_NORMAL_TANGENT" #define VERTEX_TEXCOORD_NAME "TEXCOORD" -#define VERTEX_PREV_POSITION_NAME "PREV_POSITION" #define INSTANCE_DRAW_ID_NAME "DRAW_ID" +#define CURVE_VERTEX_POSITION_LOC 0 +#define CURVE_VERTEX_RADIUS_LOC 1 +#define CURVE_VERTEX_TANGENT_LOC 2 +#define CURVE_VERTEX_NORMAL_LOC 3 +#define CURVE_VERTEX_TEXCOORD_LOC 4 + +#define CURVE_VERTEX_LOCATION_COUNT 5 + +#define CURVE_VERTEX_POSITION_NAME "POSITION" +#define CURVE_VERTEX_RADIUS_NAME "RADIUS" +#define CURVE_VERTEX_TANGENT_NAME "TANGENT" +#define CURVE_VERTEX_NORMAL_NAME "NORMAL" +#define CURVE_VERTEX_TEXCOORD_NAME "TEXCOORD" + END_NAMESPACE_FALCOR diff --git a/Source/Falcor/Scene/Volume/Grid.cpp b/Source/Falcor/Scene/Volume/Grid.cpp new file mode 100644 index 0000000000..67bea398ea --- /dev/null +++ b/Source/Falcor/Scene/Volume/Grid.cpp @@ -0,0 +1,255 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#include "stdafx.h" +#include "Grid.h" +#pragma warning(disable:4146 4244 4267 4275 4996) +#include +#include +#include +#include +#include +#pragma warning(default:4146 4244 4267 4275 4996) + +namespace Falcor +{ + namespace + { + float3 cast(const nanovdb::Vec3f& v) + { + return float3(v[0], v[1], v[2]); + } + + float3 cast(const nanovdb::Vec3R& v) + { + return float3(v[0], v[1], v[2]); + } + + int3 cast(const nanovdb::Coord& c) + { + return int3(c[0], c[1], c[2]); + } + } + + Grid::SharedPtr Grid::createSphere(float radius, float voxelSize, float blendRange) + { + auto handle = nanovdb::createFogVolumeSphere(radius, nanovdb::Vec3R(0.0), voxelSize, blendRange); + return SharedPtr(new Grid(std::move(handle))); + } + + Grid::SharedPtr Grid::createBox(float width, float height, float depth, float voxelSize, float blendRange) + { + auto handle = nanovdb::createFogVolumeBox(width, height, depth, nanovdb::Vec3R(0.0), voxelSize, blendRange); + return SharedPtr(new Grid(std::move(handle))); + } + + Grid::SharedPtr Grid::createFromFile(const std::string& filename, const std::string& gridname) + { + std::string fullpath; + if (!findFileInDataDirectories(filename, fullpath)) + { + logWarning("Error when loading grid. Can't find grid file '" + filename + "'"); + return nullptr; + } + + auto ext = getExtensionFromFile(fullpath); + if (ext == "nvdb") + { + return createFromNanoVDBFile(fullpath, gridname); + } + else if (ext == "vdb") + { + return createFromOpenVDBFile(fullpath, gridname); + } + else + { + logWarning("Error when loading grid. Unsupported grid file '" + filename + "'"); + return nullptr; + } + } + + void Grid::renderUI(Gui::Widgets& widget) + { + std::ostringstream oss; + oss << "Voxel count: " << getVoxelCount() << std::endl + << "Minimum index: " << to_string(getMinIndex()) << std::endl + << "Maximum index: " << to_string(getMaxIndex()) << std::endl + << "Minimum value: " << getMinValue() << std::endl + << "Maximum value: " << getMaxValue() << std::endl + << "Memory: " << formatByteSize(getGridSizeInBytes()) << std::endl; + widget.text(oss.str()); + } + + void Grid::setShaderData(const ShaderVar& var) + { + var["buf"] = mpBuffer; + } + + int3 Grid::getMinIndex() const + { + return cast(mpFloatGrid->indexBBox().min()); + } + + int3 Grid::getMaxIndex() const + { + return cast(mpFloatGrid->indexBBox().max()); + } + + float Grid::getMinValue() const + { + return mpFloatGrid->tree().root().valueMin(); + } + + float Grid::getMaxValue() const + { + return mpFloatGrid->tree().root().valueMax(); + } + + uint64_t Grid::getVoxelCount() const + { + return mpFloatGrid->activeVoxelCount(); + } + + uint64_t Grid::getGridSizeInBytes() const + { + return mpBuffer ? mpBuffer->getSize() : (uint64_t)0; + } + + AABB Grid::getWorldBounds() const + { + auto bounds = mpFloatGrid->worldBBox(); + return AABB(cast(bounds.min()), cast(bounds.max())); + } + + float Grid::getValue(const int3& ijk) const + { + return mAccessor.getValue(nanovdb::Coord(ijk.x, ijk.y, ijk.z)); + } + + const nanovdb::GridHandle& Grid::getGridHandle() const + { + return mGridHandle; + } + + Grid::Grid(nanovdb::GridHandle gridHandle) + : mGridHandle(std::move(gridHandle)) + , mpFloatGrid(mGridHandle.grid()) + , mAccessor(mpFloatGrid->getAccessor()) + { + if (!mpFloatGrid->hasMinMax()) + { + nanovdb::gridStats(*mpFloatGrid); + } + + mpBuffer = Buffer::createStructured( + sizeof(uint32_t), + uint32_t(div_round_up(mGridHandle.size(), sizeof(uint32_t))), + ResourceBindFlags::UnorderedAccess | ResourceBindFlags::ShaderResource, + Buffer::CpuAccess::None, + mGridHandle.data() + ); + } + + Grid::SharedPtr Grid::createFromNanoVDBFile(const std::string& path, const std::string& gridname) + { + if (!nanovdb::io::hasGrid(path, gridname)) + { + logWarning("Error when loading grid. Can't find grid '" + gridname + "' in '" + path + "'"); + return nullptr; + } + + auto handle = nanovdb::io::readGrid(path, gridname); + if (!handle) + { + logWarning("Error when loading grid."); + return nullptr; + } + + auto floatGrid = handle.grid(); + if (!floatGrid || floatGrid->gridType() != nanovdb::GridType::Float) + { + logWarning("Error when loading grid. Grid '" + gridname + "' in '" + path + "' is not of type float"); + return nullptr; + } + + return SharedPtr(new Grid(std::move(handle))); + } + + Grid::SharedPtr Grid::createFromOpenVDBFile(const std::string& path, const std::string& gridname) + { + openvdb::initialize(); + + openvdb::io::File file(path); + file.open(); + + openvdb::GridBase::Ptr baseGrid; + for (auto it = file.beginName(); it != file.endName(); ++it) + { + if (it.gridName() == gridname) + { + baseGrid = file.readGrid(it.gridName()); + break; + } + } + + file.close(); + + if (!baseGrid) + { + logWarning("Error when loading grid. Can't find grid '" + gridname + "' in '" + path + "'"); + return nullptr; + } + + if (!baseGrid->isType()) + { + logWarning("Error when loading grid. Grid '" + gridname + "' in '" + path + "' is not of type float"); + return nullptr; + } + + openvdb::FloatGrid::Ptr floatGrid = openvdb::gridPtrCast(baseGrid); + auto handle = nanovdb::openToNanoVDB(floatGrid); + + return SharedPtr(new Grid(std::move(handle))); + } + + + SCRIPT_BINDING(Grid) + { + pybind11::class_ grid(m, "Grid"); + grid.def_property_readonly("voxelCount", &Grid::getVoxelCount); + grid.def_property_readonly("minIndex", &Grid::getMinIndex); + grid.def_property_readonly("maxIndex", &Grid::getMaxIndex); + grid.def_property_readonly("minValue", &Grid::getMinValue); + grid.def_property_readonly("maxValue", &Grid::getMaxValue); + + grid.def("getValue", &Grid::getValue, "ijk"_a); + + grid.def_static("createSphere", &Grid::createSphere, "radius"_a, "voxelSize"_a, "blendRange"_a = 3.f); + grid.def_static("createBox", &Grid::createBox, "width"_a, "height"_a, "depth"_a, "voxelSize"_a, "blendRange"_a = 3.f); + grid.def_static("createFromFile", &Grid::createFromFile, "filename"_a, "gridname"_a); + } +} diff --git a/Source/Falcor/Scene/Volume/Grid.h b/Source/Falcor/Scene/Volume/Grid.h new file mode 100644 index 0000000000..8586d2b2b5 --- /dev/null +++ b/Source/Falcor/Scene/Volume/Grid.h @@ -0,0 +1,128 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#pragma once +#pragma warning(disable:4244 4267) +#include +#include +#include +#pragma warning(default:4244 4267) + +namespace Falcor +{ + /** Voxel grid based on NanoVDB. + */ + class dlldecl Grid + { + public: + using SharedPtr = std::shared_ptr; + + /** Create a sphere voxel grid. + \param[in] radius Radius of the sphere in world units. + \param[in] voxelSize Size of a voxel in world units. + \param[in] blendRange Range in voxels to blend from 0 to 1 (starting at surface inwards). + \return A new grid. + */ + static SharedPtr createSphere(float radius, float voxelSize, float blendRange = 2.f); + + /** Create a box voxel grid. + \param[in] width Width of the box in world units. + \param[in] height Height of the box in world units. + \param[in] depth Depth of the box in world units. + \param[in] voxelSize Size of a voxel in world units. + \param[in] blendRange Range in voxels to blend from 0 to 1 (starting at surface inwards). + \return A new grid. + */ + static SharedPtr createBox(float width, float height, float depth, float voxelSize, float blendRange = 2.f); + + /** Create a grid from a file. + Currently only OpenVDB and NanoVDB grids of type float are supported. + \param[in] filename Filename of the grid. Can also include a full path or relative path from a data directory. + \param[in] gridname Name of the grid to load. + \return A new grid, or nullptr if the grid failed to load. + */ + static SharedPtr createFromFile(const std::string& filename, const std::string& gridname); + + /** Render the UI. + */ + void renderUI(Gui::Widgets& widget); + + /** Bind the grid to a given shader var. + \param[in] var The shader variable to set the data into. + */ + void setShaderData(const ShaderVar& var); + + /** Get the minimum index stored in the grid. + */ + int3 getMinIndex() const; + + /** Get the maximum index stored in the grid. + */ + int3 getMaxIndex() const; + + /** Get the minimum value stored in the grid. + */ + float getMinValue() const; + + /** Get the maximum value stored in the grid. + */ + float getMaxValue() const; + + /** Get the total number of active voxels in the grid. + */ + uint64_t getVoxelCount() const; + + /** Get the size of the grid in bytes as allocated in GPU memory. + */ + uint64_t getGridSizeInBytes() const; + + /** Get the grid's bounds in world space. + */ + AABB getWorldBounds() const; + + /** Get a value stored in the grid. + Note: This function is not safe for access from multiple threads. + \param[in] ijk The index-space position to access the data from. + */ + float getValue(const int3& ijk) const; + + /** Get the raw NanoVDB grid handle. + */ + const nanovdb::GridHandle& getGridHandle() const; + + private: + Grid(nanovdb::GridHandle gridHandle); + + static SharedPtr createFromNanoVDBFile(const std::string& path, const std::string& gridname); + static SharedPtr createFromOpenVDBFile(const std::string& path, const std::string& gridname); + + nanovdb::GridHandle mGridHandle; + nanovdb::FloatGrid* mpFloatGrid; + nanovdb::FloatGrid::AccessorType mAccessor; + Buffer::SharedPtr mpBuffer; + }; +} diff --git a/Source/Falcor/Scene/Volume/Grid.slang b/Source/Falcor/Scene/Volume/Grid.slang new file mode 100644 index 0000000000..bdeed7acb4 --- /dev/null +++ b/Source/Falcor/Scene/Volume/Grid.slang @@ -0,0 +1,195 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#define PNANOVDB_HLSL +#include "nanovdb/PNanoVDB.h" + +/** Voxel grid based on NanoVDB. +*/ +struct Grid +{ + typedef pnanovdb_readaccessor_t Accessor; + + StructuredBuffer buf; + + /** Get the minimum index stored in the grid. + \return Returns minimum index stored in the grid. + */ + int3 getMinIndex() + { + pnanovdb_root_handle_t root = pnanovdb_tree_get_root(buf, pnanovdb_grid_get_tree(buf, { pnanovdb_address_null() })); + return pnanovdb_root_get_bbox_min(buf, root); + } + + /** Get the maximum index stored in the grid. + \return Returns maximum index stored in the grid. + */ + int3 getMaxIndex() + { + pnanovdb_root_handle_t root = pnanovdb_tree_get_root(buf, pnanovdb_grid_get_tree(buf, { pnanovdb_address_null() })); + return pnanovdb_root_get_bbox_max(buf, root); + } + + /** Get the minimum value stored in the grid. + \return Returns minimum value stored in the grid. + */ + float getMinValue() + { + pnanovdb_root_handle_t root = pnanovdb_tree_get_root(buf, pnanovdb_grid_get_tree(buf, { pnanovdb_address_null() })); + return pnanovdb_read_float(buf, pnanovdb_root_get_min_address(PNANOVDB_GRID_TYPE_FLOAT, buf, root)); + } + + /** Get the maximum value stored in the grid. + \return Returns maximum value stored in the grid. + */ + float getMaxValue() + { + pnanovdb_root_handle_t root = pnanovdb_tree_get_root(buf, pnanovdb_grid_get_tree(buf, { pnanovdb_address_null() })); + return pnanovdb_read_float(buf, pnanovdb_root_get_max_address(PNANOVDB_GRID_TYPE_FLOAT, buf, root)); + } + + /** Transform position from world- to index-space. + \param[in] pos Position in world-space. + \return Returns position in index-space. + */ + float3 worldToIndexPos(float3 pos) + { + return pnanovdb_grid_world_to_indexf(buf, { pnanovdb_address_null() }, pos); + } + + /** Transform direction from world- to index-space. + \param[in] dir Direction in world-space. + \return Returns direction in index-space. + */ + float3 worldToIndexDir(float3 dir) + { + return normalize(pnanovdb_grid_world_to_index_dirf(buf, { pnanovdb_address_null() }, dir)); + } + + /** Transform position from index- to world-space. + \param[in] pos Position in index-space. + \return Returns position in world-space. + */ + float3 indexToWorldPos(float3 pos) + { + return pnanovdb_grid_index_to_worldf(buf, { pnanovdb_address_null() }, pos); + } + + /** Transform direction from index- to world-space. + \param[in] dir Direction in index-space. + \return Returns direction in world-space. + */ + float3 indexToWorldDir(float3 dir) + { + return normalize(pnanovdb_grid_index_to_world_dirf(buf, { pnanovdb_address_null() }, dir)); + } + + /** Create an grid accessor. + \return Returns the new grid accessor. + */ + Accessor createAccessor() + { + Accessor accessor; + pnanovdb_root_handle_t root = pnanovdb_tree_get_root(buf, pnanovdb_grid_get_tree(buf, { pnanovdb_address_null() })); + pnanovdb_readaccessor_init(accessor, root); + return accessor; + } + + /** Lookup the grid using nearest-neighbor sampling. + \param[in] pos Position in world-space. + \param[in,out] accessor Grid accessor. + \return Returns the value in the grid. + */ + float lookupWorld(const float3 pos, inout Accessor accessor) + { + return lookupIndex(worldToIndexPos(pos), accessor); + } + + /** Lookup the grid using nearest-neighbor sampling. + \param[in] index Fractional voxel index. + \param[in,out] accessor Grid accessor. + \return Returns the value in the grid. + */ + float lookupIndex(const int3 index, inout Accessor accessor) + { + pnanovdb_address_t address = pnanovdb_readaccessor_get_value_address(PNANOVDB_GRID_TYPE_FLOAT, buf, accessor, index); + return pnanovdb_read_float(buf, address); + } + + /** Lookup the grid using tri-linear sampling. + \param[in] pos Position in world-space. + \param[in,out] accessor Grid accessor. + \return Returns the interpolated value in the grid. + */ + float lookupLinearWorld(const float3 pos, inout Accessor accessor) + { + return lookupLinearIndex(worldToIndexPos(pos), accessor); + } + + /** Lookup the grid using tri-linear sampling. + \param[in] index Fractional voxel index. + \param[in,out] accessor Grid accessor. + \return Returns the interpolated value in the grid. + */ + float lookupLinearIndex(const float3 index, inout Accessor accessor) + { + const float3 indexOffset = index - 0.5f; + const int3 i = floor(indexOffset); + const float3 f = indexOffset - i; + const float x0z0 = lerp(lookupIndex(i + int3(0, 0, 0), accessor), lookupIndex(i + int3(1, 0, 0), accessor), f.x); + const float x1z0 = lerp(lookupIndex(i + int3(0, 1, 0), accessor), lookupIndex(i + int3(1, 1, 0), accessor), f.x); + const float y0 = lerp(x0z0, x1z0, f.y); + const float x0z1 = lerp(lookupIndex(i + int3(0, 0, 1), accessor), lookupIndex(i + int3(1, 0, 1), accessor), f.x); + const float x1z1 = lerp(lookupIndex(i + int3(0, 1, 1), accessor), lookupIndex(i + int3(1, 1, 1), accessor), f.x); + const float y1 = lerp(x0z1, x1z1, f.y); + return lerp(y0, y1, f.z); + } + + /** Lookup the grid using stochastic tri-linear sampling. + \param[in] pos Position in world-space. + \param[in] u Uniform random number in [0..1). + \param[in,out] accessor Grid accessor. + \return Returns the sampled value in the grid. + */ + float lookupStochasticWorld(const float3 pos, const float3 u, inout Accessor accessor) + { + return lookupStochasticIndex(worldToIndexPos(pos), u, accessor); + } + + /** Lookup the grid using stochastic tri-linear sampling. + \param[in] index Fractional voxel index. + \param[in] u Uniform random number in [0..1). + \param[in,out] accessor Grid accessor. + \return Returns the sampled value in the grid. + */ + float lookupStochasticIndex(const float3 index, const float3 u, inout Accessor accessor) + { + const float3 dist = frac(index) - 0.5f; + const int3 offset = u < abs(dist) ? sign(dist) : int3(0); + return lookupIndex(index + offset, accessor); + } +}; diff --git a/Source/Falcor/Scene/Volume/Volume.cpp b/Source/Falcor/Scene/Volume/Volume.cpp new file mode 100644 index 0000000000..c23597074b --- /dev/null +++ b/Source/Falcor/Scene/Volume/Volume.cpp @@ -0,0 +1,360 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#include "stdafx.h" +#include "Volume.h" +#include + +namespace Falcor +{ + namespace + { + // UI variables. + const Gui::DropdownList kEmissionModeList = + { + { (uint32_t)Volume::EmissionMode::Direct, "Direct" }, + { (uint32_t)Volume::EmissionMode::Blackbody, "Blackbody" }, + }; + + // Constants. + const float kMaxAnisotropy = 0.99f; + } + + static_assert(sizeof(VolumeData) % 16 == 0, "Volume::VolumeData size should be a multiple of 16"); + + Volume::Volume(const std::string& name) : mName(name) + { + mData.transform = glm::identity(); + mData.invTransform = glm::identity(); + } + + Volume::SharedPtr Volume::create(const std::string& name) + { + Volume* pVolume = new Volume(name); + return SharedPtr(pVolume); + } + + bool Volume::renderUI(Gui::Widgets& widget) + { + // We're re-using the volumes's update flags here to track changes. + // Cache the previous flag so we can restore it before returning. + UpdateFlags prevUpdates = mUpdates; + mUpdates = UpdateFlags::None; + + if (mGridFrameCount > 1) + { + uint32_t gridFrame = getGridFrame(); + if (widget.var("Grid frame", gridFrame, 0u, mGridFrameCount - 1, 1u)) setGridFrame(gridFrame); + } + + if (const auto& densityGrid = getDensityGrid()) + { + if (auto group = widget.group("Density Grid")) densityGrid->renderUI(group); + + float densityScale = getDensityScale(); + if (widget.var("Density scale", densityScale, 0.f, std::numeric_limits::max(), 0.01f)) setDensityScale(densityScale); + } + + if (const auto& emissionGrid = getEmissionGrid()) + { + if (auto group = widget.group("Emission Grid")) emissionGrid->renderUI(group); + + float emissionScale = getEmissionScale(); + if (widget.var("Emission scale", emissionScale, 0.f, std::numeric_limits::max(), 0.01f)) setEmissionScale(emissionScale); + } + + float3 albedo = getAlbedo(); + if (widget.var("Albedo", albedo, 0.f, 1.f, 0.01f)) setAlbedo(albedo); + + float anisotropy = getAnisotropy(); + if (widget.var("Anisotropy", anisotropy, -kMaxAnisotropy, kMaxAnisotropy, 0.01f)) setAnisotropy(anisotropy); + + EmissionMode emissionMode = getEmissionMode(); + if (widget.dropdown("Emission mode", kEmissionModeList, reinterpret_cast(emissionMode))) setEmissionMode(emissionMode); + + if (getEmissionMode() == EmissionMode::Blackbody) + { + float emissionTemperature = getEmissionTemperature(); + if (widget.var("Emission temperature", emissionTemperature, 0.f, std::numeric_limits::max(), 0.01f)) setEmissionTemperature(emissionTemperature); + } + + // Restore update flags. + bool changed = mUpdates != UpdateFlags::None; + markUpdates(prevUpdates | mUpdates); + + return changed; + } + + bool Volume::loadGrid(GridSlot slot, const std::string& filename, const std::string& gridname) + { + auto grid = Grid::createFromFile(filename, gridname); + if (grid) setGrid(slot, grid); + return grid != nullptr; + } + + uint32_t Volume::loadGridSequence(GridSlot slot, const std::vector& filenames, const std::string& gridname, bool keepEmpty) + { + GridSequence grids; + for (const auto& filename : filenames) + { + auto grid = Grid::createFromFile(filename, gridname); + if (keepEmpty || grid) grids.push_back(grid); + } + setGridSequence(slot, grids); + return (uint32_t)grids.size(); + } + + uint32_t Volume::loadGridSequence(GridSlot slot, const std::string& path, const std::string& gridname, bool keepEmpty) + { + std::string fullpath; + if (!findFileInDataDirectories(path, fullpath)) + { + logWarning("Cannot find directory '" + path + "'"); + return 0; + } + if (!std::filesystem::is_directory(fullpath)) + { + logWarning("'" + path + "' is not a directory"); + return 0; + } + + // Enumerate grid files. + std::vector files; + for (auto p : std::filesystem::directory_iterator(fullpath)) + { + if (p.path().extension() == ".nvdb" || p.path().extension() == ".vdb") files.push_back(p.path().string()); + } + + // Sort by length first, then alpha-numerically. + auto cmp = [](const std::string& a, const std::string& b) { return a.length() != b.length() ? a.length() < b.length() : a < b; }; + std::sort(files.begin(), files.end(), cmp); + + return loadGridSequence(slot, files, gridname, keepEmpty); + } + + void Volume::setGridSequence(GridSlot slot, const GridSequence& grids) + { + uint32_t slotIndex = (uint32_t)slot; + assert(slotIndex >= 0 && slotIndex < (uint32_t)GridSlot::Count); + + if (mGrids[slotIndex] != grids) + { + mGrids[slotIndex] = grids; + updateSequence(); + updateBounds(); + markUpdates(UpdateFlags::GridsChanged); + } + } + + const Volume::GridSequence& Volume::getGridSequence(GridSlot slot) const + { + uint32_t slotIndex = (uint32_t)slot; + assert(slotIndex >= 0 && slotIndex < (uint32_t)GridSlot::Count); + + return mGrids[slotIndex]; + } + + void Volume::setGrid(GridSlot slot, const Grid::SharedPtr& grid) + { + setGridSequence(slot, grid ? GridSequence{grid} : GridSequence{}); + } + + const Grid::SharedPtr& Volume::getGrid(GridSlot slot) const + { + static const Grid::SharedPtr kNullGrid; + + uint32_t slotIndex = (uint32_t)slot; + assert(slotIndex >= 0 && slotIndex < (uint32_t)GridSlot::Count); + + const auto& gridSequence = mGrids[slotIndex]; + uint32_t gridIndex = std::min(mGridFrame, (uint32_t)gridSequence.size() - 1); + return gridSequence.empty() ? kNullGrid : gridSequence[gridIndex]; + } + + std::vector Volume::getAllGrids() const + { + std::set uniqueGrids; + for (const auto& grids : mGrids) + { + std::copy_if(grids.begin(), grids.end(), std::inserter(uniqueGrids, uniqueGrids.begin()), [] (const auto& grid) { return grid != nullptr; }); + } + return std::vector(uniqueGrids.begin(), uniqueGrids.end()); + } + + void Volume::setGridFrame(uint32_t gridFrame) + { + if (mGridFrame != gridFrame) + { + mGridFrame = gridFrame; + markUpdates(UpdateFlags::GridsChanged); + updateBounds(); + } + } + + void Volume::setDensityScale(float densityScale) + { + if (mData.densityScale != densityScale) + { + mData.densityScale = densityScale; + markUpdates(UpdateFlags::PropertiesChanged); + } + } + + void Volume::setEmissionScale(float emissionScale) + { + if (mData.emissionScale != emissionScale) + { + mData.emissionScale = emissionScale; + markUpdates(UpdateFlags::PropertiesChanged); + } + } + + void Volume::setAlbedo(const float3& albedo) + { + auto clampedAlbedo = clamp(albedo, float3(0.f), float3(1.f)); + if (mData.albedo != clampedAlbedo) + { + mData.albedo = clampedAlbedo; + markUpdates(UpdateFlags::PropertiesChanged); + } + } + + void Volume::setAnisotropy(float anisotropy) + { + auto clampedAnisotropy = clamp(anisotropy, -kMaxAnisotropy, kMaxAnisotropy); + if (mData.anisotropy != clampedAnisotropy) + { + mData.anisotropy = clampedAnisotropy; + markUpdates(UpdateFlags::PropertiesChanged); + } + } + + void Volume::setEmissionMode(EmissionMode emissionMode) + { + if (mData.flags != (uint32_t)emissionMode) + { + mData.flags = (uint32_t)emissionMode; + markUpdates(UpdateFlags::PropertiesChanged); + } + } + + Volume::EmissionMode Volume::getEmissionMode() const + { + return (EmissionMode)mData.flags; + } + + void Volume::setEmissionTemperature(float emissionTemperature) + { + if (mData.emissionTemperature != emissionTemperature) + { + mData.emissionTemperature = emissionTemperature; + markUpdates(UpdateFlags::PropertiesChanged); + } + } + + void Volume::updateFromAnimation(const glm::mat4& transform) + { + if (mData.transform != transform) + { + mData.transform = transform; + mData.invTransform = glm::inverse(transform); + markUpdates(UpdateFlags::TransformChanged); + updateBounds(); + } + } + + void Volume::updateSequence() + { + mGridFrameCount = 1; + for (const auto& grids : mGrids) mGridFrameCount = std::max(mGridFrameCount, (uint32_t)grids.size()); + setGridFrame(std::min(mGridFrame, mGridFrameCount - 1)); + } + + void Volume::updateBounds() + { + AABB bounds; + for (uint32_t slotIndex = 0; slotIndex < (uint32_t)GridSlot::Count; ++slotIndex) + { + const auto& grid = getGrid((GridSlot)slotIndex); + if (grid && grid->getVoxelCount() > 0) bounds.include(grid->getWorldBounds()); + } + bounds = bounds.transform(mData.transform); + + if (mBounds != bounds) + { + mBounds = bounds; + mData.boundsMin = mBounds.minPoint; + mData.boundsMax = mBounds.maxPoint; + markUpdates(UpdateFlags::BoundsChanged); + } + } + + void Volume::markUpdates(UpdateFlags updates) + { + mUpdates |= updates; + } + + void Volume::setFlags(uint32_t flags) + { + if (mData.flags != flags) + { + mData.flags = flags; + markUpdates(UpdateFlags::PropertiesChanged); + } + } + + SCRIPT_BINDING(Volume) + { + pybind11::class_ volume(m, "Volume"); + volume.def_property("name", &Volume::getName, &Volume::setName); + volume.def_property("gridFrame", &Volume::getGridFrame, &Volume::setGridFrame); + volume.def_property_readonly("gridFrameCount", &Volume::getGridFrameCount); + volume.def_property("densityGrid", &Volume::getDensityGrid, &Volume::setDensityGrid); + volume.def_property("densityScale", &Volume::getDensityScale, &Volume::setDensityScale); + volume.def_property("emissionGrid", &Volume::getEmissionGrid, &Volume::setEmissionGrid); + volume.def_property("emissionScale", &Volume::getEmissionScale, &Volume::setEmissionScale); + volume.def_property("albedo", &Volume::getAlbedo, &Volume::setAlbedo); + volume.def_property("anisotropy", &Volume::getAnisotropy, &Volume::setAnisotropy); + volume.def_property("emissionMode", &Volume::getEmissionMode, &Volume::setEmissionMode); + volume.def_property("emissionTemperature", &Volume::getEmissionTemperature, &Volume::setEmissionTemperature); + volume.def(pybind11::init(&Volume::create), "name"_a); + volume.def("loadGrid", &Volume::loadGrid, "slot"_a, "filename"_a, "gridname"_a); + volume.def("loadGridSequence", + pybind11::overload_cast&, const std::string&, bool>(&Volume::loadGridSequence), + "slot"_a, "filenames"_a, "gridname"_a, "keepEmpty"_a = true); + volume.def("loadGridSequence", + pybind11::overload_cast(&Volume::loadGridSequence), + "slot"_a, "path"_a, "gridnames"_a, "keepEmpty"_a = true); + pybind11::enum_ gridSlot(volume, "GridSlot"); + gridSlot.value("Density", Volume::GridSlot::Density); + gridSlot.value("Emission", Volume::GridSlot::Emission); + + pybind11::enum_ emissionMode(volume, "EmissionMode"); + emissionMode.value("Direct", Volume::EmissionMode::Direct); + emissionMode.value("Blackbody", Volume::EmissionMode::Blackbody); + } +} diff --git a/Source/Falcor/Scene/Volume/Volume.h b/Source/Falcor/Scene/Volume/Volume.h new file mode 100644 index 0000000000..387d56395e --- /dev/null +++ b/Source/Falcor/Scene/Volume/Volume.h @@ -0,0 +1,259 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#pragma once +#include "Grid.h" +#include "VolumeData.slang" +#include "Scene/Animation/Animatable.h" + +namespace Falcor +{ + /** Describes a heterogeneous volume in the scene. + The absorbing/scattering medium is defined by a density voxel grid and additional parameters. + The emission is defined by an emission voxel grid and additional parameters. + Grids are stored in grid slots (density, emission) and can either be static, using one grid per slot, + or dynamic, using a sequence of grids per slot. + */ + class dlldecl Volume : public Animatable + { + public: + using SharedPtr = std::shared_ptr; + + using GridSequence = std::vector; + + /** Flags indicating if and what was updated in the volume. + */ + enum class UpdateFlags + { + None = 0x0, ///< Nothing updated. + PropertiesChanged = 0x1, ///< Volume properties changed. + GridsChanged = 0x2, ///< Volume grids changed. + TransformChanged = 0x4, ///< Volume transform changed. + BoundsChanged = 0x8, ///< Volume world-space bounds changed. + }; + + /** Grid slots available in the volume. + */ + enum class GridSlot + { + Density, + Emission, + + Count // Must be last + }; + + /** Specifies how emission is rendered. + */ + enum class EmissionMode + { + Direct, + Blackbody, + }; + + /** Create a new volume. + \param[in] name The volume name. + */ + static SharedPtr create(const std::string& name); + + /** Render the UI. + \return True if the volume was modified. + */ + bool renderUI(Gui::Widgets& widget); + + /** Returns the updates since the last call to clearUpdates. + */ + UpdateFlags getUpdates() const { return mUpdates; } + + /** Clears the updates. + */ + void clearUpdates() { mUpdates = UpdateFlags::None; } + + /** Set the volume name. + */ + void setName(const std::string& name) { mName = name; } + + /** Get the volume name. + */ + const std::string& getName() const { return mName; } + + /** Load a single grid from a file to a grid slot. + Note: This will replace any existing grid sequence for that slot with just a single grid. + \param[in] slot Grid slot. + \param[in] filename Filename of the grid. Can also include a full path or relative path from a data directory. + \param[in] gridname Name of the grid to load. + \return Returns true if grid was loaded successfully. + */ + bool loadGrid(GridSlot slot, const std::string& filename, const std::string& gridname); + + /** Load a sequence of grids from files to a grid slot. + Note: This will replace any existing grid sequence for that slot. + \param[in] slot Grid slot. + \param[in] filenames Filenames of the grids. Can also include a full path or relative path from a data directory. + \param[in] gridname Name of the grid to load. + \param[in] keepEmpty Add empty (nullptr) grids to the sequence if one cannot be loaded from the file. + \return Returns the length of the loaded sequence. + */ + uint32_t loadGridSequence(GridSlot slot, const std::vector& filenames, const std::string& gridname, bool keepEmpty = true); + + /** Load a sequence of grids from a directory to a grid slot. + Note: This will replace any existing grid sequence for that slot. + \param[in] slot Grid slot. + \param[in] path Directory containing grid files. Can also include a full path or relative path from a data directory. + \param[in] gridname Name of the grid to load. + \param[in] keepEmpty Add empty (nullptr) grids to the sequence if one cannot be loaded from the file. + \return Returns the length of the loaded sequence. + */ + uint32_t loadGridSequence(GridSlot slot, const std::string& path, const std::string& gridname, bool keepEmpty = true); + + /** Set the grid sequence for the specified slot. + */ + void setGridSequence(GridSlot slot, const GridSequence& grids); + + /** Get the grid sequence for the specified slot. + */ + const GridSequence& getGridSequence(GridSlot slot) const; + + /** Set the grid for the specified slot. + Note: This will replace any existing grid sequence for that slot with just a single grid. + */ + void setGrid(GridSlot slot, const Grid::SharedPtr& grid); + + /** Get the current grid from the specified slot. + */ + const Grid::SharedPtr& getGrid(GridSlot slot) const; + + /** Get a list of all grids used for this volume. + */ + std::vector getAllGrids() const; + + /** Sets the current frame of the grid sequence to use. + */ + void setGridFrame(uint32_t gridFrame); + + /** Get the current frame of the grid sequence. + */ + uint32_t getGridFrame() const { return mGridFrame; } + + /** Get the number of frames in the grid sequence. + Note: This returns 1 even if there are no grids loaded. + */ + uint32_t getGridFrameCount() const { return mGridFrameCount; } + + /** Set the density grid. + */ + void setDensityGrid(const Grid::SharedPtr& densityGrid) { setGrid(GridSlot::Density, densityGrid); }; + + /** Get the density grid. + */ + const Grid::SharedPtr& getDensityGrid() const { return getGrid(GridSlot::Density); } + + /** Set the density scale factor. + */ + void setDensityScale(float densityScale); + + /** Get the density scale factor. + */ + float getDensityScale() const { return mData.densityScale; } + + /** Set the emission grid. + */ + void setEmissionGrid(const Grid::SharedPtr& emissionGrid) { setGrid(GridSlot::Emission, emissionGrid); } + + /** Get the emission grid. + */ + const Grid::SharedPtr& getEmissionGrid() const { return getGrid(GridSlot::Emission); } + + /** Set the emission scale factor. + */ + void setEmissionScale(float emissionScale); + + /** Get the emission scale factor. + */ + float getEmissionScale() const { return mData.emissionScale; } + + /** Set the scattering albedo. + */ + void setAlbedo(const float3& albedo); + + /** Get the scattering albedo. + */ + const float3& getAlbedo() const { return mData.albedo; } + + /** Set the anisotropy (forward or backward scattering). + */ + void setAnisotropy(float anisotropy); + + /** Get the anisotropy. + */ + float getAnisotropy() const { return mData.anisotropy; } + + /** Set the emission mode. + */ + void setEmissionMode(EmissionMode emissionMode); + + /** Get the emission mode. + */ + EmissionMode getEmissionMode() const; + + /** Set the emission base temperature (K). + */ + void setEmissionTemperature(float emissionTemperature); + + /** Get the emission base temperature (K). + */ + float getEmissionTemperature() const { return mData.emissionTemperature; } + + /** Returns the volume data struct. + */ + const VolumeData& getData() const { return mData; } + + /** Returns the volume bounds in world space. + */ + const AABB& getBounds() const { return mBounds; } + + void updateFromAnimation(const glm::mat4& transform) override; + + private: + Volume(const std::string& name); + + void updateSequence(); + void updateBounds(); + + void markUpdates(UpdateFlags updates); + void setFlags(uint32_t flags); + + std::string mName; + std::array mGrids; + uint32_t mGridFrame = 0; + uint32_t mGridFrameCount = 1; + AABB mBounds; + VolumeData mData; + mutable UpdateFlags mUpdates = UpdateFlags::None; + }; + + enum_class_operators(Volume::UpdateFlags); +} diff --git a/Source/Falcor/Scene/Volume/Volume.slang b/Source/Falcor/Scene/Volume/Volume.slang new file mode 100644 index 0000000000..c01884c4ee --- /dev/null +++ b/Source/Falcor/Scene/Volume/Volume.slang @@ -0,0 +1,66 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +__exported import Utils.Math.AABB; +import Scene.Volume.VolumeData; + +/** Heterogeneous volume. +*/ +struct Volume +{ + /** Specifies how emission is rendered. + */ + enum class EmissionMode + { + Direct = 0, + Blackbody = 1, + }; + + static const uint kInvalidGrid = -1; + + VolumeData data; + + /** Get the world-space bounds. + */ + AABB getBounds() + { + return AABB.create(data.boundsMin, data.boundsMax); + } + + /** Get the emission mode. + */ + EmissionMode getEmissionMode() + { + return (EmissionMode)data.flags; + } + + bool hasDensityGrid() { return data.densityGrid != kInvalidGrid; } + bool hasEmissionGrid() { return data.emissionGrid != kInvalidGrid; } + + uint getDensityGrid() { return data.densityGrid; } + uint getEmissionGrid() { return data.emissionGrid; } +}; diff --git a/Source/Falcor/Scene/Volume/VolumeData.slang b/Source/Falcor/Scene/Volume/VolumeData.slang new file mode 100644 index 0000000000..28c449230e --- /dev/null +++ b/Source/Falcor/Scene/Volume/VolumeData.slang @@ -0,0 +1,51 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#pragma once +#include "Utils/HostDeviceShared.slangh" + +BEGIN_NAMESPACE_FALCOR + +/** This is a host/device structure that describes a heterogeneous volume. +*/ +struct VolumeData +{ + float4x4 transform; ///< Local-space to world-space transform. + float4x4 invTransform; ///< World-space to local-space transform. + float3 boundsMin = float3(0); ///< World-space bounds (minimum). + float densityScale = 1.f; ///< Density scale factor. + float3 boundsMax = float3(0); ///< World-space bounds (maximum). + float emissionScale = 1.f; ///< Emission scale factor. + uint densityGrid = 0; ///< Index of the density grid. + uint emissionGrid = 0; ///< Index of the emission grid. + uint flags = 0; ///< Flags (contains only emission mode for now). + float anisotropy = 0.f; ///< Phase function anisotropy. + float3 albedo = float3(1); ///< Medium scattering albedo. + float emissionTemperature = 0.f; ///< Emission base temperature (K). +}; + +END_NAMESPACE_FALCOR diff --git a/Source/Falcor/Testing/UnitTest.cpp b/Source/Falcor/Testing/UnitTest.cpp index 8fd2a4c860..6236e787d0 100644 --- a/Source/Falcor/Testing/UnitTest.cpp +++ b/Source/Falcor/Testing/UnitTest.cpp @@ -129,6 +129,9 @@ namespace Falcor auto endTime = std::chrono::steady_clock::now(); result.elapsedMS = std::chrono::duration_cast(endTime - startTime).count(); + // Release GPU resources. + if (test.gpuFunc) gpDevice->flushAndSync(); + return result; } diff --git a/Source/Falcor/Utils/AsyncTextureLoader.cpp b/Source/Falcor/Utils/AsyncTextureLoader.cpp new file mode 100644 index 0000000000..e6f6f6961e --- /dev/null +++ b/Source/Falcor/Utils/AsyncTextureLoader.cpp @@ -0,0 +1,129 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#include "stdafx.h" +#include "AsyncTextureLoader.h" + +namespace Falcor +{ + namespace + { + constexpr size_t kUploadsPerFlush = 16; ///< Number of texture uploads before issuing a flush (to keep upload heap from growing). + } + + AsyncTextureLoader::AsyncTextureLoader(size_t threadCount) + { + runWorkers(threadCount); + } + + AsyncTextureLoader::~AsyncTextureLoader() + { + terminateWorkers(); + + gpDevice->flushAndSync(); + } + + std::future AsyncTextureLoader::loadFromFile(const std::string& filename, bool generateMipLevels, bool loadAsSrgb, Resource::BindFlags bindFlags) + { + std::lock_guard lock(mMutex); + mRequestQueue.push(Request{filename, generateMipLevels, loadAsSrgb, bindFlags}); + mCondition.notify_one(); + return mRequestQueue.back().promise.get_future(); + } + + void AsyncTextureLoader::runWorkers(size_t threadCount) + { + // Create a barrier to synchronize worker threads before issuing a global flush. + auto barrier = std::make_shared(threadCount, [&] () { + gpDevice->flushAndSync(); + mFlushPending = false; + mUploadCounter = 0; + }); + + // Start worker threads. + for (size_t i = 0; i < threadCount; ++i) + { + mThreads.emplace_back([&, barrier] () { + while (true) + { + // Wait on condition until more work is ready. + std::unique_lock lock(mMutex); + mCondition.wait(lock, [&] () { return mTerminate || !mRequestQueue.empty() || mFlushPending; }); + + // Sync thread if a flush is pending. + if (mFlushPending) + { + lock.unlock(); + barrier->wait(); + mCondition.notify_one(); + continue; + } + + // Terminate thread unless there is more work to do. + if (mTerminate && mRequestQueue.empty() && !mFlushPending) break; + + // Go back waiting if queue is currently empty. + if (mRequestQueue.empty()) continue; + + // Pop next loading request from queue. + auto request = std::move(mRequestQueue.front()); + mRequestQueue.pop(); + + lock.unlock(); + + // Load the textures (this part is running in parallel). + Texture::SharedPtr pTexture = Texture::createFromFile(request.filename, request.generateMipLevels, request.loadAsSrgb, request.bindFlags); + request.promise.set_value(pTexture); + + lock.lock(); + + // Issue a global flush if necessary. + // TODO: It would be better to check the size of the upload heap instead. + if (!mTerminate && ++mUploadCounter >= kUploadsPerFlush) + { + mFlushPending = true; + mCondition.notify_all(); + } + + mCondition.notify_one(); + } + }); + } + } + + void AsyncTextureLoader::terminateWorkers() + { + { + std::lock_guard lock(mMutex); + mTerminate = true; + } + + mCondition.notify_all(); + + for (auto& thread : mThreads) thread.join(); + } +} diff --git a/Source/Falcor/Utils/AsyncTextureLoader.h b/Source/Falcor/Utils/AsyncTextureLoader.h new file mode 100644 index 0000000000..99d21c566f --- /dev/null +++ b/Source/Falcor/Utils/AsyncTextureLoader.h @@ -0,0 +1,79 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#pragma once +#include +#include "Falcor.h" + +namespace Falcor +{ + /** Utility class to load textures asynchronously using multiple worker threads. + */ + class dlldecl AsyncTextureLoader + { + public: + /** Constructor. + \param[in] threadCount Number of worker threads. + */ + AsyncTextureLoader(size_t threadCount = std::thread::hardware_concurrency()); + + /** Destructor. + Blocks until all textures are loaded. + */ + ~AsyncTextureLoader(); + + /** Request loading a texture. + \param[in] filename Filename of the image. Can also include a full path or relative path from a data directory. + \param[in] generateMipLevels Whether the mip-chain should be generated. + \param[in] loadAsSrgb Load the texture using sRGB format. Only valid for 3 or 4 component textures. + \param[in] bindFlags The bind flags to create the texture with. + \return A future to a new texture, or nullptr if the texture failed to load. + */ + std::future loadFromFile(const std::string& filename, bool generateMipLevels, bool loadAsSrgb, Resource::BindFlags bindFlags = Resource::BindFlags::ShaderResource); + + private: + void runWorkers(size_t threadCount); + void terminateWorkers(); + + struct Request + { + std::string filename; + bool generateMipLevels; + bool loadAsSrgb; + Resource::BindFlags bindFlags; + std::promise promise; + }; + + std::queue mRequestQueue; ///< Texture loading request queue. + std::condition_variable mCondition; ///< Condition variable for workers to wait on. + std::mutex mMutex; ///< Mutex for synchronizing access to shared resources. + std::vector mThreads; ///< Worker threads. + bool mTerminate = false; ///< Flag to terminate worker threads. + bool mFlushPending = false; ///< Flag to indicate a flush is pending. + uint32_t mUploadCounter = 0; ///< Counter to issue a flush every few uploads. + }; +} diff --git a/Source/Falcor/Utils/Debug/PixelDebug.cpp b/Source/Falcor/Utils/Debug/PixelDebug.cpp index 940d9f4c64..d442bedc07 100644 --- a/Source/Falcor/Utils/Debug/PixelDebug.cpp +++ b/Source/Falcor/Utils/Debug/PixelDebug.cpp @@ -146,6 +146,7 @@ namespace Falcor if (mEnabled) { widget.var("Selected pixel", mSelectedPixel); + widget.checkbox("Enable logging", mEnableLogging); } // Fetch stats and show log if available. @@ -199,7 +200,10 @@ namespace Falcor } } - widget.text(oss.str().c_str()); + widget.text(oss.str()); + + bool isEmpty = mPixelLogData.empty() && mAssertLogData.empty(); + if (mEnableLogging && !isEmpty) logInfo("\n" + oss.str()); } } diff --git a/Source/Falcor/Utils/Debug/PixelDebug.h b/Source/Falcor/Utils/Debug/PixelDebug.h index 4c16c051f5..5cdaad68f5 100644 --- a/Source/Falcor/Utils/Debug/PixelDebug.h +++ b/Source/Falcor/Utils/Debug/PixelDebug.h @@ -86,8 +86,9 @@ namespace Falcor GpuFence::SharedPtr mpFence; ///< GPU fence for sychronizing readback. // Configuration - bool mEnabled = false; ///< Enables debugging features. + bool mEnabled = false; ///< Enable debugging features. uint2 mSelectedPixel = { 0, 0 }; ///< Currently selected pixel. + bool mEnableLogging = false; ///< Enable printing to logfile. // Runtime data uint2 mFrameDim = { 0, 0 }; diff --git a/Source/Falcor/Utils/Helpers.slang b/Source/Falcor/Utils/Helpers.slang index 9aae3284bd..4dc3049ad3 100644 --- a/Source/Falcor/Utils/Helpers.slang +++ b/Source/Falcor/Utils/Helpers.slang @@ -30,6 +30,8 @@ __exported import Utils.Color.ColorHelpers; +import Utils.Math.MathHelpers; + /******************************************************************* Spherical map sampling *******************************************************************/ @@ -111,10 +113,10 @@ float3 computeRayOrigin(float3 pos, float3 normal) \param[in] rayDir Ray direction (does not have to be normalized). \param[in] center Sphere center. \param[in] radius Sphere radius. - \param[in] intersectionPos Position on the sphere for the closest intersection (if any). + \param[out] t Distance to the closest intersection. \return True if the ray intersects the sphere. */ -bool intersectRaySphere(float3 rayOrigin, float3 rayDir, float3 sphereCenter, float sphereRadius, out float3 intersectionPos) +bool intersectRaySphere(float3 rayOrigin, float3 rayDir, float3 sphereCenter, float sphereRadius, out float t) { // Implementation is taken from Chapter 7 of Ray-Tracing Gems float3 f = rayOrigin - sphereCenter; @@ -128,7 +130,7 @@ bool intersectRaySphere(float3 rayOrigin, float3 rayDir, float3 sphereCenter, fl // If b and discriminant are both 0, then the ray's origin lies on the sphere if (b == 0 && discriminant == 0) { - intersectionPos = rayOrigin; + t = 0.f; return true; } @@ -142,20 +144,152 @@ bool intersectRaySphere(float3 rayOrigin, float3 rayDir, float3 sphereCenter, fl float tc = t0 < 0.f ? t1 : t0; // tc is the closest hit we care about if (tc < 0.f) return false; - intersectionPos = rayOrigin + tc * rayDir; + t = tc; return true; } -/******************************************************************* - Shading -*******************************************************************/ +/** Ray-AABB intersection. + \param[in] rayOrigin Ray origin. + \param[in] rayDir Ray direction (does not have to be normalized). + \param[in] aabbMin AABB minimum. + \param[in] aabbMax AABB maximum. + \param[out] nearFar Returns intersection interval along ray. + \return True if the ray intersects the AABB. +*/ +bool intersectRayAABB(const float3 rayOrigin, const float3 rayDir, const float3 aabbMin, const float3 aabbMax, out float2 nearFar) +{ + const float3 invDir = 1.f / rayDir; + const float3 lo = (aabbMin - rayOrigin) * invDir; + const float3 hi = (aabbMax - rayOrigin) * invDir; + const float3 tmin = min(lo, hi), tmax = max(lo, hi); + nearFar.x = max(0.f, max(tmin.x, max(tmin.y, tmin.z))); + nearFar.y = min(tmax.x, min(tmax.y, tmax.z)); + return nearFar.x <= nearFar.y; +} -float4 applyAmbientOcclusion(float4 color, Texture2D aoTex, SamplerState s, float2 texC) +/** Ray intersection against linear swept sphere based on [Han et al. 2019], Ray Tracing Generalized Tube Primitives: Method and Applications. + Paper link: http://www.sci.utah.edu/publications/Han2019a/tubes-final.pdf + \param[in] rayOrigin Ray origin position. + \param[in] rayDir Unit ray direction vector. + \param[in] sphereA Sphere (3D position + radius) at one end point. + \param[in] sphereB Sphere at the other end point. + \param[in] useSphereJoints Indicating whether we test ray-sphere intersection at curve joints or not. + \param[out] result The closest intersection distance t, and a parameter u for linear interpolation (between 0 and 1). + \return True if the ray intersects the linear swept sphere segment. +*/ +bool intersectLinearSweptSphereHan19(float3 rayOrigin, float3 rayDir, float4 sphereA, float4 sphereB, bool useSphereJoints, out float2 result) { - float aoFactor = aoTex.SampleLevel(s, texC, 0).r; - return float4(color.rgb * aoFactor, color.a); + result = float2(FLT_MAX); + + bool reversed = false; + if (sphereA.w > sphereB.w) + { + float4 tmp = sphereA; + sphereA = sphereB; + sphereB = tmp; + reversed = true; + } + + const float3 P1 = sphereA.xyz; + const float3 P2 = sphereB.xyz; + const float r1 = sphereA.w; + const float r2 = sphereB.w; + + // Perpendicular distance to closest of (p0, p1) minus the max radius. + float t0 = min(dot(rayDir, P1 - rayOrigin), dot(rayDir, P2 - rayOrigin)) - max(r1, r2); + t0 = max(0.f, t0); + // For better numerical stability, push the ray to be as close as possible to the curve. + rayOrigin += t0 * rayDir; + + if (useSphereJoints) + { + // Intersecting two sphere endcaps. + float t; + if (intersectRaySphere(rayOrigin, rayDir, P1, r1, t)) + { + if (t < result.x) + { + result.x = t; + result.y = (reversed ? 1.f : 0.f); + } + } + if (intersectRaySphere(rayOrigin, rayDir, P2, r2, t)) + { + if (t < result.x) + { + result.x = t; + result.y = (reversed ? 0.f : 1.f); + } + } + } + + // Intersecting cone. + float3 C = P2 - P1; + const float lengthC = length(C); + C /= lengthC; + + const float p1 = lengthC * r1 / (r2 - r1); + const float p2 = p1 * r2 / r1; + const float3 A = P1 - p1 * C; + const float z1 = p1 - r1 * r1 / p1; + const float z2 = p2 - r2 * r2 / p2; + const float w = p2 * r2 / sqrt(p2 * p2 - r2 * r2); + + const float3 vz = C; + const float3 vx = perp_stark(vz); + const float3 vy = cross(vz, vx); + + const float tmp1 = 1.f / z2; + const float tmp2 = p2 / w; + + // Row-major matrix. + float4x4 M = + { + tmp1 * tmp2 * float4(vx, -dot(vx, A)), + tmp1 * tmp2 * float4(vy, -dot(vy, A)), + tmp1 * float4(vz, -dot(vz, A)), + float4(0, 0, 0, 1) + }; + + const float zCap = z1 * tmp1; + const float3 rayOriginXf = mul(M, float4(rayOrigin, 1.f)).xyz; + const float3 rayDirXf = mul((float3x3)M, rayDir); + + const float a = rayDirXf.x * rayDirXf.x + rayDirXf.y * rayDirXf.y - rayDirXf.z * rayDirXf.z; + const float b = 2.f * (rayOriginXf.x * rayDirXf.x + rayOriginXf.y * rayDirXf.y - rayOriginXf.z * rayDirXf.z); + const float c = rayOriginXf.x * rayOriginXf.x + rayOriginXf.y * rayOriginXf.y - rayOriginXf.z * rayOriginXf.z; + const float disc = b * b - 4 * a * c; + if (disc >= 0) + { + const float sqrtDisc = sqrt(disc); + const float inv2a = 0.5f / a; + + [unroll] + for (int i = 0; i < 2; i++) + { + float t = (-b + (i * 2.f - 1.f) * sqrtDisc) * inv2a; + if (t >= 0 && t < result.x) + { + // Check if z is in the valid range. + const float z = rayOriginXf.z + t * rayDirXf.z; + if (z >= zCap && z <= 1.f) + { + const float u = (z - zCap) / (1.f - zCap); + result.x = t; + result.y = (reversed ? 1.f - u : u); + } + } + } + } + + result.x += t0; + return (result.x < FLT_MAX); } +/******************************************************************* + Shading +*******************************************************************/ + // TODO: this function is broken an may return negative values. float getMetallic(float3 diffuse, float3 spec) { diff --git a/Source/Falcor/Utils/Image/Bitmap.cpp b/Source/Falcor/Utils/Image/Bitmap.cpp index 9cb2c9f313..81230ff235 100644 --- a/Source/Falcor/Utils/Image/Bitmap.cpp +++ b/Source/Falcor/Utils/Image/Bitmap.cpp @@ -27,10 +27,11 @@ **************************************************************************/ #include "stdafx.h" #include "Bitmap.h" -#include "FreeImage.h" #include "Core/API/Texture.h" #include "Utils/StringUtils.h" +#include + namespace Falcor { #ifdef FALCOR_VK @@ -43,10 +44,10 @@ namespace Falcor #else static bool isRGB32fSupported() { return false; } // FIX THIS #endif - static void genError(const std::string& errMsg, const std::string& filename) + static void genWarning(const std::string& errMsg, const std::string& filename) { - std::string err = "Error when loading image file " + filename + '\n' + errMsg + '.'; - logError(err); + std::string err = "Error when loading image file from '" + filename + "' (" + errMsg + ")"; + logWarning(err); } static bool isConvertibleToRGBA32Float(ResourceFormat format) @@ -182,12 +183,17 @@ namespace Falcor return pNew; } + Bitmap::UniqueConstPtr Bitmap::create(uint32_t width, uint32_t height, ResourceFormat format, const uint8_t* pData) + { + return Bitmap::UniqueConstPtr(new Bitmap(width, height, format, pData)); + } + Bitmap::UniqueConstPtr Bitmap::createFromFile(const std::string& filename, bool isTopDown) { std::string fullpath; if (findFileInDataDirectories(filename, fullpath) == false) { - logError("Error when loading image file. Can't find image file " + filename); + logWarning("Error when loading image file. Can't find image file '" + filename + "'"); return nullptr; } @@ -201,15 +207,15 @@ namespace Falcor if (fifFormat == FIF_UNKNOWN) { - genError("Image Type unknown", filename); + genWarning("Image type unknown", filename); return nullptr; } } - // Check the the library supports loading this image Type + // Check the library supports loading this image type if (FreeImage_FIFSupportsReading(fifFormat) == false) { - genError("Library doesn't support the file format", filename); + genWarning("Library doesn't support the file format", filename); return nullptr; } @@ -217,50 +223,65 @@ namespace Falcor FIBITMAP* pDib = FreeImage_Load(fifFormat, fullpath.c_str()); if (pDib == nullptr) { - genError("Can't read image file", filename); + genWarning("Can't read image file", filename); return nullptr; } // Create the bitmap - auto pBmp = new Bitmap; - pBmp->mHeight = FreeImage_GetHeight(pDib); - pBmp->mWidth = FreeImage_GetWidth(pDib); + const uint32_t height = FreeImage_GetHeight(pDib); + const uint32_t width = FreeImage_GetWidth(pDib); - if (pBmp->mHeight == 0 || pBmp->mWidth == 0 || FreeImage_GetBits(pDib) == nullptr) + if (height == 0 || width == 0 || FreeImage_GetBits(pDib) == nullptr) { - genError("Invalid image", filename); + genWarning("Invalid image", filename); return nullptr; } + // Convert palettized images to RGBA. + FREE_IMAGE_COLOR_TYPE colorType = FreeImage_GetColorType(pDib); + if (colorType == FIC_PALETTE) + { + auto pNew = FreeImage_ConvertTo32Bits(pDib); + FreeImage_Unload(pDib); + pDib = pNew; + + if (pDib == nullptr) + { + genWarning("Failed to convert palettized image to RGBA format", filename); + return nullptr; + } + } + + ResourceFormat format = ResourceFormat::Unknown; uint32_t bpp = FreeImage_GetBPP(pDib); switch(bpp) { case 128: - pBmp->mFormat = ResourceFormat::RGBA32Float; // 4xfloat32 HDR format + format = ResourceFormat::RGBA32Float; // 4xfloat32 HDR format break; case 96: - pBmp->mFormat = isRGB32fSupported() ? ResourceFormat::RGB32Float : ResourceFormat::RGBA32Float; // 3xfloat32 HDR format + format = isRGB32fSupported() ? ResourceFormat::RGB32Float : ResourceFormat::RGBA32Float; // 3xfloat32 HDR format break; case 64: - pBmp->mFormat = ResourceFormat::RGBA16Float; // 4xfloat16 HDR format + format = ResourceFormat::RGBA16Float; // 4xfloat16 HDR format break; case 48: - pBmp->mFormat = ResourceFormat::RGB16Float; // 3xfloat16 HDR format + format = ResourceFormat::RGB16Float; // 3xfloat16 HDR format break; case 32: - pBmp->mFormat = ResourceFormat::BGRA8Unorm; + format = ResourceFormat::BGRA8Unorm; break; case 24: - pBmp->mFormat = ResourceFormat::BGRX8Unorm; + format = ResourceFormat::BGRX8Unorm; break; case 16: - pBmp->mFormat = ResourceFormat::RG8Unorm; + format = ResourceFormat::RG8Unorm; break; case 8: - pBmp->mFormat = ResourceFormat::R8Unorm; + format = ResourceFormat::R8Unorm; break; default: - genError("Unknown bits-per-pixel", filename); + genWarning("Unknown bits-per-pixel", filename); return nullptr; } @@ -280,19 +301,36 @@ namespace Falcor pDib = pNew; } - uint32_t bytesPerPixel = bpp / 8; + UniqueConstPtr pBmp = UniqueConstPtr(new Bitmap(width, height, format)); + FreeImage_ConvertToRawBits(pBmp->getData(), pDib, pBmp->getRowPitch(), bpp, FI_RGBA_RED_MASK, FI_RGBA_GREEN_MASK, FI_RGBA_BLUE_MASK, isTopDown); + FreeImage_Unload(pDib); + return pBmp; + } - pBmp->mpData = new uint8_t[pBmp->mHeight * pBmp->mWidth * bytesPerPixel]; - FreeImage_ConvertToRawBits(pBmp->mpData, pDib, pBmp->mWidth * bytesPerPixel, bpp, FI_RGBA_RED_MASK, FI_RGBA_GREEN_MASK, FI_RGBA_BLUE_MASK, isTopDown); + Bitmap::Bitmap(uint32_t width, uint32_t height, ResourceFormat format) + : mWidth(width) + , mHeight(height) + , mFormat(format) + , mRowPitch(getFormatRowPitch(format, width)) + { + if (isCompressedFormat(format)) + { + uint32_t blockSizeY = getFormatHeightCompressionRatio(format); + assert(height % blockSizeY == 0); // Should divide evenly + mSize = mRowPitch * (height / blockSizeY); + } + else + { + mSize = height * mRowPitch; + } - FreeImage_Unload(pDib); - return UniqueConstPtr(pBmp); + mpData = std::unique_ptr(new uint8_t[mSize]); } - Bitmap::~Bitmap() + Bitmap::Bitmap(uint32_t width, uint32_t height, ResourceFormat format, const uint8_t* pData) + : Bitmap(width, height, format) { - delete[] mpData; - mpData = nullptr; + std::memcpy(mpData.get(), pData, mSize); } static FREE_IMAGE_FORMAT toFreeImageFormat(Bitmap::FileFormat fmt) @@ -342,7 +380,8 @@ namespace Falcor /* TgaFile */ "tga", /* BmpFile */ "bmp", /* PfmFile */ "pfm", - /* ExrFile */ "exr" + /* ExrFile */ "exr", + /* DdsFile */ "dds" }; for (uint32_t i = 0 ; i < arraysize(kExtensions) ; i++) @@ -380,6 +419,9 @@ namespace Falcor filters.push_back({ "tga", "Truevision Graphics Adapter" }); } + // DDS can store all formats + filters.push_back({ "dds", "DirectDraw Surface" }); + // List of formats we can only load from if (format == ResourceFormat::Unknown) { @@ -421,6 +463,12 @@ namespace Falcor return; } + if (fileFormat == FileFormat::DdsFile) + { + logError("Bitmap::saveImage cannot save DDS files. Use ImageIO instead."); + return; + } + int flags = 0; FIBITMAP* pImage = nullptr; uint32_t bytesPerPixel = getFormatBytesPerBlock(resourceFormat); diff --git a/Source/Falcor/Utils/Image/Bitmap.h b/Source/Falcor/Utils/Image/Bitmap.h index 3d2b97929c..6195015810 100644 --- a/Source/Falcor/Utils/Image/Bitmap.h +++ b/Source/Falcor/Utils/Image/Bitmap.h @@ -52,11 +52,22 @@ namespace Falcor BmpFile, //< BMP file for lossless uncompressed 8-bits images with optional alpha PfmFile, //< PFM file for floating point HDR images with 32-bit float per channel ExrFile, //< EXR file for floating point HDR images with 16-bit float per channel + DdsFile, //< DDS file for storing GPU resource formats, including block compressed formats + //< See ImageIO. TODO: Remove(?) Bitmap IO implementation when ImageIO supports other formats }; using UniquePtr = std::unique_ptr; using UniqueConstPtr = std::unique_ptr; + /** Create from memory. + \param[in] width Width in pixels. + \param[in] height Height in pixels + \param[in] format Resource format. + \param[in] pData Pointer to data. Data will be copied internally during creation and does not need to be managed by the caller. + \return A new bitmap object. + */ + static UniqueConstPtr create(uint32_t width, uint32_t height, ResourceFormat format, const uint8_t* pData); + /** Create a new object from file. \param[in] filename Filename, including a path. If the file can't be found relative to the current directory, Falcor will search for it in the common directories. \param[in] isTopDown Control the memory layout of the image. If true, the top-left pixel is the first pixel in the buffer, otherwise the bottom-left pixel is first. @@ -64,7 +75,7 @@ namespace Falcor */ static UniqueConstPtr createFromFile(const std::string& filename, bool isTopDown); - /** Store a memory buffer to a PNG file. + /** Store a memory buffer to a file. \param[in] filename Output filename. Can include a path - absolute or relative to the executable directory. \param[in] width The width of the image. \param[in] height The height of the image. @@ -78,15 +89,12 @@ namespace Falcor /** Open dialog to save image to a file \param[in] pTexture Texture to save to file - */ static void saveImageDialog(Texture* pTexture); - ~Bitmap(); - /** Get a pointer to the bitmap's data store */ - uint8_t* getData() const { return mpData; } + uint8_t* getData() const { return mpData.get(); } /** Get the width of the bitmap */ @@ -96,10 +104,18 @@ namespace Falcor */ uint32_t getHeight() const { return mHeight; } - /** Get the number of bytes per pixel + /** Get the data format */ ResourceFormat getFormat() const { return mFormat; } + /** Get the row pitch in bytes. For compressed formats this corresponds to one row of blocks, not pixels. + */ + uint32_t getRowPitch() const { return mRowPitch; } + + /** Get the data size in bytes + */ + uint32_t getSize() const { return mSize; } + /** Get the file dialog filter vec for images. \param[in] format If set to ResourceFormat::Unknown, will return all the supported image file formats. If set to something else, will only return file types which support this format. */ @@ -114,12 +130,17 @@ namespace Falcor */ static FileFormat getFormatFromFileExtension(const std::string& ext); - private: + protected: Bitmap() = default; - uint8_t* mpData = nullptr; + Bitmap(uint32_t width, uint32_t height, ResourceFormat format); + Bitmap(uint32_t width, uint32_t height, ResourceFormat format, const uint8_t* pData); + + std::unique_ptr mpData; uint32_t mWidth = 0; uint32_t mHeight = 0; - ResourceFormat mFormat; + uint32_t mRowPitch = 0; + uint32_t mSize = 0; + ResourceFormat mFormat = ResourceFormat::Unknown; }; enum_class_operators(Bitmap::ExportFlags); diff --git a/Source/Falcor/Utils/Image/DDSHeader.h b/Source/Falcor/Utils/Image/DDSHeader.h deleted file mode 100644 index e738dc58ca..0000000000 --- a/Source/Falcor/Utils/Image/DDSHeader.h +++ /dev/null @@ -1,120 +0,0 @@ -/*************************************************************************** - # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. - # - # Redistribution and use in source and binary forms, with or without - # modification, are permitted provided that the following conditions - # are met: - # * Redistributions of source code must retain the above copyright - # notice, this list of conditions and the following disclaimer. - # * Redistributions in binary form must reproduce the above copyright - # notice, this list of conditions and the following disclaimer in the - # documentation and/or other materials provided with the distribution. - # * Neither the name of NVIDIA CORPORATION nor the names of its - # contributors may be used to endorse or promote products derived - # from this software without specific prior written permission. - # - # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY - # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - **************************************************************************/ -#pragma once -#include "Utils/Image/DXHeader.h" - -namespace Falcor -{ - namespace DdsHelper - { - struct DdsHeader - { - struct PixelFormat - { - uint32_t structSize; - uint32_t flags; - uint32_t fourCC; - uint32_t bitcount; - uint32_t rMask; - uint32_t gMask; - uint32_t bMask; - uint32_t aMask; - - // flags - static const uint32_t kAlphaPixelsMask = 0x1; - static const uint32_t kAlphaMask = 0x2; - static const uint32_t kFourCCFlag = 0x4; - static const uint32_t kRgbMask = 0x40; - static const uint32_t kYuvMask = 0x200; - static const uint32_t kLuminanceMask = 0x20000; - static const uint32_t kBumpMask = 0x00080000; - }; - - uint32_t headerSize; - uint32_t flags; - uint32_t height; - uint32_t width; - union - { - uint32_t pitch; - uint32_t linearSize; - }; - - uint32_t depth; - uint32_t mipCount; - uint32_t reserved[11]; - PixelFormat pixelFormat; - uint32_t caps[4]; - uint32_t reserved2; - - // Flags - static const uint32_t kCapsMask = 0x1; - static const uint32_t kHeightMask = 0x2; - static const uint32_t kWidthMask = 0x4; - static const uint32_t kPitchMask = 0x8; - static const uint32_t kPixelFormatMask = 0x1000; - static const uint32_t kMipCountMask = 0x20000; - static const uint32_t kLinearSizeMask = 0x80000; - static const uint32_t kDepthMask = 0x800000; - - // Caps[0] - static const uint32_t kCapsComplexMask = 0x8; - static const uint32_t kCapsMipMapMask = 0x400000; - static const uint32_t kCapsTextureMask = 0x1000; - - // Caps[1] - static const uint32_t kCaps2CubeMapMask = 0x200; - static const uint32_t kCaps2CubeMapPosXMask = 0x400; - static const uint32_t kCaps2CubeMapNegXMask = 0x800; - static const uint32_t kCaps2CubeMapPosYMask = 0x1000; - static const uint32_t kCaps2CubeMapNegYMask = 0x2000; - static const uint32_t kCaps2CubeMapPosZMask = 0x4000; - static const uint32_t kCaps2CubeMapNegZMask = 0x8000; - static const uint32_t kCaps2VolumeMask = 0x200000; - }; - - struct DdsHeaderDX10 - { - DXFormat dxgiFormat; - DXResourceDimension resourceDimension; - uint32_t miscFlag; - uint32_t arraySize; - uint32_t miscFlags2; - - static const uint32_t kCubeMapMask = 0x4; - }; - - struct DdsData - { - DdsHeader header; - DdsHeaderDX10 dx10Header; - bool hasDX10Header; - std::vector data; - }; - } -} diff --git a/Source/Falcor/Utils/Image/DXHeader.cpp b/Source/Falcor/Utils/Image/DXHeader.cpp deleted file mode 100644 index 348288c738..0000000000 --- a/Source/Falcor/Utils/Image/DXHeader.cpp +++ /dev/null @@ -1,165 +0,0 @@ -/*************************************************************************** - # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. - # - # Redistribution and use in source and binary forms, with or without - # modification, are permitted provided that the following conditions - # are met: - # * Redistributions of source code must retain the above copyright - # notice, this list of conditions and the following disclaimer. - # * Redistributions in binary form must reproduce the above copyright - # notice, this list of conditions and the following disclaimer in the - # documentation and/or other materials provided with the distribution. - # * Neither the name of NVIDIA CORPORATION nor the names of its - # contributors may be used to endorse or promote products derived - # from this software without specific prior written permission. - # - # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY - # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - **************************************************************************/ -#include "stdafx.h" -#ifdef _WIN32 -#include "DXHeader.h" -#include - -namespace Falcor -{ -#define assert_enum_value(prefix, val) static_assert(val == prefix##val, #val " enum value differs from D3D!") - - assert_enum_value(D3D12_, RESOURCE_DIMENSION_UNKNOWN); - assert_enum_value(D3D12_, RESOURCE_DIMENSION_BUFFER); - assert_enum_value(D3D12_, RESOURCE_DIMENSION_TEXTURE1D); - assert_enum_value(D3D12_, RESOURCE_DIMENSION_TEXTURE2D); - assert_enum_value(D3D12_, RESOURCE_DIMENSION_TEXTURE3D); - - assert_enum_value(DXGI_, FORMAT_UNKNOWN); - assert_enum_value(DXGI_, FORMAT_R32G32B32A32_TYPELESS); - assert_enum_value(DXGI_, FORMAT_R32G32B32A32_FLOAT); - assert_enum_value(DXGI_, FORMAT_R32G32B32A32_UINT); - assert_enum_value(DXGI_, FORMAT_R32G32B32A32_SINT); - assert_enum_value(DXGI_, FORMAT_R32G32B32_TYPELESS); - assert_enum_value(DXGI_, FORMAT_R32G32B32_FLOAT); - assert_enum_value(DXGI_, FORMAT_R32G32B32_UINT); - assert_enum_value(DXGI_, FORMAT_R32G32B32_SINT); - assert_enum_value(DXGI_, FORMAT_R16G16B16A16_TYPELESS); - assert_enum_value(DXGI_, FORMAT_R16G16B16A16_FLOAT); - assert_enum_value(DXGI_, FORMAT_R16G16B16A16_UNORM); - assert_enum_value(DXGI_, FORMAT_R16G16B16A16_UINT); - assert_enum_value(DXGI_, FORMAT_R16G16B16A16_SNORM); - assert_enum_value(DXGI_, FORMAT_R16G16B16A16_SINT); - assert_enum_value(DXGI_, FORMAT_R32G32_TYPELESS); - assert_enum_value(DXGI_, FORMAT_R32G32_FLOAT); - assert_enum_value(DXGI_, FORMAT_R32G32_UINT); - assert_enum_value(DXGI_, FORMAT_R32G32_SINT); - assert_enum_value(DXGI_, FORMAT_R32G8X24_TYPELESS); - assert_enum_value(DXGI_, FORMAT_D32_FLOAT_S8X24_UINT); - assert_enum_value(DXGI_, FORMAT_R32_FLOAT_X8X24_TYPELESS); - assert_enum_value(DXGI_, FORMAT_X32_TYPELESS_G8X24_UINT); - assert_enum_value(DXGI_, FORMAT_R10G10B10A2_TYPELESS); - assert_enum_value(DXGI_, FORMAT_R10G10B10A2_UNORM); - assert_enum_value(DXGI_, FORMAT_R10G10B10A2_UINT); - assert_enum_value(DXGI_, FORMAT_R11G11B10_FLOAT); - assert_enum_value(DXGI_, FORMAT_R8G8B8A8_TYPELESS); - assert_enum_value(DXGI_, FORMAT_R8G8B8A8_UNORM); - assert_enum_value(DXGI_, FORMAT_R8G8B8A8_UNORM_SRGB); - assert_enum_value(DXGI_, FORMAT_R8G8B8A8_UINT); - assert_enum_value(DXGI_, FORMAT_R8G8B8A8_SNORM); - assert_enum_value(DXGI_, FORMAT_R8G8B8A8_SINT); - assert_enum_value(DXGI_, FORMAT_R16G16_TYPELESS); - assert_enum_value(DXGI_, FORMAT_R16G16_FLOAT); - assert_enum_value(DXGI_, FORMAT_R16G16_UNORM); - assert_enum_value(DXGI_, FORMAT_R16G16_UINT); - assert_enum_value(DXGI_, FORMAT_R16G16_SNORM); - assert_enum_value(DXGI_, FORMAT_R16G16_SINT); - assert_enum_value(DXGI_, FORMAT_R32_TYPELESS); - assert_enum_value(DXGI_, FORMAT_D32_FLOAT); - assert_enum_value(DXGI_, FORMAT_R32_FLOAT); - assert_enum_value(DXGI_, FORMAT_R32_UINT); - assert_enum_value(DXGI_, FORMAT_R32_SINT); - assert_enum_value(DXGI_, FORMAT_R24G8_TYPELESS); - assert_enum_value(DXGI_, FORMAT_D24_UNORM_S8_UINT); - assert_enum_value(DXGI_, FORMAT_R24_UNORM_X8_TYPELESS); - assert_enum_value(DXGI_, FORMAT_X24_TYPELESS_G8_UINT); - assert_enum_value(DXGI_, FORMAT_R8G8_TYPELESS); - assert_enum_value(DXGI_, FORMAT_R8G8_UNORM); - assert_enum_value(DXGI_, FORMAT_R8G8_UINT); - assert_enum_value(DXGI_, FORMAT_R8G8_SNORM); - assert_enum_value(DXGI_, FORMAT_R8G8_SINT); - assert_enum_value(DXGI_, FORMAT_R16_TYPELESS); - assert_enum_value(DXGI_, FORMAT_R16_FLOAT); - assert_enum_value(DXGI_, FORMAT_D16_UNORM); - assert_enum_value(DXGI_, FORMAT_R16_UNORM); - assert_enum_value(DXGI_, FORMAT_R16_UINT); - assert_enum_value(DXGI_, FORMAT_R16_SNORM); - assert_enum_value(DXGI_, FORMAT_R16_SINT); - assert_enum_value(DXGI_, FORMAT_R8_TYPELESS); - assert_enum_value(DXGI_, FORMAT_R8_UNORM); - assert_enum_value(DXGI_, FORMAT_R8_UINT); - assert_enum_value(DXGI_, FORMAT_R8_SNORM); - assert_enum_value(DXGI_, FORMAT_R8_SINT); - assert_enum_value(DXGI_, FORMAT_A8_UNORM); - assert_enum_value(DXGI_, FORMAT_R1_UNORM); - assert_enum_value(DXGI_, FORMAT_R9G9B9E5_SHAREDEXP); - assert_enum_value(DXGI_, FORMAT_R8G8_B8G8_UNORM); - assert_enum_value(DXGI_, FORMAT_G8R8_G8B8_UNORM); - assert_enum_value(DXGI_, FORMAT_BC1_TYPELESS); - assert_enum_value(DXGI_, FORMAT_BC1_UNORM); - assert_enum_value(DXGI_, FORMAT_BC1_UNORM_SRGB); - assert_enum_value(DXGI_, FORMAT_BC2_TYPELESS); - assert_enum_value(DXGI_, FORMAT_BC2_UNORM); - assert_enum_value(DXGI_, FORMAT_BC2_UNORM_SRGB); - assert_enum_value(DXGI_, FORMAT_BC3_TYPELESS); - assert_enum_value(DXGI_, FORMAT_BC3_UNORM); - assert_enum_value(DXGI_, FORMAT_BC3_UNORM_SRGB); - assert_enum_value(DXGI_, FORMAT_BC4_TYPELESS); - assert_enum_value(DXGI_, FORMAT_BC4_UNORM); - assert_enum_value(DXGI_, FORMAT_BC4_SNORM); - assert_enum_value(DXGI_, FORMAT_BC5_TYPELESS); - assert_enum_value(DXGI_, FORMAT_BC5_UNORM); - assert_enum_value(DXGI_, FORMAT_BC5_SNORM); - assert_enum_value(DXGI_, FORMAT_B5G6R5_UNORM); - assert_enum_value(DXGI_, FORMAT_B5G5R5A1_UNORM); - assert_enum_value(DXGI_, FORMAT_B8G8R8A8_UNORM); - assert_enum_value(DXGI_, FORMAT_B8G8R8X8_UNORM); - assert_enum_value(DXGI_, FORMAT_R10G10B10_XR_BIAS_A2_UNORM); - assert_enum_value(DXGI_, FORMAT_B8G8R8A8_TYPELESS); - assert_enum_value(DXGI_, FORMAT_B8G8R8A8_UNORM_SRGB); - assert_enum_value(DXGI_, FORMAT_B8G8R8X8_TYPELESS); - assert_enum_value(DXGI_, FORMAT_B8G8R8X8_UNORM_SRGB); - assert_enum_value(DXGI_, FORMAT_BC6H_TYPELESS); - assert_enum_value(DXGI_, FORMAT_BC6H_UF16); - assert_enum_value(DXGI_, FORMAT_BC6H_SF16); - assert_enum_value(DXGI_, FORMAT_BC7_TYPELESS); - assert_enum_value(DXGI_, FORMAT_BC7_UNORM); - assert_enum_value(DXGI_, FORMAT_BC7_UNORM_SRGB); - assert_enum_value(DXGI_, FORMAT_AYUV); - assert_enum_value(DXGI_, FORMAT_Y410); - assert_enum_value(DXGI_, FORMAT_Y416); - assert_enum_value(DXGI_, FORMAT_NV12); - assert_enum_value(DXGI_, FORMAT_P010); - assert_enum_value(DXGI_, FORMAT_P016); - assert_enum_value(DXGI_, FORMAT_420_OPAQUE); - assert_enum_value(DXGI_, FORMAT_YUY2); - assert_enum_value(DXGI_, FORMAT_Y210); - assert_enum_value(DXGI_, FORMAT_Y216); - assert_enum_value(DXGI_, FORMAT_NV11); - assert_enum_value(DXGI_, FORMAT_AI44); - assert_enum_value(DXGI_, FORMAT_IA44); - assert_enum_value(DXGI_, FORMAT_P8); - assert_enum_value(DXGI_, FORMAT_A8P8); - assert_enum_value(DXGI_, FORMAT_B4G4R4A4_UNORM); - assert_enum_value(DXGI_, FORMAT_P208); - assert_enum_value(DXGI_, FORMAT_V208); - assert_enum_value(DXGI_, FORMAT_V408); - assert_enum_value(DXGI_, FORMAT_FORCE_UINT); -} - -#endif // _WIN32 diff --git a/Source/Falcor/Utils/Image/DXHeader.h b/Source/Falcor/Utils/Image/DXHeader.h deleted file mode 100644 index 043671b0d4..0000000000 --- a/Source/Falcor/Utils/Image/DXHeader.h +++ /dev/null @@ -1,167 +0,0 @@ -/*************************************************************************** - # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. - # - # Redistribution and use in source and binary forms, with or without - # modification, are permitted provided that the following conditions - # are met: - # * Redistributions of source code must retain the above copyright - # notice, this list of conditions and the following disclaimer. - # * Redistributions in binary form must reproduce the above copyright - # notice, this list of conditions and the following disclaimer in the - # documentation and/or other materials provided with the distribution. - # * Neither the name of NVIDIA CORPORATION nor the names of its - # contributors may be used to endorse or promote products derived - # from this software without specific prior written permission. - # - # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY - # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - **************************************************************************/ -#pragma once - -namespace Falcor -{ - // DX enums for portable DDS support - - enum DXResourceDimension - { - RESOURCE_DIMENSION_UNKNOWN = 0, - RESOURCE_DIMENSION_BUFFER = 1, - RESOURCE_DIMENSION_TEXTURE1D = 2, - RESOURCE_DIMENSION_TEXTURE2D = 3, - RESOURCE_DIMENSION_TEXTURE3D = 4 - }; - - enum DXFormat - { - FORMAT_UNKNOWN = 0, - FORMAT_R32G32B32A32_TYPELESS = 1, - FORMAT_R32G32B32A32_FLOAT = 2, - FORMAT_R32G32B32A32_UINT = 3, - FORMAT_R32G32B32A32_SINT = 4, - FORMAT_R32G32B32_TYPELESS = 5, - FORMAT_R32G32B32_FLOAT = 6, - FORMAT_R32G32B32_UINT = 7, - FORMAT_R32G32B32_SINT = 8, - FORMAT_R16G16B16A16_TYPELESS = 9, - FORMAT_R16G16B16A16_FLOAT = 10, - FORMAT_R16G16B16A16_UNORM = 11, - FORMAT_R16G16B16A16_UINT = 12, - FORMAT_R16G16B16A16_SNORM = 13, - FORMAT_R16G16B16A16_SINT = 14, - FORMAT_R32G32_TYPELESS = 15, - FORMAT_R32G32_FLOAT = 16, - FORMAT_R32G32_UINT = 17, - FORMAT_R32G32_SINT = 18, - FORMAT_R32G8X24_TYPELESS = 19, - FORMAT_D32_FLOAT_S8X24_UINT = 20, - FORMAT_R32_FLOAT_X8X24_TYPELESS = 21, - FORMAT_X32_TYPELESS_G8X24_UINT = 22, - FORMAT_R10G10B10A2_TYPELESS = 23, - FORMAT_R10G10B10A2_UNORM = 24, - FORMAT_R10G10B10A2_UINT = 25, - FORMAT_R11G11B10_FLOAT = 26, - FORMAT_R8G8B8A8_TYPELESS = 27, - FORMAT_R8G8B8A8_UNORM = 28, - FORMAT_R8G8B8A8_UNORM_SRGB = 29, - FORMAT_R8G8B8A8_UINT = 30, - FORMAT_R8G8B8A8_SNORM = 31, - FORMAT_R8G8B8A8_SINT = 32, - FORMAT_R16G16_TYPELESS = 33, - FORMAT_R16G16_FLOAT = 34, - FORMAT_R16G16_UNORM = 35, - FORMAT_R16G16_UINT = 36, - FORMAT_R16G16_SNORM = 37, - FORMAT_R16G16_SINT = 38, - FORMAT_R32_TYPELESS = 39, - FORMAT_D32_FLOAT = 40, - FORMAT_R32_FLOAT = 41, - FORMAT_R32_UINT = 42, - FORMAT_R32_SINT = 43, - FORMAT_R24G8_TYPELESS = 44, - FORMAT_D24_UNORM_S8_UINT = 45, - FORMAT_R24_UNORM_X8_TYPELESS = 46, - FORMAT_X24_TYPELESS_G8_UINT = 47, - FORMAT_R8G8_TYPELESS = 48, - FORMAT_R8G8_UNORM = 49, - FORMAT_R8G8_UINT = 50, - FORMAT_R8G8_SNORM = 51, - FORMAT_R8G8_SINT = 52, - FORMAT_R16_TYPELESS = 53, - FORMAT_R16_FLOAT = 54, - FORMAT_D16_UNORM = 55, - FORMAT_R16_UNORM = 56, - FORMAT_R16_UINT = 57, - FORMAT_R16_SNORM = 58, - FORMAT_R16_SINT = 59, - FORMAT_R8_TYPELESS = 60, - FORMAT_R8_UNORM = 61, - FORMAT_R8_UINT = 62, - FORMAT_R8_SNORM = 63, - FORMAT_R8_SINT = 64, - FORMAT_A8_UNORM = 65, - FORMAT_R1_UNORM = 66, - FORMAT_R9G9B9E5_SHAREDEXP = 67, - FORMAT_R8G8_B8G8_UNORM = 68, - FORMAT_G8R8_G8B8_UNORM = 69, - FORMAT_BC1_TYPELESS = 70, - FORMAT_BC1_UNORM = 71, - FORMAT_BC1_UNORM_SRGB = 72, - FORMAT_BC2_TYPELESS = 73, - FORMAT_BC2_UNORM = 74, - FORMAT_BC2_UNORM_SRGB = 75, - FORMAT_BC3_TYPELESS = 76, - FORMAT_BC3_UNORM = 77, - FORMAT_BC3_UNORM_SRGB = 78, - FORMAT_BC4_TYPELESS = 79, - FORMAT_BC4_UNORM = 80, - FORMAT_BC4_SNORM = 81, - FORMAT_BC5_TYPELESS = 82, - FORMAT_BC5_UNORM = 83, - FORMAT_BC5_SNORM = 84, - FORMAT_B5G6R5_UNORM = 85, - FORMAT_B5G5R5A1_UNORM = 86, - FORMAT_B8G8R8A8_UNORM = 87, - FORMAT_B8G8R8X8_UNORM = 88, - FORMAT_R10G10B10_XR_BIAS_A2_UNORM = 89, - FORMAT_B8G8R8A8_TYPELESS = 90, - FORMAT_B8G8R8A8_UNORM_SRGB = 91, - FORMAT_B8G8R8X8_TYPELESS = 92, - FORMAT_B8G8R8X8_UNORM_SRGB = 93, - FORMAT_BC6H_TYPELESS = 94, - FORMAT_BC6H_UF16 = 95, - FORMAT_BC6H_SF16 = 96, - FORMAT_BC7_TYPELESS = 97, - FORMAT_BC7_UNORM = 98, - FORMAT_BC7_UNORM_SRGB = 99, - FORMAT_AYUV = 100, - FORMAT_Y410 = 101, - FORMAT_Y416 = 102, - FORMAT_NV12 = 103, - FORMAT_P010 = 104, - FORMAT_P016 = 105, - FORMAT_420_OPAQUE = 106, - FORMAT_YUY2 = 107, - FORMAT_Y210 = 108, - FORMAT_Y216 = 109, - FORMAT_NV11 = 110, - FORMAT_AI44 = 111, - FORMAT_IA44 = 112, - FORMAT_P8 = 113, - FORMAT_A8P8 = 114, - FORMAT_B4G4R4A4_UNORM = 115, - FORMAT_P208 = 130, - FORMAT_V208 = 131, - FORMAT_V408 = 132, - FORMAT_FORCE_UINT = 0xffffffff - }; - -} \ No newline at end of file diff --git a/Source/Falcor/Utils/Image/ImageIO.cpp b/Source/Falcor/Utils/Image/ImageIO.cpp new file mode 100644 index 0000000000..eb2d51b343 --- /dev/null +++ b/Source/Falcor/Utils/Image/ImageIO.cpp @@ -0,0 +1,349 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#include "stdafx.h" +#include "ImageIO.h" +#include "DirectXTex.h" +#include + +namespace Falcor +{ + namespace + { + /** Wrapper around DirectXTex image containers because there are separate API calls for each type. + */ + struct ApiImage + { + /** If true, Image is holding the data. Otherwise data is in ScratchImage. + */ + bool isSingleImage() const + { + return image.pixels != nullptr; + } + + /** Get the DXGI Format of the image. + */ + DXGI_FORMAT getFormat() const + { + return isSingleImage() ? image.format : scratchImage.GetMetadata().format; + } + + // Single Raw Bitmap + DirectX::Image image = {}; + + // One or more images with metadata and managed memory. + DirectX::ScratchImage scratchImage; + }; + + struct ImportData + { + // The full path of the file that was found in data directories + std::string fullpath; + + // Commonly used values converted or casted for cleaner access + ResourceFormat format; + uint32_t width; + uint32_t height; + uint32_t depth; + uint32_t arraySize; + uint32_t mipLevels; + + // Original API data + ApiImage image; + }; + + ImportData loadDDS(const std::string& filename, bool loadAsSrgb) + { + assert(hasSuffix(filename, ".dds", false)); + + ImportData data; + if (findFileInDataDirectories(filename, data.fullpath) == false) + { + throw std::exception(("Can't find file: " + filename).c_str()); + } + + DirectX::DDS_FLAGS flags = DirectX::DDS_FLAGS_NONE; + if (FAILED(DirectX::LoadFromDDSFile(string_2_wstring(data.fullpath).c_str(), flags, nullptr, data.image.scratchImage))) + { + throw std::exception(("Failed to load file: " + filename).c_str()); + } + + const auto& meta = data.image.scratchImage.GetMetadata(); + ResourceFormat format = getResourceFormat(meta.format); + data.format = loadAsSrgb ? linearToSrgbFormat(format) : format; + data.width = (uint32_t)meta.width; + data.height = (uint32_t)meta.height; + data.depth = (uint32_t)meta.depth; + data.arraySize = (uint32_t)meta.arraySize; + data.mipLevels = (uint32_t)meta.mipLevels; + + return data; + } + + void validateSavePath(const std::string& filename) + { + if (std::filesystem::path(filename).is_absolute() == false) + { + throw std::exception((filename + " is not an absolute path.").c_str()); + } + + if (getExtensionFromFile(filename) != "dds") + { + throw std::exception((filename + " does not end in dds").c_str()); + } + } + + DXGI_FORMAT asCompressedFormat(DXGI_FORMAT format, ImageIO::CompressionMode mode) + { + // 7 block compression formats, and 3 variants for each. + // Most of them are UNORM, SRGB, and TYPELESS, but BC4 and BC5 are different + static const DXGI_FORMAT kCompressedFormats[7][3] = + { + // Unorm sRGB Typeless + {DXGI_FORMAT_BC1_UNORM, DXGI_FORMAT_BC1_UNORM_SRGB, DXGI_FORMAT_BC1_TYPELESS}, + {DXGI_FORMAT_BC2_UNORM, DXGI_FORMAT_BC2_UNORM_SRGB, DXGI_FORMAT_BC2_TYPELESS}, + {DXGI_FORMAT_BC3_UNORM, DXGI_FORMAT_BC3_UNORM_SRGB, DXGI_FORMAT_BC3_TYPELESS}, + + // Unsigned Signed Typeless + {DXGI_FORMAT_BC4_UNORM, DXGI_FORMAT_BC4_SNORM, DXGI_FORMAT_BC4_TYPELESS}, + {DXGI_FORMAT_BC5_UNORM, DXGI_FORMAT_BC5_SNORM, DXGI_FORMAT_BC5_TYPELESS}, + {DXGI_FORMAT_BC6H_UF16, DXGI_FORMAT_BC6H_SF16, DXGI_FORMAT_BC6H_TYPELESS}, + + // Unorm sRGB Typeless + {DXGI_FORMAT_BC7_UNORM, DXGI_FORMAT_BC7_UNORM_SRGB, DXGI_FORMAT_BC7_TYPELESS} + }; + + bool isSRGB = DirectX::IsSRGB(format); // Applicable for 8-bit per channel RGB color modes + bool isTypeless = DirectX::IsTypeless(format); + + switch (mode) + { + case ImageIO::CompressionMode::BC1: + case ImageIO::CompressionMode::BC2: + case ImageIO::CompressionMode::BC3: + case ImageIO::CompressionMode::BC7: + return kCompressedFormats[uint32_t(mode)][isSRGB ? 1 : (isTypeless ? 2 : 0)]; + case ImageIO::CompressionMode::BC4: + case ImageIO::CompressionMode::BC5: + // Always Unorm for single/two-channel (BC4 grayscale, or BC5 normals) + return kCompressedFormats[uint32_t(mode)][(isTypeless ? 2 : 0)]; + case ImageIO::CompressionMode::BC6: + // Always use Snorm for float formats + // TODO: Check if format is signed + return kCompressedFormats[uint32_t(mode)][isTypeless ? 2 : 1]; + default: + return format; + } + } + + void compress(ApiImage& image, ImageIO::CompressionMode mode) + { + if (isCompressedFormat(getResourceFormat(image.getFormat()))) + { + throw std::exception("Image is already compressed."); + } + + const DirectX::TEX_COMPRESS_FLAGS flags = mode == ImageIO::CompressionMode::BC7 ? DirectX::TEX_COMPRESS_BC7_QUICK : DirectX::TEX_COMPRESS_DEFAULT; + + HRESULT result = S_OK; + if (image.isSingleImage()) + { + result = DirectX::Compress(image.image, asCompressedFormat(image.getFormat(), mode), flags, DirectX::TEX_THRESHOLD_DEFAULT, image.scratchImage); + + // Clear bitmap since compression outputted to the scratchImage + image.image = {}; + } + else + { + // Compression will output "in place" back to ApiImage, so move the input out here + DirectX::ScratchImage inputImage(std::move(image.scratchImage)); + const auto& meta = inputImage.GetMetadata(); + + result = DirectX::Compress(inputImage.GetImages(), inputImage.GetImageCount(), meta, asCompressedFormat(meta.format, mode), flags, DirectX::TEX_THRESHOLD_DEFAULT, image.scratchImage); + } + + if (FAILED(result)) + { + throw std::exception("Failed to compress."); + } + } + + /** Saves image data to a DDS file. Optionally compresses image. + */ + void exportDDS(const std::string& filename, ApiImage& image, ImageIO::CompressionMode mode) + { + validateSavePath(filename); + + // Compress + try + { + if (mode != ImageIO::CompressionMode::None) + { + compress(image, mode); + } + } + catch (const std::exception& e) + { + // Forward exception along with filename for context + throw std::exception((filename + ": " + e.what()).c_str()); + } + + // Save + const DirectX::DDS_FLAGS saveFlags = DirectX::DDS_FLAGS_NONE; + HRESULT result = S_OK; + if (image.isSingleImage()) + { + result = DirectX::SaveToDDSFile(image.image, saveFlags, string_2_wstring(filename).c_str()); + } + else + { + const auto& scratchImage = image.scratchImage; + result = DirectX::SaveToDDSFile(scratchImage.GetImages(), scratchImage.GetImageCount(), scratchImage.GetMetadata(), saveFlags, string_2_wstring(filename).c_str()); + } + + if (FAILED(result)) + { + throw std::exception(("Failed to export " + filename).c_str()); + } + } + } + + Bitmap::UniqueConstPtr ImageIO::loadBitmapFromDDS(const std::string& filename) + { + ImportData data = loadDDS(filename, false); + + const auto& scratchImage = data.image.scratchImage; + const auto& meta = scratchImage.GetMetadata(); + if (meta.IsCubemap() || meta.IsVolumemap()) + { + throw std::exception(("Cannot load " + filename + " as a Bitmap. Invalid resource dimension.").c_str()); + } + + // Create from first image + auto pImage = scratchImage.GetImage(0, 0, 0); + return Bitmap::create((uint32_t)data.width, (uint32_t)data.height, data.format, pImage->pixels); + } + + Texture::SharedPtr ImageIO::loadTextureFromDDS(const std::string& filename, bool loadAsSrgb) + { + ImportData data = loadDDS(filename, loadAsSrgb); + + const auto& scratchImage = data.image.scratchImage; + const auto& meta = scratchImage.GetMetadata(); + + Texture::SharedPtr pTex; + switch (meta.dimension) + { + case DirectX::TEX_DIMENSION_TEXTURE1D: + pTex = Texture::create1D(data.width, data.format, data.arraySize, data.mipLevels, scratchImage.GetPixels()); + break; + case DirectX::TEX_DIMENSION_TEXTURE2D: + if (meta.IsCubemap()) + { + pTex = Texture::createCube(data.width, data.height, data.format, data.arraySize / 6, data.mipLevels, scratchImage.GetPixels()); + } + else + { + pTex = Texture::create2D(data.width, data.height, data.format, data.arraySize, data.mipLevels, scratchImage.GetPixels()); + } + break; + case DirectX::TEX_DIMENSION_TEXTURE3D: + pTex = Texture::create3D(data.width, data.height, data.depth, data.format, data.mipLevels, scratchImage.GetPixels()); + break; + } + + if (pTex != nullptr) + { + pTex->setSourceFilename(data.fullpath); + } + + return pTex; + } + + void ImageIO::saveToDDS(const std::string& filename, const Bitmap& bitmap, CompressionMode mode) + { + ApiImage image; + image.image.width = bitmap.getWidth(); + image.image.height = bitmap.getHeight(); + image.image.format = getDxgiFormat(bitmap.getFormat()); + image.image.rowPitch = bitmap.getRowPitch(); + image.image.slicePitch = bitmap.getSize(); + image.image.pixels = bitmap.getData(); + + exportDDS(filename, image, mode); + } + + void ImageIO::saveToDDS(CopyContext* pContext, const std::string& filename, const Texture::SharedPtr& pTexture, CompressionMode mode) + { + DirectX::TexMetadata meta = {}; + meta.width = pTexture->getWidth(); + meta.height = pTexture->getHeight(); + meta.depth = pTexture->getDepth(); + meta.arraySize = pTexture->getArraySize(); + meta.mipLevels = pTexture->getMipCount(); + meta.format = getDxgiFormat(pTexture->getFormat()); + + switch (pTexture->getType()) + { + case Resource::Type::Texture1D: + meta.dimension = DirectX::TEX_DIMENSION_TEXTURE1D; + break; + case Resource::Type::TextureCube: + meta.miscFlags |= DirectX::TEX_MISC_TEXTURECUBE; + // No break, cubes are also Texture2Ds + case Resource::Type::Texture2D: + meta.dimension = DirectX::TEX_DIMENSION_TEXTURE2D; + break; + case Resource::Type::Texture3D: + meta.dimension = DirectX::TEX_DIMENSION_TEXTURE3D; + throw std::exception("saveToDDS: Saving 3D textures currently not supported."); + default: + throw std::exception("saveToDDS: Invalid resource dimension."); + } + + ApiImage image; + auto& scratchImage = image.scratchImage; + HRESULT result = scratchImage.Initialize(meta); + assert(SUCCEEDED(result)); + + for (uint32_t i = 0; i < pTexture->getArraySize(); i++) + { + for (uint32_t m = 0; m < pTexture->getMipCount(); m++) + { + uint32_t subresource = pTexture->getSubresourceIndex(i, m); + const DirectX::Image* pImage = scratchImage.GetImage(m, i, 0); + + std::vector subresourceData = pContext->readTextureSubresource(pTexture.get(), subresource); + assert(subresourceData.size() == pImage->slicePitch); + std::memcpy(pImage->pixels, subresourceData.data(), subresourceData.size()); + } + } + + exportDDS(filename, image, mode); + } + +} diff --git a/Source/Falcor/Utils/Image/ImageIO.h b/Source/Falcor/Utils/Image/ImageIO.h new file mode 100644 index 0000000000..6910882460 --- /dev/null +++ b/Source/Falcor/Utils/Image/ImageIO.h @@ -0,0 +1,116 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#pragma once +#include "Utils/Image/Bitmap.h" +#include "Core/API/Texture.h" + +namespace Falcor +{ + class dlldecl ImageIO + { + public: + enum class CompressionMode + { + /** Stores RGB data with 1 bit of alpha. + 8 bytes per block. + */ + BC1, + + /** Stores RGBA data. Combines BC1 for RGB with 4 bits of alpha. + 16 bytes per block. + */ + BC2, + + /** Stores RGBA data. Combines BC1 for RGB and BC4 for alpha. + 16 bytes per block. + */ + BC3, + + /** Stores a single grayscale channel. + 8 bytes per block. + */ + BC4, + + /** Stores two channels using BC4 for each channel. + 16 bytes per block. + */ + BC5, + + /** Stores RGB 16-bit floating point data. + 16 bytes per block. + */ + BC6, + + /** Stores 8-bit RGB or RGBA data. + 16 bytes per block. + */ + BC7, + + /** No compression mode specified. + */ + None + }; + + /** Load a DDS file to a Bitmap. If the file contains an image array and/or mips, only the first image will be loaded. + Throws an exception if file cannot be found or there is a loading error. + \param[in] filename Path of file to load. + \return Bitmap object containing image data. + */ + static Bitmap::UniqueConstPtr loadBitmapFromDDS(const std::string& filename); // top down = true + + /** Load a DDS file to a Texture. + Throws an exception if file cannot be found or there is a loading error. + \param[in] filename Path of file to load. + \param[in] loadAsSrgb If true, convert the image format property to a corresponding sRGB format if available. Image data is not changed. + \return Texture object containing image data. + */ + static Texture::SharedPtr loadTextureFromDDS(const std::string& filename, bool loadAsSrgb); + + /** Saves a bitmap to a DDS file. + Throws an exception of filename is invalid or image cannot be saved. + \param[in] filename Filename to save to. + \param[in] bitmap Bitmap object to save. + \param[in] mode Block compression mode. By default, will save data as-is and will not decompress if already compressed. + */ + static void saveToDDS(const std::string& filename, const Bitmap& bitmap, CompressionMode mode = CompressionMode::None); + static void saveToDDS(const std::string& filename, const Bitmap::UniqueConstPtr& pBitmap, CompressionMode mode = CompressionMode::None); + + /** Saves a Texture to a DDS file. All mips and array images are saved. + Throws an exception of filename is invalid or image cannot be saved. + + TODO: Support exporting single subresource. Options for one or all are probably enough? + + \param[in] pContext Copy context used to read texture data from the GPU. + \param[in] filename Filename to save to. + \param[in] pBitmap Bitmap object to save. + \param[in] mode Block compression mode. By default, will save data as-is and will not decompress if already compressed. + */ + static void saveToDDS(CopyContext* pContext, const std::string& filename, const Texture::SharedPtr& pTexture, CompressionMode mode = CompressionMode::None); + }; + +} diff --git a/Source/Falcor/Utils/Logger.cpp b/Source/Falcor/Utils/Logger.cpp index d0a4d8f48c..8e938e387c 100644 --- a/Source/Falcor/Utils/Logger.cpp +++ b/Source/Falcor/Utils/Logger.cpp @@ -108,29 +108,32 @@ namespace Falcor #endif } - const char* getLogLevelString(Logger::Level L) + const char* getLogLevelString(Logger::Level level) { - const char* c = nullptr; -#define create_level_case(_l) case _l: c = "(" #_l ")" ;break; - switch(L) + switch (level) { - create_level_case(Logger::Level::Info); - create_level_case(Logger::Level::Warning); - create_level_case(Logger::Level::Error); - create_level_case(Logger::Level::Fatal); + case Logger::Level::Fatal: + return "(Fatal)"; + case Logger::Level::Error: + return "(Error)"; + case Logger::Level::Warning: + return "(Warning)"; + case Logger::Level::Info: + return "(Info)"; + case Logger::Level::Debug: + return "(Debug)"; default: should_not_get_here(); + return nullptr; } -#undef create_level_case - return c; } - void Logger::log(Level L, const std::string& msg, MsgBox mbox, bool terminateOnError) + void Logger::log(Level level, const std::string& msg, MsgBox mbox, bool terminateOnError) { #if _LOG_ENABLED - if (L >= sVerbosity) + if (level <= sVerbosity) { - std::string s = getLogLevelString(L) + std::string("\t") + msg + "\n"; + std::string s = getLogLevelString(level) + std::string(" ") + msg + "\n"; // Write to log file. printToLogFile(s); @@ -139,7 +142,7 @@ namespace Falcor if (isDebuggerPresent()) printToDebugWindow(s); // Write errors to stderr unconditionally, other messages to stdout if enabled. - if (L < Logger::Level::Error) + if (level > Logger::Level::Error) { if (sLogToConsole) std::cout << s; } @@ -154,7 +157,7 @@ namespace Falcor { if (mbox == MsgBox::Auto) { - mbox = (L >= Level::Error) ? MsgBox::ContinueAbort : MsgBox::None; + mbox = (level <= Level::Error) ? MsgBox::ContinueAbort : MsgBox::None; } if (mbox != MsgBox::None) @@ -167,14 +170,14 @@ namespace Falcor // Setup message box buttons std::vector buttons; - if (L != Level::Fatal) buttons.push_back({ContinueOrRetry, mbox == MsgBox::ContinueAbort ? "Continue" : "Retry"}); + if (level != Level::Fatal) buttons.push_back({ContinueOrRetry, mbox == MsgBox::ContinueAbort ? "Continue" : "Retry"}); if (isDebuggerPresent()) buttons.push_back({Debug, "Debug"}); buttons.push_back({Abort, "Abort"}); // Setup icon MsgBoxIcon icon = MsgBoxIcon::Info; - if (L == Level::Warning) icon = MsgBoxIcon::Warning; - else if (L >= Level::Error) icon = MsgBoxIcon::Error; + if (level == Level::Warning) icon = MsgBoxIcon::Warning; + else if (level <= Level::Error) icon = MsgBoxIcon::Error; // Show message box auto result = msgBox(msg, buttons, icon); @@ -184,10 +187,10 @@ namespace Falcor } // Terminate on errors if not displaying message box and terminateOnError is enabled - if (L == Level::Error && !sShowBoxOnError && terminateOnError) exit(1); + if (level == Level::Error && !sShowBoxOnError && terminateOnError) exit(1); // Always terminate on fatal errors - if (L == Level::Fatal) exit(1); + if (level == Level::Fatal) exit(1); } bool Logger::setLogFilePath(const std::string& path) diff --git a/Source/Falcor/Utils/Logger.h b/Source/Falcor/Utils/Logger.h index 2c0fe915ff..96dffde099 100644 --- a/Source/Falcor/Utils/Logger.h +++ b/Source/Falcor/Utils/Logger.h @@ -40,11 +40,14 @@ namespace Falcor */ enum class Level { - Info = 0, ///< Informative messages. - Warning = 1, ///< Warning messages. - Error = 2, ///< Error messages. Application might be able to continue running, but incorrectly. - Fatal = 3, ///< Unrecoverable error messages. Terminates application immediately. - Disabled = -1 + Disabled, ///< Disable log messages. + Fatal, ///< Unrecoverable error messages. Terminates application immediately. + Error, ///< Error messages. Application might be able to continue running, but incorrectly. + Warning, ///< Warning messages. + Info, ///< Informative messages. + Debug, ///< Debugging messages. + + Count, ///< Keep this last. }; /** Message box behavior @@ -103,15 +106,17 @@ namespace Falcor static void setVerbosity(Level level); private: + friend void logDebug(const std::string& msg, MsgBox mbox); friend void logInfo(const std::string& msg, MsgBox mbox); friend void logWarning(const std::string& msg, MsgBox mbox); friend void logError(const std::string& msg, MsgBox mbox, bool terminate); friend void logFatal(const std::string& msg, MsgBox mbox); - static void log(Level L, const std::string& msg, MsgBox mbox = Logger::MsgBox::Auto, bool terminateOnError = true); + static void log(Level level, const std::string& msg, MsgBox mbox = Logger::MsgBox::Auto, bool terminateOnError = true); Logger() = delete; }; + inline void logDebug(const std::string& msg, Logger::MsgBox mbox = Logger::MsgBox::Auto) { Logger::log(Logger::Level::Debug, msg, mbox); } inline void logInfo(const std::string& msg, Logger::MsgBox mbox = Logger::MsgBox::Auto) { Logger::log(Logger::Level::Info, msg, mbox); } inline void logWarning(const std::string& msg, Logger::MsgBox mbox = Logger::MsgBox::Auto) { Logger::log(Logger::Level::Warning, msg, mbox); } inline void logError(const std::string& msg, Logger::MsgBox mbox = Logger::MsgBox::Auto, bool terminate = true) { Logger::log(Logger::Level::Error, msg, mbox, terminate); } diff --git a/Source/Falcor/Utils/Math/AABB.cpp b/Source/Falcor/Utils/Math/AABB.cpp new file mode 100644 index 0000000000..67f49462bc --- /dev/null +++ b/Source/Falcor/Utils/Math/AABB.cpp @@ -0,0 +1,82 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#include "stdafx.h" +#include "AABB.h" + +#include + +namespace Falcor +{ + SCRIPT_BINDING(AABB) + { + pybind11::class_ aabb(m, "AABB"); + + aabb.def(pybind11::init<>()); + aabb.def(pybind11::init(), "p"_a); + aabb.def(pybind11::init(), "pmin"_a, "pmax"_a); + + aabb.def("__repr__", [] (const AABB& aabb) { + return + "AABB(minPoint=" + + std::string(pybind11::repr(pybind11::cast(aabb.minPoint))) + + ", maxPoint=" + + std::string(pybind11::repr(pybind11::cast(aabb.maxPoint))) + + ")"; + }); + aabb.def("__str__", [] (const AABB& aabb) { + return + "[" + + std::string(pybind11::str(pybind11::cast(aabb.minPoint))) + + ", " + + std::string(pybind11::str(pybind11::cast(aabb.maxPoint))) + + "]"; + }); + + aabb.def_readwrite("minPoint", &AABB::minPoint); + aabb.def_readwrite("maxPoint", &AABB::maxPoint); + + aabb.def_property_readonly("valid", &AABB::valid); + aabb.def_property_readonly("center", &AABB::center); + aabb.def_property_readonly("extent", &AABB::extent); + aabb.def_property_readonly("area", &AABB::area); + aabb.def_property_readonly("volume", &AABB::volume); + aabb.def_property_readonly("radius", &AABB::radius); + + aabb.def("invalidate", &AABB::invalidate); + aabb.def("include", pybind11::overload_cast(&AABB::include), "p"_a); + aabb.def("include", pybind11::overload_cast(&AABB::include), "b"_a); + aabb.def("intersection", &AABB::intersection); + + aabb.def(pybind11::self == pybind11::self); + aabb.def(pybind11::self != pybind11::self); + aabb.def(pybind11::self | pybind11::self); + aabb.def(pybind11::self |= pybind11::self); + aabb.def(pybind11::self & pybind11::self); + aabb.def(pybind11::self &= pybind11::self); + } +} diff --git a/Source/Falcor/Utils/Math/AABB.h b/Source/Falcor/Utils/Math/AABB.h index 53ccd98ae6..1d80f2c1ae 100644 --- a/Source/Falcor/Utils/Math/AABB.h +++ b/Source/Falcor/Utils/Math/AABB.h @@ -26,100 +26,198 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **************************************************************************/ #pragma once -#include "Vector.h" +#include "Utils/Math/Vector.h" +#include namespace Falcor { - /** An Axis-Aligned Bounding Box + /** Axis-aligned bounding box (AABB) stored by its min/max points. + + The user is responsible for checking the validity of returned AABBs. + There is an equivalent GPU-side implementation in the AABB.slang module. */ - struct BoundingBox + struct AABB { - float3 center; ///< Center position of the bounding box - float3 extent; ///< Half length of each side. Essentially the coordinates to the max corner relative to the center. + float3 minPoint = float3(std::numeric_limits::infinity()); ///< Minimum point. + float3 maxPoint = float3(-std::numeric_limits::infinity()); ///< Maximum point. If any minPoint > maxPoint the box is invalid. + + /** Construct bounding box initialized to +/-inf. + */ + AABB() = default; + + /** Construct bounding box initialized to single point. + */ + AABB(const float3& p) : minPoint(p), maxPoint(p) {} + + /** Construct bounding box initialized to min/max point. + */ + AABB(const float3& pmin, const float3& pmax) : minPoint(pmin), maxPoint(pmax) {} - /** Checks whether two bounding boxes are equivalent in position and size + /** Set box to single point. */ - bool operator==(const BoundingBox& other) + void set(const float3& p) { minPoint = maxPoint = p; } + + /** Set the box corners explicitly. + */ + void set(const float3& pmin, const float3& pmax) { - return (other.center == center) && (other.extent == extent); + minPoint = pmin; + maxPoint = pmax; } - /** Calculates the bounding box transformed by a matrix - \param[in] mat Transform matrix - \return Bounding box after transformation + /** Invalidates the box. + */ + void invalidate() + { + minPoint = float3(std::numeric_limits::infinity()); + maxPoint = float3(-std::numeric_limits::infinity()); + } + + /** Returns true if bounding box is valid (all dimensions zero or larger). */ - BoundingBox transform(const glm::mat4& mat) const + bool valid() const { - float3 min = center - extent; - float3 max = center + extent; + return maxPoint.x >= minPoint.x && maxPoint.y >= minPoint.y && maxPoint.z >= minPoint.z; + } + + /** Grows the box to include the point p. + */ + AABB& include(const float3& p) + { + minPoint = min(minPoint, p); + maxPoint = max(maxPoint, p); + return *this; + } + + /** Grows the box to include another box. + */ + AABB& include(const AABB& b) + { + minPoint = min(minPoint, b.minPoint); + maxPoint = max(maxPoint, b.maxPoint); + return *this; + } - float3 xa = float3(mat[0] * min.x); - float3 xb = float3(mat[0] * max.x); + /** Make the box be the intersection between this and another box. + */ + AABB& intersection(const AABB& b) + { + minPoint = glm::max(minPoint, b.minPoint); + maxPoint = glm::min(maxPoint, b.maxPoint); + return *this; + } + + /** Returns the box center. + \return Center of the box if valid, undefined otherwise. + */ + float3 center() const + { + return (minPoint + maxPoint) * 0.5f; + } + + /** Returns the box extent. + \return Size of the box if valid, undefined otherwise. + */ + float3 extent() const + { + return maxPoint - minPoint; + } + + /** Returns the surface area of the box. + \return Surface area if box is valid, undefined otherwise. + */ + float area() const + { + float3 e = extent(); + return (e.x * e.y + e.x * e.z + e.y * e.z) * 2.f; + } + + /** Return the volume of the box. + \return Volume if the box is valid, undefined otherwise. + */ + float volume() const + { + float3 e = extent(); + return e.x * e.y * e.z; + } + + /** Returns the radius of the minimal sphere that encloses the box. + \return Radius of minimal bounding sphere, or undefined if box is invalid. + */ + float radius() const + { + return 0.5f * glm::length(extent()); + } + + /** Calculates the bounding box transformed by a matrix. + \param[in] mat Transform matrix + \return Bounding box after transformation. + */ + AABB transform(const glm::mat4& mat) const + { + float3 xa = float3(mat[0] * minPoint.x); + float3 xb = float3(mat[0] * maxPoint.x); float3 xMin = glm::min(xa, xb); float3 xMax = glm::max(xa, xb); - float3 ya = float3(mat[1] * min.y); - float3 yb = float3(mat[1] * max.y); + float3 ya = float3(mat[1] * minPoint.y); + float3 yb = float3(mat[1] * maxPoint.y); float3 yMin = glm::min(ya, yb); float3 yMax = glm::max(ya, yb); - float3 za = float3(mat[2] * min.z); - float3 zb = float3(mat[2] * max.z); + float3 za = float3(mat[2] * minPoint.z); + float3 zb = float3(mat[2] * maxPoint.z); float3 zMin = glm::min(za, zb); float3 zMax = glm::max(za, zb); - float3 newMin = xMin + yMin + zMin + float3(mat[3]); float3 newMax = xMax + yMax + zMax + float3(mat[3]); - return BoundingBox::fromMinMax(newMin, newMax); + return AABB(newMin, newMax); + } + + /** Checks whether two bounding boxes are equal. + */ + bool operator== (const AABB& rhs) const + { + return minPoint == rhs.minPoint && maxPoint == rhs.maxPoint; } - /** Gets the minimum position of the bounding box - \return Minimum position + /** Checks whether two bounding boxes are not equal. */ - float3 getMinPos() const + bool operator!= (const AABB& rhs) const { - return center - extent; + return minPoint != rhs.minPoint || maxPoint != rhs.maxPoint; } - /** Gets the maximum position of the bounding box - \return Maximum position + /** Union of two boxes. */ - float3 getMaxPos() const + AABB& operator|= (const AABB& rhs) { - return center + extent; + return include(rhs); } - /** Gets the size of each dimension of the bounding box. - \return X,Y and Z lengths of the bounding box + /** Union of two boxes. */ - float3 getSize() const + AABB operator| (const AABB& rhs) const { - return extent * 2.0f; + AABB bb = *this; + return bb |= rhs; } - /** Construct a bounding box from a minimum and maximum point. - \param[in] min Minimum point - \param[in] max Maximum point - \return A bounding box + /** Intersection of two boxes. */ - static BoundingBox fromMinMax(const float3& min, const float3& max) + AABB& operator&= (const AABB& rhs) { - BoundingBox box; - box.center = (max + min) * float3(0.5f); - box.extent = (max - min) * float3(0.5f); - return box; + return intersection(rhs); } - /** Constructs a bounding box from the union of two other bounding boxes. - \param[in] bb0 First bounding box - \param[in] bb1 Second bounding box - \return A bounding box + /** Intersection of two boxes. */ - static BoundingBox fromUnion(const BoundingBox& bb0, const BoundingBox& bb1) + AABB operator& (const AABB& rhs) const { - return BoundingBox::fromMinMax(min(bb0.getMinPos(), bb1.getMinPos()), max(bb0.getMaxPos(), bb1.getMaxPos())); + AABB bb = *this; + return bb &= rhs; } }; } diff --git a/Source/Falcor/Utils/Math/AABB.slang b/Source/Falcor/Utils/Math/AABB.slang index 2e8043dff7..c0c39640f1 100644 --- a/Source/Falcor/Utils/Math/AABB.slang +++ b/Source/Falcor/Utils/Math/AABB.slang @@ -34,6 +34,14 @@ struct AABB float3 minPoint; ///< Minimum point. float3 maxPoint; ///< Maximum point. If any minPoint > maxPoint the box is invalid. + /** Create a new AABB. + Note if minPoint > maxPoint in any component the box is invalid. + */ + static AABB create(float3 minPoint, float3 maxPoint) + { + return { minPoint, maxPoint }; + } + /** Set box to single point. */ [mutating] void set(float3 p) diff --git a/Source/Falcor/Utils/Math/BBox.h b/Source/Falcor/Utils/Math/BBox.h deleted file mode 100644 index a51cf07144..0000000000 --- a/Source/Falcor/Utils/Math/BBox.h +++ /dev/null @@ -1,97 +0,0 @@ -/*************************************************************************** - # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. - # - # Redistribution and use in source and binary forms, with or without - # modification, are permitted provided that the following conditions - # are met: - # * Redistributions of source code must retain the above copyright - # notice, this list of conditions and the following disclaimer. - # * Redistributions in binary form must reproduce the above copyright - # notice, this list of conditions and the following disclaimer in the - # documentation and/or other materials provided with the distribution. - # * Neither the name of NVIDIA CORPORATION nor the names of its - # contributors may be used to endorse or promote products derived - # from this software without specific prior written permission. - # - # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY - # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR - # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, - # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, - # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR - # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY - # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - **************************************************************************/ -#pragma once -#include "Utils/Math/Vector.h" -#include - -namespace Falcor -{ - /** An axis-aligned bounding box stored by its min/max points. - The user is responsible for checking the validity of returned bounding-boxes before using them. - Note: Falcor already has an AABB class that works differently, hence the name. - */ - struct BBox - { - float3 minPoint = float3(std::numeric_limits::infinity()); // +inf - float3 maxPoint = float3(-std::numeric_limits::infinity()); // -inf - - BBox() {} - BBox(const glm::float3& p) : minPoint(p), maxPoint(p) {} - - /** Returns true if bounding box is valid (all dimensions zero or larger). */ - bool valid() const { return maxPoint.x >= minPoint.x && maxPoint.y >= minPoint.y && maxPoint.z >= minPoint.z; } - - /** Returns the dimensions of the bounding box. */ - float3 dimensions() const { return maxPoint - minPoint; } - - /** Returns the centroid of the bounding box. */ - float3 centroid() const { return (minPoint + maxPoint) * 0.5f; } - - /** Returns the surface area of the bounding box. */ - float surfaceArea() const - { - const float3 dims = dimensions(); - return 2.0f * (dims.x * dims.y + dims.y * dims.z + dims.x * dims.z); - } - - /** Returns the volume of the bounding box. - \param[in] epsilon Replace dimensions that are zero by this value. - \return the volume of the bounding box if it is valid, -inf otherwise. - */ - float volume(float epsilon = 0.0f) const - { - if (valid() == false) - { - return -std::numeric_limits::infinity(); - } - - const float3 dims = glm::max(float3(epsilon), dimensions()); - return dims.x * dims.y * dims.z; - } - - /** Union of two boxes. */ - BBox& operator|= (const BBox& rhs) - { - minPoint = glm::min(minPoint, rhs.minPoint); - maxPoint = glm::max(maxPoint, rhs.maxPoint); - return *this; - } - - BBox operator| (const BBox& rhs) const { BBox bb = *this; bb |= rhs; return bb; } - - /** Intersection of two boxes. */ - BBox& operator&= (const BBox& rhs) - { - minPoint = glm::max(minPoint, rhs.minPoint); - maxPoint = glm::min(maxPoint, rhs.maxPoint); - return *this; - } - - BBox operator& (const BBox& rhs) const { BBox bb = *this; bb &= rhs; return bb; } - }; -} diff --git a/Source/Falcor/Utils/Math/FormatConversion.slang b/Source/Falcor/Utils/Math/FormatConversion.slang index 2e134827fc..1b41e14d43 100644 --- a/Source/Falcor/Utils/Math/FormatConversion.slang +++ b/Source/Falcor/Utils/Math/FormatConversion.slang @@ -36,6 +36,57 @@ propagation etc., as well as make the header shareable between the CPU/GPU. */ +/////////////////////////////////////////////////////////////////////////////// +// 8-bit snorm +/////////////////////////////////////////////////////////////////////////////// + +/** Convert float value to 8-bit snorm value. + Values outside [-1,1] are clamped and NaN is encoded as zero. + \return 8-bit snorm value in low bits, high bits are all zeros or ones depending on sign. +*/ +int floatToSnorm8(float v) +{ + v = isnan(v) ? 0.f : min(max(v, -1.f), 1.f); + return (int)trunc(v * 127.f + (v >= 0.f ? 0.5f : -0.5f)); +} + +/** Unpack a single 8-bit snorm from the lower bits of a dword. + \param[in] packed 8-bit snorm in low bits, high bits don't care. + \return Float value in [-1,1]. +*/ +float unpackSnorm8(uint packed) +{ + int bits = (int)(packed << 24) >> 24; + precise float unpacked = max((float)bits / 127.f, -1.0f); + return unpacked; +} + +/** Pack single float into a 8-bit snorm in the lower bits of the returned dword. + \return 8-bit snorm in low bits, high bits all zero. +*/ +uint packSnorm8(precise float v) +{ + return floatToSnorm8(v) & 0x000000ff; +} + +/** Unpack two 8-bit snorm values from the lo bits of a dword. + \param[in] packed Two 8-bit snorm in low bits, high bits don't care. + \return Two float values in [-1,1]. +*/ +float2 unpackSnorm2x8(uint packed) +{ + int2 bits = int2((int)(packed << 24), (int)(packed << 16)) >> 24; + precise float2 unpacked = max((float2)bits / 127.f, -1.0f); + return unpacked; +} + +/** Pack two floats into 8-bit snorm values in the lo bits of a dword. + \return Two 8-bit snorm in low bits, high bits all zero. +*/ +uint packSnorm2x8(precise float2 v) +{ + return (floatToSnorm8(v.x) & 0x000000ff) | ((floatToSnorm8(v.y) << 8) & 0x0000ff00); +} /////////////////////////////////////////////////////////////////////////////// // 16-bit snorm @@ -71,6 +122,8 @@ uint packSnorm16(precise float v) } /** Unpack two 16-bit snorm values from the lo/hi bits of a dword. + \param[in] packed Two 16-bit snorm in low/high bits. + \return Two float values in [-1,1]. */ float2 unpackSnorm2x16(uint packed) { @@ -80,6 +133,7 @@ float2 unpackSnorm2x16(uint packed) } /** Pack two floats into 16-bit snorm values in the lo/hi bits of a dword. + \return Two 16-bit snorm in low/high bits. */ uint packSnorm2x16(precise float2 v) { @@ -150,8 +204,8 @@ float2 unpackUnorm2x16(uint packed) */ uint packR11G11B10(float3 v) { - // Clamp upper bound so that it doesn't accidentally round up to INF - v = min(v, asfloat(0x477C0000)); + // Clamp upper bound so that it doesn't accidentally round up to INF + v = min(v, asfloat(0x477C0000)); // Exponent=15, Mantissa=1.11111 uint r = ((f32tof16(v.x) + 8) >> 4) & 0x000007ff; uint g = ((f32tof16(v.y) + 8) << 7) & 0x003ff800; diff --git a/Source/Falcor/Utils/Math/MathHelpers.h b/Source/Falcor/Utils/Math/MathHelpers.h new file mode 100644 index 0000000000..f4d57c6e10 --- /dev/null +++ b/Source/Falcor/Utils/Math/MathHelpers.h @@ -0,0 +1,66 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#pragma once +#include "glm/gtc/quaternion.hpp" +#include "glm/geometric.hpp" +#include "glm/gtc/matrix_transform.hpp" +#define _USE_MATH_DEFINES +#include + +namespace Falcor +{ + /** Generate a vector that is orthogonal to the input vector + This can be used to invent a tangent frame for meshes that don't have real tangents/bitangents. + \param[in] u Unit vector. + \return v Unit vector that is orthogonal to u. + */ + inline float3 perp_stark(const float3& u) + { + // TODO: Validate this and look at numerical precision etc. Are there better ways to do it? + float3 a = abs(u); + uint32_t uyx = (a.x - a.y) < 0 ? 1 : 0; + uint32_t uzx = (a.x - a.z) < 0 ? 1 : 0; + uint32_t uzy = (a.y - a.z) < 0 ? 1 : 0; + uint32_t xm = uyx & uzx; + uint32_t ym = (1 ^ xm) & uzy; + uint32_t zm = 1 ^ (xm | ym); // 1 ^ (xm & ym) + float3 v = cross(u, float3(xm, ym, zm)); + return v; + } + + /** Builds a local frame from a unit normal vector. + \param[in] n Unit normal vector. + \param[out] t Unit tangent vector. + \param[out] b Unit bitangent vector. + */ + inline void buildFrame(const float3& n, float3& t, float3& b) + { + t = perp_stark(n); + b = cross(n, t); + } +} diff --git a/Source/Falcor/Utils/Math/MathHelpers.slang b/Source/Falcor/Utils/Math/MathHelpers.slang index c9ebf8f1b7..cd71cb0862 100644 --- a/Source/Falcor/Utils/Math/MathHelpers.slang +++ b/Source/Falcor/Utils/Math/MathHelpers.slang @@ -98,6 +98,22 @@ float2 world_to_latlong_map(float3 dir) return uv; } +/** Convert a coordinate in latitude-longitude map (unsigned normalized) to a world space direction. + The map is centered around the -z axis and wrapping around in clockwise order (left to right). + \param[in] latlong Position in latitude-longitude map in [0,1] for each component. + \return Normalized direction in world space. +*/ +float3 latlong_map_to_world(float2 latlong) +{ + float phi = M_PI * (2.f * saturate(latlong.x) - 1.f); + float theta = M_PI * saturate(latlong.y); + float sinTheta = sin(theta); + float cosTheta = cos(theta); + float sinPhi = sin(phi); + float cosPhi = cos(phi); + return float3(sinTheta * sinPhi, cosTheta, -sinTheta * cosPhi); +} + /****************************************************************************** Octahedral mapping @@ -267,8 +283,8 @@ float2 sample_disk_concentric(float2 u) { r = u.x; phi = (u.y / u.x) * M_PI_4; - } - else + } + else { r = u.y; phi = M_PI_2 - (u.x / u.y) * M_PI_4; @@ -352,7 +368,7 @@ float3x3 inverse(float3x3 M) return inv; } -/** Gernerate a vector that is orthogonal to the input vector. +/** Generate a vector that is orthogonal to the input vector. This can be used to invent a tangent frame for meshes that don't have real tangents/bitangents. */ float3 perp_stark(float3 u) diff --git a/Source/Falcor/Utils/Math/PackedFormats.slang b/Source/Falcor/Utils/Math/PackedFormats.slang index ad93ae28e3..54b8a4640e 100644 --- a/Source/Falcor/Utils/Math/PackedFormats.slang +++ b/Source/Falcor/Utils/Math/PackedFormats.slang @@ -29,6 +29,22 @@ import Utils.Math.MathHelpers; import Utils.Math.FormatConversion; import Utils.Color.ColorHelpers; +/** Encode a normal packed as 2x 8-bit snorms in the octahedral mapping. The high 16 bits are unused. +*/ +uint encodeNormal2x8(float3 normal) +{ + float2 octNormal = ndir_to_oct_snorm(normal); + return packSnorm2x8(octNormal); +} + +/** Decode a normal packed as 2x 8-bit snorms in the octahedral mapping. +*/ +float3 decodeNormal2x8(uint packedNormal) +{ + float2 octNormal = unpackSnorm2x8(packedNormal); + return oct_to_ndir_snorm(octNormal); +} + /** Encode a normal packed as 2x 16-bit snorms in the octahedral mapping. */ uint encodeNormal2x16(float3 normal) @@ -107,7 +123,7 @@ uint encodeLogLuvHDR(float3 color) } /** Decode an RGB color stored in a 32-bit LogLuv HDR format. - See encodeLogLuvHDR() for details. + See encodeLogLuvHDR() for details. */ float3 decodeLogLuvHDR(uint packedColor) { diff --git a/Source/Falcor/Utils/NumericRange.h b/Source/Falcor/Utils/NumericRange.h new file mode 100644 index 0000000000..0fff2aaabe --- /dev/null +++ b/Source/Falcor/Utils/NumericRange.h @@ -0,0 +1,71 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#pragma once +#include +#include + +namespace Falcor +{ + template + class NumericRange final {}; + + /** Numeric range that can be iterated over. + Should be replaced with C++20 std::views::iota when available. + */ + template + class NumericRange::value>::type> final + { + public: + class Iterator : public std::iterator + { + public: + explicit Iterator(const T& value = T(0)) : mValue(value) {} + const Iterator& operator++() { ++mValue; return *this; } + bool operator!=(const Iterator& other) const { return other.mValue != mValue; } + T operator*() const { return mValue; } + private: + T mValue; + }; + + explicit NumericRange(const T& begin, const T& end) + : mBegin(begin) + , mEnd(end) + { + if (begin > end) throw std::out_of_range("Invalid range"); + } + NumericRange() = delete; + NumericRange(const NumericRange&) = delete; + NumericRange(NumericRange&& other) = delete; + + Iterator begin() const { return Iterator(mBegin); } + Iterator end() const { return Iterator(mEnd); } + + private: + T mBegin, mEnd; + }; +}; diff --git a/Source/Falcor/Utils/Sampling/AliasTable.cpp b/Source/Falcor/Utils/Sampling/AliasTable.cpp new file mode 100644 index 0000000000..c1a5cbc624 --- /dev/null +++ b/Source/Falcor/Utils/Sampling/AliasTable.cpp @@ -0,0 +1,127 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#include "stdafx.h" +#include "AliasTable.h" + +namespace Falcor +{ + AliasTable::SharedPtr AliasTable::create(std::vector weights, std::mt19937& rng) + { + return SharedPtr(new AliasTable(std::move(weights), rng)); + } + + void AliasTable::setShaderData(const ShaderVar& var) const + { + var["items"] = mpItems; + var["weights"] = mpWeights; + var["count"] = mCount; + var["weightSum"] = (float)mWeightSum; + } + + AliasTable::AliasTable(std::vector weights, std::mt19937& rng) + : mCount((uint32_t)weights.size()) + { + if (weights.size() > std::numeric_limits::max()) throw std::exception("Too many entries for alias table."); + + std::uniform_int_distribution rngDist; + + mpWeights = Buffer::createStructured(sizeof(float), mCount, Resource::BindFlags::ShaderResource, Buffer::CpuAccess::None, weights.data()); + + mWeightSum = 0.0; + for (float f : weights) mWeightSum += f; + + double factor = mCount / mWeightSum; + for (float& f : weights) f = (float)(f * factor); + + std::vector permutation(mCount); + for (uint32_t i = 0; i < mCount; ++i) permutation[i] = i; + std::sort(permutation.begin(), permutation.end(), [&](uint32_t a, uint32_t b) { return weights[a] < weights[b]; }); + + std::vector thresholds(mCount); + std::vector redirect(mCount); + std::vector mergedTable(mCount); + + uint32_t head = 0; + uint32_t tail = mCount - 1; + + while (head != tail) + { + int i = permutation[head]; + int j = permutation[tail]; + + thresholds[i] = weights[i]; + redirect[i] = j; + weights[j] -= 1.f - weights[i]; + + if (head == tail - 1) + { + thresholds[j] = 1.f; + redirect[j] = j; + break; + } + else if (weights[j] < 1.f) + { + std::swap(permutation[head], permutation[tail]); + tail--; + } + else + { + head++; + } + } + + for (uint32_t i = 0; i < mCount; ++i) + { + permutation[i] = i; + } + + for (uint32_t i = 0; i < mCount; ++i) + { + uint32_t dst = i + (rngDist(rng) % (mCount - i)); + std::swap(thresholds[i], thresholds[dst]); + std::swap(redirect[i], redirect[dst]); + std::swap(permutation[i], permutation[dst]); + } + + struct Item + { + float threshold; + uint32_t indexA; + uint32_t indexB; + uint32_t _pad; + }; + + std::vector items(mCount); + for (uint32_t i = 0; i < mCount; ++i) + { + items[i] = { thresholds[i], redirect[i], permutation[i], 0 }; + } + + mpItems = Buffer::createStructured(sizeof(Item), mCount, Resource::BindFlags::ShaderResource, Buffer::CpuAccess::None, items.data()); + } +} diff --git a/Source/Falcor/Utils/Sampling/AliasTable.h b/Source/Falcor/Utils/Sampling/AliasTable.h new file mode 100644 index 0000000000..5040868c32 --- /dev/null +++ b/Source/Falcor/Utils/Sampling/AliasTable.h @@ -0,0 +1,69 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#pragma once +#include + +namespace Falcor +{ + /** Implements the alias method for sampling from a discrete probability distribution. + */ + class dlldecl AliasTable + { + public: + using SharedPtr = std::shared_ptr; + + /** Create an alias table. + The weights don't need to be normalized to sum up to 1. + \param[in] weights The weights we'd like to sample each entry proportional to. + \param[in] rng The random number generator to use when creating the table. + \returns The alias table. + */ + static SharedPtr create(std::vector weights, std::mt19937& rng); + + /** Bind the alias table data to a given shader var. + \param[in] var The shader variable to set the data into. + */ + void setShaderData(const ShaderVar& var) const; + + /** Get the number of weights in the table. + */ + uint32_t getCount() const { return mCount; } + + /** Get the total sum of all weights in the table. + */ + double getWeightSum() const { return mWeightSum; } + + private: + AliasTable(std::vector weights, std::mt19937& rng); + + uint32_t mCount; ///< Number of items in the alias table. + double mWeightSum; ///< Total weight of all elements used to create the alias table. + Buffer::SharedPtr mpItems; ///< Buffer containing table items. + Buffer::SharedPtr mpWeights; ///< Buffer containing item weights. + }; +} diff --git a/Source/Falcor/Utils/Sampling/AliasTable.slang b/Source/Falcor/Utils/Sampling/AliasTable.slang new file mode 100644 index 0000000000..c530871699 --- /dev/null +++ b/Source/Falcor/Utils/Sampling/AliasTable.slang @@ -0,0 +1,80 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ + +/** Implements the alias method for sampling from a discrete probability distribution. +*/ +struct AliasTable +{ + struct Item + { + uint threshold; + uint indexA; + uint indexB; + uint _pad; + + float getThreshold() { return asfloat(threshold); } + uint getIndexA() { return indexA; } + uint getIndexB() { return indexB; } + }; + + StructuredBuffer items; ///< List of items used for sampling. + StructuredBuffer weights; ///< List of original weights. + uint count; ///< Total number of weights in the table. + float weightSum; ///< Total sum of all weights in the table. + uint _pad[2]; + + /** Sample from the table proportional to the weights. + \param[in] index Uniform random index in [0..count). + \param[in] rnd Uniform random number in [0..1). + \return Returns the sampled item index. + */ + uint sample(uint index, float rnd) + { + Item item = items[index]; + return rnd >= item.getThreshold() ? item.getIndexA() : item.getIndexB(); + } + + /** Sample from the table proportional to the weights. + \param[in] rnd Two uniform random number in [0..1). + \return Returns the sampled item index. + */ + uint sample(float2 rnd) + { + uint index = min(count - 1, (uint)(rnd.x * count)); + return sample(index, rnd.y); + } + + /** Get the original weight at a given index. + \param[in] index Table index. + \return Returns the original weight. + */ + float getWeight(uint index) + { + return weights[index]; + } +}; diff --git a/Source/Falcor/Utils/Sampling/SampleGenerator.slang b/Source/Falcor/Utils/Sampling/SampleGenerator.slang index 1bd7ea683f..4efcf7a0e4 100644 --- a/Source/Falcor/Utils/Sampling/SampleGenerator.slang +++ b/Source/Falcor/Utils/Sampling/SampleGenerator.slang @@ -27,6 +27,8 @@ **************************************************************************/ #include "Utils/Sampling/SampleGeneratorType.slangh" +__exported import Utils.Sampling.SampleGeneratorInterface; + /** The host sets the SAMPLE_GENERATOR_TYPE define to select sample generator. This code typedefs the chosen type to the type 'SampleGenerator'. @@ -46,38 +48,3 @@ import Utils.Sampling.UniformSampleGenerator; typedef UniformSampleGenerator SampleGenerator; #endif - -/** Convenience functions for generating 1D/2D/3D values in the range [0,1). - - Note: These are global instead of member functions in the sample generator - interface, as there seems to be no way in Slang currently to specify default - implementations without duplicating the code into all classes that implement - the interace. -*/ - -float sampleNext1D(inout SampleGenerator sg) -{ - // Use upper 24 bits and divide by 2^24 to get a number u in [0,1). - // In floating-point precision this also ensures that 1.0-u != 0.0. - uint bits = sg.next(); - return (bits >> 8) * 0x1p-24; -} - -float2 sampleNext2D(inout SampleGenerator sg) -{ - float2 sample; - // Don't use the float2 initializer to ensure consistent order of evaluation. - sample.x = sampleNext1D(sg); - sample.y = sampleNext1D(sg); - return sample; -} - -float3 sampleNext3D(inout SampleGenerator sg) -{ - float3 sample; - // Don't use the float3 initializer to ensure consistent order of evaluation. - sample.x = sampleNext1D(sg); - sample.y = sampleNext1D(sg); - sample.z = sampleNext1D(sg); - return sample; -} diff --git a/Source/Falcor/Utils/Sampling/SampleGeneratorInterface.slang b/Source/Falcor/Utils/Sampling/SampleGeneratorInterface.slang index 7cae6ec645..9c97f674f4 100644 --- a/Source/Falcor/Utils/Sampling/SampleGeneratorInterface.slang +++ b/Source/Falcor/Utils/Sampling/SampleGeneratorInterface.slang @@ -34,3 +34,38 @@ interface ISampleGenerator */ [mutating] uint next(); }; + +/** Convenience functions for generating 1D/2D/3D values in the range [0,1). + + Note: These are global instead of member functions in the sample generator + interface, as there seems to be no way in Slang currently to specify default + implementations without duplicating the code into all classes that implement + the interace. +*/ + +float sampleNext1D(inout S sg) +{ + // Use upper 24 bits and divide by 2^24 to get a number u in [0,1). + // In floating-point precision this also ensures that 1.0-u != 0.0. + uint bits = sg.next(); + return (bits >> 8) * 0x1p-24; +} + +float2 sampleNext2D(inout S sg) +{ + float2 sample; + // Don't use the float2 initializer to ensure consistent order of evaluation. + sample.x = sampleNext1D(sg); + sample.y = sampleNext1D(sg); + return sample; +} + +float3 sampleNext3D(inout S sg) +{ + float3 sample; + // Don't use the float3 initializer to ensure consistent order of evaluation. + sample.x = sampleNext1D(sg); + sample.y = sampleNext1D(sg); + sample.z = sampleNext1D(sg); + return sample; +} diff --git a/Source/Falcor/Utils/Sampling/TinyUniformSampleGenerator.slang b/Source/Falcor/Utils/Sampling/TinyUniformSampleGenerator.slang index 0035b6ef53..2adf7b4540 100644 --- a/Source/Falcor/Utils/Sampling/TinyUniformSampleGenerator.slang +++ b/Source/Falcor/Utils/Sampling/TinyUniformSampleGenerator.slang @@ -46,15 +46,26 @@ struct TinyUniformSampleGenerator : ISampleGenerator }; /** Create sample generator. + \param[in] seed Seed value. + \return Return a new sample generator. */ - static TinyUniformSampleGenerator create(uint2 pixel, uint sampleNumber) + static TinyUniformSampleGenerator create(uint seed) { TinyUniformSampleGenerator sampleGenerator; + sampleGenerator.rng = createLCG(seed); + return sampleGenerator; + } + /** Create sample generator for a given pixel and sample number. + \param[in] pixel Pixel id. + \param[in] sampleNumber Sample number. + \return Return a new sample generator. + */ + static TinyUniformSampleGenerator create(uint2 pixel, uint sampleNumber) + { // Use block cipher to generate a pseudorandom initial seed. uint seed = blockCipherTEA(interleave_32bit(pixel), sampleNumber).x; - sampleGenerator.rng = createLCG(seed); - return sampleGenerator; + return create(seed); } /** Returns the next sample value. This function updates the state. diff --git a/Source/Falcor/Utils/Scripting/ScriptBindings.cpp b/Source/Falcor/Utils/Scripting/ScriptBindings.cpp index 8288361c39..1eb8403863 100644 --- a/Source/Falcor/Utils/Scripting/ScriptBindings.cpp +++ b/Source/Falcor/Utils/Scripting/ScriptBindings.cpp @@ -28,16 +28,40 @@ #include "stdafx.h" #include "ScriptBindings.h" #include "pybind11/embed.h" +#include "pybind11/operators.h" #include namespace Falcor::ScriptBindings { namespace { - /** `gRegisterFuncs` is declared as pointer so that we can ensure it can be explicitly - allocated when registerBinding() is called. (The C++ static objectinitialization fiasco.) + struct DeferredBinding + { + std::string name; + RegisterBindingFunc bindingFunc; + bool isRegistered = false; + + DeferredBinding(const std::string& name, RegisterBindingFunc bindingFunc) + : name(name) + , bindingFunc(bindingFunc) + {} + + void bind(pybind11::module& m) + { + if (!isRegistered) + { + isRegistered = true; + bindingFunc(m); + } + } + }; + + /** `gDeferredBindings` is declared as pointer so that we can ensure it can be explicitly + allocated when registerDeferredBinding() is called. (The C++ static objectinitialization fiasco.) */ - std::unique_ptr> gRegisterFuncs; + std::unique_ptr> gDeferredBindings; + + uint32_t gDeferredBindingID = 0; } void registerBinding(RegisterBindingFunc f) @@ -48,8 +72,8 @@ namespace Falcor::ScriptBindings { auto m = pybind11::module::import("falcor"); f(m); - // Re-import falcor - pybind11::exec("from falcor import *"); + // Re-import falcor into default scripting context. + Scripting::runScript("from falcor import *"); } catch (const std::exception& e) { @@ -60,62 +84,130 @@ namespace Falcor::ScriptBindings } else { - if (!gRegisterFuncs) gRegisterFuncs.reset(new std::vector()); - gRegisterFuncs->push_back(f); + // Create unique name for deferred binding. + std::string name = "DeferredBinding" + std::to_string(gDeferredBindingID++); + registerDeferredBinding(name, f); } } - template - VecT makeVec(Args...args) + void registerDeferredBinding(const std::string& name, RegisterBindingFunc f) { - return VecT(args...); + if (!gDeferredBindings) gDeferredBindings.reset(new std::map()); + if (gDeferredBindings->find(name) != gDeferredBindings->end()) + { + throw std::exception(("A script binding with the name '" + name + "' already exists!").c_str()); + } + gDeferredBindings->emplace(name, DeferredBinding(name, f)); + } + + void resolveDeferredBinding(const std::string &name, pybind11::module& m) + { + auto it = gDeferredBindings->find(name); + if (it != gDeferredBindings->end()) it->second.bind(m); } - template + template void addVecType(pybind11::module& m, const std::string name) { - auto ctor = [](Args...components) { return makeVec(components...); }; + using ScalarT = typename VecT::value_type; + + auto constexpr length = VecT::length(); + static_assert(length >= 2 && length <= 4, "Unsupported number of components"); + + pybind11::class_ vec(m, name.c_str()); + + vec.def_readwrite("x", &VecT::x); + vec.def_readwrite("y", &VecT::y); + if constexpr (length >= 3) vec.def_readwrite("z", &VecT::z); + if constexpr (length >= 4) vec.def_readwrite("w", &VecT::w); + + auto initEmpty = []() { return VecT(ScalarT(0)); }; + vec.def(pybind11::init(initEmpty)); + + auto initScalar = [](ScalarT c) { return VecT(c); }; + vec.def(pybind11::init(initScalar), "c"_a); + + if constexpr (length == 2) + { + auto initVector = [](ScalarT x, ScalarT y) { return VecT(x, y); }; + vec.def(pybind11::init(initVector), "x"_a, "y"_a); + } + else if constexpr (length == 3) + { + auto initVector = [](ScalarT x, ScalarT y, ScalarT z) { return VecT(x, y, z); }; + vec.def(pybind11::init(initVector), "x"_a, "y"_a, "z"_a); + } + else if constexpr (length == 4) + { + auto initVector = [](ScalarT x, ScalarT y, ScalarT z, ScalarT w) { return VecT(x, y, z, w); }; + vec.def(pybind11::init(initVector), "x"_a, "y"_a, "z"_a, "w"_a); + } + auto repr = [](const VecT& v) { return Falcor::to_string(v); }; - auto vecStr = [](const VecT& v) { + vec.def("__repr__", repr); + + auto str = [](const VecT& v) { std::string vec = "[" + std::to_string(v[0]); - for (int i = 1; i < v.length(); i++) + for (int i = 1; i < VecT::length(); i++) { vec += ", " + std::to_string(v[i]); } vec += "]"; return vec; }; - pybind11::class_(m, name.c_str()) - .def(pybind11::init(ctor)) - .def("__repr__", repr) - .def("__str__", vecStr); + vec.def("__str__", str); + + if constexpr (withOperators) + { + vec.def(pybind11::self + pybind11::self); + vec.def(pybind11::self += pybind11::self); + vec.def(pybind11::self - pybind11::self); + vec.def(pybind11::self -= pybind11::self); + vec.def(pybind11::self * pybind11::self); + vec.def(pybind11::self *= pybind11::self); + vec.def(pybind11::self / pybind11::self); + vec.def(pybind11::self /= pybind11::self); + vec.def(pybind11::self + ScalarT()); + vec.def(pybind11::self += ScalarT()); + vec.def(pybind11::self - ScalarT()); + vec.def(pybind11::self -= ScalarT()); + vec.def(pybind11::self * ScalarT()); + vec.def(pybind11::self *= ScalarT()); + vec.def(pybind11::self / ScalarT()); + vec.def(pybind11::self /= ScalarT()); + } } PYBIND11_EMBEDDED_MODULE(falcor, m) { // bool2, bool3, bool4 - addVecType(m, "bool2"); - addVecType(m, "bool3"); - addVecType(m, "bool4"); + addVecType(m, "bool2"); + addVecType(m, "bool3"); + addVecType(m, "bool4"); // float2, float3, float4 - addVecType(m, "float2"); - addVecType(m, "float3"); - addVecType(m, "float4"); + addVecType(m, "float2"); + addVecType(m, "float3"); + addVecType(m, "float4"); // int2, int3, int4 - addVecType(m, "int2"); - addVecType(m, "int3"); - addVecType(m, "int4"); + addVecType(m, "int2"); + addVecType(m, "int3"); + addVecType(m, "int4"); // uint2, uint3, uint4 - addVecType(m, "uint2"); - addVecType(m, "uint3"); - addVecType(m, "uint4"); + addVecType(m, "uint2"); + addVecType(m, "uint3"); + addVecType(m, "uint4"); + + // float3x3, float4x4 + // Note: We register these as simple data types without any operations because semantics may change in the future. + pybind11::class_(m, "float3x3"); + pybind11::class_(m, "float4x4"); - if (gRegisterFuncs) + if (gDeferredBindings) { - for (auto f : *gRegisterFuncs) f(m); + for (auto& [name, binding] : *gDeferredBindings) binding.bind(m); } } } diff --git a/Source/Falcor/Utils/Scripting/ScriptBindings.h b/Source/Falcor/Utils/Scripting/ScriptBindings.h index 556cc67de9..2e2585790c 100644 --- a/Source/Falcor/Utils/Scripting/ScriptBindings.h +++ b/Source/Falcor/Utils/Scripting/ScriptBindings.h @@ -33,11 +33,31 @@ namespace Falcor::ScriptBindings using RegisterBindingFunc = std::function; /** Register a script binding function. - This function will be called when scripting is initialized. - \param[in] f Function to be called for registering script bindings. + The binding function will be called when scripting is initialized. + \param[in] f Function to be called for registering the binding. */ dlldecl void registerBinding(RegisterBindingFunc f); + /** Register a deferred script binding function. + This is used to register a script binding function before scripting is initialized. + The execution of the binding function is deferred until scripting is finally initialized. + Note: This is called from `registerBinding()` if called before scripting is initialized + and from the SCRIPT_BINDING macro. + \param[in] name Name if the binding. + \param[in] f Function to be called for registering the binding. + */ + dlldecl void registerDeferredBinding(const std::string& name, RegisterBindingFunc f); + + /** Resolve a deferred script binding by name. + This immediately executes the deferred binding function registered to the given name + and can be used to control the order of execution of the binding functions. + Note: This is used by the SCRIPT_BINDING_DEPENDENCY macro to ensure dependent bindings + are registered ahead of time. + \param[in] name Name of the binding to resolve. + \param[in] m Python module. + */ + dlldecl void resolveDeferredBinding(const std::string &name, pybind11::module& m); + /************************************************************************/ /* Helpers */ /************************************************************************/ @@ -172,15 +192,17 @@ namespace Falcor::ScriptBindings }; #ifndef _staticlibrary -#define SCRIPT_BINDING(Name) \ - static void ScriptBinding##Name(pybind11::module& m); \ - struct ScriptBindingRegisterer##Name { \ - ScriptBindingRegisterer##Name() \ - { \ - ScriptBindings::registerBinding(ScriptBinding##Name); \ - } \ - } gScriptBinding##Name; \ +#define SCRIPT_BINDING(Name) \ + static void ScriptBinding##Name(pybind11::module& m); \ + struct ScriptBindingRegisterer##Name { \ + ScriptBindingRegisterer##Name() \ + { \ + ScriptBindings::registerDeferredBinding(#Name, ScriptBinding##Name); \ + } \ + } gScriptBinding##Name; \ static void ScriptBinding##Name(pybind11::module& m) /* over to the user for the braces */ +#define SCRIPT_BINDING_DEPENDENCY(Name) \ + ScriptBindings::resolveDeferredBinding(#Name, m); #else #define SCRIPT_BINDING(Name) static_assert(false, "Using SCRIPT_BINDING() in a static-library is not supported. The C++ linker usually doesn't pull static-initializers into the EXE. " \ "Call 'registerBinding()' yourself from a code that is guarenteed to run."); diff --git a/Source/Falcor/Utils/Scripting/ScriptWriter.h b/Source/Falcor/Utils/Scripting/ScriptWriter.h new file mode 100644 index 0000000000..fb0d1b6fc9 --- /dev/null +++ b/Source/Falcor/Utils/Scripting/ScriptWriter.h @@ -0,0 +1,113 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#pragma once +#include "Core/Platform/OS.h" +#include "Utils/Scripting/Dictionary.h" + +namespace Falcor +{ + /** Helper class to write Python script code including: + - calling functions + - calling member functions + - getting/setting properties + + Arguments are automatically converted from C++ types to Python code using `repr()`. + */ + class ScriptWriter + { + public: + struct VariableName + { + std::string name; + explicit VariableName(const std::string& name) : name(name) {} + }; + + static std::string makeFunc(const std::string& func) + { + return func + "()\n"; + } + + template + static std::string getArgString(const T& arg) + { + return ScriptBindings::repr(arg); + } + + template<> + static std::string getArgString(const Dictionary& dictionary) + { + return dictionary.toString(); + } + + template<> + static std::string getArgString(const VariableName& varName) + { + return varName.name; + } + + template + static std::string makeFunc(const std::string& func, Arg first, Args...args) + { + std::string s = func + "(" + getArgString(first); + int32_t dummy[] = { 0, (s += ", " + getArgString(args), 0)... }; + s += ")\n"; + return s; + } + + static std::string makeMemberFunc(const std::string& var, const std::string& func) + { + return std::string(var) + "." + makeFunc(func); + } + + template + static std::string makeMemberFunc(const std::string& var, const std::string& func, Arg first, Args...args) + { + std::string s(var); + s += std::string(".") + makeFunc(func, first, args...); + return s; + } + + static std::string makeGetProperty(const std::string& var, const std::string& property) + { + return var + "." + property + "\n"; + } + + template + static std::string makeSetProperty(const std::string& var, const std::string& property, Arg arg) + { + return var + "." + property + " = " + getArgString(arg) + "\n"; + } + + static std::string getFilenameString(const std::string& s, bool stripDataDirs = true) + { + std::string filename = stripDataDirs ? stripDataDirectories(s) : s; + std::replace(filename.begin(), filename.end(), '\\', '/'); + return filename; + } + }; +} diff --git a/Source/Falcor/Utils/Scripting/Scripting.cpp b/Source/Falcor/Utils/Scripting/Scripting.cpp index 199b2c90df..d2f787fb4d 100644 --- a/Source/Falcor/Utils/Scripting/Scripting.cpp +++ b/Source/Falcor/Utils/Scripting/Scripting.cpp @@ -34,6 +34,7 @@ namespace Falcor { const FileDialogFilterVec Scripting::kFileExtensionFilters = { { "py", "Script Files"} }; bool Scripting::sRunning = false; + std::unique_ptr Scripting::sDefaultContext; bool Scripting::start() { @@ -50,7 +51,9 @@ namespace Falcor try { pybind11::initialize_interpreter(); - pybind11::exec("from falcor import *"); + sDefaultContext.reset(new Context()); + // Import falcor into default scripting context. + Scripting::runScript("from falcor import *"); } catch (const std::exception& e) { @@ -67,10 +70,22 @@ namespace Falcor if (sRunning) { sRunning = false; + sDefaultContext.reset(); pybind11::finalize_interpreter(); } } + Scripting::Context& Scripting::getDefaultContext() + { + assert(sDefaultContext); + return *sDefaultContext; + } + + Scripting::Context Scripting::getCurrentContext() + { + return Context(pybind11::globals()); + } + class RedirectStream { public: @@ -100,42 +115,42 @@ namespace Falcor pybind11::object mBuffer; }; - static std::string runScript(const std::string& script, pybind11::dict& locals) + static Scripting::RunResult runScript(const std::string& script, pybind11::dict& globals, bool captureOutput) { - RedirectStream rs; - pybind11::exec(script.c_str(), pybind11::globals(), locals); - return rs; - } + Scripting::RunResult result; - std::string Scripting::runScript(const std::string& script) - { - auto ref = pybind11::globals(); - return Falcor::runScript(script, ref); - } + if (captureOutput) + { + RedirectStream rstdout("stdout"); + RedirectStream rstderr("stderr"); + pybind11::exec(script.c_str(), globals); + result.out = rstdout; + result.err = rstderr; + } + else + { + pybind11::exec(script.c_str(), globals); + } - std::string Scripting::runScript(const std::string& script, Context& context) - { - return Falcor::runScript(script, context.mLocals); + return result; } - Scripting::Context Scripting::getGlobalContext() + Scripting::RunResult Scripting::runScript(const std::string& script, Context& context, bool captureOutput) { - Context c; - c.mLocals = pybind11::globals(); - return c; + return Falcor::runScript(script, context.mGlobals, captureOutput); } - std::string Scripting::runScriptFromFile(const std::string& filename, Context& context) + Scripting::RunResult Scripting::runScriptFromFile(const std::string& filename, Context& context, bool captureOutput) { - if (std::filesystem::exists(filename)) return Scripting::runScript(readFile(filename), context); + if (std::filesystem::exists(filename)) return Scripting::runScript(readFile(filename), context, captureOutput); throw std::exception(std::string("Failed to run script. Can't find the file '" + filename + "'.").c_str()); } - std::string Scripting::interpretScript(const std::string& script) + std::string Scripting::interpretScript(const std::string& script, Context& context) { pybind11::module code = pybind11::module::import("code"); pybind11::object InteractiveInterpreter = code.attr("InteractiveInterpreter"); - auto interpreter = InteractiveInterpreter(pybind11::globals()); + auto interpreter = InteractiveInterpreter(context.mGlobals); auto runsource = interpreter.attr("runsource"); RedirectStream rstdout("stdout"); diff --git a/Source/Falcor/Utils/Scripting/Scripting.h b/Source/Falcor/Utils/Scripting/Scripting.h index f52db178d7..f4c8facf38 100644 --- a/Source/Falcor/Utils/Scripting/Scripting.h +++ b/Source/Falcor/Utils/Scripting/Scripting.h @@ -27,9 +27,7 @@ **************************************************************************/ #pragma once #include "ScriptBindings.h" -#include -#include "Utils/StringUtils.h" -#include "Utils/Scripting/Dictionary.h" +#include "ScriptWriter.h" using namespace pybind11::literals; @@ -39,128 +37,126 @@ namespace Falcor class dlldecl Scripting { + public: + static const FileDialogFilterVec kFileExtensionFilters; + + /** Represents a context for executing scripts. + Wraps the globals dictionary that is passed to the script on execution. + The context can be used to pass/retrieve variables to/from the executing script. + */ + class Context + { public: - static const FileDialogFilterVec kFileExtensionFilters; + Context(pybind11::dict globals) : mGlobals(globals) {} - class Context + Context() { - public: - template - struct ObjectDesc - { - ObjectDesc(const std::string& name_, const T& obj_) : name(name_), obj(obj_) {} - operator const T&() const { return obj; } - std::string name; - T obj; - }; - - template - std::vector> getObjects() + // Copy __builtins__ to our empty globals dictionary. + mGlobals["__builtins__"] = pybind11::globals()["__builtins__"]; + } + + template + struct ObjectDesc + { + ObjectDesc(const std::string& name_, const T& obj_) : name(name_), obj(obj_) {} + operator const T&() const { return obj; } + std::string name; + T obj; + }; + + template + std::vector> getObjects() + { + std::vector> v; + for (const auto& l : mGlobals) { - std::vector> v; - for (const auto& l : mLocals) + try { - try + if(!l.second.is_none()) { - if(!l.second.is_none()) - { - v.push_back(ObjectDesc(l.first.cast(), l.second.cast())); - } + v.push_back(ObjectDesc(l.first.cast(), l.second.cast())); } - catch (const std::exception&) {} } - return v; + catch (const std::exception&) {} } - - template - void setObject(const std::string& name, T obj) - { - mLocals[name.c_str()] = obj; - } - - template - T getObject(const std::string& name) const - { - return mLocals[name.c_str()].cast(); - } - - bool containsObject(const std::string& name) const - { - return mLocals.contains(name.c_str()); - } - - private: - friend class Scripting; - pybind11::dict mLocals; - }; - - static bool start(); - static void shutdown(); - static std::string runScript(const std::string& script); - static std::string runScript(const std::string& script, Context& context); - static std::string runScriptFromFile(const std::string& filename, Context& context); - static std::string interpretScript(const std::string& script); - static Context getGlobalContext(); - static bool isRunning() { return sRunning; } - - static std::string makeFunc(const std::string& func) - { - return func + "()\n"; + return v; } template - static std::string getArgString(const T& arg) - { - return ScriptBindings::repr(arg); - } - - template<> - static std::string getArgString(const Dictionary& dictionary) - { - return dictionary.toString(); - } - - template - static std::string makeFunc(const std::string& func, Arg first, Args...args) + void setObject(const std::string& name, T obj) { - std::string s = func + "(" + getArgString(first); - int32_t dummy[] = { 0, (s += ", " + getArgString(args), 0)... }; - s += ")\n"; - return s; + mGlobals[name.c_str()] = obj; } - static std::string makeMemberFunc(const std::string& var, const std::string& func) - { - return std::string(var) + "." + makeFunc(func); - } - - template - static std::string makeMemberFunc(const std::string& var, const std::string& func, Arg first, Args...args) - { - std::string s(var); - s += std::string(".") + makeFunc(func, first, args...); - return s; - } - - static std::string makeGetProperty(const std::string& var, const std::string& property) + template + T getObject(const std::string& name) const { - return var + "." + property + "\n"; + return mGlobals[name.c_str()].cast(); } - template - static std::string makeSetProperty(const std::string& var, const std::string& property, Arg arg) + bool containsObject(const std::string& name) const { - return var + "." + property + " = " + getArgString(arg) + "\n"; + return mGlobals.contains(name.c_str()); } - static std::string getFilenameString(const std::string& s, bool stripDataDirs = true) - { - std::string filename = stripDataDirs ? stripDataDirectories(s) : s; - std::replace(filename.begin(), filename.end(), '\\', '/'); - return filename; - } + private: + friend class Scripting; + pybind11::dict mGlobals; + }; + + /** Starts the script engine. + This will initialize the Python interpreter and setup the default context. + \return Returns true if successful. + */ + static bool start(); + + /** Shuts the script engine down. + */ + static void shutdown(); + + /** Returns true if the script engine is running. + */ + static bool isRunning() { return sRunning; } + + /** Returns the default context. + */ + static Context& getDefaultContext(); + + /** Returns the context of the currently executing script. + */ + static Context getCurrentContext(); + + struct RunResult + { + std::string out; + std::string err; + }; + + /** Run a script. + \param[in] script Script to run. + \param[in] context Script execution context. + \param[in] captureOutput Enable capturing stdout/stderr and returning it in RunResult. + \return Returns the captured output if enabled. + */ + static RunResult runScript(const std::string& script, Context& context = getDefaultContext(), bool captureOutput = false); + + /** Run a script from a file. + \param[in] filename Filename of the script to run. + \param[in] context Script execution context. + \param[in] captureOutput Enable capturing stdout/stderr and returning it in RunResult. + \return Returns the captured output if enabled. + */ + static RunResult runScriptFromFile(const std::string& filename, Context& context = getDefaultContext(), bool captureOutput = false); + + /** Interpret a script and return the evaluated result. + \param[in] script Script to run. + \param[in] context Script execution context. + \return Returns a string representation of the evaluated result of the script. + */ + static std::string interpretScript(const std::string& script, Context& context = getDefaultContext()); private: static bool sRunning; + static std::unique_ptr sDefaultContext; }; } diff --git a/Source/Falcor/Utils/StringUtils.h b/Source/Falcor/Utils/StringUtils.h index 2ba9c84af7..9a37ae5d10 100644 --- a/Source/Falcor/Utils/StringUtils.h +++ b/Source/Falcor/Utils/StringUtils.h @@ -157,11 +157,11 @@ namespace Falcor return result; } - /** Remove leading whitespaces (space, tab, newline, carriage-return) - \param[in] str String to operate on - \return String with leading whitespaces removed. + /** Remove leading whitespace (space, tab, newline, carriage-return). + \param[in] str String to operate on. + \return String with leading whitespace removed. */ - inline std::string removeLeadingWhitespaces(const std::string& str) + inline std::string removeLeadingWhitespace(const std::string& str) { size_t offset = str.find_first_not_of(" \n\r\t"); std::string ret; @@ -172,11 +172,11 @@ namespace Falcor return ret; } - /** Remove trailing whitespaces (space, tab, newline, carriage-return) - \param[in] str String to operate on - + /** Remove trailing whitespace (space, tab, newline, carriage-return). + \param[in] str String to operate on. + \return String with trailing whitespace removed. */ - inline std::string removeTrailingWhitespaces(const std::string& str) + inline std::string removeTrailingWhitespace(const std::string& str) { size_t offset = str.find_last_not_of(" \n\r\t"); std::string ret; @@ -187,11 +187,13 @@ namespace Falcor return ret; } - /** Remove trailing and leading whitespaces + /** Remove leading and trailing whitespace (space, tab, newline, carriage-return). + \param[in] str String to operate on. + \return String with leading and trailing whitespace removed. */ - inline std::string removeLeadingTrailingWhitespaces(const std::string& str) + inline std::string removeLeadingTrailingWhitespace(const std::string& str) { - return removeTrailingWhitespaces(removeLeadingWhitespaces(str)); + return removeTrailingWhitespace(removeLeadingWhitespace(str)); } /** Pad string to minimum length. @@ -260,22 +262,25 @@ namespace Falcor } /** Converts a size in bytes to a human readable string: - - prints bytes (B) if size < 512 bytes - - prints kilobytes (KB) if size < 512 kilobytes - - prints megabytes (MB) if size < 512 megabytes - - otherwise prints gigabytes (GB) + - prints bytes (B) if size < 1000 bytes + - prints kilobytes (KB) if size < 1000 kilobytes + - prints megabytes (MB) if size < 1000 megabytes + - prints gigabytes (GB) if size < 1000 gigabytes + - otherwise prints terabytes (TB) \param[in] size Size in bytes \return Returns a human readable string. */ inline std::string formatByteSize(size_t size) { std::ostringstream oss; - oss << std::fixed << std::setprecision(1); + oss << std::fixed << std::setprecision(2); - if (size < 512) oss << size << "B"; - else if (size < 512 * 1024) oss << (size / 1024.0) << "KB"; - else if (size < 512 * 1024 * 1024) oss << (size / (1024.0 * 1024.0)) << "MB"; - else oss << (size / (1024.0 * 1024.0 * 1024.0)) << "GB"; + const size_t KB = 1000, MB = KB * KB, GB = MB * KB, TB = GB * KB; + if (size < KB) oss << size << " B"; + else if (size < MB) oss << (size / (double)KB) << " KB"; + else if (size < GB) oss << (size / (double)MB) << " MB"; + else if (size < TB) oss << (size / (double)GB) << " GB"; + else oss << (size / (double)TB) << " TB"; return oss.str(); } diff --git a/Source/Falcor/Utils/Threading.h b/Source/Falcor/Utils/Threading.h index 6279d09e13..8517ad9591 100644 --- a/Source/Falcor/Utils/Threading.h +++ b/Source/Falcor/Utils/Threading.h @@ -27,6 +27,8 @@ **************************************************************************/ #pragma once #include +#include +#include namespace Falcor { @@ -77,4 +79,48 @@ namespace Falcor */ static Task dispatchTask(const std::function& func); }; + + /** Simple thread barrier class. + TODO: Once we move to C++20, we should change users of Barrier to use std::barrier instead. + The only change necessary will be to use std::barrier::arrive_and_wait() in place of Barrier::wait(). + */ + class dlldecl Barrier + { + public: + Barrier(size_t threadCount, std::function completionFunc = nullptr) + : mThreadCount(threadCount) + , mWaitCount(threadCount) + , mCompletionFunc(completionFunc) + {} + + Barrier(const Barrier& barrier) = delete; + Barrier& operator=(const Barrier& barrier) = delete; + + void wait() + { + std::unique_lock lock(mMutex); + + auto generation = mGeneration; + + if (--mWaitCount == 0) + { + if (mCompletionFunc) mCompletionFunc(); + ++mGeneration; + mWaitCount = mThreadCount; + mCondition.notify_all(); + } + else + { + mCondition.wait(lock, [this, generation] () { return generation != mGeneration; }); + } + } + + private: + size_t mThreadCount; + size_t mWaitCount; + size_t mGeneration = 0; + std::function mCompletionFunc; + std::mutex mMutex; + std::condition_variable mCondition; + }; } diff --git a/Source/Falcor/Utils/Timing/Clock.cpp b/Source/Falcor/Utils/Timing/Clock.cpp index 1d4e444429..391f475dca 100644 --- a/Source/Falcor/Utils/Timing/Clock.cpp +++ b/Source/Falcor/Utils/Timing/Clock.cpp @@ -293,13 +293,13 @@ namespace Falcor std::string Clock::getScript(const std::string& var) const { std::string s; - s += Scripting::makeSetProperty(var, kTime, 0); - s += Scripting::makeSetProperty(var, kFramerate, mFramerate); - if (mExitTime) s += Scripting::makeSetProperty(var, kExitTime, mExitTime); - if (mExitFrame) s += Scripting::makeSetProperty(var, kExitFrame, mExitFrame); + s += ScriptWriter::makeSetProperty(var, kTime, 0); + s += ScriptWriter::makeSetProperty(var, kFramerate, mFramerate); + if (mExitTime) s += ScriptWriter::makeSetProperty(var, kExitTime, mExitTime); + if (mExitFrame) s += ScriptWriter::makeSetProperty(var, kExitFrame, mExitFrame); s += std::string("# If ") + kFramerate + " is not zero, you can use the frame property to set the start frame\n"; - s += "# " + Scripting::makeSetProperty(var, kFrame, 0); - if (mPaused) s += Scripting::makeMemberFunc(var, kPause); + s += "# " + ScriptWriter::makeSetProperty(var, kFrame, 0); + if (mPaused) s += ScriptWriter::makeMemberFunc(var, kPause); return s; } } diff --git a/Source/Falcor/Utils/Timing/Clock.h b/Source/Falcor/Utils/Timing/Clock.h index eececef262..29ae6df2f6 100644 --- a/Source/Falcor/Utils/Timing/Clock.h +++ b/Source/Falcor/Utils/Timing/Clock.h @@ -52,25 +52,16 @@ namespace Falcor */ Clock& setTime(double seconds, bool deferToNextTick = false); - deprecate("4.0.1", "Use setTime() instead.") - Clock& now(double seconds, bool deferToNextTick = false) { return setTime(seconds, deferToNextTick); } - /** Get the time of the last `tick()` call */ double getTime() const { return mTime.now; } - deprecate("4.0.1", "Use getTime() instead.") - double now() const { return getTime(); } - /** Get the time delta between the 2 previous ticks. This function respects the FPS simulation setting Note that due to floating-point precision, this function won't necessarily return exactly (1/FPS) when simulating framerate. This function will potentially return a negative number, for example when resetting the time to zero */ double getDelta() const { return mTime.delta; } - deprecate("4.0.1", "Use getDelta() instead.") - double delta() const { return getDelta(); } - /** Set the current frame ID. Calling this will cause the next `tick()` call to be skipped When running in real-time mode, it will only change the frame number without affecting the time When simulating FPS, it will change the time to match the current frame ID @@ -79,26 +70,17 @@ namespace Falcor */ Clock& setFrame(uint64_t f, bool deferToNextTick = false); - deprecate("4.0.1", "Use setFrame() instead.") - Clock& frame(uint64_t f, bool deferToNextTick = false) { return setFrame(f, deferToNextTick); } - /** Get the current frame ID. When running in real-time mode, this is the number of frames since the last time the time was set. When simulating FPS, the number of frames according to the time */ uint64_t getFrame() const { return mFrames; } - deprecate("4.0.1", "Use getFrame() instead.") - uint64_t frame() const { return getFrame(); } - /** Get the real-time delta between the 2 previous ticks. This function returns the actual time that passed between the 2 `tick()` calls. It doesn't any time-manipulation setting like time-scaling and FPS simulation */ double getRealTimeDelta() const { return mRealtime.delta; } - deprecate("4.0.1", "Use getRealTimeDelta() instead.") - double realTimeDelta() const { return getRealTimeDelta(); } - /** Set the time at which to terminate the application. */ Clock& setExitTime(double seconds); @@ -129,16 +111,10 @@ namespace Falcor */ Clock& setFramerate(uint32_t fps); - deprecate("4.0.1", "Use setFramerate() instead.") - Clock& framerate(uint32_t fps) { return setFramerate(fps); } - /** Get the requested FPS value */ uint32_t getFramerate() const { return mFramerate; } - deprecate("4.0.1", "Use getFramerate() instead.") - uint32_t framerate() const { return getFramerate(); } - /** Pause the clock */ Clock& pause() { mPaused = true; return *this; } @@ -161,16 +137,10 @@ namespace Falcor */ Clock& setTimeScale(double scale) { mScale = scale; return *this; } - deprecate("4.0.1", "Use setTimeScale() instead.") - Clock& timeScale(double scale) { return setTimeScale(scale); } - /** Get the scale */ double getTimeScale() const { return mScale; } - deprecate("4.0.1", "Use getTimeScale() instead.") - double timeScale() const { return getTimeScale(); } - /** Check if the clock is paused */ bool isPaused() const { return mPaused; } @@ -179,9 +149,6 @@ namespace Falcor */ bool isSimulatingFps() const { return mFramerate != 0; } - deprecate("4.0.1", "Use isSimulatingFps() instead.") - bool simulatingFps() const { return isSimulatingFps(); } - /** Render the UI */ void renderUI(Gui::Window& w); diff --git a/Source/Falcor/Utils/Timing/Profiler.cpp b/Source/Falcor/Utils/Timing/Profiler.cpp index 123258b366..792bc17255 100644 --- a/Source/Falcor/Utils/Timing/Profiler.cpp +++ b/Source/Falcor/Utils/Timing/Profiler.cpp @@ -35,18 +35,10 @@ namespace Falcor { - bool gProfileEnabled = false; - - std::unordered_map Profiler::sProfilerEvents; - std::vector Profiler::sRegisteredEvents; - std::string curEventName = ""; - uint32_t Profiler::sCurrentLevel = 0; - uint32_t Profiler::sGpuTimerIndex = 0; - void Profiler::initNewEvent(EventData *pEvent, const std::string& name) { pEvent->name = name; - sProfilerEvents[curEventName] = pEvent; + mEvents[mCurEventName] = pEvent; } Profiler::EventData* Profiler::createNewEvent(const std::string& name) @@ -58,8 +50,8 @@ namespace Falcor Profiler::EventData* Profiler::isEventRegistered(const std::string& name) { - auto event = sProfilerEvents.find(name); - return (event == sProfilerEvents.end()) ? nullptr : event->second; + auto event = mEvents.find(name); + return (event == mEvents.end()) ? nullptr : event->second; } Profiler::EventData* Profiler::getEvent(const std::string& name) @@ -70,10 +62,10 @@ namespace Falcor void Profiler::startEvent(const std::string& name, Flags flags, bool showInMsg) { - if (gProfileEnabled && is_set(flags, Flags::Internal)) + if (mEnabled && is_set(flags, Flags::Internal)) { - curEventName = curEventName + "#" + name; - EventData* pData = getEvent(curEventName); + mCurEventName = mCurEventName + "#" + name; + EventData* pData = getEvent(mCurEventName); pData->triggered++; if (pData->triggered > 1) { @@ -82,9 +74,9 @@ namespace Falcor } pData->showInMsg = showInMsg; - pData->level = sCurrentLevel; + pData->level = mCurrentLevel; pData->cpuStart = CpuTimer::getCurrentTimePoint(); - EventData::FrameData& frame = pData->frameData[sGpuTimerIndex]; + EventData::FrameData& frame = pData->frameData[mGpuTimerIndex]; if (frame.currentTimer >= frame.pTimers.size()) { frame.pTimers.push_back(GpuTimer::create()); @@ -92,11 +84,11 @@ namespace Falcor frame.pTimers[frame.currentTimer]->begin(); pData->callStack.push(frame.currentTimer); frame.currentTimer++; - sCurrentLevel++; + mCurrentLevel++; if (!pData->registered) { - sRegisteredEvents.push_back(pData); + mRegisteredEvents.push_back(pData); pData->registered = true; } } @@ -108,21 +100,21 @@ namespace Falcor void Profiler::endEvent(const std::string& name, Flags flags) { - if (gProfileEnabled && is_set(flags, Flags::Internal)) + if (mEnabled && is_set(flags, Flags::Internal)) { - assert(isEventRegistered(curEventName)); - EventData* pData = getEvent(curEventName); + assert(isEventRegistered(mCurEventName)); + EventData* pData = getEvent(mCurEventName); pData->triggered--; if (pData->triggered != 0) return; pData->cpuEnd = CpuTimer::getCurrentTimePoint(); pData->cpuTotal += CpuTimer::calcDuration(pData->cpuStart, pData->cpuEnd); - pData->frameData[sGpuTimerIndex].pTimers[pData->callStack.top()]->end(); + pData->frameData[mGpuTimerIndex].pTimers[pData->callStack.top()]->end(); pData->callStack.pop(); - sCurrentLevel--; - curEventName.erase(curEventName.find_last_of("#")); + mCurrentLevel--; + mCurEventName.erase(mCurEventName.find_last_of("#")); } if (is_set(flags, Flags::Pix)) { @@ -145,9 +137,9 @@ namespace Falcor double Profiler::getGpuTime(const EventData* pData) { double gpuTime = 0; - for (size_t i = 0; i < pData->frameData[1 - sGpuTimerIndex].currentTimer; i++) + for (size_t i = 0; i < pData->frameData[1 - mGpuTimerIndex].currentTimer; i++) { - gpuTime += pData->frameData[1 - sGpuTimerIndex].pTimers[i]->getElapsedTime(); + gpuTime += pData->frameData[1 - mGpuTimerIndex].pTimers[i]->getElapsedTime(); } return gpuTime; } @@ -159,9 +151,9 @@ namespace Falcor std::string Profiler::getEventsString() { - std::string results("Name\t\t\t\t\tCPU time(ms)\t\t GPU time(ms)\n"); + std::string results("Name CPU time (ms) GPU time (ms)\n"); - for (EventData* pData : sRegisteredEvents) + for (EventData* pData : mRegisteredEvents) { assert(pData->triggered == 0); if(pData->showInMsg == false) continue; @@ -170,9 +162,10 @@ namespace Falcor assert(pData->callStack.empty()); char event[1000]; + std::string name = pData->name.substr(pData->name.find_last_of("#") + 1); uint32_t nameIndent = pData->level * 2 + 1; - uint32_t cpuIndent = 30 - (nameIndent + (uint32_t)pData->name.substr(pData->name.find_last_of("#") + 1).size()); - snprintf(event, 1000, "%*s%s %*.2f (%.2f) %14.2f (%.2f)\n", nameIndent, " ", pData->name.substr(pData->name.find_last_of("#") + 1).c_str(), cpuIndent, getCpuTime(pData), + uint32_t cpuIndent = 45 - (nameIndent + (uint32_t)name.size()); + snprintf(event, 1000, "%*s%s %*.2f (%.2f) %14.2f (%.2f)\n", nameIndent, " ", name.c_str(), cpuIndent, getCpuTime(pData), pData->cpuRunningAverageMS, gpuTime, pData->gpuRunningAverageMS); #if _PROFILING_LOG == 1 pData->cpuMs[pData->stepNr] = (float)pData->cpuTotal; @@ -200,7 +193,7 @@ namespace Falcor void Profiler::endFrame() { - for (EventData* pData : sRegisteredEvents) + for (EventData* pData : mRegisteredEvents) { // Update CPU/GPU time running averages. const double cpuTime = getCpuTime(pData); @@ -217,17 +210,17 @@ namespace Falcor pData->showInMsg = false; pData->cpuTotal = 0; pData->triggered = 0; - pData->frameData[1 - sGpuTimerIndex].currentTimer = 0; + pData->frameData[1 - mGpuTimerIndex].currentTimer = 0; pData->registered = false; } - sRegisteredEvents.clear(); - sGpuTimerIndex = 1 - sGpuTimerIndex; + mLastFrameEvents = std::move(mRegisteredEvents); + mGpuTimerIndex = 1 - mGpuTimerIndex; } #if _PROFILING_LOG == 1 void Profiler::flushLog() { - for (EventData* pData : sRegisteredEvents) + for (EventData* pData : mRegisteredEvents) { std::ostringstream logOss, fileOss; logOss << "dumping " << "profile_" << pData->name << "_" << pData->filesWritten; @@ -245,14 +238,50 @@ namespace Falcor void Profiler::clearEvents() { - for (EventData* pData : sRegisteredEvents) - { - delete pData; - } - sProfilerEvents.clear(); - sRegisteredEvents.clear(); - sCurrentLevel = 0; - sGpuTimerIndex = 0; - curEventName = ""; + for (auto& [_, pData] : mEvents) delete pData; + mEvents.clear(); + mRegisteredEvents.clear(); + mLastFrameEvents.clear(); + mCurrentLevel = 0; + mGpuTimerIndex = 0; + mCurEventName = ""; + } + + const Profiler::SharedPtr& Profiler::instancePtr() + { + static Profiler::SharedPtr pInstance; + if (!pInstance) pInstance = std::make_shared(); + return pInstance; + } + + pybind11::dict Profiler::EventData::toPython() const + { + pybind11::dict d; + + d["name"] = name; + d["cpuTime"] = cpuRunningAverageMS / 1000.f; + d["gpuTime"] = gpuRunningAverageMS / 1000.f; + + return d; + } + + SCRIPT_BINDING(Profiler) + { + auto getEvents = [] (Profiler* pProfiler) { + pybind11::dict d; + + for (const Profiler::EventData* pData : pProfiler->getLastFrameEvents()) + { + auto name = pData->name; + d[name.c_str()] = pData->toPython(); + } + + return d; + }; + + pybind11::class_ profiler(m, "Profiler"); + profiler.def_property("enabled", &Profiler::isEnabled, &Profiler::setEnabled); + profiler.def_property_readonly("events", getEvents); + profiler.def("clearEvents", &Profiler::clearEvents); } } diff --git a/Source/Falcor/Utils/Timing/Profiler.h b/Source/Falcor/Utils/Timing/Profiler.h index 51faf0e3fe..4b290e2b82 100644 --- a/Source/Falcor/Utils/Timing/Profiler.h +++ b/Source/Falcor/Utils/Timing/Profiler.h @@ -28,13 +28,13 @@ #pragma once #include #include +#include #include "CpuTimer.h" #include "Core/API/GpuTimer.h" +#include "Utils/Scripting/ScriptBindings.h" namespace Falcor { - extern dlldecl bool gProfileEnabled; - class GpuTimer; /** Container class for CPU/GPU profiling. @@ -45,9 +45,10 @@ namespace Falcor class dlldecl Profiler { public: + using SharedPtr = std::shared_ptr; #if _PROFILING_LOG == 1 - static void flushLog(); + void flushLog(); #endif enum class Flags @@ -61,7 +62,6 @@ namespace Falcor struct EventData { - virtual ~EventData() {} std::string name; struct FrameData { @@ -85,73 +85,100 @@ namespace Falcor float cpuMs[_PROFILING_LOG_BATCH_SIZE]; float gpuMs[_PROFILING_LOG_BATCH_SIZE]; #endif + + /** Convert to python dict. + */ + pybind11::dict toPython() const; }; + /** Return true if profiler is enabled. + */ + bool isEnabled() { return mEnabled; } + + /** Enable/disable profiler. + */ + void setEnabled(bool enabled) { mEnabled = enabled; } + /** Start profiling a new event and update the events hierarchies. \param[in] name The event name. */ - static void startEvent(const std::string& name, Flags flags = Flags::Default, bool showInMsg = true); + void startEvent(const std::string& name, Flags flags = Flags::Default, bool showInMsg = true); /** Finish profiling a new event and update the events hierarchies. \param[in] name The event name. */ - static void endEvent(const std::string& name, Flags flags = Flags::Default); + void endEvent(const std::string& name, Flags flags = Flags::Default); /** Finish profiling for the entire frame. Due to the double-buffering nature of the profiler, the results returned are for the previous frame. \param[out] profileResults A string containing the the profiling results. */ - static void endFrame(); + void endFrame(); /** Get a string with the current frame results */ - static std::string getEventsString(); + std::string getEventsString(); /** Create a new event and register and initialize it using \ref initNewEvent. \param[in] name The event name. */ - static EventData* createNewEvent(const std::string& name); - + EventData* createNewEvent(const std::string& name); + /** Initialize a previously generated event. Used to do the default initialization without creating the actual event instance, to support derived event types. See \ref Cuda::Profiler::EventData. \param[out] pEvent Event to initialize \param[in] name New event name */ - static void initNewEvent(EventData *pEvent, const std::string& name); + void initNewEvent(EventData *pEvent, const std::string& name); /** Get the event, or create a new one if the event does not yet exist. This is a public interface to facilitate more complicated construction of event names and finegrained control over the profiled region. */ - static EventData* getEvent(const std::string& name); + EventData* getEvent(const std::string& name); /** Get the event, or create a new one if the event does not yet exist. This is a public interface to facilitate more complicated construction of event names and finegrained control over the profiled region. */ - static double getEventCpuTime(const std::string& name); + double getEventCpuTime(const std::string& name); /** Get the event, or create a new one if the event does not yet exist. This is a public interface to facilitate more complicated construction of event names and finegrained control over the profiled region. */ - static double getEventGpuTime(const std::string& name); + double getEventGpuTime(const std::string& name); /** Returns the event or \c nullptr if the event is not known. Can be used as a predicate. */ - static EventData* isEventRegistered(const std::string& name); + EventData* isEventRegistered(const std::string& name); - /** Clears all the events. + /** Clears all the events. Useful if you want to start profiling a different technique with different events. */ - static void clearEvents(); + void clearEvents(); - private: - static double getGpuTime(const EventData* pData); - static double getCpuTime(const EventData* pData); + /** Get profile events from last frame. + */ + const std::vector& getLastFrameEvents() { return mLastFrameEvents; } + + /** Global profiler instance pointer. + */ + static const Profiler::SharedPtr& instancePtr(); - static std::unordered_map sProfilerEvents; - static std::vector sRegisteredEvents; - static uint32_t sCurrentLevel; - static uint32_t sGpuTimerIndex; + /** Global profiler instance. + */ + static Profiler& instance() { return *instancePtr(); } + + private: + double getGpuTime(const EventData* pData); + double getCpuTime(const EventData* pData); + + bool mEnabled = false; + std::unordered_map mEvents; + std::vector mRegisteredEvents; + std::vector mLastFrameEvents; + std::string mCurEventName; + uint32_t mCurrentLevel = 0; + uint32_t mGpuTimerIndex = 0; }; /** Helper class for starting and ending profiling events. @@ -163,10 +190,10 @@ namespace Falcor public: /** C'tor */ - ProfilerEvent(const std::string& name, Profiler::Flags flags = Profiler::Flags::Default) : mName(name), mFlags(flags) { Profiler::startEvent(name, flags); } + ProfilerEvent(const std::string& name, Profiler::Flags flags = Profiler::Flags::Default) : mName(name), mFlags(flags) { Profiler::instance().startEvent(name, flags); } /** D'tor */ - ~ProfilerEvent() { Profiler::endEvent(mName, mFlags); } + ~ProfilerEvent() { Profiler::instance().endEvent(mName, mFlags); } private: const std::string mName; diff --git a/Source/Falcor/Utils/Timing/TimeReport.h b/Source/Falcor/Utils/Timing/TimeReport.h index 8f3333fd74..18a2aaec20 100644 --- a/Source/Falcor/Utils/Timing/TimeReport.h +++ b/Source/Falcor/Utils/Timing/TimeReport.h @@ -50,7 +50,7 @@ namespace Falcor void printToLog(); /** Records a time measurement. - Measures time since last call to reset() or reportTime(), whichever happened more recently. + Measures time since last call to reset() or measure(), whichever happened more recently. \param[in] name Name of the record. */ void measure(const std::string& name); diff --git a/Source/Falcor/Utils/UI/DebugDrawer.cpp b/Source/Falcor/Utils/UI/DebugDrawer.cpp index 6a0a56c215..028d72a1b1 100644 --- a/Source/Falcor/Utils/UI/DebugDrawer.cpp +++ b/Source/Falcor/Utils/UI/DebugDrawer.cpp @@ -56,10 +56,10 @@ namespace Falcor addLine(quad[3], quad[0]); } - void DebugDrawer::addBoundingBox(const BoundingBox& aabb) + void DebugDrawer::addBoundingBox(const AABB& aabb) { - float3 min = aabb.center - aabb.extent; - float3 max = aabb.center + aabb.extent; + float3 min = aabb.minPoint; + float3 max = aabb.maxPoint; Quad bottomFace = { min, float3(max.x, min.y, min.z), float3(max.x, min.y, max.z), float3(min.x, min.y, max.z) }; addQuad(bottomFace); diff --git a/Source/Falcor/Utils/UI/DebugDrawer.h b/Source/Falcor/Utils/UI/DebugDrawer.h index 812880d341..affcab5ff4 100644 --- a/Source/Falcor/Utils/UI/DebugDrawer.h +++ b/Source/Falcor/Utils/UI/DebugDrawer.h @@ -70,7 +70,7 @@ namespace Falcor /** Adds a world space AABB */ - void addBoundingBox(const BoundingBox& aabb); + void addBoundingBox(const AABB& aabb); /** Renders the contents of the debug drawer */ diff --git a/Source/Falcor/Utils/UI/Gui.cpp b/Source/Falcor/Utils/UI/Gui.cpp index bb919c7307..dbf2a695f3 100644 --- a/Source/Falcor/Utils/UI/Gui.cpp +++ b/Source/Falcor/Utils/UI/Gui.cpp @@ -117,6 +117,7 @@ namespace Falcor bool addDragDropDest(const char dataLabel[], std::string& payloadString); void addText(const char text[], bool sameLine = false); + void addTextWrapped(const char text[]); bool addTextbox(const char label[], std::string& text, uint32_t lineCount = 1, Gui::TextFlags flags = Gui::TextFlags::Empty); bool addTextbox(const char label[], char buf[], size_t bufSize, uint32_t lineCount = 1, Gui::TextFlags flags = Gui::TextFlags::Empty); bool addMultiTextbox(const char label[], const std::vector& textLabels, std::vector& textEntries); @@ -512,9 +513,9 @@ namespace Falcor bool GuiImpl::addDirectionWidget(const char label[], float3& direction) { - float3 dir = direction; + float3 dir = glm::normalize(direction); bool b = addVecVar(label, dir, -1.f, 1.f, 0.001f, false, "%.3f"); - direction = glm::normalize(dir); + if (b) direction = glm::normalize(dir); return b; } @@ -576,6 +577,11 @@ namespace Falcor ImGui::TextUnformatted(text); } + void GuiImpl::addTextWrapped(const char text[]) + { + ImGui::TextWrapped("%s", text); + } + bool GuiImpl::addTextbox(const char label[], char buf[], size_t bufSize, uint32_t lineCount, Gui::TextFlags flags) { bool fitWindow = is_set(flags, Gui::TextFlags::FitWindow); @@ -700,6 +706,10 @@ namespace Falcor { return addScalarVarHelper(label, var, ImGuiDataType_U64, minVal, maxVal, step, sameLine, displayFormat); } + else if (std::is_same::value) + { + return addScalarVarHelper(label, var, ImGuiDataType_Double, minVal, maxVal, step, sameLine, displayFormat); + } else { logError("Unsupported slider type"); @@ -732,6 +742,10 @@ namespace Falcor { return addScalarSliderHelper(label, var, ImGuiDataType_Float, minVal, maxVal, sameLine, displayFormat); } + else if (std::is_same::value) + { + return addScalarSliderHelper(label, var, ImGuiDataType_Double, minVal, maxVal, sameLine, displayFormat); + } else { logError("Unsupported slider type"); @@ -1191,6 +1205,7 @@ namespace Falcor add_scalarVar_type(uint32_t); add_scalarVar_type(uint64_t); add_scalarVar_type(float); + add_scalarVar_type(double); #undef add_scalarVar_type @@ -1208,6 +1223,7 @@ namespace Falcor add_scalarSlider_type(uint32_t); add_scalarSlider_type(uint64_t); add_scalarSlider_type(float); + add_scalarSlider_type(double); #undef add_scalarSlider_type @@ -1258,6 +1274,11 @@ namespace Falcor if (mpGui) mpGui->mpWrapper->addText(text.c_str(), sameLine); } + void Gui::Widgets::textWrapped(const std::string& text) + { + if (mpGui) mpGui->mpWrapper->addTextWrapped(text.c_str()); + } + bool Gui::Widgets::textbox(const std::string& label, std::string& text, TextFlags flags) { return mpGui ? mpGui->mpWrapper->addTextbox(label.c_str(), text, 1, flags) : false; diff --git a/Source/Falcor/Utils/UI/Gui.h b/Source/Falcor/Utils/UI/Gui.h index 99a32f3e10..757f6c1644 100644 --- a/Source/Falcor/Utils/UI/Gui.h +++ b/Source/Falcor/Utils/UI/Gui.h @@ -197,6 +197,11 @@ namespace Falcor */ void text(const std::string& text, bool sameLine = false); + /** Static text wrapped to the window + \param[in] text The string to display + */ + void textWrapped(const std::string& text); + /** Adds a text box. \param[in] label The name of the variable. \param[in] text A string with the initialize text. The string will be updated if a text is entered. diff --git a/Source/Falcor/Utils/Video/VideoEncoderUI.cpp b/Source/Falcor/Utils/Video/VideoEncoderUI.cpp index a58b5df0ba..f91b15f65e 100644 --- a/Source/Falcor/Utils/Video/VideoEncoderUI.cpp +++ b/Source/Falcor/Utils/Video/VideoEncoderUI.cpp @@ -31,7 +31,7 @@ namespace Falcor { - static const Gui::DropdownList kCodecID = + static const Gui::DropdownList kCodecID = { { (uint32_t)VideoEncoder::Codec::Raw, std::string("Uncompressed") }, { (uint32_t)VideoEncoder::Codec::H264, std::string("H.264") }, @@ -73,16 +73,22 @@ namespace Falcor if (codecOnly) return; - w.checkbox("Capture UI", mCaptureUI); - w.tooltip("Check this box if you want the GUI recorded"); - w.checkbox("Reset rendering", mResetOnFirstFrame); - w.tooltip("Check this box if you want the rendering to be reset for the first frame, for example to reset temporal accumulation"); - w.checkbox("Use Time-Range", mUseTimeRange); - if (mUseTimeRange) { - auto g = w.group("Time Range"); - g.var("Start Time", mStartTime, 0.f, FLT_MAX, 0.001f); - g.var("End Time", mEndTime, 0.f, FLT_MAX, 0.001f); + auto g = w.group("Capture Options", true); + + g.checkbox("Capture UI", mCaptureUI); + g.tooltip("Check this box if you want the GUI recorded"); + + g.checkbox("Reset rendering", mResetOnFirstFrame); + g.tooltip("Check this box if you want the rendering to be reset for the first frame, for example to reset temporal accumulation"); + + g.checkbox("Use Time-Range", mUseTimeRange); + if (mUseTimeRange) + { + auto g = w.group("Time Range", true); + g.var("Start Time", mStartTime, 0.f, FLT_MAX, 0.001f); + g.var("End Time", mEndTime, 0.f, FLT_MAX, 0.001f); + } } if (mStartCB && w.button("Start Recording")) startCapture(); diff --git a/Source/Falcor/Utils/Video/VideoEncoderUI.h b/Source/Falcor/Utils/Video/VideoEncoderUI.h index 246ec4caca..f37ae3ad89 100644 --- a/Source/Falcor/Utils/Video/VideoEncoderUI.h +++ b/Source/Falcor/Utils/Video/VideoEncoderUI.h @@ -88,10 +88,10 @@ namespace Falcor bool mCaptureUI = false; bool mResetOnFirstFrame = false; float mStartTime = 0; - float mEndTime = FLT_MAX; + float mEndTime = 4.f; std::string mFilename; - float mBitrate = 4; + float mBitrate = 30.f; uint32_t mGopSize = 10; }; } diff --git a/Source/Falcor/dependencies.xml b/Source/Falcor/dependencies.xml index 364951afbd..75ce6efad0 100644 --- a/Source/Falcor/dependencies.xml +++ b/Source/Falcor/dependencies.xml @@ -1,46 +1,27 @@ - - - - + + + + + - - - - - - - - - - - + + - - + + + - - - - - - - - - - - - + + - - + + - - + + diff --git a/Source/Mogwai/Data/Config.py b/Source/Mogwai/Data/Config.py index e36c37f853..9e8041dda1 100644 --- a/Source/Mogwai/Data/Config.py +++ b/Source/Mogwai/Data/Config.py @@ -1,5 +1,5 @@ # Scene -m.loadScene("Arcade/Arcade.fscene") +m.loadScene("Arcade/Arcade.pyscene") # Graphs m.script("Data/ForwardRenderer.py") diff --git a/Source/Mogwai/Data/PathTracer.py b/Source/Mogwai/Data/PathTracer.py index 57942a0db7..6badef4bc1 100644 --- a/Source/Mogwai/Data/PathTracer.py +++ b/Source/Mogwai/Data/PathTracer.py @@ -6,7 +6,7 @@ def render_graph_PathTracerGraph(): loadRenderPassLibrary("MegakernelPathTracer.dll") AccumulatePass = createPass("AccumulatePass", {'enableAccumulation': True}) g.addPass(AccumulatePass, "AccumulatePass") - ToneMappingPass = createPass("ToneMapper", {'autoExposure': False, 'exposureValue': 0.0}) + ToneMappingPass = createPass("ToneMapper", {'autoExposure': False, 'exposureCompensation': 0.0}) g.addPass(ToneMappingPass, "ToneMappingPass") GBufferRT = createPass("GBufferRT", {'forceCullMode': False, 'cull': CullMode.CullBack, 'samplePattern': SamplePattern.Stratified, 'sampleCount': 16}) g.addPass(GBufferRT, "GBufferRT") diff --git a/Source/Mogwai/Data/SceneDebugger.py b/Source/Mogwai/Data/SceneDebugger.py new file mode 100644 index 0000000000..56291b6f38 --- /dev/null +++ b/Source/Mogwai/Data/SceneDebugger.py @@ -0,0 +1,13 @@ +from falcor import * + +def render_graph_SceneDebugger(): + g = RenderGraph('SceneDebugger') + loadRenderPassLibrary('SceneDebugger.dll') + SceneDebugger = createPass('SceneDebugger') + g.addPass(SceneDebugger, 'SceneDebugger') + g.markOutput('SceneDebugger.output') + return g + +SceneDebugger = render_graph_SceneDebugger() +try: m.addGraph(SceneDebugger) +except NameError: None diff --git a/Source/Mogwai/Data/VBufferPathTracer.py b/Source/Mogwai/Data/VBufferPathTracer.py index 7cacb8ec06..8ac6266b5c 100644 --- a/Source/Mogwai/Data/VBufferPathTracer.py +++ b/Source/Mogwai/Data/VBufferPathTracer.py @@ -6,7 +6,7 @@ def render_graph_VBufferPathTracerGraph(): loadRenderPassLibrary("MegakernelPathTracer.dll") AccumulatePass = createPass("AccumulatePass", {'enableAccumulation': True}) g.addPass(AccumulatePass, "AccumulatePass") - ToneMappingPass = createPass("ToneMapper", {'autoExposure': False, 'exposureValue': 0.0}) + ToneMappingPass = createPass("ToneMapper", {'autoExposure': False, 'exposureCompensation': 0.0}) g.addPass(ToneMappingPass, "ToneMappingPass") VBufferRT = createPass("VBufferRT", {'samplePattern': SamplePattern.Stratified, 'sampleCount': 16}) g.addPass(VBufferRT, "VBufferRT") diff --git a/Source/Mogwai/Extensions/Capture/CaptureTrigger.cpp b/Source/Mogwai/Extensions/Capture/CaptureTrigger.cpp index 9de8e91872..0da040f2df 100644 --- a/Source/Mogwai/Extensions/Capture/CaptureTrigger.cpp +++ b/Source/Mogwai/Extensions/Capture/CaptureTrigger.cpp @@ -168,9 +168,8 @@ namespace Mogwai mBaseFilename = baseFilename; } - void CaptureTrigger::scriptBindings(Bindings& bindings) + void CaptureTrigger::registerScriptBindings(pybind11::module& m) { - auto& m = bindings.getModule(); if (pybind11::hasattr(m, "CaptureTrigger")) return; pybind11::class_ captureTrigger(m, "CaptureTrigger"); @@ -183,11 +182,11 @@ namespace Mogwai captureTrigger.def_property(kBaseFilename.c_str(), &CaptureTrigger::getBaseFilename, &CaptureTrigger::setBaseFilename); } - std::string CaptureTrigger::getScript(const std::string& var) + std::string CaptureTrigger::getScript(const std::string& var) const { std::string s; - s += Scripting::makeSetProperty(var, kOutputDir, Scripting::getFilenameString(mOutputDir, false)); - s += Scripting::makeSetProperty(var, kBaseFilename, mBaseFilename); + s += ScriptWriter::makeSetProperty(var, kOutputDir, ScriptWriter::getFilenameString(mOutputDir, false)); + s += ScriptWriter::makeSetProperty(var, kBaseFilename, mBaseFilename); return s; } diff --git a/Source/Mogwai/Extensions/Capture/CaptureTrigger.h b/Source/Mogwai/Extensions/Capture/CaptureTrigger.h index 8a8116683d..af79401f04 100644 --- a/Source/Mogwai/Extensions/Capture/CaptureTrigger.h +++ b/Source/Mogwai/Extensions/Capture/CaptureTrigger.h @@ -40,7 +40,7 @@ namespace Mogwai virtual bool hasWindow() const override { return true; } virtual bool isWindowShown() const override { return mShowUI; } virtual void toggleWindow() override { mShowUI = !mShowUI; } - virtual void scriptBindings(Bindings& bindings) override; + virtual void registerScriptBindings(pybind11::module& m) override; virtual void activeGraphChanged(RenderGraph* pNewGraph, RenderGraph* pPrevGraph) override; protected: CaptureTrigger(Renderer* pRenderer, const std::string& name) : Extension(pRenderer, name) {} @@ -61,7 +61,7 @@ namespace Mogwai void setBaseFilename(const std::string& baseFilename); const std::string& getBaseFilename() const { return mBaseFilename; } - std::string getScript(const std::string& var); + std::string getScript(const std::string& var) const; std::filesystem::path getOutputPath() const; std::string getOutputNamePrefix(const std::string& output) const; diff --git a/Source/Mogwai/Extensions/Capture/FrameCapture.cpp b/Source/Mogwai/Extensions/Capture/FrameCapture.cpp index 34165744af..b6402471d2 100644 --- a/Source/Mogwai/Extensions/Capture/FrameCapture.cpp +++ b/Source/Mogwai/Extensions/Capture/FrameCapture.cpp @@ -33,7 +33,7 @@ namespace Mogwai { namespace { - const std::string kScriptVar = "fc"; + const std::string kScriptVar = "frameCapture"; const std::string kPrintFrames = "print"; const std::string kFrames = "frames"; const std::string kAddFrames = "addFrames"; @@ -73,18 +73,13 @@ namespace Mogwai } } - void FrameCapture::scriptBindings(Bindings& bindings) + void FrameCapture::registerScriptBindings(pybind11::module& m) { - CaptureTrigger::scriptBindings(bindings); - auto& m = bindings.getModule(); + CaptureTrigger::registerScriptBindings(m); pybind11::class_ frameCapture(m, "FrameCapture"); - bindings.addGlobalObject(kScriptVar, this, "Frame Capture Helpers"); - // Members - frameCapture.def(kFrames.c_str(), pybind11::overload_cast(&FrameCapture::addFrames)); // PYTHONDEPRECATED - frameCapture.def(kFrames.c_str(), pybind11::overload_cast(&FrameCapture::addFrames)); // PYTHONDEPRECATED frameCapture.def(kAddFrames.c_str(), pybind11::overload_cast(&FrameCapture::addFrames), "graph"_a, "frames"_a); frameCapture.def(kAddFrames.c_str(), pybind11::overload_cast(&FrameCapture::addFrames), "name"_a, "frames"_a); @@ -105,16 +100,21 @@ namespace Mogwai frameCapture.def_property(kUI.c_str(), getUI, setUI); } - std::string FrameCapture::getScript() + std::string FrameCapture::getScriptVar() const + { + return kScriptVar; + } + + std::string FrameCapture::getScript(const std::string& var) const { std::string s; s += "# Frame Capture\n"; - s += CaptureTrigger::getScript(kScriptVar); + s += CaptureTrigger::getScript(var); for (const auto& g : mGraphRanges) { - s += Scripting::makeMemberFunc(kScriptVar, kAddFrames, g.first->getName(), getFirstOfPair(g.second)); + s += ScriptWriter::makeMemberFunc(var, kAddFrames, g.first->getName(), getFirstOfPair(g.second)); } return s; } diff --git a/Source/Mogwai/Extensions/Capture/FrameCapture.h b/Source/Mogwai/Extensions/Capture/FrameCapture.h index 7dbfceb1b2..ffb73ab031 100644 --- a/Source/Mogwai/Extensions/Capture/FrameCapture.h +++ b/Source/Mogwai/Extensions/Capture/FrameCapture.h @@ -36,8 +36,9 @@ namespace Mogwai public: static UniquePtr create(Renderer* pRenderer); virtual void renderUI(Gui* pGui) override; - virtual void scriptBindings(Bindings& bindings) override; - virtual std::string getScript() override; + virtual void registerScriptBindings(pybind11::module& m) override; + virtual std::string getScriptVar() const override; + virtual std::string getScript(const std::string& var) const override; virtual void triggerFrame(RenderContext* pCtx, RenderGraph* pGraph, uint64_t frameID) override; void capture(); private: diff --git a/Source/Mogwai/Extensions/Capture/VideoCapture.cpp b/Source/Mogwai/Extensions/Capture/VideoCapture.cpp index a2374b41cb..601b0fe03c 100644 --- a/Source/Mogwai/Extensions/Capture/VideoCapture.cpp +++ b/Source/Mogwai/Extensions/Capture/VideoCapture.cpp @@ -32,7 +32,7 @@ namespace Mogwai { namespace { - const std::string kScriptVar = "vc"; + const std::string kScriptVar = "videoCapture"; const std::string kUI = "ui"; const std::string kCodec = "codec"; const std::string kFps = "fps"; @@ -131,15 +131,12 @@ namespace Mogwai } } - void VideoCapture::scriptBindings(Bindings& bindings) + void VideoCapture::registerScriptBindings(pybind11::module& m) { - CaptureTrigger::scriptBindings(bindings); - auto& m = bindings.getModule(); + CaptureTrigger::registerScriptBindings(m); pybind11::class_ videoCapture(m, "VideoCapture"); - bindings.addGlobalObject(kScriptVar, this, "Video Capture Helpers"); - // UI auto getUI = [](VideoCapture* pVC) { return pVC->mShowUI; }; auto setUI = [](VideoCapture* pVC, bool show) { pVC->mShowUI = show; }; @@ -163,8 +160,6 @@ namespace Mogwai videoCapture.def_property(kGopSize.c_str(), getGopSize, setGopSize); // Ranges - videoCapture.def(kRanges.c_str(), pybind11::overload_cast(&VideoCapture::addRanges)); // PYTHONDEPRECATED - videoCapture.def(kRanges.c_str(), pybind11::overload_cast(&VideoCapture::addRanges)); // PYTHONDEPRECATED videoCapture.def(kAddRanges.c_str(), pybind11::overload_cast(&VideoCapture::addRanges), "graph"_a, "ranges"_a); videoCapture.def(kAddRanges.c_str(), pybind11::overload_cast(&VideoCapture::addRanges), "name"_a, "ranges"_a); @@ -180,20 +175,25 @@ namespace Mogwai videoCapture.def(kPrint.c_str(), printAllGraphs); } - std::string VideoCapture::getScript() + std::string VideoCapture::getScriptVar() const + { + return kScriptVar; + } + + std::string VideoCapture::getScript(const std::string& var) const { if (mGraphRanges.empty()) return ""; std::string s("# Video Capture\n"); - s += CaptureTrigger::getScript(kScriptVar); - s += Scripting::makeSetProperty(kScriptVar, kCodec, mpEncoderUI->getCodec()); - s += Scripting::makeSetProperty(kScriptVar, kFps, mpEncoderUI->getFPS()); - s += Scripting::makeSetProperty(kScriptVar, kBitrate, mpEncoderUI->getBitrate()); - s += Scripting::makeSetProperty(kScriptVar, kGopSize, mpEncoderUI->getGopSize()); + s += CaptureTrigger::getScript(var); + s += ScriptWriter::makeSetProperty(var, kCodec, mpEncoderUI->getCodec()); + s += ScriptWriter::makeSetProperty(var, kFps, mpEncoderUI->getFPS()); + s += ScriptWriter::makeSetProperty(var, kBitrate, mpEncoderUI->getBitrate()); + s += ScriptWriter::makeSetProperty(var, kGopSize, mpEncoderUI->getGopSize()); for (const auto& g : mGraphRanges) { - s += Scripting::makeMemberFunc(kScriptVar, kAddRanges, g.first->getName(), g.second); + s += ScriptWriter::makeMemberFunc(var, kAddRanges, g.first->getName(), g.second); } return s; } diff --git a/Source/Mogwai/Extensions/Capture/VideoCapture.h b/Source/Mogwai/Extensions/Capture/VideoCapture.h index fc132cc2ad..587427421b 100644 --- a/Source/Mogwai/Extensions/Capture/VideoCapture.h +++ b/Source/Mogwai/Extensions/Capture/VideoCapture.h @@ -40,8 +40,9 @@ namespace Mogwai virtual void renderUI(Gui* pGui) override; virtual void beginRange(RenderGraph* pGraph, const Range& r) override; virtual void endRange(RenderGraph* pGraph, const Range& r) override; - virtual void scriptBindings(Bindings& bindings) override; - virtual std::string getScript() override; + virtual void registerScriptBindings(pybind11::module& m) override; + virtual std::string getScriptVar() const override; + virtual std::string getScript(const std::string& var) const override; virtual void triggerFrame(RenderContext* pCtx, RenderGraph* pGraph, uint64_t frameID) override; private: diff --git a/Source/Mogwai/Extensions/Profiler/TimingCapture.cpp b/Source/Mogwai/Extensions/Profiler/TimingCapture.cpp index 82aadf3f61..622ee160fc 100644 --- a/Source/Mogwai/Extensions/Profiler/TimingCapture.cpp +++ b/Source/Mogwai/Extensions/Profiler/TimingCapture.cpp @@ -32,7 +32,7 @@ namespace Mogwai { namespace { - const std::string kScriptVar = "tc"; + const std::string kScriptVar = "timingCapture"; const std::string kCaptureFrameTime = "captureFrameTime"; } @@ -43,18 +43,19 @@ namespace Mogwai return UniquePtr(new TimingCapture(pRenderer)); } - void TimingCapture::scriptBindings(Bindings& bindings) + void TimingCapture::registerScriptBindings(pybind11::module& m) { - auto& m = bindings.getModule(); - pybind11::class_ timingCapture(m, "TimingCapture"); - bindings.addGlobalObject(kScriptVar, this, "Timing Capture Helpers"); - // Members timingCapture.def(kCaptureFrameTime.c_str(), &TimingCapture::captureFrameTime, "filename"_a); } + std::string TimingCapture::getScriptVar() const + { + return kScriptVar; + } + void TimingCapture::beginFrame(RenderContext* pRenderContext, const Fbo::SharedPtr& pTargetFbo) { recordPreviousFrameTime(); diff --git a/Source/Mogwai/Extensions/Profiler/TimingCapture.h b/Source/Mogwai/Extensions/Profiler/TimingCapture.h index f1982cb425..c7b47143f6 100644 --- a/Source/Mogwai/Extensions/Profiler/TimingCapture.h +++ b/Source/Mogwai/Extensions/Profiler/TimingCapture.h @@ -37,7 +37,8 @@ namespace Mogwai static UniquePtr create(Renderer* pRenderer); virtual void beginFrame(RenderContext* pRenderContext, const Fbo::SharedPtr& pTargetFbo) override; - virtual void scriptBindings(Bindings& bindings) override; + virtual void registerScriptBindings(pybind11::module& m) override; + virtual std::string getScriptVar() const override; protected: TimingCapture(Renderer *pRenderer) : Extension(pRenderer, "Timing Capture") {} diff --git a/Source/Mogwai/Mogwai.cpp b/Source/Mogwai/Mogwai.cpp index 6f4a0e4724..42b37c5414 100644 --- a/Source/Mogwai/Mogwai.cpp +++ b/Source/Mogwai/Mogwai.cpp @@ -28,7 +28,9 @@ #include "stdafx.h" #include "Mogwai.h" #include "MogwaiSettings.h" -#include "args.h" + +#include + #include #include @@ -74,7 +76,7 @@ namespace Mogwai void Renderer::onLoad(RenderContext* pRenderContext) { mpExtensions.push_back(MogwaiSettings::create(this)); - if(gExtensions) + if (gExtensions) { for (auto& f : (*gExtensions)) mpExtensions.push_back(f.second(this)); safe_delete(gExtensions); @@ -90,6 +92,8 @@ namespace Mogwai // Add script to recent files only if not in silent mode (which is used during image tests). if (!mOptions.silentMode) mAppData.addRecentScript(mOptions.scriptFile); } + + Scene::nullTracePass(pRenderContext, uint2(1024)); } RenderGraph* Renderer::getActiveGraph() const @@ -287,7 +291,7 @@ namespace Mogwai mEditorProcess = executeProcess(kEditorExecutableName, commandLineArgs); // Mark the output if it's required - if (unmarkOut) mGraphs[mActiveGraph].pGraph->markOutput(mGraphs[mActiveGraph].mainOutput); + if (unmarkOut) mGraphs[mActiveGraph].pGraph->markOutput(mGraphs[mActiveGraph].mainOutput); } void Renderer::resetEditor() @@ -396,8 +400,14 @@ namespace Mogwai try { if (ProgressBar::isActive()) ProgressBar::show("Loading Configuration"); - auto c = Scripting::getGlobalContext(); - Scripting::runScriptFromFile(filename, c); + + // Add script directory to search paths (add it to the front to make it highest priority). + const std::string directory = getDirectoryFromFile(filename); + addDataDirectory(directory, true); + + Scripting::runScriptFromFile(filename); + + removeDataDirectory(directory); } catch (const std::exception& e) { @@ -460,6 +470,11 @@ namespace Mogwai timeReport.printToLog(); } + void Renderer::unloadScene() + { + setScene(nullptr); + } + void Renderer::setScene(const Scene::SharedPtr& pScene) { mpScene = pScene; @@ -504,7 +519,8 @@ namespace Mogwai if (hasUnmarkedOut) pActiveGraph->unmarkOutput(mGraphs[mActiveGraph].mainOutput); // Run the scripting - Scripting::getGlobalContext().setObject("g", pActiveGraph); + // TODO: Rendergraph scripts should be executed in an isolated scripting context. + Scripting::getDefaultContext().setObject("g", pActiveGraph); Scripting::runScript(mEditorScript); // Update the list of marked outputs @@ -652,6 +668,7 @@ int main(int argc, char** argv) args::HelpFlag helpFlag(parser, "help", "Display this help menu.", {'h', "help"}); args::ValueFlag scriptFlag(parser, "path", "Python script file to run.", {'s', "script"}); args::ValueFlag logfileFlag(parser, "path", "File to write log into.", {'l', "logfile"}); + args::ValueFlag verbosityFlag(parser, "verbosity", "Logging verbosity (0=disabled, 1=fatal errors, 2=errors, 3=warnings, 4=infos, 5=debugging)", { 'v', "verbosity" }, 4); args::Flag silentFlag(parser, "", "Starts Mogwai with a minimized window and disables mouse/keyboard input as well as error message dialogs.", {"silent"}); args::ValueFlag widthFlag(parser, "pixels", "Initial window width.", {"width"}); args::ValueFlag heightFlag(parser, "pixels", "Initial window height.", {"height"}); @@ -684,13 +701,28 @@ int main(int argc, char** argv) return 1; } + int32_t verbosity = args::get(verbosityFlag); + + if (verbosity < 0 || verbosity >= (int32_t)Logger::Level::Count) + { + std::cerr << argv[0] << ": invalid verbosity level " << verbosity << std::endl; + return 1; + } + + Logger::setVerbosity((Logger::Level)verbosity); + Logger::logToConsole(true); + + if (logfileFlag) + { + std::string logfile = args::get(logfileFlag); + Logger::setLogFilePath(logfile); + } + Mogwai::Renderer::Options options; if (scriptFlag) options.scriptFile = args::get(scriptFlag); if (silentFlag) options.silentMode = true; - Logger::logToConsole(true); - try { msgBoxTitle("Mogwai"); @@ -709,12 +741,6 @@ int main(int argc, char** argv) Logger::showBoxOnError(false); } - if (logfileFlag) - { - std::string logfile = args::get(logfileFlag); - Logger::setLogFilePath(logfile); - } - if (widthFlag) config.windowDesc.width = args::get(widthFlag); if (heightFlag) config.windowDesc.height = args::get(heightFlag); diff --git a/Source/Mogwai/Mogwai.h b/Source/Mogwai/Mogwai.h index bbf9725606..debd6981e4 100644 --- a/Source/Mogwai/Mogwai.h +++ b/Source/Mogwai/Mogwai.h @@ -38,27 +38,6 @@ namespace Mogwai class Extension { public: - class Bindings - { - public: - pybind11::module& getModule() { return mModule; } - pybind11::class_& getMogwaiClass() { return mMogwai; } - template - void addGlobalObject(const std::string& name, const T& obj, const std::string& desc) - { - if (mGlobalObjects.find(name) != mGlobalObjects.end()) throw std::exception(("Object '" + name + "' already exists").c_str()); - Scripting::getGlobalContext().setObject(name, obj); - mGlobalObjects[name] = desc; - } - - private: - Bindings(pybind11::module& m, pybind11::class_& c) : mModule(m), mMogwai(c) {} - friend class Renderer; - std::unordered_map mGlobalObjects; - pybind11::module& mModule; - pybind11::class_& mMogwai; - }; - using UniquePtr = std::unique_ptr; virtual ~Extension() = default; @@ -73,8 +52,9 @@ namespace Mogwai virtual void renderUI(Gui* pGui) {}; virtual bool mouseEvent(const MouseEvent& e) { return false; } virtual bool keyboardEvent(const KeyboardEvent& e) { return false; } - virtual void scriptBindings(Bindings& bindings) {}; - virtual std::string getScript() { return {}; } + virtual void registerScriptBindings(pybind11::module& m) {}; + virtual std::string getScriptVar() const { return {}; } + virtual std::string getScript(const std::string& var) const { return {}; } virtual void addGraph(RenderGraph* pGraph) {}; virtual void removeGraph(RenderGraph* pGraph) {}; virtual void activeGraphChanged(RenderGraph* pNewGraph, RenderGraph* pPrevGraph) {}; @@ -158,6 +138,7 @@ namespace Mogwai void removeActiveGraph(); void loadSceneDialog(); void loadScene(std::string filename, SceneBuilder::Flags buildFlags = SceneBuilder::Flags::Default); + void unloadScene(); void setScene(const Scene::SharedPtr& pScene); Scene::SharedPtr getScene() const; void executeActiveGraph(RenderContext* pRenderContext); @@ -195,7 +176,6 @@ namespace Mogwai // Scripting void registerScriptBindings(pybind11::module& m); - std::string mGlobalHelpMessage; }; #define MOGWAI_EXTENSION(Name) \ diff --git a/Source/Mogwai/Mogwai.vcxproj b/Source/Mogwai/Mogwai.vcxproj index 3912e87a0b..5948e317f5 100644 --- a/Source/Mogwai/Mogwai.vcxproj +++ b/Source/Mogwai/Mogwai.vcxproj @@ -48,7 +48,7 @@ {204D1CBA-6D34-4EB7-9F78-A1369F8F0F49} Win32Proj Mogwai - 10.0.18362.0 + 10.0.19041.0 Mogwai diff --git a/Source/Mogwai/MogwaiScripting.cpp b/Source/Mogwai/MogwaiScripting.cpp index 4c4d79fa32..7bad2b8a6e 100644 --- a/Source/Mogwai/MogwaiScripting.cpp +++ b/Source/Mogwai/MogwaiScripting.cpp @@ -33,44 +33,36 @@ namespace Mogwai { const std::string kRunScript = "script"; const std::string kLoadScene = "loadScene"; + const std::string kUnloadScene = "unloadScene"; const std::string kSaveConfig = "saveConfig"; const std::string kAddGraph = "addGraph"; const std::string kRemoveGraph = "removeGraph"; const std::string kGetGraph = "getGraph"; const std::string kUI = "ui"; const std::string kResizeSwapChain = "resizeSwapChain"; + const std::string kRenderFrame = "renderFrame"; const std::string kActiveGraph = "activeGraph"; const std::string kScene = "scene"; + const std::string kClock = "clock"; + const std::string kProfiler = "profiler"; const std::string kRendererVar = "m"; - const std::string kTimeVar = "t"; - template - std::string prepareHelpMessage(const T& g) - { - std::string s = Renderer::getVersionString() + "\nGlobal utility objects:\n"; - static const size_t kMaxSpace = 8; - for (auto n : g) - { - s += "\t'" + n.first + "'"; - s += (n.first.size() >= kMaxSpace) ? " " : std::string(kMaxSpace - n.first.size(), ' '); - s += n.second; - s += "\n"; - } - - s += "\nGlobal functions\n"; - s += "\trenderFrame() Render a frame. If the clock is not paused, it will advance by one tick. You can use it inside for loops, for example to loop over a specific time-range\n"; - s += "\texit() Exit Mogwai\n"; - return s; - } + const std::string kGlobalHelp = + Renderer::getVersionString() + + "\nGlobal variables:\n" + + "\tm Mogwai instance.\n" + "\nGlobal functions\n" + + "\trenderFrame() Render a frame. If the clock is not paused, it will advance by one tick. You can use it inside for loops, for example to loop over a specific time-range.\n" + + "\texit() Terminate.\n"; std::string windowConfig() { std::string s; SampleConfig c = gpFramework->getConfig(); s += "# Window Configuration\n"; - s += Scripting::makeMemberFunc(kRendererVar, kResizeSwapChain, c.windowDesc.width, c.windowDesc.height); - s += Scripting::makeSetProperty(kRendererVar, kUI, c.showUI); + s += ScriptWriter::makeMemberFunc(kRendererVar, kResizeSwapChain, c.windowDesc.width, c.windowDesc.height); + s += ScriptWriter::makeSetProperty(kRendererVar, kUI, c.showUI); return s; } } @@ -93,7 +85,7 @@ namespace Mogwai if (mpScene) { s += "# Scene\n"; - s += Scripting::makeMemberFunc(kRendererVar, kLoadScene, Scripting::getFilenameString(mpScene->getFilename())); + s += ScriptWriter::makeMemberFunc(kRendererVar, kLoadScene, ScriptWriter::getFilenameString(mpScene->getFilename())); const std::string sceneVar = kRendererVar + "." + kScene; s += mpScene->getScript(sceneVar); s += "\n"; @@ -101,13 +93,20 @@ namespace Mogwai s += windowConfig() + "\n"; - s += "# Time Settings\n"; - s += gpFramework->getGlobalClock().getScript(kTimeVar) + "\n"; + { + s += "# Clock Settings\n"; + const std::string clockVar = kRendererVar + "." + kClock; + s += gpFramework->getGlobalClock().getScript(clockVar) + "\n"; + } for (auto& pe : mpExtensions) { - auto eStr = pe->getScript(); - if (eStr.size()) s += eStr + "\n"; + if (auto var = pe->getScriptVar(); !var.empty()) + { + var = kRendererVar + "." + var; + auto eStr = pe->getScript(var); + if (eStr.size()) s += eStr + "\n"; + } } std::ofstream(filename) << s; @@ -118,34 +117,39 @@ namespace Mogwai pybind11::class_ renderer(m, "Renderer"); renderer.def(kRunScript.c_str(), &Renderer::loadScript, "filename"_a = std::string()); renderer.def(kLoadScene.c_str(), &Renderer::loadScene, "filename"_a = std::string(), "buildFlags"_a = SceneBuilder::Flags::Default); + renderer.def(kUnloadScene.c_str(), &Renderer::unloadScene); renderer.def(kSaveConfig.c_str(), &Renderer::saveConfig, "filename"_a); renderer.def(kAddGraph.c_str(), &Renderer::addGraph, "graph"_a); renderer.def(kRemoveGraph.c_str(), pybind11::overload_cast(&Renderer::removeGraph), "name"_a); renderer.def(kRemoveGraph.c_str(), pybind11::overload_cast(&Renderer::removeGraph), "graph"_a); renderer.def(kGetGraph.c_str(), &Renderer::getGraph, "name"_a); - renderer.def("graph", &Renderer::getGraph); // PYTHONDEPRECATED - auto envMap = [](Renderer* pRenderer, const std::string& filename) { if (pRenderer->getScene()) pRenderer->getScene()->loadEnvMap(filename); }; - renderer.def("envMap", envMap, "filename"_a); // PYTHONDEPRECATED - // PYTHONDEPRECATED Use the global function defined in the script bindings in Sample.cpp when resizing from a Python script. - auto resize = [](Renderer* pRenderer, uint32_t width, uint32_t height) {gpFramework->resizeSwapChain(width, height); }; - renderer.def(kResizeSwapChain.c_str(), resize); + auto resizeSwapChain = [](Renderer* pRenderer, uint32_t width, uint32_t height) { gpFramework->resizeSwapChain(width, height); }; + renderer.def(kResizeSwapChain.c_str(), resizeSwapChain); + + auto renderFrame = [](Renderer* pRenderer) { ProgressBar::close(); gpFramework->renderFrame(); }; + renderer.def(kRenderFrame.c_str(), renderFrame); renderer.def_property_readonly(kScene.c_str(), &Renderer::getScene); renderer.def_property_readonly(kActiveGraph.c_str(), &Renderer::getActiveGraph); + renderer.def_property_readonly(kClock.c_str(), [] (Renderer* pRenderer) { return &gpFramework->getGlobalClock(); }); + renderer.def_property_readonly(kProfiler.c_str(), [] (Renderer* pRenderer) { return Profiler::instancePtr(); }); auto getUI = [](Renderer* pRenderer) { return gpFramework->isUiEnabled(); }; auto setUI = [](Renderer* pRenderer, bool show) { gpFramework->toggleUI(show); }; renderer.def_property(kUI.c_str(), getUI, setUI); - Extension::Bindings b(m, renderer); - b.addGlobalObject(kRendererVar, this, "The engine"); - b.addGlobalObject(kTimeVar, &gpFramework->getGlobalClock(), "Time Utilities"); - for (auto& pe : mpExtensions) pe->scriptBindings(b); - mGlobalHelpMessage = prepareHelpMessage(b.mGlobalObjects); + for (auto& pe : mpExtensions) + { + pe->registerScriptBindings(m); + if (auto var = pe->getScriptVar(); !var.empty()) + { + renderer.def_property_readonly(var.c_str(), [&pe] (Renderer* pRenderer) { return pe.get(); }); + } + } // Replace the `help` function - auto globalHelp = [this]() { pybind11::print(mGlobalHelpMessage);}; + auto globalHelp = [this]() { pybind11::print(kGlobalHelp); }; m.def("help", globalHelp); auto objectHelp = [](pybind11::object o) @@ -155,5 +159,25 @@ namespace Mogwai h(o); }; m.def("help", objectHelp, "object"_a); + + // Register global renderer variable. + Scripting::getDefaultContext().setObject(kRendererVar, this); + + // Register deprecated global variables. + Scripting::getDefaultContext().setObject("t", &gpFramework->getGlobalClock()); // PYTHONDEPRECATED + + auto findExtension = [this](const std::string& name) + { + for (auto& pe : mpExtensions) + { + if (pe->getName() == name) return pe.get(); + } + assert(false); + return static_cast(nullptr); + }; + + Scripting::getDefaultContext().setObject("fc", findExtension("Frame Capture")); // PYTHONDEPRECATED + Scripting::getDefaultContext().setObject("vc", findExtension("Video Capture")); // PYTHONDEPRECATED + Scripting::getDefaultContext().setObject("tc", findExtension("Timing Capture")); // PYTHONDEPRECATED } } diff --git a/Source/Mogwai/MogwaiSettings.cpp b/Source/Mogwai/MogwaiSettings.cpp index 15740aed9e..57261a107d 100644 --- a/Source/Mogwai/MogwaiSettings.cpp +++ b/Source/Mogwai/MogwaiSettings.cpp @@ -193,7 +193,7 @@ namespace Mogwai // Graph UI w.separator(); - Gui::Group graphGroup(pGui, (mpRenderer->mGraphs[mpRenderer->mActiveGraph].pGraph->getName() + "##Graph").c_str()); + Gui::Group graphGroup(pGui, mpRenderer->mGraphs[mpRenderer->mActiveGraph].pGraph->getName() + "##Graph"); mpRenderer->mGraphs[mpRenderer->mActiveGraph].pGraph->renderUI(graphGroup); } diff --git a/Source/RenderPasses/AccumulatePass/AccumulatePass.cpp b/Source/RenderPasses/AccumulatePass/AccumulatePass.cpp index a41f801433..6c34ba3c87 100644 --- a/Source/RenderPasses/AccumulatePass/AccumulatePass.cpp +++ b/Source/RenderPasses/AccumulatePass/AccumulatePass.cpp @@ -221,7 +221,7 @@ void AccumulatePass::renderUI(Gui::Widgets& widget) } const std::string text = std::string("Frames accumulated ") + std::to_string(mFrameCount); - widget.text(text.c_str()); + widget.text(text); } } diff --git a/Source/RenderPasses/AccumulatePass/AccumulatePass.vcxproj b/Source/RenderPasses/AccumulatePass/AccumulatePass.vcxproj index 338d5c39b0..007221fa6d 100644 --- a/Source/RenderPasses/AccumulatePass/AccumulatePass.vcxproj +++ b/Source/RenderPasses/AccumulatePass/AccumulatePass.vcxproj @@ -14,7 +14,7 @@ {081FD8DE-6C92-4CDC-84AD-C514F7E83F93} Win32Proj AccumulatePass - 10.0.18362.0 + 10.0.19041.0 AccumulatePass diff --git a/Source/RenderPasses/Antialiasing/Antialiasing.vcxproj b/Source/RenderPasses/Antialiasing/Antialiasing.vcxproj index f60e4c2f8e..be179643ac 100644 --- a/Source/RenderPasses/Antialiasing/Antialiasing.vcxproj +++ b/Source/RenderPasses/Antialiasing/Antialiasing.vcxproj @@ -14,7 +14,7 @@ {8ECC586D-1F5E-46D4-82BB-28044E6D67A2} Win32Proj Antialiasing - 10.0.18362.0 + 10.0.19041.0 Antialiasing diff --git a/Source/RenderPasses/BSDFViewer/BSDFViewer.cpp b/Source/RenderPasses/BSDFViewer/BSDFViewer.cpp index 16d990dd6b..2059b27b12 100644 --- a/Source/RenderPasses/BSDFViewer/BSDFViewer.cpp +++ b/Source/RenderPasses/BSDFViewer/BSDFViewer.cpp @@ -62,7 +62,8 @@ BSDFViewer::BSDFViewer(const Dictionary& dict) { {"_MS_DISABLE_ALPHA_TEST", ""}, {"_DEFAULT_ALPHA_TEST", ""}, - {"MATERIAL_COUNT", "1"}, + {"SCENE_MATERIAL_COUNT", "1"}, + {"SCENE_GRID_COUNT", "0"}, }; defines.add(mpSampleGenerator->getDefines()); @@ -303,12 +304,8 @@ void BSDFViewer::renderUI(Gui::Widgets& widget) if (lightGroup.button("Load environment map")) { - // Get file dialog filters. - auto filters = Bitmap::getFileDialogFilters(); - filters.push_back({ "dds", "DDS textures" }); - std::string filename; - if (openFileDialog(filters, filename)) + if (openFileDialog(Bitmap::getFileDialogFilters(), filename)) { // TODO: RenderContext* should maybe be a parameter to renderUI()? if (loadEnvMap(filename)) diff --git a/Source/RenderPasses/BSDFViewer/BSDFViewer.cs.slang b/Source/RenderPasses/BSDFViewer/BSDFViewer.cs.slang index 8d11ba305c..9dc08aebc0 100644 --- a/Source/RenderPasses/BSDFViewer/BSDFViewer.cs.slang +++ b/Source/RenderPasses/BSDFViewer/BSDFViewer.cs.slang @@ -227,13 +227,13 @@ SurfaceData prepareMaterial(VertexData v, float3 viewDir) sd.uv = v.texC; sd.V = normalize(viewDir); sd.N = normalize(v.normalW); - sd.T = normalize(v.tangentW.xyz - sd.N * (dot(v.tangentW.xyz, sd.N))); - sd.B = normalize(cross(sd.N, sd.T) * v.tangentW.w); sd.NdotV = dot(sd.N, sd.V); sd.faceN = v.faceNormalW; sd.frontFacing = dot(sd.V, sd.faceN) >= 0.f; sd.doubleSided = false; + computeTangentSpace(sd, v.tangentW); + // Set material parameters. // Calculate the specular reflectance for dielectrics from the IoR. sd.IoR = gParams.IoR; @@ -296,7 +296,7 @@ uint getActiveLobes() { uint lobes = 0; if (gParams.enableDiffuse) lobes |= (uint)LobeType::DiffuseReflection; - if (gParams.enableSpecular) lobes |= (uint)LobeType::SpecularReflection; + if (gParams.enableSpecular) lobes |= (uint)LobeType::SpecularReflection | (uint)LobeType::DeltaReflection; // TODO: Viewer doesn't support transmission lobes yet return lobes; } diff --git a/Source/RenderPasses/BSDFViewer/BSDFViewer.vcxproj b/Source/RenderPasses/BSDFViewer/BSDFViewer.vcxproj index 1ae7c18b18..f36c88d27c 100644 --- a/Source/RenderPasses/BSDFViewer/BSDFViewer.vcxproj +++ b/Source/RenderPasses/BSDFViewer/BSDFViewer.vcxproj @@ -14,7 +14,7 @@ {B219C161-94D2-4DCB-A75D-E6A0906F534D} Win32Proj BSDFViewer - 10.0.18362.0 + 10.0.19041.0 BSDFViewer diff --git a/Source/RenderPasses/BlitPass/BlitPass.cpp b/Source/RenderPasses/BlitPass/BlitPass.cpp index 1c45e7d960..b76c4e08b1 100644 --- a/Source/RenderPasses/BlitPass/BlitPass.cpp +++ b/Source/RenderPasses/BlitPass/BlitPass.cpp @@ -27,6 +27,21 @@ **************************************************************************/ #include "BlitPass.h" +namespace +{ + const char kDesc[] = "Blit a texture into a different texture"; + + const char kDst[] = "dst"; + const char kSrc[] = "src"; + const char kFilter[] = "filter"; + + void regBlitPass(pybind11::module& m) + { + pybind11::class_ pass(m, "BlitPass"); + pass.def_property(kFilter, &BlitPass::getFilter, &BlitPass::setFilter); + } +} + // Don't remove this. it's required for hot-reload to function properly extern "C" __declspec(dllexport) const char* getProjDir() { @@ -35,23 +50,16 @@ extern "C" __declspec(dllexport) const char* getProjDir() extern "C" __declspec(dllexport) void getPasses(Falcor::RenderPassLibrary& lib) { - lib.registerClass("BlitPass", "Blit a texture into a different texture", BlitPass::create); + lib.registerClass("BlitPass", kDesc, BlitPass::create); + ScriptBindings::registerBinding(regBlitPass); } -const char* BlitPass::kDesc = "Blit a texture into a different texture"; - -static const std::string kDst = "dst"; -static const std::string kSrc = "src"; -static const std::string kFilter = "filter"; - RenderPassReflection BlitPass::reflect(const CompileData& compileData) { - RenderPassReflection reflector; - - reflector.addOutput(kDst, "The destination texture"); - reflector.addInput(kSrc, "The source texture"); - - return reflector; + RenderPassReflection r; + r.addOutput(kDst, "The destination texture"); + r.addInput(kSrc, "The source texture"); + return r; } void BlitPass::parseDictionary(const Dictionary& dict) @@ -73,11 +81,13 @@ BlitPass::BlitPass(const Dictionary& dict) parseDictionary(dict); } +std::string BlitPass::getDesc() { return kDesc; } + Dictionary BlitPass::getScriptingDictionary() { - Dictionary dict; - dict[kFilter] = mFilter; - return dict; + Dictionary d; + d[kFilter] = mFilter; + return d; } void BlitPass::execute(RenderContext* pContext, const RenderData& renderData) diff --git a/Source/RenderPasses/BlitPass/BlitPass.h b/Source/RenderPasses/BlitPass/BlitPass.h index c5770fd673..9f4cb167c4 100644 --- a/Source/RenderPasses/BlitPass/BlitPass.h +++ b/Source/RenderPasses/BlitPass/BlitPass.h @@ -31,23 +31,27 @@ using namespace Falcor; +/** Render pass that blits an input texture to an output texture. + + This pass is useful for format conversion. +*/ class BlitPass : public RenderPass { public: using SharedPtr = std::shared_ptr; - static const char* kDesc; - /** Create a new object */ static SharedPtr create(RenderContext* pRenderContext = nullptr, const Dictionary& dict = {}); + virtual std::string getDesc() override; + virtual Dictionary getScriptingDictionary() override; virtual RenderPassReflection reflect(const CompileData& compileData) override; virtual void execute(RenderContext* pContext, const RenderData& renderData) override; virtual void renderUI(Gui::Widgets& widget) override; - virtual Dictionary getScriptingDictionary() override; - virtual std::string getDesc() override { return kDesc; } + // Scripting functions + Sampler::Filter getFilter() const { return mFilter; } void setFilter(Sampler::Filter filter) { mFilter = filter; } private: diff --git a/Source/RenderPasses/BlitPass/BlitPass.vcxproj b/Source/RenderPasses/BlitPass/BlitPass.vcxproj index 741f8da3e6..b21903d993 100644 --- a/Source/RenderPasses/BlitPass/BlitPass.vcxproj +++ b/Source/RenderPasses/BlitPass/BlitPass.vcxproj @@ -14,7 +14,7 @@ {15770768-65FC-4889-A34B-3CC27CC452BC} Win32Proj BlitPass - 10.0.18362.0 + 10.0.19041.0 BlitPass diff --git a/Source/RenderPasses/CSM/CSM.cpp b/Source/RenderPasses/CSM/CSM.cpp index 2f6bee272f..76525f18a8 100644 --- a/Source/RenderPasses/CSM/CSM.cpp +++ b/Source/RenderPasses/CSM/CSM.cpp @@ -616,7 +616,7 @@ void CSM::renderScene(RenderContext* pCtx) pCB->setBlob(&mCsmData, 0, sizeof(mCsmData)); mpLightCamera->setProjectionMatrix(mCsmData.globalMat); - mpScene->render(pCtx, mShadowPass.pState.get(), mShadowPass.pVars.get()); + mpScene->rasterize(pCtx, mShadowPass.pState.get(), mShadowPass.pVars.get()); // mpCsmSceneRenderer->renderScene(pCtx, mShadowPass.pState.get(), mShadowPass.pVars.get(), mpLightCamera.get()); } @@ -900,7 +900,7 @@ void CSM::renderUI(Gui::Widgets& widget) setSdsmReadbackLatency(latency); } std::string range = "SDSM Range=[" + std::to_string(mSdsmData.sdsmResult.x) + ", " + std::to_string(mSdsmData.sdsmResult.y) + ']'; - sdsmGroup.text(range.c_str()); + sdsmGroup.text(range); } } diff --git a/Source/RenderPasses/CSM/CSM.vcxproj b/Source/RenderPasses/CSM/CSM.vcxproj index a310f41f5a..ad5a7d952d 100644 --- a/Source/RenderPasses/CSM/CSM.vcxproj +++ b/Source/RenderPasses/CSM/CSM.vcxproj @@ -14,7 +14,7 @@ {52CB6295-6163-49B8-B3B6-8A6140E0479D} Win32Proj CSM - 10.0.18362.0 + 10.0.19041.0 CSM diff --git a/Source/RenderPasses/DebugPasses/ComparisonPass.cpp b/Source/RenderPasses/DebugPasses/ComparisonPass.cpp index 337d50e5b4..e448981b5c 100644 --- a/Source/RenderPasses/DebugPasses/ComparisonPass.cpp +++ b/Source/RenderPasses/DebugPasses/ComparisonPass.cpp @@ -107,16 +107,17 @@ void ComparisonPass::execute(RenderContext* pContext, const RenderData& renderDa // Render some labels if (mShowLabels) { - int32_t screenLoc = int32_t(mSplitLoc * renderData.getDefaultTextureDims().x); + const int32_t screenLocX = int32_t(mSplitLoc * renderData.getDefaultTextureDims().x); + const int32_t screenLocY = int32_t(renderData.getDefaultTextureDims().y - 32); // Draw text labeling the right side image std::string rightSide = mSwapSides ? mLeftLabel : mRightLabel; - TextRenderer::render(pContext, rightSide.c_str(), pDstFbo, float2(screenLoc + 16, 48)); + TextRenderer::render(pContext, rightSide, pDstFbo, float2(screenLocX + 16, screenLocY)); // Draw text labeling the left side image std::string leftSide = mSwapSides ? mRightLabel : mLeftLabel; uint32_t leftLength = uint32_t(leftSide.length()) * 9; - TextRenderer::render(pContext, leftSide.c_str(), pDstFbo, float2(screenLoc - 16 - leftLength, 48)); + TextRenderer::render(pContext, leftSide, pDstFbo, float2(screenLocX - 16 - leftLength, screenLocY)); } } diff --git a/Source/RenderPasses/DebugPasses/DebugPasses.vcxproj b/Source/RenderPasses/DebugPasses/DebugPasses.vcxproj index 9457186e10..113b17a9be 100644 --- a/Source/RenderPasses/DebugPasses/DebugPasses.vcxproj +++ b/Source/RenderPasses/DebugPasses/DebugPasses.vcxproj @@ -14,7 +14,7 @@ {E92137D5-B374-4216-9A96-6AD67965B2EE} Win32Proj DebugPasses - 10.0.18362.0 + 10.0.19041.0 DebugPasses diff --git a/Source/RenderPasses/DebugPasses/InvalidPixelDetectionPass/InvalidPixelDetection.ps.slang b/Source/RenderPasses/DebugPasses/InvalidPixelDetectionPass/InvalidPixelDetection.ps.slang index 6ab2990713..85d11e145b 100644 --- a/Source/RenderPasses/DebugPasses/InvalidPixelDetectionPass/InvalidPixelDetection.ps.slang +++ b/Source/RenderPasses/DebugPasses/InvalidPixelDetectionPass/InvalidPixelDetection.ps.slang @@ -27,25 +27,13 @@ **************************************************************************/ Texture2D gTexture; -bool4 is_nan(float4 f) -{ - uint4 expMask = 0x7f800000; - uint4 mantissaMask = (~expMask) & (~0x80000000); - uint4 u = asuint(f); - if(all((expMask & u) != expMask)) return false; - if(any(mantissaMask & u)) return true; - return false; -} - float4 main(float2 texC : TEXCOORD) : SV_TARGET0 { uint2 uv; gTexture.GetDimensions(uv.x, uv.y); int2 xy = int2(uv * texC); float4 value = gTexture.Load(int3(xy, 0)); - if (any(is_nan(value))) - return float4(1, 0, 0, 1); - else if (any(isinf(value))) - return float4(0, 1, 0, 1); + if (any(isnan(value))) return float4(1, 0, 0, 1); + else if (any(isinf(value))) return float4(0, 1, 0, 1); return float4(0, 0, 0, 1); } diff --git a/Source/RenderPasses/DebugPasses/InvalidPixelDetectionPass/InvalidPixelDetectionPass.cpp b/Source/RenderPasses/DebugPasses/InvalidPixelDetectionPass/InvalidPixelDetectionPass.cpp index 65f8911924..a85d0f8484 100644 --- a/Source/RenderPasses/DebugPasses/InvalidPixelDetectionPass/InvalidPixelDetectionPass.cpp +++ b/Source/RenderPasses/DebugPasses/InvalidPixelDetectionPass/InvalidPixelDetectionPass.cpp @@ -33,6 +33,7 @@ namespace { const std::string kSrc = "src"; const std::string kDst = "dst"; + const std::string kFormatWarning = "Non-float format can't represent Inf/NaN values. Expect black output."; } InvalidPixelDetectionPass::InvalidPixelDetectionPass() @@ -86,8 +87,34 @@ void InvalidPixelDetectionPass::compile(RenderContext* pContext, const CompileDa void InvalidPixelDetectionPass::execute(RenderContext* pRenderContext, const RenderData& renderData) { - mpInvalidPixelDetectPass["gTexture"] = renderData[kSrc]->asTexture(); + const auto& pSrc = renderData[kSrc]->asTexture(); + mFormat = ResourceFormat::Unknown; + if (pSrc) + { + mFormat = pSrc->getFormat(); + if (getFormatType(mFormat) != FormatType::Float) + { + logWarning("InvalidPixelDetectionPass::execute() - " + kFormatWarning); + } + } + + mpInvalidPixelDetectPass["gTexture"] = pSrc; mpFbo->attachColorTarget(renderData[kDst]->asTexture(), 0); mpInvalidPixelDetectPass->getState()->setFbo(mpFbo); mpInvalidPixelDetectPass->execute(pRenderContext, mpFbo); } + +void InvalidPixelDetectionPass::renderUI(Gui::Widgets& widget) +{ + widget.textWrapped("Pixels are colored red if NaN, green if Inf, and black otherwise."); + + if (mFormat != ResourceFormat::Unknown) + { + widget.dummy("#space", { 1, 10 }); + widget.text("Input format: " + to_string(mFormat)); + if (getFormatType(mFormat) != FormatType::Float) + { + widget.textWrapped("Warning: " + kFormatWarning); + } + } +} diff --git a/Source/RenderPasses/DebugPasses/InvalidPixelDetectionPass/InvalidPixelDetectionPass.h b/Source/RenderPasses/DebugPasses/InvalidPixelDetectionPass/InvalidPixelDetectionPass.h index cc472633e9..8fbb7520f0 100644 --- a/Source/RenderPasses/DebugPasses/InvalidPixelDetectionPass/InvalidPixelDetectionPass.h +++ b/Source/RenderPasses/DebugPasses/InvalidPixelDetectionPass/InvalidPixelDetectionPass.h @@ -43,12 +43,15 @@ class InvalidPixelDetectionPass : public RenderPass virtual RenderPassReflection reflect(const CompileData& compileData) override; virtual void compile(RenderContext* pContext, const CompileData& compileData) override; virtual void execute(RenderContext* pRenderContext, const RenderData& renderData) override; + virtual void renderUI(Gui::Widgets& widget) override; static const char* kDesc; private: InvalidPixelDetectionPass(); + FullScreenPass::SharedPtr mpInvalidPixelDetectPass; Fbo::SharedPtr mpFbo; + ResourceFormat mFormat = ResourceFormat::Unknown; bool mReady = false; }; diff --git a/Source/RenderPasses/DepthPass/DepthPass.cpp b/Source/RenderPasses/DepthPass/DepthPass.cpp index f98c39c8c2..686bdb4026 100644 --- a/Source/RenderPasses/DepthPass/DepthPass.cpp +++ b/Source/RenderPasses/DepthPass/DepthPass.cpp @@ -103,7 +103,7 @@ void DepthPass::execute(RenderContext* pContext, const RenderData& renderData) mpState->setFbo(mpFbo); pContext->clearDsv(pDepth->getDSV().get(), 1, 0); - if (mpScene) mpScene->render(pContext, mpState.get(), mpVars.get(), mpRsState ? Scene::RenderFlags::UserRasterizerState : Scene::RenderFlags::None); + if (mpScene) mpScene->rasterize(pContext, mpState.get(), mpVars.get(), mpRsState ? Scene::RenderFlags::UserRasterizerState : Scene::RenderFlags::None); } DepthPass& DepthPass::setDepthBufferFormat(ResourceFormat format) diff --git a/Source/RenderPasses/DepthPass/DepthPass.vcxproj b/Source/RenderPasses/DepthPass/DepthPass.vcxproj index 84e3cf9028..e9bac89943 100644 --- a/Source/RenderPasses/DepthPass/DepthPass.vcxproj +++ b/Source/RenderPasses/DepthPass/DepthPass.vcxproj @@ -14,7 +14,7 @@ {730AA5B9-7DDB-40AF-A882-EB39B9D3AC7C} Win32Proj DepthPass - 10.0.18362.0 + 10.0.19041.0 DepthPass diff --git a/Source/RenderPasses/ErrorMeasurePass/ErrorMeasurePass.cpp b/Source/RenderPasses/ErrorMeasurePass/ErrorMeasurePass.cpp index 28077dd8bc..41c6f05707 100644 --- a/Source/RenderPasses/ErrorMeasurePass/ErrorMeasurePass.cpp +++ b/Source/RenderPasses/ErrorMeasurePass/ErrorMeasurePass.cpp @@ -278,8 +278,8 @@ void ErrorMeasurePass::renderUI(Gui::Widgets& widget) } widget.checkbox("Ignore background", mIgnoreBackground); - widget.tooltip(("Do not include background pixels in the error measurements.\n" - "This option requires the optional input '" + std::string(kInputChannelWorldPosition) + "' to be bound").c_str(), true); + widget.tooltip("Do not include background pixels in the error measurements.\n" + "This option requires the optional input '" + std::string(kInputChannelWorldPosition) + "' to be bound", true); widget.checkbox("Compute L2 error (rather than L1)", mComputeSquaredDifference); widget.checkbox("Compute RGB average", mComputeAverage); widget.tooltip("When enabled, the average error over the RGB components is computed when creating the difference image.\n" @@ -290,18 +290,18 @@ void ErrorMeasurePass::renderUI(Gui::Widgets& widget) "If the chosen reference doesn't exist, the error measurements are disabled.", true); // Display the filename of the reference file. const std::string referenceText = "Reference: " + getFilename(mReferenceImagePath); - widget.text(referenceText.c_str()); + widget.text(referenceText); if (!mReferenceImagePath.empty()) { - widget.tooltip(mReferenceImagePath.c_str()); + widget.tooltip(mReferenceImagePath); } // Display the filename of the measurement file. const std::string outputText = "Output: " + getFilename(mMeasurementsFilePath); - widget.text(outputText.c_str()); + widget.text(outputText); if (!mMeasurementsFilePath.empty()) { - widget.tooltip(mMeasurementsFilePath.c_str()); + widget.tooltip(mMeasurementsFilePath); } // Print numerical error (scalar and RGB). @@ -310,7 +310,7 @@ void ErrorMeasurePass::renderUI(Gui::Widgets& widget) // The checkbox was enabled; mark the running error values invalid so that they start fresh. mRunningAvgError = -1.f; } - widget.tooltip(("Exponential moving average, sigma = " + std::to_string(mRunningErrorSigma)).c_str()); + widget.tooltip("Exponential moving average, sigma = " + std::to_string(mRunningErrorSigma)); if (mMeasurements.valid) { // Use stream so we can control formatting. @@ -322,7 +322,7 @@ void ErrorMeasurePass::renderUI(Gui::Widgets& widget) (mReportRunningError ? mRunningError.r : mMeasurements.error.r) << ", " << (mReportRunningError ? mRunningError.g : mMeasurements.error.g) << ", " << (mReportRunningError ? mRunningError.b : mMeasurements.error.b); - widget.text(oss.str().c_str()); + widget.text(oss.str()); } else { diff --git a/Source/RenderPasses/ErrorMeasurePass/ErrorMeasurePass.vcxproj b/Source/RenderPasses/ErrorMeasurePass/ErrorMeasurePass.vcxproj index 46b7b40d8f..8336743c12 100644 --- a/Source/RenderPasses/ErrorMeasurePass/ErrorMeasurePass.vcxproj +++ b/Source/RenderPasses/ErrorMeasurePass/ErrorMeasurePass.vcxproj @@ -14,7 +14,7 @@ {DAE949BD-2C44-40DA-A592-7CCAB00D4A7D} Win32Proj ErrorMeasurePass - 10.0.18362.0 + 10.0.19041.0 ErrorMeasurePass diff --git a/Source/RenderPasses/ForwardLightingPass/ForwardLightingPass.cpp b/Source/RenderPasses/ForwardLightingPass/ForwardLightingPass.cpp index 08a3629a9b..0b4db65a75 100644 --- a/Source/RenderPasses/ForwardLightingPass/ForwardLightingPass.cpp +++ b/Source/RenderPasses/ForwardLightingPass/ForwardLightingPass.cpp @@ -166,14 +166,27 @@ void ForwardLightingPass::execute(RenderContext* pContext, const RenderData& ren initDepth(renderData); initFbo(pContext, renderData); - if (mpScene) - { - mpVars["PerFrameCB"]["gRenderTargetDim"] = float2(mpFbo->getWidth(), mpFbo->getHeight()); - mpVars->setTexture(kVisBuffer, renderData[kVisBuffer]->asTexture()); + if (!mpScene) return; - mpState->setFbo(mpFbo); - mpScene->render(pContext, mpState.get(), mpVars.get()); + // Update env map lighting + const auto& pEnvMap = mpScene->getEnvMap(); + if (pEnvMap && (!mpEnvMapLighting || mpEnvMapLighting->getEnvMap() != pEnvMap)) + { + mpEnvMapLighting = EnvMapLighting::create(pContext, pEnvMap); + mpEnvMapLighting->setShaderData(mpVars["gEnvMapLighting"]); + mpState->getProgram()->addDefine("_USE_ENV_MAP"); + } + else if (!pEnvMap) + { + mpEnvMapLighting = nullptr; + mpState->getProgram()->removeDefine("_USE_ENV_MAP"); } + + mpVars["PerFrameCB"]["gRenderTargetDim"] = float2(mpFbo->getWidth(), mpFbo->getHeight()); + mpVars->setTexture(kVisBuffer, renderData[kVisBuffer]->asTexture()); + + mpState->setFbo(mpFbo); + mpScene->rasterize(pContext, mpState.get(), mpVars.get()); } void ForwardLightingPass::renderUI(Gui::Widgets& widget) diff --git a/Source/RenderPasses/ForwardLightingPass/ForwardLightingPass.h b/Source/RenderPasses/ForwardLightingPass/ForwardLightingPass.h index 0bd3bce155..e3b6c9e82e 100644 --- a/Source/RenderPasses/ForwardLightingPass/ForwardLightingPass.h +++ b/Source/RenderPasses/ForwardLightingPass/ForwardLightingPass.h @@ -90,6 +90,7 @@ class ForwardLightingPass : public RenderPass GraphicsState::SharedPtr mpState; DepthStencilState::SharedPtr mpDsNoDepthWrite; Scene::SharedPtr mpScene; + EnvMapLighting::SharedPtr mpEnvMapLighting; GraphicsVars::SharedPtr mpVars; ResourceFormat mColorFormat = ResourceFormat::Unknown; diff --git a/Source/RenderPasses/ForwardLightingPass/ForwardLightingPass.slang b/Source/RenderPasses/ForwardLightingPass/ForwardLightingPass.slang index 771213fd6e..7336769696 100644 --- a/Source/RenderPasses/ForwardLightingPass/ForwardLightingPass.slang +++ b/Source/RenderPasses/ForwardLightingPass/ForwardLightingPass.slang @@ -28,12 +28,14 @@ import Scene.Raster; import Scene.Shading; import Utils.Helpers; +import Experimental.Scene.Lights.EnvMapLighting; cbuffer PerFrameCB { float2 gRenderTargetDim; }; +EnvMapLighting gEnvMapLighting; SamplerState gSampler; Texture2D visibilityBuffer; static VSOut vsData; @@ -80,10 +82,12 @@ PsOut ps(VSOut vOut, uint triangleIndex : SV_PrimitiveID) // Add the emissive component finalColor.rgb += sd.emissive; finalColor.a = sd.opacity; - finalColor.rgb += evalMaterial(sd, gScene.lightProbe).color; +#ifdef _USE_ENV_MAP + finalColor.rgb += evalMaterial(sd, gEnvMapLighting).color; +#endif psOut.color = finalColor; - psOut.normal = float4(vOut.normalW * 0.5f + 0.5f, 1.0f); + psOut.normal = float4(sd.N * 0.5f + 0.5f, 1.0f); #ifdef _OUTPUT_MOTION_VECTORS // Using vOut.posH.xy as pixel coordinate since it has the SV_Position semantic. diff --git a/Source/RenderPasses/ForwardLightingPass/ForwardLightingPass.vcxproj b/Source/RenderPasses/ForwardLightingPass/ForwardLightingPass.vcxproj index c81701b10b..53c8b9b737 100644 --- a/Source/RenderPasses/ForwardLightingPass/ForwardLightingPass.vcxproj +++ b/Source/RenderPasses/ForwardLightingPass/ForwardLightingPass.vcxproj @@ -14,7 +14,7 @@ {C028BF86-8F34-42C2-84DA-4CBE1CF783FE} Win32Proj ForwardLightingPass - 10.0.18362.0 + 10.0.19041.0 ForwardLightingPass diff --git a/Source/RenderPasses/GBuffer/GBuffer.vcxproj b/Source/RenderPasses/GBuffer/GBuffer.vcxproj index 8c1eef6e48..349bb4844c 100644 --- a/Source/RenderPasses/GBuffer/GBuffer.vcxproj +++ b/Source/RenderPasses/GBuffer/GBuffer.vcxproj @@ -14,7 +14,7 @@ {CA155B01-7528-4D81-B5CD-E801B4AA4027} Win32Proj GBuffer - 10.0.18362.0 + 10.0.19041.0 GBuffer @@ -29,6 +29,7 @@ + @@ -45,6 +46,7 @@ + diff --git a/Source/RenderPasses/GBuffer/GBuffer.vcxproj.filters b/Source/RenderPasses/GBuffer/GBuffer.vcxproj.filters index 58a73d90c1..56469a7314 100644 --- a/Source/RenderPasses/GBuffer/GBuffer.vcxproj.filters +++ b/Source/RenderPasses/GBuffer/GBuffer.vcxproj.filters @@ -17,6 +17,9 @@ VBuffer + + GBuffer + @@ -35,6 +38,9 @@ VBuffer + + GBuffer + diff --git a/Source/RenderPasses/GBuffer/GBuffer/GBufferHelpers.slang b/Source/RenderPasses/GBuffer/GBuffer/GBufferHelpers.slang index 8b57ce1154..1cd0d51235 100644 --- a/Source/RenderPasses/GBuffer/GBuffer/GBufferHelpers.slang +++ b/Source/RenderPasses/GBuffer/GBuffer/GBufferHelpers.slang @@ -57,13 +57,14 @@ GBuffer storeGBufferOutput(ShadingData sd, VertexData v) { GBuffer gbuf; - // Check that tangent space exists, otherwise create one based on the normal. - // Note that this check also catches NaNs, should they occur. - float4 tangent = v.tangentW.w != 0.f ? v.tangentW : float4(perp_stark(sd.N), 1.f); + // We store the final normal and tangent in the G-buffer. + // In order to reconstruct the bitangent later, we also need to store its handedness (sign). + float3 B = cross(sd.N, sd.T); + float tangentSign = dot(sd.B, B) >= 0.f ? 1.f : -1.f; gbuf.posW = float4(sd.posW, 1.f); gbuf.normW = float4(sd.N, 0.f); - gbuf.tangentW = tangent; + gbuf.tangentW = float4(sd.T, tangentSign); gbuf.texC = float4(sd.uv, 0.f, 0.f); MaterialParams matParams = getMaterialParams(sd); diff --git a/Source/RenderPasses/GBuffer/GBuffer/GBufferRT.cpp b/Source/RenderPasses/GBuffer/GBuffer/GBufferRT.cpp index b45e445217..4117a41775 100644 --- a/Source/RenderPasses/GBuffer/GBuffer/GBufferRT.cpp +++ b/Source/RenderPasses/GBuffer/GBuffer/GBufferRT.cpp @@ -51,9 +51,12 @@ namespace }; // Additional output channels. + const std::string kVBufferName = "vbuffer"; const ChannelList kGBufferExtraChannels = { - { "vbuffer", "gVBuffer", "Visibility buffer", true /* optional */, ResourceFormat::RG32Uint }, + { kVBufferName, "gVBuffer", "Visibility buffer", true /* optional */, ResourceFormat::Unknown /* set at runtime */ }, + { "linearZ", "gLinearZ", "Linear Z and slope", true /* optional */, ResourceFormat::RG32Float }, + { "deviceZ", "gDeviceZ", "Device (NDC) Z-buffer value", true /* optional */, ResourceFormat::R32Float }, { "mvec", "gMotionVectors", "Motion vectors", true /* optional */, ResourceFormat::RG32Float }, { "faceNormalW", "gFaceNormalW", "Face normal in world space", true /* optional */, ResourceFormat::RGBA32Float }, { "viewW", "gViewW", "View direction in world space", true /* optional */, ResourceFormat::RGBA32Float }, // TODO: Switch to packed 2x16-bit snorm format. @@ -76,6 +79,7 @@ RenderPassReflection GBufferRT::reflect(const CompileData& compileData) // Add all outputs as UAVs. addRenderPassOutputs(reflector, kGBufferChannels); addRenderPassOutputs(reflector, kGBufferExtraChannels); + reflector.getField(kVBufferName)->format(mVBufferFormat); return reflector; } @@ -162,6 +166,7 @@ void GBufferRT::execute(RenderContext* pRenderContext, const RenderData& renderD mRaytrace.pProgram->addDefine("USE_DEPTH_OF_FIELD", useDOF ? "1" : "0"); mRaytrace.pProgram->addDefine("USE_RAY_DIFFERENTIALS", mLODMode == LODMode::RayDifferentials ? "1" : "0"); mRaytrace.pProgram->addDefine("USE_RAY_CONES", mLODMode == LODMode::RayCones ? "1" : "0"); + mRaytrace.pProgram->addDefine("ADJUST_SHADING_NORMALS", mAdjustShadingNormals ? "1" : "0"); mRaytrace.pProgram->addDefine("DISABLE_ALPHA_TEST", mDisableAlphaTest ? "1" : "0"); // For optional I/O resources, set 'is_valid_' defines to inform the program of which ones it can access. diff --git a/Source/RenderPasses/GBuffer/GBuffer/GBufferRT.h b/Source/RenderPasses/GBuffer/GBuffer/GBufferRT.h index e56bffb22f..932a490a08 100644 --- a/Source/RenderPasses/GBuffer/GBuffer/GBufferRT.h +++ b/Source/RenderPasses/GBuffer/GBuffer/GBufferRT.h @@ -55,8 +55,8 @@ class GBufferRT : public GBuffer RayCones = 2, // Cone based LOD computation (not implemented yet) }; -private: - GBufferRT(const Dictionary& dict); +protected: + GBufferRT() : GBuffer() {} void parseDictionary(const Dictionary& dict) override; // Internal state @@ -75,4 +75,7 @@ class GBufferRT : public GBuffer static const char* kDesc; static void registerBindings(pybind11::module& m); friend void getPasses(Falcor::RenderPassLibrary& lib); + +private: + GBufferRT(const Dictionary& dict); }; diff --git a/Source/RenderPasses/GBuffer/GBuffer/GBufferRT.rt.slang b/Source/RenderPasses/GBuffer/GBuffer/GBufferRT.rt.slang index 4c7b600f7f..e3da05a9fb 100644 --- a/Source/RenderPasses/GBuffer/GBuffer/GBufferRT.rt.slang +++ b/Source/RenderPasses/GBuffer/GBuffer/GBufferRT.rt.slang @@ -27,6 +27,7 @@ **************************************************************************/ import Scene.Raytracing; import Scene.HitInfo; +import Scene.Intersection; import Utils.Helpers; import Utils.Timing.GpuTimer; import Utils.Sampling.SampleGenerator; @@ -44,7 +45,9 @@ RWTexture2D gEmissive; RWTexture2D gMatlExtra; // GBufferRT channels -RWTexture2D gVBuffer; +RWTexture2D gVBuffer; +RWTexture2D gLinearZ; +RWTexture2D gDeviceZ; RWTexture2D gMotionVectors; RWTexture2D gFaceNormalW; RWTexture2D gViewW; @@ -66,7 +69,7 @@ struct RayData void miss(inout RayData rayData) { uint2 launchIndex = DispatchRaysIndex().xy; - gVBuffer[launchIndex] = uint2(HitInfo::kInvalidIndex); + gVBuffer[launchIndex] = { HitInfo::kInvalidIndex }; } [shader("anyhit")] @@ -132,6 +135,24 @@ void computeAnisotropicAxesRayCones(uint meshInstanceID, uint triangleIndex, Ver computeAnisotropicEllipseAxes(v.posW, v.faceNormalW, WorldRayDirection(), coneRadiusAtHitPoint, positions, txcoords, v.texC, ddx, ddy); } +float3 computeDdxPosW(float3 posW, float3 normW) +{ + float3 projRight = normalize(cross(normW, cross(normW, gScene.camera.data.cameraV))); + float distanceToHit = length(posW - gScene.camera.data.posW); + float2 ddNdc = float2(2.f, -2.f) * (1.f / DispatchRaysDimensions().xy); + float distRight = distanceToHit * ddNdc.x / dot(normalize(gScene.camera.data.cameraV), projRight); + return distRight * projRight; +} + +float3 computeDdyPosW(float3 posW, float3 normW) +{ + float3 projUp = normalize(cross(normW, cross(normW, gScene.camera.data.cameraU))); + float distanceToHit = length(posW - gScene.camera.data.posW); + float2 ddNdc = float2(2.f, -2.f) * (1.f / DispatchRaysDimensions().xy); + float distUp = distanceToHit * ddNdc.y / dot(normalize(gScene.camera.data.cameraU), projUp); + return distUp * projUp; +} + /** Closest hit shader for primary rays. */ [shader("closesthit")] @@ -159,6 +180,10 @@ void closestHit( ShadingData sd = prepareShadingData(v, materialID, gScene.materials[materialID], gScene.materialResources[materialID], -WorldRayDirection(), 0.f); #endif +#if ADJUST_SHADING_NORMALS + adjustShadingNormal(sd, v); +#endif + // Write the outputs. GBuffer gbuf = storeGBufferOutput(sd, v); @@ -176,6 +201,29 @@ void closestHit( gFaceNormalW[launchIndex] = float4(v.faceNormalW, 0.f); } + if (isValid(gLinearZ)) + { + float4 curPosH = mul(float4(sd.posW, 1.f), gScene.camera.data.viewProjMatNoJitter); + float curLinearZ = curPosH.w; + + // TODO: Improve computation of derivatives: + float3 ddxPosW = computeDdxPosW(sd.posW, sd.faceN); + float3 ddyPosW = computeDdyPosW(sd.posW, sd.faceN); + float4 curPosH_dx = mul(float4(sd.posW + ddxPosW, 1.f), gScene.camera.data.viewProjMatNoJitter); + float4 curPosH_dy = mul(float4(sd.posW + ddxPosW, 1.f), gScene.camera.data.viewProjMatNoJitter); + float ddxLinearZ = abs(curPosH_dx.w - curLinearZ); + float ddyLinearZ = abs(curPosH_dy.w - curLinearZ); + float dLinearZ = max(ddxLinearZ, ddyLinearZ); + gLinearZ[launchIndex] = float2(curLinearZ, dLinearZ); + } + + // Output a device Z-buffer similar to raster + if (isValid(gDeviceZ)) + { + float4 curPosH = mul(float4(sd.posW, 1.f), gScene.camera.data.viewProjMatNoJitter); + gDeviceZ[launchIndex] = curPosH.z / curPosH.w; + } + // Compute motion vectors. if (isValid(gMotionVectors)) { @@ -190,7 +238,8 @@ void closestHit( if (isValid(gVBuffer)) { HitInfo hit; - hit.meshInstanceID = hitParams.getGlobalHitID(); + hit.type = InstanceType::TriangleMesh; + hit.instanceID = hitParams.getGlobalHitID(); hit.primitiveIndex = PrimitiveIndex(); hit.barycentrics = attribs.barycentrics; gVBuffer[launchIndex] = hit.encode(); @@ -230,3 +279,109 @@ void rayGen() // Write time. if (isValid(gTime)) gTime[launchIndex] = timer.getElapsed(); } + + +#ifdef USE_CURVES +/** ******************************** Curve intersection ******************************** */ + +[shader("intersection")] +void trivialIntersection(uniform HitShaderParams hitParams) +{ + BuiltInTriangleIntersectionAttributes attribs; + ReportHit(RayTCurrent(), 0, attribs); +} + +[shader("intersection")] +void linearSweptSphereIntersection(uniform HitShaderParams hitParams) +{ + BuiltInTriangleIntersectionAttributes attribs; + + float3 rayDir = WorldRayDirection(); + const float rayLength = length(rayDir); + const float invRayLength = 1.f / rayLength; + rayDir *= invRayLength; + + const float tmax = RayTCurrent(); + + uint curveInstanceID = gScene.getCurveInstanceID(InstanceID(), hitParams.geometryIndex); + uint v0Index = gScene.getFirstCurveVertexIndex(curveInstanceID, PrimitiveIndex()); + StaticCurveVertexData v0 = gScene.getCurveVertex(v0Index); + StaticCurveVertexData v1 = gScene.getCurveVertex(v0Index + 1); + + float2 result; + bool isect = intersectLinearSweptSphere(WorldRayOrigin(), rayDir, float4(v0.position, v0.radius), float4(v1.position, v1.radius), result); + result.x *= invRayLength; + + if (isect && result.x < RayTCurrent()) + { + attribs.barycentrics = float2(result.y, 0.f); + ReportHit(result.x, 0, attribs); + } +} + +// Functions mostly identical to primaryClosestHit() +[shader("closesthit")] +void curveClosestHit(uniform HitShaderParams hitParams, inout RayData hitData, in BuiltInTriangleIntersectionAttributes attribs) +{ + uint2 launchIndex = DispatchRaysIndex().xy; + + const float3 posW = WorldRayOrigin() + RayTCurrent() * WorldRayDirection(); + const uint curveInstanceID = gScene.getCurveInstanceID(InstanceID(), hitParams.geometryIndex); + const uint curveSegIndex = PrimitiveIndex(); + const uint materialID = gScene.getCurveMaterialID(curveInstanceID); + + float radius; + VertexData v = gScene.getVertexDataFromCurve(curveInstanceID, curveSegIndex, attribs.barycentrics.x, posW, radius); + + ShadingData sd = prepareShadingData(v, materialID, gScene.materials[materialID], gScene.materialResources[materialID], -WorldRayDirection(), 0.f); + + GBuffer gbuf = storeGBufferOutput(sd, v); + + gPosW[launchIndex] = float4(gbuf.posW.xyz, radius); + gNormW[launchIndex] = gbuf.normW; + gTangentW[launchIndex] = gbuf.tangentW; + gTexC[launchIndex] = gbuf.texC; + gDiffuseOpacity[launchIndex] = gbuf.diffuseOpacity; + gSpecRough[launchIndex] = gbuf.specRough; + gEmissive[launchIndex] = gbuf.emissive; + gMatlExtra[launchIndex] = gbuf.matlExtra; + + if (isValid(gFaceNormalW)) + { + gFaceNormalW[launchIndex] = float4(v.faceNormalW, 0.f); + } + + if (isValid(gLinearZ)) + { + float4 curPosH = mul(float4(sd.posW, 1.f), gScene.camera.data.viewProjMatNoJitter); + float curLinearZ = curPosH.w; + + // TODO: Improve computation of derivatives: + float3 ddxPosW = computeDdxPosW(sd.posW, sd.faceN); + float3 ddyPosW = computeDdyPosW(sd.posW, sd.faceN); + float4 curPosH_dx = mul(float4(sd.posW + ddxPosW, 1.f), gScene.camera.data.viewProjMatNoJitter); + float4 curPosH_dy = mul(float4(sd.posW + ddxPosW, 1.f), gScene.camera.data.viewProjMatNoJitter); + float ddxLinearZ = abs(curPosH_dx.w - curLinearZ); + float ddyLinearZ = abs(curPosH_dy.w - curLinearZ); + float dLinearZ = max(ddxLinearZ, ddyLinearZ); + gLinearZ[launchIndex] = float2(curLinearZ, dLinearZ); + } + + // TODO: Compute motion vectors for curves. + if (isValid(gMotionVectors)) + { + gMotionVectors[launchIndex] = float2(0.f); + } + + // Encode hit information. + if (isValid(gVBuffer)) + { + HitInfo hit; + hit.type = InstanceType::Curve; + hit.instanceID = curveInstanceID; + hit.primitiveIndex = PrimitiveIndex(); + hit.barycentrics = attribs.barycentrics; + gVBuffer[launchIndex] = hit.encode(); + } +} +#endif diff --git a/Source/RenderPasses/GBuffer/GBuffer/GBufferRTCurves.cpp b/Source/RenderPasses/GBuffer/GBuffer/GBufferRTCurves.cpp new file mode 100644 index 0000000000..1e633901b6 --- /dev/null +++ b/Source/RenderPasses/GBuffer/GBuffer/GBufferRTCurves.cpp @@ -0,0 +1,74 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#include "Falcor.h" +#include "RenderGraph/RenderPassStandardFlags.h" +#include "GBufferRTCurves.h" + +const char* GBufferRTCurves::kDesc = "Ray traced G-buffer generation pass that supports curves"; + +namespace +{ + const std::string kProgramFile = "RenderPasses/GBuffer/GBuffer/GBufferRT.rt.slang"; + + // Ray tracing settings that affect the traversal stack size. Set as small as possible. + const uint32_t kMaxPayloadSizeBytes = 4; + const uint32_t kMaxAttributesSizeBytes = 8; + const uint32_t kMaxRecursionDepth = 1; +}; + +GBufferRTCurves::SharedPtr GBufferRTCurves::create(RenderContext* pRenderContext, const Dictionary& dict) +{ + return SharedPtr(new GBufferRTCurves(dict)); +} + +GBufferRTCurves::GBufferRTCurves(const Dictionary& dict) + : GBufferRT() +{ + parseDictionary(dict); + + // Create random engine + mpSampleGenerator = SampleGenerator::create(SAMPLE_GENERATOR_DEFAULT); + + // Create ray tracing program + RtProgram::Desc desc; + desc.addShaderLibrary(kProgramFile).setRayGen("rayGen"); + desc.addHitGroup(0, "closestHit", "anyHit").addMiss(0, "miss"); + + // Add intersection shaders for custom primitives + // Now we only support curve primitives (represented as linear swept spheres) + desc.addIntersection(0, "linearSweptSphereIntersection"); + desc.addAABBHitGroup(0, "curveClosestHit", ""); + + desc.setMaxTraceRecursionDepth(kMaxRecursionDepth); + desc.addDefines(mpSampleGenerator->getDefines()); + desc.addDefine("USE_CURVES", "1"); + mRaytrace.pProgram = RtProgram::create(desc, kMaxPayloadSizeBytes, kMaxAttributesSizeBytes); + + // Set default cull mode + setCullMode(mCullMode); +} diff --git a/Source/RenderPasses/GBuffer/GBuffer/GBufferRTCurves.h b/Source/RenderPasses/GBuffer/GBuffer/GBufferRTCurves.h new file mode 100644 index 0000000000..70a2141c4e --- /dev/null +++ b/Source/RenderPasses/GBuffer/GBuffer/GBufferRTCurves.h @@ -0,0 +1,55 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ + +/** This pass is for INTERNAL distribution only. +*/ + +#pragma once +#include "GBufferRT.h" +#include "Utils/Sampling/SampleGenerator.h" + +using namespace Falcor; + +/** Ray traced G-buffer pass that supports curves. + This pass renders a fixed set of G-buffer channels using ray tracing. +*/ +class GBufferRTCurves : public GBufferRT +{ +public: + using SharedPtr = std::shared_ptr; + + static SharedPtr create(RenderContext* pRenderContext = nullptr, const Dictionary& dict = {}); + + std::string getDesc() override { return kDesc; } + +private: + GBufferRTCurves(const Dictionary& dict); + + static const char* kDesc; + friend void getPasses(Falcor::RenderPassLibrary& lib); +}; diff --git a/Source/RenderPasses/GBuffer/GBuffer/GBufferRaster.3d.slang b/Source/RenderPasses/GBuffer/GBuffer/GBufferRaster.3d.slang index ca054d2a35..a226e276e7 100644 --- a/Source/RenderPasses/GBuffer/GBuffer/GBufferRaster.3d.slang +++ b/Source/RenderPasses/GBuffer/GBuffer/GBufferRaster.3d.slang @@ -34,7 +34,7 @@ import GBufferHelpers; import Experimental.Scene.Material.TexLODHelpers; // UAV output channels -RWTexture2D gVBuffer; +RWTexture2D gVBuffer; RWTexture2D gMotionVectors; RWTexture2D gFaceNormalW; RWTexture2D gPosNormalFwidth; @@ -77,6 +77,10 @@ GBufferPSOut psMain(VSOut vsOut, uint triangleIndex : SV_PrimitiveID, float3 bar float3 viewDir = normalize(gScene.camera.getPosition() - v.posW); ShadingData sd = prepareShadingData(v, vsOut.materialID, gScene.materials[vsOut.materialID], gScene.materialResources[vsOut.materialID], viewDir); +#if ADJUST_SHADING_NORMALS + adjustShadingNormal(sd, v); +#endif + GBuffer gbuf = storeGBufferOutput(sd, v); // Store render target outputs. @@ -128,7 +132,8 @@ GBufferPSOut psMain(VSOut vsOut, uint triangleIndex : SV_PrimitiveID, float3 bar if (isValid(gVBuffer)) { HitInfo hit; - hit.meshInstanceID = vsOut.meshInstanceID; + hit.type = InstanceType::TriangleMesh; + hit.instanceID = vsOut.meshInstanceID; hit.primitiveIndex = triangleIndex; hit.barycentrics = barycentrics.yz; gVBuffer[ipos] = hit.encode(); diff --git a/Source/RenderPasses/GBuffer/GBuffer/GBufferRaster.cpp b/Source/RenderPasses/GBuffer/GBuffer/GBufferRaster.cpp index 87bd6f8ce3..63e0419966 100644 --- a/Source/RenderPasses/GBuffer/GBuffer/GBufferRaster.cpp +++ b/Source/RenderPasses/GBuffer/GBuffer/GBufferRaster.cpp @@ -34,13 +34,14 @@ const char* GBufferRaster::kDesc = "Rasterized G-buffer generation pass"; namespace { const std::string kProgramFile = "RenderPasses/GBuffer/GBuffer/GBufferRaster.3d.slang"; - const std::string shaderModel = "6_1"; + const std::string shaderModel = "6_2"; // Additional output channels. // TODO: Some are RG32 floats now. I'm sure that all of these could be fp16. + const std::string kVBufferName = "vbuffer"; const ChannelList kGBufferExtraChannels = { - { "vbuffer", "gVBuffer", "Visibility buffer", true /* optional */, ResourceFormat::RG32Uint }, + { kVBufferName, "gVBuffer", "Visibility buffer", true /* optional */, ResourceFormat::Unknown /* set at runtime */ }, { "mvec", "gMotionVectors", "Motion vectors", true /* optional */, ResourceFormat::RG32Float }, { "faceNormalW", "gFaceNormalW", "Face normal in world space", true /* optional */, ResourceFormat::RGBA32Float }, { "pnFwidth", "gPosNormalFwidth", "position and normal filter width", true /* optional */, ResourceFormat::RG32Float }, @@ -62,6 +63,7 @@ RenderPassReflection GBufferRaster::reflect(const CompileData& compileData) // The default channels are written as render targets, the rest as UAVs as there is way to assign/pack render targets yet. addRenderPassOutputs(reflector, kGBufferChannels, Resource::BindFlags::RenderTarget); addRenderPassOutputs(reflector, kGBufferExtraChannels, Resource::BindFlags::UnorderedAccess); + reflector.getField(kVBufferName)->format(mVBufferFormat); return reflector; } @@ -74,6 +76,11 @@ GBufferRaster::SharedPtr GBufferRaster::create(RenderContext* pRenderContext, co GBufferRaster::GBufferRaster(const Dictionary& dict) : GBuffer() { + if (!gpDevice->isFeatureSupported(Device::SupportedFeatures::Barycentrics)) + { + throw std::exception("Pixel shader barycentrics are not supported by the current device"); + } + parseDictionary(dict); // Create raster program @@ -167,6 +174,7 @@ void GBufferRaster::execute(RenderContext* pRenderContext, const RenderData& ren } // Set program defines. + mRaster.pProgram->addDefine("ADJUST_SHADING_NORMALS", mAdjustShadingNormals ? "1" : "0"); mRaster.pProgram->addDefine("DISABLE_ALPHA_TEST", mDisableAlphaTest ? "1" : "0"); // For optional I/O resources, set 'is_valid_' defines to inform the program of which ones it can access. @@ -199,7 +207,7 @@ void GBufferRaster::execute(RenderContext* pRenderContext, const RenderData& ren mRaster.pState->setFbo(mpFbo); // Sets the viewport Scene::RenderFlags flags = mForceCullMode ? Scene::RenderFlags::UserRasterizerState : Scene::RenderFlags::None; - mpScene->render(pRenderContext, mRaster.pState.get(), mRaster.pVars.get(), flags); + mpScene->rasterize(pRenderContext, mRaster.pState.get(), mRaster.pVars.get(), flags); mGBufferParams.frameCount++; } diff --git a/Source/RenderPasses/GBuffer/GBufferBase.cpp b/Source/RenderPasses/GBuffer/GBufferBase.cpp index e73a234c73..473a82084e 100644 --- a/Source/RenderPasses/GBuffer/GBufferBase.cpp +++ b/Source/RenderPasses/GBuffer/GBufferBase.cpp @@ -28,6 +28,7 @@ #include "GBufferBase.h" #include "GBuffer/GBufferRaster.h" #include "GBuffer/GBufferRT.h" +#include "GBuffer/GBufferRTCurves.h" #include "VBuffer/VBufferRaster.h" #include "VBuffer/VBufferRT.h" @@ -41,6 +42,7 @@ extern "C" __declspec(dllexport) void getPasses(Falcor::RenderPassLibrary& lib) { lib.registerClass("GBufferRaster", GBufferRaster::kDesc, GBufferRaster::create); lib.registerClass("GBufferRT", GBufferRT::kDesc, GBufferRT::create); + lib.registerClass("GBufferRTCurves", GBufferRTCurves::kDesc, GBufferRTCurves::create); lib.registerClass("VBufferRaster", VBufferRaster::kDesc, VBufferRaster::create); lib.registerClass("VBufferRT", VBufferRT::kDesc, VBufferRT::create); @@ -63,6 +65,7 @@ namespace const char kSamplePattern[] = "samplePattern"; const char kSampleCount[] = "sampleCount"; const char kDisableAlphaTest[] = "disableAlphaTest"; + const char kAdjustShadingNormals[] = "adjustShadingNormals"; // UI variables. const Gui::DropdownList kSamplePatternList = @@ -81,6 +84,7 @@ void GBufferBase::parseDictionary(const Dictionary& dict) if (key == kSamplePattern) mSamplePattern = value; else if (key == kSampleCount) mSampleCount = value; else if (key == kDisableAlphaTest) mDisableAlphaTest = value; + else if (key == kAdjustShadingNormals) mAdjustShadingNormals = value; // TODO: Check for unparsed fields, including those parsed in derived classes. } } @@ -91,6 +95,7 @@ Dictionary GBufferBase::getScriptingDictionary() dict[kSamplePattern] = mSamplePattern; dict[kSampleCount] = mSampleCount; dict[kDisableAlphaTest] = mDisableAlphaTest; + dict[kAdjustShadingNormals] = mAdjustShadingNormals; return dict; } @@ -113,6 +118,9 @@ void GBufferBase::renderUI(Gui::Widgets& widget) } mOptionsChanged |= widget.checkbox("Disable Alpha Test", mDisableAlphaTest); + + mOptionsChanged |= widget.checkbox("Adjust shading normals", mAdjustShadingNormals); + widget.tooltip("Enables adjustment of the shading normals to reduce the risk of black pixels due to back-facing vectors.", true); } void GBufferBase::compile(RenderContext* pContext, const CompileData& compileData) @@ -138,6 +146,10 @@ void GBufferBase::execute(RenderContext* pRenderContext, const RenderData& rende mOptionsChanged = false; } + // Pass flag for adjust shading normals to subsequent passes via the dictionary. + // Adjusted shading normals cannot be passed via the VBuffer, so this flag allows consuming passes to compute them when enabled. + dict[Falcor::kRenderPassGBufferAdjustShadingNormals] = mAdjustShadingNormals; + // Setup camera with sample generator. if (mpScene) mpScene->getCamera()->setPatternGenerator(mpSampleGenerator, mInvFrameDim); } @@ -146,6 +158,17 @@ void GBufferBase::setScene(RenderContext* pRenderContext, const Scene::SharedPtr { mpScene = pScene; updateSamplePattern(); + + if (pScene) + { + // Trigger graph recompilation if we need to change the V-buffer format. + ResourceFormat format = pScene->getHitInfo().getFormat(); + if (format != mVBufferFormat) + { + mVBufferFormat = format; + mPassChangedCB(); + } + } } static CPUSampleGenerator::SharedPtr createSamplePattern(GBufferBase::SamplePattern type, uint32_t sampleCount) diff --git a/Source/RenderPasses/GBuffer/GBufferBase.h b/Source/RenderPasses/GBuffer/GBufferBase.h index 0fa9c24f10..16d0e7895f 100644 --- a/Source/RenderPasses/GBuffer/GBufferBase.h +++ b/Source/RenderPasses/GBuffer/GBufferBase.h @@ -62,11 +62,13 @@ class GBufferBase : public RenderPass uint2 mFrameDim = {}; float2 mInvFrameDim = {}; + ResourceFormat mVBufferFormat = HitInfo::kDefaultFormat; // UI variables SamplePattern mSamplePattern = SamplePattern::Center; ///< Which camera jitter sample pattern to use. uint32_t mSampleCount = 16; ///< Sample count for camera jitter. bool mDisableAlphaTest = false; ///< Disable alpha test. + bool mAdjustShadingNormals = true; ///< Adjust shading normals. bool mOptionsChanged = false; static void registerBindings(pybind11::module& m); diff --git a/Source/RenderPasses/GBuffer/VBuffer/VBufferRT.cpp b/Source/RenderPasses/GBuffer/VBuffer/VBufferRT.cpp index c9c0d60612..5e478c570d 100644 --- a/Source/RenderPasses/GBuffer/VBuffer/VBufferRT.cpp +++ b/Source/RenderPasses/GBuffer/VBuffer/VBufferRT.cpp @@ -41,8 +41,8 @@ namespace const uint32_t kMaxAttributesSizeBytes = 8; const uint32_t kMaxRecursionDepth = 1; - const std::string kOutputName = "vbuffer"; - const std::string kOutputDesc = "V-buffer packed into 64 bits (indices + barys)"; + const std::string kVBufferName = "vbuffer"; + const std::string kVBufferDesc = "V-buffer in packed format (indices + barycentrics)"; // Additional output channels. const ChannelList kVBufferExtraChannels = @@ -55,7 +55,7 @@ RenderPassReflection VBufferRT::reflect(const CompileData& compileData) { RenderPassReflection reflector; - reflector.addOutput(kOutputName, kOutputDesc).bindFlags(Resource::BindFlags::UnorderedAccess).format(ResourceFormat::RG32Uint); + reflector.addOutput(kVBufferName, kVBufferDesc).bindFlags(Resource::BindFlags::UnorderedAccess).format(mVBufferFormat); addRenderPassOutputs(reflector, kVBufferExtraChannels); return reflector; @@ -103,7 +103,7 @@ void VBufferRT::execute(RenderContext* pRenderContext, const RenderData& renderD // If there is no scene, clear the output and return. if (mpScene == nullptr) { - auto pOutput = renderData[kOutputName]->asTexture(); + auto pOutput = renderData[kVBufferName]->asTexture(); pRenderContext->clearUAV(pOutput->getUAV().get(), uint4(HitInfo::kInvalidIndex)); auto clear = [&](const ChannelDesc& channel) @@ -142,7 +142,7 @@ void VBufferRT::execute(RenderContext* pRenderContext, const RenderData& renderD // Bind resources. ShaderVar var = mRaytrace.pVars->getRootVar(); var["PerFrameCB"]["frameCount"] = mFrameCount++; - var["gVBuffer"] = renderData[kOutputName]->asTexture(); + var["gVBuffer"] = renderData[kVBufferName]->asTexture(); // Bind output channels as UAV buffers. auto bind = [&](const ChannelDesc& channel) diff --git a/Source/RenderPasses/GBuffer/VBuffer/VBufferRT.rt.slang b/Source/RenderPasses/GBuffer/VBuffer/VBufferRT.rt.slang index 829755b914..69dcb4497a 100644 --- a/Source/RenderPasses/GBuffer/VBuffer/VBufferRT.rt.slang +++ b/Source/RenderPasses/GBuffer/VBuffer/VBufferRT.rt.slang @@ -35,7 +35,7 @@ cbuffer PerFrameCB uint frameCount; }; -RWTexture2D gVBuffer; +RWTexture2D gVBuffer; RWTexture2D gTime; #define isValid(name) (is_valid_##name != 0) @@ -52,7 +52,7 @@ void miss(inout RayData rayData) { // Write invalid hit to output buffer. uint2 launchIndex = DispatchRaysIndex().xy; - gVBuffer[launchIndex] = uint2(HitInfo::kInvalidIndex); + gVBuffer[launchIndex] = { HitInfo::kInvalidIndex }; } [shader("anyhit")] @@ -77,14 +77,14 @@ void closestHit( { // Store hit information. Note we don't access the materials here. HitInfo hit; - hit.meshInstanceID = hitParams.getGlobalHitID(); + hit.type = InstanceType::TriangleMesh; + hit.instanceID = hitParams.getGlobalHitID(); hit.primitiveIndex = PrimitiveIndex(); hit.barycentrics = attribs.barycentrics; - uint2 packedHitInfo = hit.encode(); // Write hit info to output buffer. uint2 launchIndex = DispatchRaysIndex().xy; - gVBuffer[launchIndex] = packedHitInfo; + gVBuffer[launchIndex] = hit.encode(); } diff --git a/Source/RenderPasses/GBuffer/VBuffer/VBufferRaster.3d.slang b/Source/RenderPasses/GBuffer/VBuffer/VBufferRaster.3d.slang index c34a2038f1..6b24a89d61 100644 --- a/Source/RenderPasses/GBuffer/VBuffer/VBufferRaster.3d.slang +++ b/Source/RenderPasses/GBuffer/VBuffer/VBufferRaster.3d.slang @@ -30,7 +30,7 @@ import Scene.HitInfo; struct VBufferPSOut { - uint2 packedHitInfo : SV_TARGET0; + PackedHitInfo packedHitInfo : SV_TARGET0; }; struct VBufferVSOut @@ -83,7 +83,8 @@ VBufferPSOut psMain(VBufferVSOut vsOut, uint triangleIndex : SV_PrimitiveID, flo // Store hit information. HitInfo hit; - hit.meshInstanceID = vsOut.meshInstanceID; + hit.type = InstanceType::TriangleMesh; + hit.instanceID = vsOut.meshInstanceID; hit.primitiveIndex = triangleIndex; hit.barycentrics = barycentrics.yz; psOut.packedHitInfo = hit.encode(); diff --git a/Source/RenderPasses/GBuffer/VBuffer/VBufferRaster.cpp b/Source/RenderPasses/GBuffer/VBuffer/VBufferRaster.cpp index 873ec804e2..a2013b8232 100644 --- a/Source/RenderPasses/GBuffer/VBuffer/VBufferRaster.cpp +++ b/Source/RenderPasses/GBuffer/VBuffer/VBufferRaster.cpp @@ -34,10 +34,10 @@ const char* VBufferRaster::kDesc = "Rasterized V-buffer generation pass"; namespace { const std::string kProgramFile = "RenderPasses/GBuffer/VBuffer/VBufferRaster.3d.slang"; - const std::string kShaderModel = "6_1"; + const std::string kShaderModel = "6_2"; - const std::string kOutputName = "vbuffer"; - const std::string kOutputDesc = "V-buffer packed into 64 bits (indices + barys)"; + const std::string kVBufferName = "vbuffer"; + const std::string kVBufferDesc = "V-buffer in packed format (indices + barycentrics)"; const std::string kDepthName = "depth"; } @@ -47,7 +47,7 @@ RenderPassReflection VBufferRaster::reflect(const CompileData& compileData) RenderPassReflection reflector; reflector.addOutput(kDepthName, "Depth buffer").format(ResourceFormat::D32Float).bindFlags(Resource::BindFlags::DepthStencil); - reflector.addOutput(kOutputName, kOutputDesc).bindFlags(Resource::BindFlags::RenderTarget | Resource::BindFlags::UnorderedAccess).format(ResourceFormat::RG32Uint); + reflector.addOutput(kVBufferName, kVBufferDesc).bindFlags(Resource::BindFlags::RenderTarget | Resource::BindFlags::UnorderedAccess).format(mVBufferFormat); return reflector; } @@ -60,6 +60,11 @@ VBufferRaster::SharedPtr VBufferRaster::create(RenderContext* pRenderContext, co VBufferRaster::VBufferRaster(const Dictionary& dict) : GBufferBase() { + if (!gpDevice->isFeatureSupported(Device::SupportedFeatures::Barycentrics)) + { + throw std::exception("Pixel shader barycentrics are not supported by the current device"); + } + parseDictionary(dict); // Create raster program @@ -104,7 +109,7 @@ void VBufferRaster::execute(RenderContext* pRenderContext, const RenderData& ren // Clear depth and output buffer. auto pDepth = renderData[kDepthName]->asTexture(); - auto pOutput = renderData[kOutputName]->asTexture(); + auto pOutput = renderData[kVBufferName]->asTexture(); pRenderContext->clearUAV(pOutput->getUAV().get(), uint4(HitInfo::kInvalidIndex)); // Clear as UAV for integer clear value pRenderContext->clearDsv(pDepth->getDSV().get(), 1.f, 0); @@ -128,5 +133,5 @@ void VBufferRaster::execute(RenderContext* pRenderContext, const RenderData& ren mRaster.pState->setFbo(mpFbo); // Sets the viewport // Rasterize the scene. - mpScene->render(pRenderContext, mRaster.pState.get(), mRaster.pVars.get()); + mpScene->rasterize(pRenderContext, mRaster.pState.get(), mRaster.pVars.get()); } diff --git a/Source/RenderPasses/GBuffer/VBuffer/VBufferRaster.h b/Source/RenderPasses/GBuffer/VBuffer/VBufferRaster.h index 1987c12ae2..120fce2ac7 100644 --- a/Source/RenderPasses/GBuffer/VBuffer/VBufferRaster.h +++ b/Source/RenderPasses/GBuffer/VBuffer/VBufferRaster.h @@ -52,7 +52,7 @@ class VBufferRaster : public GBufferBase VBufferRaster(const Dictionary& dict); // Internal state - Fbo::SharedPtr mpFbo; + Fbo::SharedPtr mpFbo; struct { diff --git a/Source/RenderPasses/ImageLoader/ImageLoader.cpp b/Source/RenderPasses/ImageLoader/ImageLoader.cpp index adfbb05fd1..9a7007c981 100644 --- a/Source/RenderPasses/ImageLoader/ImageLoader.cpp +++ b/Source/RenderPasses/ImageLoader/ImageLoader.cpp @@ -27,21 +27,9 @@ **************************************************************************/ #include "ImageLoader.h" -// Don't remove this. it's required for hot-reload to function properly -extern "C" __declspec(dllexport) const char* getProjDir() -{ - return PROJECT_DIR; -} - -extern "C" __declspec(dllexport) void getPasses(Falcor::RenderPassLibrary& lib) -{ - lib.registerClass("ImageLoader", "Load an image into a texture", ImageLoader::create); -} - -const char* ImageLoader::kDesc = "Load an image into a texture"; - namespace { + const char kDesc[] = "Load an image into a texture"; const std::string kDst = "dst"; const std::string kOutputFormat = "outputFormat"; @@ -52,6 +40,19 @@ namespace const std::string kMipLevel = "mipLevel"; } +// Don't remove this. it's required for hot-reload to function properly +extern "C" __declspec(dllexport) const char* getProjDir() +{ + return PROJECT_DIR; +} + +extern "C" __declspec(dllexport) void getPasses(Falcor::RenderPassLibrary& lib) +{ + lib.registerClass("ImageLoader", kDesc, ImageLoader::create); +} + +std::string ImageLoader::getDesc() { return kDesc; } + RenderPassReflection ImageLoader::reflect(const CompileData& compileData) { RenderPassReflection reflector; @@ -61,32 +62,41 @@ RenderPassReflection ImageLoader::reflect(const CompileData& compileData) ImageLoader::SharedPtr ImageLoader::create(RenderContext* pRenderContext, const Dictionary& dict) { - SharedPtr pPass = SharedPtr(new ImageLoader); + return SharedPtr(new ImageLoader(dict)); +} +ImageLoader::ImageLoader(const Dictionary& dict) +{ for (const auto& [key, value] : dict) { - if (key == kOutputFormat) pPass->mOutputFormat = value; - else if (key == kImage) pPass->mImageName = value.operator std::string(); - else if (key == kSrgb) pPass->mLoadSRGB = value; - else if (key == kMips) pPass->mGenerateMips = value; - else if (key == kArraySlice) pPass->mArraySlice = value; - else if (key == kMipLevel) pPass->mMipLevel = value; + if (key == kOutputFormat) mOutputFormat = value; + else if (key == kImage) mImageName = value.operator std::string(); + else if (key == kSrgb) mLoadSRGB = value; + else if (key == kMips) mGenerateMips = value; + else if (key == kArraySlice) mArraySlice = value; + else if (key == kMipLevel) mMipLevel = value; else logWarning("Unknown field '" + key + "' in a ImageLoader dictionary"); } - if (pPass->mImageName.size()) + if (!mImageName.empty()) { - pPass->mpTex = Texture::createFromFile(pPass->mImageName, pPass->mGenerateMips, pPass->mLoadSRGB); + // Find the full path of the specified image. + // We retain this for later as the search paths may change during execution. + std::string fullPath; + if (findFileInDataDirectories(mImageName, fullPath)) + { + mImageName = fullPath; + mpTex = Texture::createFromFile(mImageName, mGenerateMips, mLoadSRGB); + } + if (!mpTex) throw std::runtime_error("ImageLoader() - Failed to load image file '" + mImageName + "'"); } - - return pPass; } Dictionary ImageLoader::getScriptingDictionary() { Dictionary dict; if (mOutputFormat != ResourceFormat::Unknown) dict[kOutputFormat] = mOutputFormat; - dict[kImage] = mImageName; + dict[kImage] = stripDataDirectories(mImageName); dict[kMips] = mGenerateMips; dict[kSrgb] = mLoadSRGB; dict[kArraySlice] = mArraySlice; @@ -94,23 +104,25 @@ Dictionary ImageLoader::getScriptingDictionary() return dict; } -ImageLoader::ImageLoader() -{ -} - void ImageLoader::compile(RenderContext* pContext, const CompileData& compileData) { - if (!mpTex) throw std::runtime_error("ImageLoader::compile - No image loaded!"); + if (!mpTex) throw std::runtime_error("ImageLoader::compile() - No image loaded!"); } void ImageLoader::execute(RenderContext* pContext, const RenderData& renderData) { const auto& pDstTex = renderData[kDst]->asTexture(); + assert(pDstTex); + mOutputFormat = pDstTex->getFormat(); + if (!mpTex) { pContext->clearRtv(pDstTex->getRTV().get(), float4(0, 0, 0, 0)); return; } + + mMipLevel = std::min(mMipLevel, mpTex->getMipCount() - 1); + mArraySlice = std::min(mArraySlice, mpTex->getArraySize() - 1); pContext->blit(mpTex->getSRV(mMipLevel, 1, mArraySlice, 1), pDstTex->getRTV()); } @@ -119,20 +131,22 @@ void ImageLoader::renderUI(Gui::Widgets& widget) bool reloadImage = widget.textbox("Image File", mImageName); reloadImage |= widget.checkbox("Load As SRGB", mLoadSRGB); reloadImage |= widget.checkbox("Generate Mipmaps", mGenerateMips); - if (mGenerateMips) + + if (widget.button("Load File")) { - reloadImage |= widget.slider("Mip Level", mMipLevel, 0u, mpTex ? mpTex->getMipCount() : 0u); + reloadImage |= openFileDialog({}, mImageName); } - reloadImage |= widget.slider("Array Slice", mArraySlice, 0u, mpTex ? mpTex->getArraySize() : 0u); - - if (widget.button("Load File")) { reloadImage |= openFileDialog({}, mImageName); } if (mpTex) { + if (mpTex->getMipCount() > 1) widget.slider("Mip Level", mMipLevel, 0u, mpTex->getMipCount() - 1); + if (mpTex->getArraySize() > 1) widget.slider("Array Slice", mArraySlice, 0u, mpTex->getArraySize() - 1); + widget.image(mImageName.c_str(), mpTex, { 320, 320 }); + widget.text("Output format: " + to_string(mOutputFormat)); } - if (reloadImage && mImageName.size()) + if (reloadImage && !mImageName.empty()) { mImageName = stripDataDirectories(mImageName); mpTex = Texture::createFromFile(mImageName, mGenerateMips, mLoadSRGB); diff --git a/Source/RenderPasses/ImageLoader/ImageLoader.h b/Source/RenderPasses/ImageLoader/ImageLoader.h index 7eb997d2ba..950e649d65 100644 --- a/Source/RenderPasses/ImageLoader/ImageLoader.h +++ b/Source/RenderPasses/ImageLoader/ImageLoader.h @@ -36,21 +36,19 @@ class ImageLoader : public RenderPass public: using SharedPtr = std::shared_ptr; - static const char* kDesc; - /** Create a new object */ - static SharedPtr create(RenderContext* pRenderContext = nullptr, const Dictionary& dict = {}); + static SharedPtr create(RenderContext* pRenderContext, const Dictionary& dict); virtual RenderPassReflection reflect(const CompileData& compileData) override; virtual void compile(RenderContext* pContext, const CompileData& compileData) override; virtual void execute(RenderContext* pContext, const RenderData& renderData) override; virtual void renderUI(Gui::Widgets& widget) override; virtual Dictionary getScriptingDictionary() override; - virtual std::string getDesc() override { return kDesc; } + virtual std::string getDesc() override; private: - ImageLoader(); + ImageLoader(const Dictionary& dict); ResourceFormat mOutputFormat = ResourceFormat::Unknown; Texture::SharedPtr mpTex; diff --git a/Source/RenderPasses/ImageLoader/ImageLoader.vcxproj b/Source/RenderPasses/ImageLoader/ImageLoader.vcxproj index cc9a3190e5..32fe647421 100644 --- a/Source/RenderPasses/ImageLoader/ImageLoader.vcxproj +++ b/Source/RenderPasses/ImageLoader/ImageLoader.vcxproj @@ -14,7 +14,7 @@ {8CE33F5F-CFB9-4B25-9298-990DAE982991} Win32Proj ImageLoader - 10.0.18362.0 + 10.0.19041.0 ImageLoader diff --git a/Source/RenderPasses/MegakernelPathTracer/Data/MegakernelPathTracer.py b/Source/RenderPasses/MegakernelPathTracer/Data/MegakernelPathTracer.py new file mode 100644 index 0000000000..6badef4bc1 --- /dev/null +++ b/Source/RenderPasses/MegakernelPathTracer/Data/MegakernelPathTracer.py @@ -0,0 +1,32 @@ +def render_graph_PathTracerGraph(): + g = RenderGraph("PathTracerGraph") + loadRenderPassLibrary("AccumulatePass.dll") + loadRenderPassLibrary("GBuffer.dll") + loadRenderPassLibrary("ToneMapper.dll") + loadRenderPassLibrary("MegakernelPathTracer.dll") + AccumulatePass = createPass("AccumulatePass", {'enableAccumulation': True}) + g.addPass(AccumulatePass, "AccumulatePass") + ToneMappingPass = createPass("ToneMapper", {'autoExposure': False, 'exposureCompensation': 0.0}) + g.addPass(ToneMappingPass, "ToneMappingPass") + GBufferRT = createPass("GBufferRT", {'forceCullMode': False, 'cull': CullMode.CullBack, 'samplePattern': SamplePattern.Stratified, 'sampleCount': 16}) + g.addPass(GBufferRT, "GBufferRT") + MegakernelPathTracer = createPass("MegakernelPathTracer", {'mSharedParams': PathTracerParams(useVBuffer=0)}) + g.addPass(MegakernelPathTracer, "MegakernelPathTracer") + g.addEdge("GBufferRT.vbuffer", "MegakernelPathTracer.vbuffer") # Required by ray footprint. + g.addEdge("GBufferRT.posW", "MegakernelPathTracer.posW") + g.addEdge("GBufferRT.normW", "MegakernelPathTracer.normalW") + g.addEdge("GBufferRT.tangentW", "MegakernelPathTracer.tangentW") + g.addEdge("GBufferRT.faceNormalW", "MegakernelPathTracer.faceNormalW") + g.addEdge("GBufferRT.viewW", "MegakernelPathTracer.viewW") + g.addEdge("GBufferRT.diffuseOpacity", "MegakernelPathTracer.mtlDiffOpacity") + g.addEdge("GBufferRT.specRough", "MegakernelPathTracer.mtlSpecRough") + g.addEdge("GBufferRT.emissive", "MegakernelPathTracer.mtlEmissive") + g.addEdge("GBufferRT.matlExtra", "MegakernelPathTracer.mtlParams") + g.addEdge("MegakernelPathTracer.color", "AccumulatePass.input") + g.addEdge("AccumulatePass.output", "ToneMappingPass.src") + g.markOutput("ToneMappingPass.dst") + return g + +PathTracerGraph = render_graph_PathTracerGraph() +try: m.addGraph(PathTracerGraph) +except NameError: None diff --git a/Source/RenderPasses/MegakernelPathTracer/Data/PathTracerTexLOD_Megakernel.py b/Source/RenderPasses/MegakernelPathTracer/Data/PathTracerTexLOD_Megakernel.py index d97e1df601..041cd7e987 100644 --- a/Source/RenderPasses/MegakernelPathTracer/Data/PathTracerTexLOD_Megakernel.py +++ b/Source/RenderPasses/MegakernelPathTracer/Data/PathTracerTexLOD_Megakernel.py @@ -2,11 +2,12 @@ def render_graph_PathTracerGraph(): g = RenderGraph("PathTracerGraph") loadRenderPassLibrary("AccumulatePass.dll") loadRenderPassLibrary("GBuffer.dll") + loadRenderPassLibrary("OptixDenoiser.dll") loadRenderPassLibrary("ToneMapper.dll") - loadRenderPassLibrary("MegakernelPathTracer.dll") + loadRenderPassLibrary("WavefrontPathTracer.dll") AccumulatePass = createPass("AccumulatePass", {'enableAccumulation': True}) g.addPass(AccumulatePass, "AccumulatePass") - ToneMappingPass = createPass("ToneMapper", {'autoExposure': False, 'exposureValue': 0.0}) + ToneMappingPass = createPass("ToneMapper", {'autoExposure': False, 'exposureCompensation': 0.0}) g.addPass(ToneMappingPass, "ToneMappingPass") GBufferRT = createPass("GBufferRT", {'forceCullMode': False, 'cull': CullMode.CullBack, 'samplePattern': SamplePattern.Stratified, 'sampleCount': 16}) GBufferRaster = createPass("GBufferRaster", {'forceCullMode': False, 'cull': CullMode.CullBack, 'samplePattern': SamplePattern.Stratified, 'sampleCount': 16}) # viewW not exported ? Not compatible with Path Tracers anymore ? diff --git a/Source/RenderPasses/MegakernelPathTracer/MegakernelPathTracer.cpp b/Source/RenderPasses/MegakernelPathTracer/MegakernelPathTracer.cpp index 3b27fa6ca0..f9001f28bd 100644 --- a/Source/RenderPasses/MegakernelPathTracer/MegakernelPathTracer.cpp +++ b/Source/RenderPasses/MegakernelPathTracer/MegakernelPathTracer.cpp @@ -27,6 +27,7 @@ **************************************************************************/ #include "MegakernelPathTracer.h" #include "RenderGraph/RenderPassHelpers.h" +#include "Scene/HitInfo.h" #include namespace @@ -36,9 +37,9 @@ namespace // Ray tracing settings that affect the traversal stack size. // These should be set as small as possible. - // The payload for the scatter rays is 8B. + // The payload for the scatter rays is 8-12B. // The payload for the shadow rays is 4B. - const uint32_t kMaxPayloadSizeBytes = 8; + const uint32_t kMaxPayloadSizeBytes = HitInfo::kMaxPackedSizeInBytes; const uint32_t kMaxAttributesSizeBytes = 8; const uint32_t kMaxRecursionDepth = 1; @@ -115,7 +116,7 @@ void MegakernelPathTracer::execute(RenderContext* pRenderContext, const RenderDa { // Specialize program for the current emissive light sampler options. assert(mpEmissiveSampler); - if (mpEmissiveSampler->prepareProgram(pProgram.get())) mTracer.pVars = nullptr; + if (pProgram->addDefines(mpEmissiveSampler->getDefines())) mTracer.pVars = nullptr; } // Prepare program vars. This may trigger shader compilation. diff --git a/Source/RenderPasses/MegakernelPathTracer/MegakernelPathTracer.vcxproj b/Source/RenderPasses/MegakernelPathTracer/MegakernelPathTracer.vcxproj index 124f53f92a..6bf411a73a 100644 --- a/Source/RenderPasses/MegakernelPathTracer/MegakernelPathTracer.vcxproj +++ b/Source/RenderPasses/MegakernelPathTracer/MegakernelPathTracer.vcxproj @@ -14,7 +14,7 @@ {873F13CA-A9C7-47BA-857D-8848C5E7F07E} Win32Proj MegakernelPathTracer - 10.0.18362.0 + 10.0.19041.0 MegakernelPathTracer diff --git a/Source/RenderPasses/MegakernelPathTracer/PathTracer.rt.slang b/Source/RenderPasses/MegakernelPathTracer/PathTracer.rt.slang index 1173d66f9a..b27bbeacb2 100644 --- a/Source/RenderPasses/MegakernelPathTracer/PathTracer.rt.slang +++ b/Source/RenderPasses/MegakernelPathTracer/PathTracer.rt.slang @@ -78,7 +78,8 @@ void scatterClosestHit( { // Store hit information. Note we don't access the materials here. HitInfo hit; - hit.meshInstanceID = hitParams.getGlobalHitID(); + hit.type = InstanceType::TriangleMesh; + hit.instanceID = hitParams.getGlobalHitID(); hit.primitiveIndex = PrimitiveIndex(); hit.barycentrics = attribs.barycentrics; rayData.packedHitInfo = hit.encode(); diff --git a/Source/RenderPasses/MegakernelPathTracer/PathTracer.slang b/Source/RenderPasses/MegakernelPathTracer/PathTracer.slang index 0b13bce0cb..b86e85b0ac 100644 --- a/Source/RenderPasses/MegakernelPathTracer/PathTracer.slang +++ b/Source/RenderPasses/MegakernelPathTracer/PathTracer.slang @@ -68,7 +68,7 @@ struct ShadowRayData */ struct ScatterRayData { - uint2 packedHitInfo; ///< Packed HitInfo data, or kInvalidIndex in the first component if ray missed. + PackedHitInfo packedHitInfo; ///< Packed HitInfo data, or kInvalidIndex in the first component if ray missed. }; /** Traces a shadow ray towards a light source. @@ -106,11 +106,14 @@ bool traceShadowRay(float3 origin, float3 dir, float distance, bool valid = true \param[in] origin Ray origin for the shadow ray. \param[in] dir Direction from ray origin towards the light source (normalized). \param[in,out] interiorList Interior list for handling nested dielectrics. - \param[out] hitInfo Hit information - \return True if scatter ray hit something, false otherwise. + \param[out] hit Hit information. The 'instanceID' field is set to HitInfo::kInvalidIndex upon miss. + \return False if path was terminated, true otherwise. */ -bool traceScatterRay(float3 origin, float3 dir, inout InteriorList interiorList, out HitInfo hitInfo) +bool traceScatterRay(float3 origin, float3 dir, inout InteriorList interiorList, out HitInfo hit) { + hit = {}; + hit.instanceID = HitInfo::kInvalidIndex; + // Setup ray based on params passed via payload. RayDesc ray; ray.Origin = origin; @@ -119,6 +122,7 @@ bool traceScatterRay(float3 origin, float3 dir, inout InteriorList interiorList, ray.TMax = kRayTMax; ScatterRayData rayData; + uint rejectedHits = 0; // For nested dielectrics, we potentially have to trace additional rays after false intersections. while (true) @@ -134,17 +138,26 @@ bool traceScatterRay(float3 origin, float3 dir, inout InteriorList interiorList, // Check for false intersections. if (kUseNestedDielectrics && rayData.packedHitInfo.x != HitInfo::kInvalidIndex) { - HitInfo hitInfo; - hitInfo.decode(rayData.packedHitInfo); - uint materialID = gScene.getMaterialID(hitInfo.meshInstanceID); + HitInfo tmpHit; + tmpHit.decode(rayData.packedHitInfo); + uint materialID = gScene.getMaterialID(tmpHit.instanceID); uint nestedPriority = gScene.materials[materialID].getNestedPriority(); if (!interiorList.isTrueIntersection(nestedPriority)) { - VertexData v = gScene.getVertexData(hitInfo); - bool frontFacing = dot(-ray.Direction, v.faceNormalW) >= 0.f; - interiorList.handleIntersection(materialID, nestedPriority, frontFacing); - ray.Origin = computeRayOrigin(v.posW, frontFacing ? -v.faceNormalW : v.faceNormalW); - continue; + if (rejectedHits < kMaxRejectedHits) + { + rejectedHits++; + VertexData v = gScene.getVertexData(tmpHit); + bool frontFacing = dot(-ray.Direction, v.faceNormalW) >= 0.f; + interiorList.handleIntersection(materialID, nestedPriority, frontFacing); + ray.Origin = computeRayOrigin(v.posW, frontFacing ? -v.faceNormalW : v.faceNormalW); + continue; + } + else + { + // Terminate path. + return false; + } } } @@ -153,11 +166,10 @@ bool traceScatterRay(float3 origin, float3 dir, inout InteriorList interiorList, if (rayData.packedHitInfo.x != HitInfo::kInvalidIndex) { - hitInfo.decode(rayData.packedHitInfo); - return true; + hit.decode(rayData.packedHitInfo); } - return false; + return true; } // Need to be kept alive until next scatter event for ray footprint to be updated after the ray bounces. @@ -171,6 +183,8 @@ static VertexData v; */ void handleHit(const PathTracerData pt, inout ShadingData sd, inout PathData path) { + logPathVertex(); + // Get vertex data for current hit point. VertexData and triangleVertices kept alive out of handleHit() to be used by rayFootprint.bounceOnSurface() if needed. v = gScene.getVertexData(path.hit, triangleVertices); @@ -180,12 +194,7 @@ void handleHit(const PathTracerData pt, inout ShadingData sd, inout PathData pat // Evaluate Falcor's material parameters at the hit point using the current ray footprint mode and doing texLOD. sd = prepareShadingData(v, path.rayFootprint, triangleVertices, path.hit, path.origin, path.dir); - // Compute tangent space if it is invalid. - if (!(dot(sd.T, sd.T) > 0.f)) // Note: Comparison written so that NaNs trigger - { - sd.T = perp_stark(sd.N); - sd.B = cross(sd.N, sd.T); - } + if (kAdjustShadingNormals) adjustShadingNormal(sd, v); if (kUseNestedDielectrics) { @@ -213,7 +222,7 @@ void handleHit(const PathTracerData pt, inout ShadingData sd, inout PathData pat // Determine if we need to compute the emissive based on the current configuration. // It's only needed if emissive is enabled, and its full contribution hasn't been sampled elsewhere. - const bool computeEmissive = kUseEmissiveLights && (kUseLightsInVolumes || !path.isInsideVolume()) && (!kUseNEE || kUseMIS || !isLightSamplable); + const bool computeEmissive = kUseEmissiveLights && (kUseLightsInDielectricVolumes || !path.isInsideVolume()) && (!kUseNEE || kUseMIS || !isLightSamplable); if (computeEmissive && any(sd.emissive > 0.f)) { @@ -225,12 +234,12 @@ void handleHit(const PathTracerData pt, inout ShadingData sd, inout PathData pat // Prepare hit point struct with data needed for emissive light PDF evaluation. TriangleHit hit; - hit.triangleIndex = gScene.lightCollection.getTriangleIndex(path.hit.meshInstanceID, path.hit.primitiveIndex); + hit.triangleIndex = gScene.lightCollection.getTriangleIndex(path.hit.instanceID, path.hit.primitiveIndex); hit.posW = sd.posW; hit.normalW = sd.frontFacing ? sd.faceN : -sd.faceN; // Evaluate PDF at the hit, had it been generated with light sampling. - float lightPdf = pt.emissiveSampler.evalPdf(path.origin, path.normal, hit) * getEmissiveLightSelectionPdf(); + float lightPdf = pt.emissiveSampler.evalPdf(path.origin, path.normal, true, hit) * getEmissiveLightSelectionPdf(); // Compute MIS weight by combining this with BRDF sampling. // Note we can assume path.pdf > 0.f since we shouldn't have got here otherwise. @@ -258,8 +267,10 @@ void handleMiss(const PathTracerData pt, inout PathData path) const bool isLightSamplable = path.isLightSamplable(); // If we have an environment, add it's weighted contribution here. - if (kUseEnvLight && (kUseLightsInVolumes || !path.isInsideVolume()) && (!kUseNEE || kUseMIS || !isLightSamplable)) + if (kUseEnvLight && (kUseLightsInDielectricVolumes || !path.isInsideVolume()) && (!kUseNEE || kUseMIS || !isLightSamplable)) { + logPathVertex(); + float misWeight = 1.f; if (kUseNEE && kUseMIS && isLightSamplable) { @@ -283,7 +294,7 @@ void evalDirect(const PathTracerData pt, ShadingData sd, inout PathData path) for (uint i = 0; i < kLightSamplesPerVertex; ++i) { ShadowRay shadowRay = {}; - bool valid = generateShadowRay(pt.params, pt.envMapSampler, pt.emissiveSampler, sd, i, path, shadowRay); + bool valid = generateShadowRay(pt.params, pt.envMapSampler, pt.emissiveSampler, sd, i, path, path.sg, shadowRay); bool visible = traceShadowRay(path.origin, shadowRay.rayParams.xyz, shadowRay.rayParams.w, valid); path.L += visible ? shadowRay.Lr : float3(0.f); } @@ -291,6 +302,8 @@ void evalDirect(const PathTracerData pt, ShadingData sd, inout PathData path) void tracePath(const PathTracerData pt, ShadingData sd, inout PathData path) { + logPathVertex(); + // Always output directly emitted light from the primary hit. // This is independent of whether emissive materials are treated as light sources or not. path.L += sd.emissive; @@ -311,7 +324,7 @@ void tracePath(const PathTracerData pt, ShadingData sd, inout PathData path) bool supportsNEE = (lobes & (uint)LobeType::DiffuseReflection) != 0 || (lobes & (uint)LobeType::SpecularReflection) != 0; // Compute direct illumination. - if (kUseNEE && supportsNEE && (kUseLightsInVolumes || !path.isInsideVolume())) + if (kUseNEE && supportsNEE && (kUseLightsInDielectricVolumes || !path.isInsideVolume())) { evalDirect(pt, sd, path); } @@ -330,7 +343,7 @@ void tracePath(const PathTracerData pt, ShadingData sd, inout PathData path) const float3 rayDirIn = path.dir; // Generate next path segment. - if (!generateScatterRay(pt.params, sd, path)) return; + if (!generateScatterRay(pt.params, sd, path, path.sg)) return; // Scatter the ray footprint out of the surface. Primary bounce is handled at footprint creation time. if (depth > 0) @@ -351,7 +364,11 @@ void tracePath(const PathTracerData pt, ShadingData sd, inout PathData path) if (kDisableCaustics && path.isSpecular() && path.nonSpecularBounces > 0) return; // Trace scatter ray. - if (traceScatterRay(path.origin, path.dir, path.interiorList, path.hit)) + // The path will either be directly terminated or a hit/miss is reported. + if (!traceScatterRay(path.origin, path.dir, path.interiorList, path.hit)) return; + + // Handle scatter ray hit/miss. + if (path.hit.instanceID != HitInfo::kInvalidIndex) { handleHit(pt, sd, path); } diff --git a/Source/RenderPasses/MinimalPathTracer/Data/MinimalPathTracer.py b/Source/RenderPasses/MinimalPathTracer/Data/MinimalPathTracer.py index 0f09f39ada..f097637455 100644 --- a/Source/RenderPasses/MinimalPathTracer/Data/MinimalPathTracer.py +++ b/Source/RenderPasses/MinimalPathTracer/Data/MinimalPathTracer.py @@ -8,7 +8,7 @@ def render_graph_MinimalPathTracer(): loadRenderPassLibrary("ToneMapper.dll") AccumulatePass = createPass("AccumulatePass", {'enableAccumulation': True, 'precisionMode': AccumulatePrecision.Single}) g.addPass(AccumulatePass, "AccumulatePass") - ToneMapper = createPass("ToneMapper", {'autoExposure': False, 'exposureValue': 0.0}) + ToneMapper = createPass("ToneMapper", {'autoExposure': False, 'exposureCompensation': 0.0}) g.addPass(ToneMapper, "ToneMapper") MinimalPathTracer = createPass("MinimalPathTracer", {'mMaxBounces': 3, 'mComputeDirect': True}) g.addPass(MinimalPathTracer, "MinimalPathTracer") diff --git a/Source/RenderPasses/MinimalPathTracer/MinimalPathTracer.rt.slang b/Source/RenderPasses/MinimalPathTracer/MinimalPathTracer.rt.slang index c5a7760652..54c18d91b6 100644 --- a/Source/RenderPasses/MinimalPathTracer/MinimalPathTracer.rt.slang +++ b/Source/RenderPasses/MinimalPathTracer/MinimalPathTracer.rt.slang @@ -281,13 +281,6 @@ void scatterClosestHit( const uint materialID = gScene.getMaterialID(hitParams.getGlobalHitID()); ShadingData sd = prepareShadingData(v, materialID, gScene.materials[materialID], gScene.materialResources[materialID], -WorldRayDirection(), 0.f); - // Compute tangent space if it is invalid. - if (!(dot(sd.T, sd.T) > 0.f)) // Note: Comparison written so that NaNs trigger - { - sd.T = perp_stark(sd.N); - sd.B = cross(sd.N, sd.T); - } - // Add emitted light. if (kUseEmissiveLights && (kComputeDirect || rayData.pathLength > 0)) { @@ -342,7 +335,7 @@ void shadowAnyHit( /** This is the entry point for the minimal path tracer. - One path per pixel is generated, which is traced into the scene. + One path per pixel is generated, which is traced into the scene. The path tracer is written as a for-loop over path segments. Built-in light sources (point, directional) are sampled explicitly at each diff --git a/Source/RenderPasses/MinimalPathTracer/MinimalPathTracer.vcxproj b/Source/RenderPasses/MinimalPathTracer/MinimalPathTracer.vcxproj index f466e3656e..fca125fdb2 100644 --- a/Source/RenderPasses/MinimalPathTracer/MinimalPathTracer.vcxproj +++ b/Source/RenderPasses/MinimalPathTracer/MinimalPathTracer.vcxproj @@ -14,7 +14,7 @@ {FF7FE9B8-2ACE-4044-869D-A5C4EF8042D3} Win32Proj MinimalPathTracer - 10.0.18362.0 + 10.0.19041.0 MinimalPathTracer diff --git a/Source/RenderPasses/PassLibraryTemplate/PassLibraryTemplate.cpp b/Source/RenderPasses/PassLibraryTemplate/PassLibraryTemplate.cpp index fa4e0bc4e8..65fbaba21f 100644 --- a/Source/RenderPasses/PassLibraryTemplate/PassLibraryTemplate.cpp +++ b/Source/RenderPasses/PassLibraryTemplate/PassLibraryTemplate.cpp @@ -27,6 +27,12 @@ **************************************************************************/ #include "PassLibraryTemplate.h" + +namespace +{ + const char kDesc[] = "Insert pass description here"; +} + // Don't remove this. it's required for hot-reload to function properly extern "C" __declspec(dllexport) const char* getProjDir() { @@ -35,7 +41,7 @@ extern "C" __declspec(dllexport) const char* getProjDir() extern "C" __declspec(dllexport) void getPasses(Falcor::RenderPassLibrary& lib) { - lib.registerClass("RenderPassTemplate", "Render Pass Template", RenderPassTemplate::create); + lib.registerClass("RenderPassTemplate", kDesc, RenderPassTemplate::create); } RenderPassTemplate::SharedPtr RenderPassTemplate::create(RenderContext* pRenderContext, const Dictionary& dict) @@ -44,6 +50,8 @@ RenderPassTemplate::SharedPtr RenderPassTemplate::create(RenderContext* pRenderC return pPass; } +std::string RenderPassTemplate::getDesc() { return kDesc; } + Dictionary RenderPassTemplate::getScriptingDictionary() { return Dictionary(); diff --git a/Source/RenderPasses/PassLibraryTemplate/PassLibraryTemplate.h b/Source/RenderPasses/PassLibraryTemplate/PassLibraryTemplate.h index c0dd0b9e04..8dbbb992c6 100644 --- a/Source/RenderPasses/PassLibraryTemplate/PassLibraryTemplate.h +++ b/Source/RenderPasses/PassLibraryTemplate/PassLibraryTemplate.h @@ -43,7 +43,7 @@ class RenderPassTemplate : public RenderPass */ static SharedPtr create(RenderContext* pRenderContext = nullptr, const Dictionary& dict = {}); - virtual std::string getDesc() override { return "Insert pass description here"; } + virtual std::string getDesc() override; virtual Dictionary getScriptingDictionary() override; virtual RenderPassReflection reflect(const CompileData& compileData) override; virtual void compile(RenderContext* pContext, const CompileData& compileData) override {} diff --git a/Source/RenderPasses/PassLibraryTemplate/PassLibraryTemplate.vcxproj b/Source/RenderPasses/PassLibraryTemplate/PassLibraryTemplate.vcxproj index fc53075d32..f9c213338c 100644 --- a/Source/RenderPasses/PassLibraryTemplate/PassLibraryTemplate.vcxproj +++ b/Source/RenderPasses/PassLibraryTemplate/PassLibraryTemplate.vcxproj @@ -14,7 +14,7 @@ {E484AEEC-ED88-408E-ADA5-66DF6301D75B} Win32Proj PassLibraryTemplate - 10.0.18362.0 + 10.0.19041.0 PassLibraryTemplate diff --git a/Source/RenderPasses/PixelInspectorPass/PixelInspectorPass.cpp b/Source/RenderPasses/PixelInspectorPass/PixelInspectorPass.cpp index 7f1006e3ef..f907c43101 100644 --- a/Source/RenderPasses/PixelInspectorPass/PixelInspectorPass.cpp +++ b/Source/RenderPasses/PixelInspectorPass/PixelInspectorPass.cpp @@ -193,7 +193,7 @@ void PixelInspectorPass::renderUI(Gui::Widgets& widget) for (const std::string& value : values) { const std::string text = value + ": out of bounds"; - widget.text(text.c_str()); + widget.text(text); } } return true; @@ -298,7 +298,7 @@ void PixelInspectorPass::renderUI(Gui::Widgets& widget) visGroup.var("##col2", M[2]); visGroup.var("##col3", M[3]); - bool flipped = instanceData.flags & (uint32_t)MeshInstanceFlags::Flipped; + bool flipped = instanceData.flags & (uint32_t)MeshInstanceFlags::TransformFlipped; visGroup.checkbox("Flipped winding", flipped); } } diff --git a/Source/RenderPasses/PixelInspectorPass/PixelInspectorPass.vcxproj b/Source/RenderPasses/PixelInspectorPass/PixelInspectorPass.vcxproj index 2eae4158bd..34cffaec5a 100644 --- a/Source/RenderPasses/PixelInspectorPass/PixelInspectorPass.vcxproj +++ b/Source/RenderPasses/PixelInspectorPass/PixelInspectorPass.vcxproj @@ -14,7 +14,7 @@ {80DDDE59-A412-4E64-880A-6B52697C967B} Win32Proj InspectorPass - 10.0.18362.0 + 10.0.19041.0 PixelInspectorPass diff --git a/Source/RenderPasses/SSAO/ApplyAO.ps.slang b/Source/RenderPasses/SSAO/ApplyAO.ps.slang index e178c3c3fe..25ce5684b7 100644 --- a/Source/RenderPasses/SSAO/ApplyAO.ps.slang +++ b/Source/RenderPasses/SSAO/ApplyAO.ps.slang @@ -25,13 +25,17 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **************************************************************************/ -import Utils.Helpers; - SamplerState gSampler; Texture2D gColor; Texture2D gAOMap; +float4 applyAmbientOcclusion(float4 color, Texture2D aoTex, SamplerState s, float2 texC) +{ + float aoFactor = aoTex.SampleLevel(s, texC, 0).r; + return float4(color.rgb * aoFactor, color.a); +} + float4 main(float2 texC : TEXCOORD) : SV_TARGET0 { return applyAmbientOcclusion(gColor.SampleLevel(gSampler, texC, 0), gAOMap, gSampler, texC); diff --git a/Source/RenderPasses/SSAO/SSAO.vcxproj b/Source/RenderPasses/SSAO/SSAO.vcxproj index fe5642f990..643a1a939b 100644 --- a/Source/RenderPasses/SSAO/SSAO.vcxproj +++ b/Source/RenderPasses/SSAO/SSAO.vcxproj @@ -14,7 +14,7 @@ {6CC70B9F-CA56-4EF2-8075-A1BE0005DCF4} Win32Proj SSAO - 10.0.18362.0 + 10.0.19041.0 SSAO diff --git a/Source/RenderPasses/SVGFPass/SVGFPass.vcxproj b/Source/RenderPasses/SVGFPass/SVGFPass.vcxproj index 0486e5e8a4..533f37eac2 100644 --- a/Source/RenderPasses/SVGFPass/SVGFPass.vcxproj +++ b/Source/RenderPasses/SVGFPass/SVGFPass.vcxproj @@ -14,7 +14,7 @@ {691F64DD-0941-49DE-B52E-949341192154} Win32Proj SVGFPass - 10.0.18362.0 + 10.0.19041.0 SVGFPass diff --git a/Source/RenderPasses/SceneDebugger/SceneDebugger.cpp b/Source/RenderPasses/SceneDebugger/SceneDebugger.cpp new file mode 100644 index 0000000000..5a2a68e95f --- /dev/null +++ b/Source/RenderPasses/SceneDebugger/SceneDebugger.cpp @@ -0,0 +1,430 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#include "SceneDebugger.h" + +namespace +{ + const char kDesc[] = "Scene debugger for identifying asset issues."; + const char kShaderFile[] = "RenderPasses/SceneDebugger/SceneDebugger.cs.slang"; + const char kShaderModel[] = "6_5"; + + const std::string kOutput = "output"; + + // UI elements + const Gui::DropdownList kModeList = + { + { (uint32_t)SceneDebuggerMode::FaceNormal, "Face normal" }, + { (uint32_t)SceneDebuggerMode::ShadingNormal, "Shading normal" }, + { (uint32_t)SceneDebuggerMode::ShadingTangent, "Shading tangent" }, + { (uint32_t)SceneDebuggerMode::ShadingBitangent, "Shading bitangent" }, + { (uint32_t)SceneDebuggerMode::FrontFacingFlag, "Front-facing flag" }, + { (uint32_t)SceneDebuggerMode::BackfacingShadingNormal, "Back-facing shading normal" }, + { (uint32_t)SceneDebuggerMode::TexCoords, "Texture coordinates" }, + { (uint32_t)SceneDebuggerMode::MeshID, "Mesh ID" }, + { (uint32_t)SceneDebuggerMode::MeshInstanceID, "Mesh instance ID" }, + { (uint32_t)SceneDebuggerMode::MaterialID, "Material ID" }, + { (uint32_t)SceneDebuggerMode::BlasID, "BLAS ID" }, + { (uint32_t)SceneDebuggerMode::CurveID, "Curve ID" }, + { (uint32_t)SceneDebuggerMode::CurveInstanceID, "Curve instance ID" }, + { (uint32_t)SceneDebuggerMode::InstancedGeometry, "Instanced geometry" }, + }; + + std::string getModeDesc(SceneDebuggerMode mode) + { + switch (mode) + { + case SceneDebuggerMode::FaceNormal: return + "Face normal in RGB color"; + case SceneDebuggerMode::ShadingNormal: return + "Shading normal in RGB color"; + case SceneDebuggerMode::ShadingTangent: return + "Shading tangent in RGB color"; + case SceneDebuggerMode::ShadingBitangent: return + "Shading bitangent in RGB color"; + case SceneDebuggerMode::FrontFacingFlag: return + "Green = front-facing\n" + "Red = back-facing"; + case SceneDebuggerMode::BackfacingShadingNormal: return + "Pixels where the shading normal is back-facing with respect to view vector are highlighted"; + case SceneDebuggerMode::TexCoords: return + "Texture coordinates in RG color wrapped to [0,1]"; + case SceneDebuggerMode::MeshID: return + "Mesh ID in pseudocolor"; + case SceneDebuggerMode::MeshInstanceID: return + "Mesh instance ID in pseudocolor"; + case SceneDebuggerMode::MaterialID: return + "Material ID in pseudocolor"; + case SceneDebuggerMode::BlasID: return + "Raytracing bottom-level acceleration structure (BLAS) ID in pseudocolor"; + case SceneDebuggerMode::CurveID: return + "Curve ID in pseudocolor"; + case SceneDebuggerMode::CurveInstanceID: return + "Curve instance ID in pseudocolor"; + case SceneDebuggerMode::InstancedGeometry: return + "Green = instanced geometry\n" + "Red = non-instanced geometry"; + default: + should_not_get_here(); + return ""; + } + } + + // Scripting + const char kMode[] = "mode"; + const char kShowVolumes[] = "showVolumes"; + + void registerBindings(pybind11::module& m) + { + pybind11::enum_ mode(m, "SceneDebuggerMode"); + mode.value("FaceNormal", SceneDebuggerMode::FaceNormal); + mode.value("ShadingNormal", SceneDebuggerMode::ShadingNormal); + mode.value("ShadingTangent", SceneDebuggerMode::ShadingTangent); + mode.value("ShadingBitangent", SceneDebuggerMode::ShadingBitangent); + mode.value("FrontFacingFlag", SceneDebuggerMode::FrontFacingFlag); + mode.value("BackfacingShadingNormal", SceneDebuggerMode::BackfacingShadingNormal); + mode.value("TexCoords", SceneDebuggerMode::TexCoords); + mode.value("MeshID", SceneDebuggerMode::MeshID); + mode.value("MeshInstanceID", SceneDebuggerMode::MeshInstanceID); + mode.value("MaterialID", SceneDebuggerMode::MaterialID); + mode.value("BlasID", SceneDebuggerMode::BlasID); + mode.value("CurveID", SceneDebuggerMode::CurveID); + mode.value("CurveInstanceID", SceneDebuggerMode::CurveInstanceID); + mode.value("InstancedGeometry", SceneDebuggerMode::InstancedGeometry); + } +} + +// Don't remove this. it's required for hot-reload to function properly +extern "C" __declspec(dllexport) const char* getProjDir() +{ + return PROJECT_DIR; +} + +extern "C" __declspec(dllexport) void getPasses(Falcor::RenderPassLibrary& lib) +{ + lib.registerClass("SceneDebugger", kDesc, SceneDebugger::create); + Falcor::ScriptBindings::registerBinding(registerBindings); +} + +SceneDebugger::SharedPtr SceneDebugger::create(RenderContext* pRenderContext, const Dictionary& dict) +{ + return SharedPtr(new SceneDebugger(dict)); +} + +SceneDebugger::SceneDebugger(const Dictionary& dict) +{ + if (!gpDevice->isFeatureSupported(Device::SupportedFeatures::RaytracingTier1_1)) + { + throw std::exception("Raytracing Tier 1.1 is not supported by the current device"); + } + + // Parse dictionary. + for (const auto& [key, value] : dict) + { + if (key == kMode) mParams.mode = (uint32_t)value; + else if (key == kShowVolumes) mParams.showVolumes = value; + else logWarning("Unknown field '" + key + "' in a SceneDebugger dictionary"); + } + + Program::Desc desc; + desc.addShaderLibrary(kShaderFile).csEntry("main").setShaderModel(kShaderModel); + mpDebugPass = ComputePass::create(desc, Program::DefineList(), false); + mpFence = GpuFence::create(); +} + +std::string SceneDebugger::getDesc() +{ + return kDesc; +} + +Dictionary SceneDebugger::getScriptingDictionary() +{ + Dictionary d; + d[kMode] = SceneDebuggerMode(mParams.mode); + d[kShowVolumes] = mParams.showVolumes; + return d; +} + +RenderPassReflection SceneDebugger::reflect(const CompileData& compileData) +{ + RenderPassReflection reflector; + reflector.addOutput(kOutput, "Scene debugger output").bindFlags(ResourceBindFlags::UnorderedAccess).format(ResourceFormat::RGBA32Float); + + return reflector; +} + +void SceneDebugger::compile(RenderContext* pContext, const CompileData& compileData) +{ + mParams.frameDim = compileData.defaultTexDims; +} + +void SceneDebugger::setScene(RenderContext* pRenderContext, const Scene::SharedPtr& pScene) +{ + mpScene = pScene; + + if (mpScene) + { + // Prepare our programs for the scene. + Shader::DefineList defines = mpScene->getSceneDefines(); + + // Disable discard and gradient operations. + defines.add("_MS_DISABLE_ALPHA_TEST"); + defines.add("_DEFAULT_ALPHA_TEST"); + + mpDebugPass->getProgram()->addDefines(defines); + mpDebugPass->setVars(nullptr); // Trigger recompile + + // Create lookup table for mesh to BLAS ID. + auto blasIDs = mpScene->getMeshBlasIDs(); + assert(!blasIDs.empty()); + mpMeshToBlasID = Buffer::createStructured(sizeof(uint32_t), (uint32_t)blasIDs.size(), ResourceBindFlags::ShaderResource, Buffer::CpuAccess::None, blasIDs.data(), false); + + // Create instance metadata. + initInstanceInfo(); + + // Bind variables. + auto var = mpDebugPass->getRootVar()["CB"]["gSceneDebugger"]; + if (!mpPixelData) + { + mpPixelData = Buffer::createStructured(var["pixelData"], 1, ResourceBindFlags::ShaderResource | ResourceBindFlags::UnorderedAccess, Buffer::CpuAccess::None, nullptr, false); + mpPixelDataStaging = Buffer::createStructured(var["pixelData"], 1, ResourceBindFlags::None, Buffer::CpuAccess::Read, nullptr, false); + } + var["pixelData"] = mpPixelData; + var["meshToBlasID"] = mpMeshToBlasID; + var["meshInstanceInfo"] = mpMeshInstanceInfo; + } +} + +void SceneDebugger::execute(RenderContext* pRenderContext, const RenderData& renderData) +{ + mPixelDataAvailable = false; + const auto& pOutput = renderData[kOutput]->asTexture(); + + if (mpScene == nullptr) + { + pRenderContext->clearUAV(pOutput->getUAV().get(), float4(0.f)); + return; + } + + mpScene->setRaytracingShaderData(pRenderContext, mpDebugPass->getRootVar()); + + ShaderVar var = mpDebugPass->getRootVar()["CB"]["gSceneDebugger"]; + var["params"].setBlob(mParams); + var["output"] = pOutput; + + mpDebugPass->execute(pRenderContext, uint3(mParams.frameDim, 1)); + + pRenderContext->copyResource(mpPixelDataStaging.get(), mpPixelData.get()); + pRenderContext->flush(false); + mpFence->gpuSignal(pRenderContext->getLowLevelData()->getCommandQueue()); + + mPixelDataAvailable = true; + mParams.frameCount++; +} + +void SceneDebugger::renderUI(Gui::Widgets& widget) +{ + widget.dropdown("Mode", kModeList, mParams.mode); + widget.tooltip("Selects visualization mode"); + + widget.checkbox("Clamp to [0,1]", mParams.clamp); + widget.tooltip("Clamp pixel values to [0,1] before output."); + + if ((SceneDebuggerMode)mParams.mode == SceneDebuggerMode::FaceNormal || + (SceneDebuggerMode)mParams.mode == SceneDebuggerMode::ShadingNormal || + (SceneDebuggerMode)mParams.mode == SceneDebuggerMode::ShadingTangent || + (SceneDebuggerMode)mParams.mode == SceneDebuggerMode::ShadingBitangent || + (SceneDebuggerMode)mParams.mode == SceneDebuggerMode::TexCoords) + { + widget.checkbox("Flip sign", mParams.flipSign); + widget.checkbox("Remap to [0,1]", mParams.remapRange); + widget.tooltip("Remap range from [-1,1] to [0,1] before output."); + } + + widget.checkbox("Show volumes", mParams.showVolumes); + if (mParams.showVolumes) + { + widget.var("Density scale", mParams.densityScale, 0.f, 1000.f, 0.1f); + } + + widget.textWrapped("Description:\n" + getModeDesc((SceneDebuggerMode)mParams.mode)); + + // Show data for the currently selected pixel. + widget.dummy("#spacer0", { 1, 20 }); + widget.var("Selected pixel", mParams.selectedPixel); + renderPixelDataUI(widget); + + widget.dummy("#spacer1", { 1, 20 }); + widget.text("Scene: " + (mpScene ? mpScene->getFilename() : "No scene loaded")); +} + +void SceneDebugger::renderPixelDataUI(Gui::Widgets& widget) +{ + if (mPixelDataAvailable) + { + assert(mpPixelDataStaging); + mpFence->syncCpu(); + const PixelData& data = *reinterpret_cast(mpPixelDataStaging->map(Buffer::MapType::Read)); + + std::ostringstream oss; + if (data.meshInstanceID != PixelData::kInvalidID) + { + oss << "Mesh ID: " << data.meshID << std::endl + << "Mesh name: " << (mpScene->hasMesh(data.meshID) ? mpScene->getMeshName(data.meshID) : "unknown") << std::endl + << "Mesh instance ID: " << data.meshInstanceID << std::endl + << "Material ID: " << data.materialID << std::endl + << "BLAS ID: " << data.blasID << std::endl; + + widget.text(oss.str()); + widget.dummy("#spacer2", { 1, 10 }); + + // Show mesh details. + if (auto g = widget.group("Mesh info"); g.open()) + { + const auto& mesh = mpScene->getMesh(data.meshID); + std::ostringstream oss; + oss << "flags: " << mesh.flags << std::endl + << "vertexCount: " << mesh.vertexCount << std::endl + << "indexCount: " << mesh.indexCount << std::endl + << "triangleCount: " << mesh.getTriangleCount() << std::endl + << "vbOffset: " << mesh.vbOffset << std::endl + << "ibOffset: " << mesh.ibOffset << std::endl + << "use16BitIndices: " << mesh.use16BitIndices() << std::endl; + g.text(oss.str()); + g.release(); + } + + // Show material info. + if (auto g = widget.group("Material info"); g.open()) + { + const auto& material = *mpScene->getMaterial(data.materialID); + std::ostringstream oss; + oss << "name: " << material.getName() << std::endl + << "emissive: " << (material.isEmissive() ? "true" : "false") << std::endl + << "doubleSided: " << (material.isDoubleSided() ? "true" : "false") << std::endl + << std::endl + << "See Scene Settings->Materials for more details" << std::endl; + g.text(oss.str()); + g.release(); + } + } + else if (data.curveInstanceID != PixelData::kInvalidID) + { + oss << "Curve ID: " << data.curveID << std::endl + << "Curve instance ID: " << data.curveInstanceID << std::endl + << "Material ID: " << data.materialID << std::endl + << "BLAS ID: " << data.blasID << std::endl; + + widget.text(oss.str()); + widget.dummy("#spacer2", { 1, 10 }); + + // Show mesh details. + if (auto g = widget.group("Curve info"); g.open()) + { + const auto& curve = mpScene->getCurve(data.curveID); + std::ostringstream oss; + oss << "degree: " << curve.degree << std::endl + << "vertexCount: " << curve.vertexCount << std::endl + << "indexCount: " << curve.indexCount << std::endl + << "vbOffset: " << curve.vbOffset << std::endl + << "ibOffset: " << curve.ibOffset << std::endl; + g.text(oss.str()); + g.release(); + } + + // Show material info. + if (auto g = widget.group("Material info"); g.open()) + { + const auto& material = *mpScene->getMaterial(data.materialID); + std::ostringstream oss; + oss << "name: " << material.getName() << std::endl + << std::endl + << "See Scene Settings->Materials for more details" << std::endl; + g.text(oss.str()); + g.release(); + } + } + else + { + oss << "Background pixel" << std::endl; + } + + mpPixelDataStaging->unmap(); + } +} + +bool SceneDebugger::onMouseEvent(const MouseEvent& mouseEvent) +{ + if (mouseEvent.type == MouseEvent::Type::LeftButtonDown) + { + float2 cursorPos = mouseEvent.pos * (float2)mParams.frameDim; + mParams.selectedPixel = (uint2)glm::clamp(cursorPos, float2(0.f), float2(mParams.frameDim.x - 1, mParams.frameDim.y - 1)); + } + + return false; +} + +void SceneDebugger::initInstanceInfo() +{ + const uint32_t instanceCount = mpScene ? mpScene->getMeshInstanceCount() : 0; + + // If there are no mesh instances. Just clear the buffer and return. + if (instanceCount == 0) + { + mpMeshInstanceInfo = nullptr; + return; + } + + // Count number of instances of each mesh. + const uint32_t meshCount = mpScene->getMeshCount(); + assert(meshCount > 0); + + std::vector meshInstanceCounts(meshCount, 0); + for (uint32_t instanceID = 0; instanceID < instanceCount; instanceID++) + { + uint32_t meshID = mpScene->getMeshInstance(instanceID).meshID; + assert(meshID < meshCount); + meshInstanceCounts[meshID]++; + } + + // Setup instance metadata. + std::vector instanceInfo(instanceCount); + for (uint32_t instanceID = 0; instanceID < instanceCount; instanceID++) + { + auto& info = instanceInfo[instanceID]; + + uint32_t meshID = mpScene->getMeshInstance(instanceID).meshID; + if (meshInstanceCounts[meshID] > 1) + { + info.flags |= (uint32_t)InstanceInfoFlags::IsInstanced; + } + } + + // Create GPU buffer. + assert(!instanceInfo.empty()); + mpMeshInstanceInfo = Buffer::createStructured(sizeof(InstanceInfo), (uint32_t)instanceInfo.size(), ResourceBindFlags::ShaderResource, Buffer::CpuAccess::None, instanceInfo.data(), false); +} diff --git a/Source/RenderPasses/SceneDebugger/SceneDebugger.cs.slang b/Source/RenderPasses/SceneDebugger/SceneDebugger.cs.slang new file mode 100644 index 0000000000..7f358c1a2d --- /dev/null +++ b/Source/RenderPasses/SceneDebugger/SceneDebugger.cs.slang @@ -0,0 +1,343 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +import Scene.RaytracingInline; +import Scene.Scene; +import Scene.Shading; +import Utils.Helpers; +import Utils.Math.HashUtils; +import Utils.Color.ColorMap; +import Utils.Color.ColorHelpers; +import SharedTypes; + +struct SceneDebugger +{ + SceneDebuggerParams params; + StructuredBuffer meshToBlasID; + StructuredBuffer meshInstanceInfo; + + RWTexture2D output; + RWStructuredBuffer pixelData; + + /** Run scene debugger for the given pixel. + */ + void execute(const uint2 pixel) + { + if (any(pixel >= params.frameDim)) return; + + // Initialize pixel data for the selected pixel. + if (all(pixel == params.selectedPixel)) + { + pixelData[0].meshInstanceID = PixelData::kInvalidID; + pixelData[0].meshID = PixelData::kInvalidID; + pixelData[0].materialID = PixelData::kInvalidID; + pixelData[0].blasID = PixelData::kInvalidID; + pixelData[0].curveInstanceID = PixelData::kInvalidID; + pixelData[0].curveID = PixelData::kInvalidID; + } + + // Trace primary ray. + RayDesc ray = gScene.camera.computeRayPinhole(pixel, params.frameDim).toRayDesc(); + + RayQuery rayQuery; + //RayQuery rayQuery; + + rayQuery.TraceRayInline( + gRtScene, + RAY_FLAG_NONE, // OR'd with template flags above + 0xff, // InstanceInclusionMask + ray); + + float2 curveCommittedAttribs = {}; + + while (rayQuery.Proceed()) + { + switch (rayQuery.CandidateType()) + { + case CANDIDATE_NON_OPAQUE_TRIANGLE: + { + // Alpha test for non-opaque geometry. + HitInfo hit = getCandidateTriangleHit(rayQuery); + VertexData v = gScene.getVertexData(hit); + uint materialID = gScene.getMaterialID(hit.instanceID); + + if (alphaTest(v, gScene.materials[materialID], gScene.materialResources[materialID], 0.f)) continue; + + rayQuery.CommitNonOpaqueTriangleHit(); + break; + } + case CANDIDATE_PROCEDURAL_PRIMITIVE: + { + float t; + HitInfo hit; + bool valid = getCandidateCurveHit(rayQuery, ray, t, hit); + + if (valid) + { + rayQuery.CommitProceduralPrimitiveHit(t); + curveCommittedAttribs = hit.barycentrics; + } + break; + } + } + } + + // Process hit/miss. + float3 color = float3(0); + float hitT = 1e30; + switch (rayQuery.CommittedStatus()) + { + case COMMITTED_TRIANGLE_HIT: + { + HitInfo hit = getCommittedTriangleHit(rayQuery); + hitT = rayQuery.CommittedRayT(); + color = handleHit(pixel, ray.Direction, hit); + break; + } + case COMMITTED_PROCEDURAL_PRIMITIVE_HIT: + { + HitInfo hit = getCommittedCurveHit(rayQuery, curveCommittedAttribs); + hitT = rayQuery.CommittedRayT(); + const float3 curveHitPosW = ray.Origin + ray.Direction * hitT; + color = handleHit(pixel, ray.Direction, hit, curveHitPosW); + break; + } + default: // COMMITTED_NOTHING + { + color = handleMiss(pixel, ray.Direction); + } + } + + // Process volumes. + if (params.showVolumes) + { + color = handleVolumes(color, ray.Origin, ray.Direction, hitT); + } + + // Clamp pixel values if necessary. + if (params.clamp) color = saturate(color); + + // Write output. + output[pixel] = float4(color, 1.f); + } + + float3 remapVector(float3 v) + { + if (params.flipSign) v = -v; + if (params.remapRange) v = 0.5f * v + 0.5f; + return v; + } + + float2 remapVector(float2 v) + { + return remapVector(float3(v, 0)).xy; + } + + float3 pseudocolor(uint value) + { + uint h = jenkinsHash(value); + return (uint3(h, h >> 8, h >> 16) & 0xff) / 255.f; + } + + float3 handleHit(const uint2 pixel, const float3 dir, const HitInfo hit, const float3 curveHitPosW = {}) + { + uint materialID = PixelData::kInvalidID; + + uint meshID = PixelData::kInvalidID; + uint meshInstanceID = PixelData::kInvalidID; + uint blasID = PixelData::kInvalidID; + + uint curveID = PixelData::kInvalidID; + uint curveInstanceID = PixelData::kInvalidID; + + VertexData v; + ShadingData sd; + + switch (hit.getType()) + { + case InstanceType::TriangleMesh: + { + meshInstanceID = hit.instanceID; + meshID = gScene.getMeshInstance(meshInstanceID).meshID; + blasID = meshToBlasID[meshID]; + materialID = gScene.getMaterialID(meshInstanceID); + + // Load shading attributes. + float3 barycentrics = float3(1.f - hit.barycentrics.x - hit.barycentrics.y, hit.barycentrics.x, hit.barycentrics.y); + v = gScene.getVertexData(hit.instanceID, hit.primitiveIndex, barycentrics); + sd = prepareShadingData(v, materialID, gScene.materials[materialID], gScene.materialResources[materialID], -dir, 0.f); + break; + } + case InstanceType::Curve: + { + curveInstanceID = hit.instanceID; + curveID = gScene.getCurveInstance(curveInstanceID).curveID; + materialID = gScene.getCurveMaterialID(curveInstanceID); + + // Load shading attributes. + float radius; + v = gScene.getVertexDataFromCurve(hit.instanceID, hit.primitiveIndex, hit.barycentrics.x, curveHitPosW, radius); + sd = prepareShadingData(v, materialID, gScene.materials[materialID], gScene.materialResources[materialID], -dir, 0.f); + break; + } + default: + // Should not happen. Return an error color. + return float3(1, 0, 0); + } + + // Write pixel data for the selected pixel. + if (all(pixel == params.selectedPixel)) + { + pixelData[0].meshInstanceID = meshInstanceID; + pixelData[0].meshID = meshID; + pixelData[0].materialID = materialID; + pixelData[0].blasID = blasID; + pixelData[0].curveInstanceID = curveInstanceID; + pixelData[0].curveID = curveID; + } + + // Compute zebra stripes. + const float z = (pixel.x + pixel.y - params.frameCount) & 0x8 ? 1.f : 0.f; + + switch ((SceneDebuggerMode)params.mode) + { + case SceneDebuggerMode::FaceNormal: + return remapVector(sd.faceN); + case SceneDebuggerMode::ShadingNormal: + return remapVector(sd.N); + case SceneDebuggerMode::ShadingTangent: + return remapVector(sd.T); + case SceneDebuggerMode::ShadingBitangent: + return remapVector(sd.B); + case SceneDebuggerMode::FrontFacingFlag: + { + float v = 0.75f * luminance(abs(sd.faceN)) + 0.25f; + return sd.frontFacing ? float3(0, v, 0) : float3(v, 0, 0); + } + case SceneDebuggerMode::BackfacingShadingNormal: + { + float v = 0.75f * luminance(abs(sd.faceN)) + 0.25f; + bool backFacing = dot(sd.N, sd.V) <= 0.f; + return backFacing ? float3(z, z, 0) : float3(v, v, v); + } + case SceneDebuggerMode::TexCoords: + return float3(frac(remapVector(sd.uv)), 0.f); + case SceneDebuggerMode::MeshID: + return pseudocolor(meshID); + case SceneDebuggerMode::MeshInstanceID: + return pseudocolor(meshInstanceID); + case SceneDebuggerMode::MaterialID: + return pseudocolor(materialID); + case SceneDebuggerMode::BlasID: + return pseudocolor(blasID); + case SceneDebuggerMode::CurveID: + return pseudocolor(curveID); + case SceneDebuggerMode::CurveInstanceID: + return pseudocolor(curveInstanceID); + case SceneDebuggerMode::InstancedGeometry: + { + float v = 0.75f * luminance(abs(sd.faceN)) + 0.25f; + if (meshInstanceID != PixelData::kInvalidID) + { + bool isInstanced = (meshInstanceInfo[meshInstanceID].flags & (uint)InstanceInfoFlags::IsInstanced) != 0; + return isInstanced ? float3(0, v, 0) : float3(v, 0, 0); + } + else + { + // For non-triangle geometry, return grayscale color to indicate instancing status is not available. + return float3(v, v, v); + } + } + default: + // Should not happen. + return float3(1, 0, 0); + } + } + + float3 handleMiss(const uint2 pixel, const float3 dir) + { + // Draw a checkerboard pattern. + return ((pixel.x ^ pixel.y) & 0x8) != 0 ? float3(1.f) : float3(0.5f); + } + + float3 handleVolumes(const float3 color, const float3 pos, const float3 dir, const float hitT) + { + float Tr = 1.f; + for (uint i = 0; i < gScene.getVolumeCount(); ++i) + { + Volume volume = gScene.getVolume(i); + Tr *= evalVolumeTransmittance(volume, pos, dir, 0.f, hitT); + } + + return Tr * color; + } + + float evalVolumeTransmittance(Volume volume, const float3 pos, const float3 dir, const float minT, const float maxT) + { + if (!volume.hasDensityGrid()) return 1.f; + + // Intersect with volume bounds and get intersection interval along the view ray. + AABB bounds = volume.getBounds(); + float2 nearFar; + bool hit = intersectRayAABB(pos, dir, bounds.minPoint, bounds.maxPoint, nearFar); + nearFar.x = max(nearFar.x, minT); + nearFar.y = min(nearFar.y, maxT); + if (nearFar.x >= nearFar.y) return 1.f; + + // Setup access to density grid. + Grid densityGrid; + gScene.getGrid(volume.getDensityGrid(), densityGrid); + Grid::Accessor accessor = densityGrid.createAccessor(); + + // Evaluate transmittance using ray-marching. + const uint kSteps = 500; + float opticalDepth = 0.f; + for (uint step = 0; step < kSteps; ++step) + { + float t = lerp(nearFar.x, nearFar.y, (step + 0.5f) / kSteps); + float3 p = pos + t * dir; + p = mul(float4(p, 1.f), volume.data.invTransform).xyz; + float density = densityGrid.lookupWorld(p, accessor); + opticalDepth += density; + } + opticalDepth *= (nearFar.y - nearFar.x) / kSteps * volume.data.densityScale * params.densityScale; + return exp(-opticalDepth); + } +}; + +cbuffer CB +{ + SceneDebugger gSceneDebugger; +} + +/** Compute shader entry point for scene debugger. +*/ +[numthreads(16, 16, 1)] +void main(uint3 dispatchThreadId : SV_DispatchThreadID) +{ + gSceneDebugger.execute(dispatchThreadId.xy); +} diff --git a/Source/RenderPasses/SceneDebugger/SceneDebugger.h b/Source/RenderPasses/SceneDebugger/SceneDebugger.h new file mode 100644 index 0000000000..36d05aafdc --- /dev/null +++ b/Source/RenderPasses/SceneDebugger/SceneDebugger.h @@ -0,0 +1,71 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#pragma once +#include "Falcor.h" +#include "FalcorExperimental.h" +#include "SharedTypes.slang" + +using namespace Falcor; + +/** Scene debugger render pass. + + This pass helps identify asset issues such as incorrect normals. +*/ +class SceneDebugger : public RenderPass +{ +public: + using SharedPtr = std::shared_ptr; + + static SharedPtr create(RenderContext* pRenderContext, const Dictionary& dict); + + std::string getDesc() override; + Dictionary getScriptingDictionary() override; + RenderPassReflection reflect(const CompileData& compileData) override; + void compile(RenderContext* pContext, const CompileData& compileData) override; + void setScene(RenderContext* pRenderContext, const Scene::SharedPtr& pScene) override; + void execute(RenderContext* pRenderContext, const RenderData& renderData) override; + void renderUI(Gui::Widgets& widget) override; + bool onMouseEvent(const MouseEvent& mouseEvent) override; + bool onKeyEvent(const KeyboardEvent& keyEvent) override { return false; } + +private: + SceneDebugger(const Dictionary& dict); + void renderPixelDataUI(Gui::Widgets& widget); + void initInstanceInfo(); + + // Internal state + Scene::SharedPtr mpScene; + SceneDebuggerParams mParams; + ComputePass::SharedPtr mpDebugPass; + GpuFence::SharedPtr mpFence; + Buffer::SharedPtr mpPixelData; ///< Buffer for recording pixel data at the selected pixel. + Buffer::SharedPtr mpPixelDataStaging; ///< Readback buffer. + Buffer::SharedPtr mpMeshToBlasID; + Buffer::SharedPtr mpMeshInstanceInfo; + bool mPixelDataAvailable = false; +}; diff --git a/Source/RenderPasses/SceneDebugger/SceneDebugger.vcxproj b/Source/RenderPasses/SceneDebugger/SceneDebugger.vcxproj new file mode 100644 index 0000000000..ea8f4f2bc7 --- /dev/null +++ b/Source/RenderPasses/SceneDebugger/SceneDebugger.vcxproj @@ -0,0 +1,106 @@ + + + + + Debug + x64 + + + Release + x64 + + + + {B1715F7A-6EFD-4910-B271-7423AB6961CB} + Win32Proj + SceneDebugger + 10.0.19041.0 + SceneDebugger + + + + + + + + + + + + + + + + + {2c535635-e4c5-4098-a928-574f0e7cd5f9} + + + + + + + + DynamicLibrary + true + v142 + Unicode + Shaders\RenderPasses\$(ProjectName) + + + DynamicLibrary + false + v142 + true + Unicode + Shaders\RenderPasses\$(ProjectName) + + + + + + + true + + + false + + + + + + Level3 + Disabled + PROJECT_DIR=R"($(ProjectDir))";_DEBUG;%(PreprocessorDefinitions) + true + true + stdcpp17 + + + Windows + true + + + + + Level3 + + + MaxSpeed + true + true + PROJECT_DIR=R"($(ProjectDir))";NDEBUG;%(PreprocessorDefinitions) + true + true + stdcpp17 + + + Windows + true + true + true + + + + + + \ No newline at end of file diff --git a/Source/RenderPasses/SceneDebugger/SceneDebugger.vcxproj.filters b/Source/RenderPasses/SceneDebugger/SceneDebugger.vcxproj.filters new file mode 100644 index 0000000000..a3bf0d9de2 --- /dev/null +++ b/Source/RenderPasses/SceneDebugger/SceneDebugger.vcxproj.filters @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/Source/RenderPasses/SceneDebugger/SharedTypes.slang b/Source/RenderPasses/SceneDebugger/SharedTypes.slang new file mode 100644 index 0000000000..613c4e1084 --- /dev/null +++ b/Source/RenderPasses/SceneDebugger/SharedTypes.slang @@ -0,0 +1,89 @@ +/*************************************************************************** + # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. + # + # Redistribution and use in source and binary forms, with or without + # modification, are permitted provided that the following conditions + # are met: + # * Redistributions of source code must retain the above copyright + # notice, this list of conditions and the following disclaimer. + # * Redistributions in binary form must reproduce the above copyright + # notice, this list of conditions and the following disclaimer in the + # documentation and/or other materials provided with the distribution. + # * Neither the name of NVIDIA CORPORATION nor the names of its + # contributors may be used to endorse or promote products derived + # from this software without specific prior written permission. + # + # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY + # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY + # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + **************************************************************************/ +#pragma once +#include "Utils/HostDeviceShared.slangh" + +BEGIN_NAMESPACE_FALCOR + +enum class SceneDebuggerMode : uint32_t +{ + FaceNormal, + ShadingNormal, + ShadingTangent, + ShadingBitangent, + FrontFacingFlag, + BackfacingShadingNormal, + TexCoords, + MeshID, + MeshInstanceID, + MaterialID, + BlasID, + CurveID, + CurveInstanceID, + InstancedGeometry, +}; + +struct SceneDebuggerParams +{ + uint mode = 0; ///< Current visualization mode. See SceneDebuggerMode. + uint2 frameDim = { 0, 0 }; + uint frameCount = 0; + + uint2 selectedPixel = { 0, 0 }; ///< The currently selected pixel for readback. + int flipSign = false; ///< Flip sign before visualization. + int remapRange = true; ///< Remap valid range to [0,1] before output. + int clamp = true; ///< Clamp pixel values to [0,1] before output. + + int showVolumes = true; ///< Show volumes. + float densityScale = 1.f; ///< Volume density scale factor. + uint2 _pad; +}; + +struct PixelData +{ + static const uint kInvalidID = 0xffffffff; + + uint meshInstanceID; + uint meshID; + uint materialID; + uint blasID; + uint curveInstanceID; + uint curveID; +}; + +enum class InstanceInfoFlags : uint32_t +{ + IsInstanced = 0x1, +}; + +struct InstanceInfo +{ + uint flags = 0; ///< Flags as a combination of 'InstanceInfoFlags' flags. +}; + +END_NAMESPACE_FALCOR diff --git a/Source/RenderPasses/SkyBox/SkyBox.cpp b/Source/RenderPasses/SkyBox/SkyBox.cpp index 199b72dd22..d0a9a83bfe 100644 --- a/Source/RenderPasses/SkyBox/SkyBox.cpp +++ b/Source/RenderPasses/SkyBox/SkyBox.cpp @@ -142,13 +142,17 @@ void SkyBox::execute(RenderContext* pRenderContext, const RenderData& renderData if (!mpScene) return; + const auto& pEnvMap = mpScene->getEnvMap(); + mpProgram->addDefine("_USE_ENV_MAP", pEnvMap ? "1" : "0"); + if (pEnvMap) pEnvMap->setShaderData(mpVars["PerFrameCB"]["gEnvMap"]); + glm::mat4 world = glm::translate(mpScene->getCamera()->getPosition()); mpVars["PerFrameCB"]["gWorld"] = world; mpVars["PerFrameCB"]["gScale"] = mScale; mpVars["PerFrameCB"]["gViewMat"] = mpScene->getCamera()->getViewMatrix(); mpVars["PerFrameCB"]["gProjMat"] = mpScene->getCamera()->getProjMatrix(); mpState->setFbo(mpFbo); - mpCubeScene->render(pRenderContext, mpState.get(), mpVars.get(), Scene::RenderFlags::UserRasterizerState); + mpCubeScene->rasterize(pRenderContext, mpState.get(), mpVars.get(), Scene::RenderFlags::UserRasterizerState); } void SkyBox::setScene(RenderContext* pRenderContext, const Scene::SharedPtr& pScene) @@ -190,7 +194,7 @@ void SkyBox::setTexture(const Texture::SharedPtr& pTexture) if (mpTexture) { assert(mpTexture->getType() == Texture::Type::TextureCube || mpTexture->getType() == Texture::Type::Texture2D); - (mpTexture->getType() == Texture::Type::Texture2D) ? mpProgram->addDefine("_SPHERICAL_MAP") : mpProgram->removeDefine("_SPHERICAL_MAP"); + mpProgram->addDefine("_USE_SPHERICAL_MAP", mpTexture->getType() == Texture::Type::Texture2D ? "1" : "0"); } mpVars["gTexture"] = mpTexture; } diff --git a/Source/RenderPasses/SkyBox/SkyBox.slang b/Source/RenderPasses/SkyBox/SkyBox.slang index 9091b025ef..a0ccf541b3 100644 --- a/Source/RenderPasses/SkyBox/SkyBox.slang +++ b/Source/RenderPasses/SkyBox/SkyBox.slang @@ -28,20 +28,22 @@ import Scene.Scene; import Scene.ShadingData; import Utils.Helpers; +import Experimental.Scene.Lights.EnvMap; -#ifdef _SPHERICAL_MAP +#if _USE_SPHERICAL_MAP Texture2D gTexture; #else TextureCube gTexture; #endif SamplerState gSampler; -cbuffer PerFrameCB : register(b0) +cbuffer PerFrameCB { float4x4 gWorld; float4x4 gViewMat; float4x4 gProjMat; float gScale; + EnvMap gEnvMap; }; void vs(float4 posL : POSITION, out float3 dir : NORMAL, out float4 posH : SV_POSITION) @@ -55,11 +57,15 @@ void vs(float4 posL : POSITION, out float3 dir : NORMAL, out float4 posH : SV_PO float4 ps(float3 dir : NORMAL) : SV_TARGET { -#ifdef _SPHERICAL_MAP +#if _USE_ENV_MAP + float3 color = gEnvMap.eval(dir); + return float4(color, 1.f); +#else +#if _USE_SPHERICAL_MAP float2 uv = dirToSphericalCrd(dir); -float4 color = gTexture.Sample(gSampler, uv); -return color; + return gTexture.Sample(gSampler, uv); #else return gTexture.SampleLevel(gSampler, normalize(dir), 0); #endif +#endif // _USE_ENV_MAP } diff --git a/Source/RenderPasses/SkyBox/SkyBox.vcxproj b/Source/RenderPasses/SkyBox/SkyBox.vcxproj index c7026da362..f11857be29 100644 --- a/Source/RenderPasses/SkyBox/SkyBox.vcxproj +++ b/Source/RenderPasses/SkyBox/SkyBox.vcxproj @@ -14,7 +14,7 @@ {C6EB0AC0-FF3B-474E-A453-44D84329D13A} Win32Proj SkyBox - 10.0.18362.0 + 10.0.19041.0 SkyBox diff --git a/Source/RenderPasses/TemporalDelayPass/TemporalDelayPass.vcxproj b/Source/RenderPasses/TemporalDelayPass/TemporalDelayPass.vcxproj index 9a8a014d68..dcfbca9f7c 100644 --- a/Source/RenderPasses/TemporalDelayPass/TemporalDelayPass.vcxproj +++ b/Source/RenderPasses/TemporalDelayPass/TemporalDelayPass.vcxproj @@ -1,4 +1,4 @@ - + @@ -25,7 +25,7 @@ {6B527E70-C2C6-4C87-BB7D-E1154F6A6FEF} Win32Proj FeatureDemo - 10.0.18362.0 + 10.0.19041.0 TemporalDelayPass diff --git a/Source/RenderPasses/ToneMapper/ToneMapper.cpp b/Source/RenderPasses/ToneMapper/ToneMapper.cpp index 1b4f34cded..bc19810211 100644 --- a/Source/RenderPasses/ToneMapper/ToneMapper.cpp +++ b/Source/RenderPasses/ToneMapper/ToneMapper.cpp @@ -161,7 +161,6 @@ ToneMapper::SharedPtr ToneMapper::create(RenderContext* pRenderContext, const Di { if (key == kExposureCompensation) pTM->setExposureCompensation(value); else if (key == kAutoExposure) pTM->setAutoExposure(value); - else if (key == kExposureValue) pTM->setExposureValue(value); else if (key == kFilmSpeed) pTM->setFilmSpeed(value); else if (key == kWhiteBalance) pTM->setWhiteBalance(value); else if (key == kWhitePoint) pTM->setWhitePoint(value); @@ -184,7 +183,6 @@ Dictionary ToneMapper::getScriptingDictionary() if (mOutputFormat != ResourceFormat::Unknown) d[kOutputFormat] = mOutputFormat; d[kExposureCompensation] = mExposureCompensation; d[kAutoExposure] = mAutoExposure; - d[kExposureValue] = mExposureValue; d[kFilmSpeed] = mFilmSpeed; d[kWhiteBalance] = mWhiteBalance; d[kWhitePoint] = mWhitePoint; @@ -289,33 +287,6 @@ void ToneMapper::createLuminanceFbo(const Texture::SharedPtr& pSrc) } } -// Update camera settings based on EV and exposure mode -void ToneMapper::updateCameraSettings() -{ - if (mExposureMode == ExposureMode::AperturePriority) - { - // Set shutter based on EV and aperture - mShutter = std::pow(2.f, mExposureValue) / (mFNumber * mFNumber); - // Clamp to plausible value - mShutter = glm::clamp(mShutter, kShutterMin, kShutterMax); - // Recompute EV based on clamped shutter - updateExposureValue(); - } - else if (mExposureMode == ExposureMode::ShutterPriority) - { - // Set aperture based on EV and shutter - mFNumber = std::sqrt(std::pow(2.f, mExposureValue) / mShutter); - // Clamp to plausible value - mFNumber = glm::clamp(mFNumber, kFNumberMin, kFNumberMax); - // Recompute EV based on clamped aperture - updateExposureValue(); - } - else - { - should_not_get_here(); - } -} - // Set EV based on fNumber and shutter void ToneMapper::updateExposureValue() { @@ -413,7 +384,25 @@ void ToneMapper::setAutoExposure(bool autoExposure) void ToneMapper::setExposureValue(float exposureValue) { mExposureValue = glm::clamp(exposureValue, kExposureValueMin, kExposureValueMax); - updateCameraSettings(); + + switch (mExposureMode) + { + case ExposureMode::AperturePriority: + // Set shutter based on EV and aperture. + mShutter = std::pow(2.f, mExposureValue) / (mFNumber * mFNumber); + mShutter = glm::clamp(mShutter, kShutterMin, kShutterMax); + break; + case ExposureMode::ShutterPriority: + // Set aperture based on EV and shutter. + mFNumber = std::sqrt(std::pow(2.f, mExposureValue) / mShutter); + mFNumber = glm::clamp(mFNumber, kFNumberMin, kFNumberMax); + break; + default: + should_not_get_here(); + } + + updateExposureValue(); + mUpdateToneMapPass = true; } diff --git a/Source/RenderPasses/ToneMapper/ToneMapper.h b/Source/RenderPasses/ToneMapper/ToneMapper.h index e693a58c35..ed92ec9823 100644 --- a/Source/RenderPasses/ToneMapper/ToneMapper.h +++ b/Source/RenderPasses/ToneMapper/ToneMapper.h @@ -96,7 +96,6 @@ class ToneMapper : public RenderPass void updateWhiteBalanceTransform(); void updateColorTransform(); - void updateCameraSettings(); void updateExposureValue(); FullScreenPass::SharedPtr mpToneMapPass; diff --git a/Source/RenderPasses/ToneMapper/ToneMapper.vcxproj b/Source/RenderPasses/ToneMapper/ToneMapper.vcxproj index 429afb8d8a..b13e5475cc 100644 --- a/Source/RenderPasses/ToneMapper/ToneMapper.vcxproj +++ b/Source/RenderPasses/ToneMapper/ToneMapper.vcxproj @@ -14,7 +14,7 @@ {E58DB0ED-58CD-4724-91EA-99419084CB3A} Win32Proj ToneMapper - 10.0.18362.0 + 10.0.19041.0 ToneMapper diff --git a/Source/RenderPasses/Utils/Composite/Composite.cpp b/Source/RenderPasses/Utils/Composite/Composite.cpp index 1d7c82b891..e8f44688b3 100644 --- a/Source/RenderPasses/Utils/Composite/Composite.cpp +++ b/Source/RenderPasses/Utils/Composite/Composite.cpp @@ -41,6 +41,7 @@ namespace const std::string kMode = "mode"; const std::string kScaleA = "scaleA"; const std::string kScaleB = "scaleB"; + const std::string kOutputFormat = "outputFormat"; const Gui::DropdownList kModeList = { @@ -56,17 +57,18 @@ Composite::SharedPtr Composite::create(RenderContext* pRenderContext, const Dict Composite::Composite(const Dictionary& dict) { - // Set defines to avoid compiler warnings about undefined macros. Proper values will be assigned at runtime. - Program::DefineList defines = { { "COMPOSITE_MODE", "0" } }; - mCompositePass = ComputePass::create(kShaderFile, "main", defines); - + // Parse dictionary. for (const auto& [key, value] : dict) { if (key == kMode) mMode = value; else if (key == kScaleA) mScaleA = value; else if (key == kScaleB) mScaleB = value; - else logError("Unknown field '" + key + "' in Composite pass dictionary"); + else if (key == kOutputFormat) mOutputFormat = value; + else logWarning("Unknown field '" + key + "' in Composite pass dictionary"); } + + // Create resources. + mCompositePass = ComputePass::create(kShaderFile, "main", Program::DefineList(), false); } Dictionary Composite::getScriptingDictionary() @@ -75,25 +77,57 @@ Dictionary Composite::getScriptingDictionary() dict[kMode] = mMode; dict[kScaleA] = mScaleA; dict[kScaleB] = mScaleB; + if (mOutputFormat != ResourceFormat::Unknown) dict[kOutputFormat] = mOutputFormat; return dict; } RenderPassReflection Composite::reflect(const CompileData& compileData) { RenderPassReflection reflector; - reflector.addInput(kInputA, "Input A").bindFlags(ResourceBindFlags::ShaderResource); - reflector.addInput(kInputB, "Input B").bindFlags(ResourceBindFlags::ShaderResource); - reflector.addOutput(kOutput, "Output").bindFlags(ResourceBindFlags::UnorderedAccess).format(ResourceFormat::RGBA32Float); // TODO: Allow user to specify output format + reflector.addInput(kInputA, "Input A").bindFlags(ResourceBindFlags::ShaderResource).flags(RenderPassReflection::Field::Flags::Optional); + reflector.addInput(kInputB, "Input B").bindFlags(ResourceBindFlags::ShaderResource).flags(RenderPassReflection::Field::Flags::Optional); + reflector.addOutput(kOutput, "Output").bindFlags(ResourceBindFlags::UnorderedAccess).format(mOutputFormat); return reflector; } void Composite::compile(RenderContext* pContext, const CompileData& compileData) { mFrameDim = compileData.defaultTexDims; - mCompositePass["CB"]["frameDim"] = mFrameDim; } void Composite::execute(RenderContext* pRenderContext, const RenderData& renderData) +{ + // Prepare program. + const auto& pOutput = renderData[kOutput]->asTexture(); + assert(pOutput); + mOutputFormat = pOutput->getFormat(); + + if (mCompositePass->getProgram()->addDefines(getDefines())) + { + mCompositePass->setVars(nullptr); + } + + // Bind resources. + auto var = mCompositePass["CB"]; + var["frameDim"] = mFrameDim; + var["scaleA"] = mScaleA; + var["scaleB"] = mScaleB; + + mCompositePass["A"] = renderData[kInputA]->asTexture(); // Can be nullptr + mCompositePass["B"] = renderData[kInputB]->asTexture(); // Can be nullptr + mCompositePass["output"] = pOutput; + mCompositePass->execute(pRenderContext, mFrameDim.x, mFrameDim.y); +} + +void Composite::renderUI(Gui::Widgets& widget) +{ + widget.text("This pass scales and composites inputs A and B together"); + widget.dropdown("Mode", kModeList, reinterpret_cast(mMode)); + widget.var("Scale A", mScaleA); + widget.var("Scale B", mScaleB); +} + +Program::DefineList Composite::getDefines() const { uint32_t compositeMode = 0; switch (mMode) @@ -108,24 +142,27 @@ void Composite::execute(RenderContext* pRenderContext, const RenderData& renderD should_not_get_here(); break; } - mCompositePass->addDefine("COMPOSITE_MODE", std::to_string(compositeMode)); - auto cb = mCompositePass["CB"]; - cb["scaleA"] = mScaleA; - cb["scaleB"] = mScaleB; + assert(mOutputFormat != ResourceFormat::Unknown); + uint32_t outputFormat = 0; + switch (getFormatType(mOutputFormat)) + { + case FormatType::Uint: + outputFormat = OUTPUT_FORMAT_UINT; + break; + case FormatType::Sint: + outputFormat = OUTPUT_FORMAT_SINT; + break; + default: + outputFormat = OUTPUT_FORMAT_FLOAT; + break; + } - mCompositePass["A"] = renderData[kInputA]->asTexture(); - mCompositePass["B"] = renderData[kInputB]->asTexture(); - mCompositePass["output"] = renderData[kOutput]->asTexture(); - mCompositePass->execute(pRenderContext, mFrameDim.x, mFrameDim.y); -} + Program::DefineList defines; + defines.add("COMPOSITE_MODE", std::to_string(compositeMode)); + defines.add("OUTPUT_FORMAT", std::to_string(outputFormat)); -void Composite::renderUI(Gui::Widgets& widget) -{ - widget.text("This pass scales and composites inputs A and B together"); - widget.dropdown("Mode", kModeList, reinterpret_cast(mMode)); - widget.var("Scale A", mScaleA); - widget.var("Scale B", mScaleB); + return defines; } void Composite::registerBindings(pybind11::module& m) diff --git a/Source/RenderPasses/Utils/Composite/Composite.cs.slang b/Source/RenderPasses/Utils/Composite/Composite.cs.slang index ef3aa234c9..b06c0bbf56 100644 --- a/Source/RenderPasses/Utils/Composite/Composite.cs.slang +++ b/Source/RenderPasses/Utils/Composite/Composite.cs.slang @@ -37,9 +37,22 @@ cbuffer CB float scaleB; } -Texture2D A; -Texture2D B; -RWTexture2D output; +// Inputs +Texture2D A; +Texture2D B; + +// Output +#if !defined(OUTPUT_FORMAT) +#error OUTPUT_FORMAT is undefined +#elif OUTPUT_FORMAT == OUTPUT_FORMAT_FLOAT +RWTexture2D output; +#elif OUTPUT_FORMAT == OUTPUT_FORMAT_UINT +RWTexture2D output; +#elif OUTPUT_FORMAT == OUTPUT_FORMAT_SINT +RWTexture2D output; +#else +#error OUTPUT_FORMAT unknown +#endif [numthreads(16, 16, 1)] void main(uint3 dispatchThreadId : SV_DispatchThreadID) @@ -47,9 +60,19 @@ void main(uint3 dispatchThreadId : SV_DispatchThreadID) const uint2 pixel = dispatchThreadId.xy; if (any(pixel >= frameDim)) return; -#if COMPOSITE_MODE == COMPOSITE_MODE_ADD - output[pixel] = (scaleA * A[pixel]) + (scaleB * B[pixel]); + float4 result = float4(0.f); +#if !defined(COMPOSITE_MODE) + #error COMPOSITE_MODE is undefined +#elif COMPOSITE_MODE == COMPOSITE_MODE_ADD + result = (scaleA * A[pixel]) + (scaleB * B[pixel]); #elif COMPOSITE_MODE == COMPOSITE_MODE_MULTIPLY - output[pixel] = (scaleA * A[pixel]) * (scaleB * B[pixel]); + result = (scaleA * A[pixel]) * (scaleB * B[pixel]); +#else + #error COMPOSITE_MODE unknown +#endif + +#if OUTPUT_FORMAT != OUTPUT_FORMAT_FLOAT + result = round(result); #endif + output[pixel] = result; } diff --git a/Source/RenderPasses/Utils/Composite/Composite.h b/Source/RenderPasses/Utils/Composite/Composite.h index 17790da748..c8c306535e 100644 --- a/Source/RenderPasses/Utils/Composite/Composite.h +++ b/Source/RenderPasses/Utils/Composite/Composite.h @@ -30,6 +30,13 @@ using namespace Falcor; +/** Simple composite pass that blends two buffers together. + + Each input A and B can be independently scaled, and the output C + is computed C = A B, where the blend operation is configurable. + If the output buffer C is of integer format, floating point values + are converted to integers using round-to-nearest-even. +*/ class Composite : public RenderPass { public: @@ -59,10 +66,13 @@ class Composite : public RenderPass private: Composite(const Dictionary& dict); + Program::DefineList getDefines() const; + uint2 mFrameDim = { 0, 0 }; Mode mMode = Mode::Add; float mScaleA = 1.f; float mScaleB = 1.f; + ResourceFormat mOutputFormat = ResourceFormat::RGBA32Float; ComputePass::SharedPtr mCompositePass; }; diff --git a/Source/RenderPasses/Utils/Composite/CompositeMode.slangh b/Source/RenderPasses/Utils/Composite/CompositeMode.slangh index 1b62e89a4c..281a015e60 100644 --- a/Source/RenderPasses/Utils/Composite/CompositeMode.slangh +++ b/Source/RenderPasses/Utils/Composite/CompositeMode.slangh @@ -30,3 +30,7 @@ // Type defines shared between host and device. #define COMPOSITE_MODE_ADD 0 #define COMPOSITE_MODE_MULTIPLY 1 + +#define OUTPUT_FORMAT_FLOAT 0 +#define OUTPUT_FORMAT_UINT 1 +#define OUTPUT_FORMAT_SINT 2 diff --git a/Source/RenderPasses/Utils/Utils.vcxproj b/Source/RenderPasses/Utils/Utils.vcxproj index 60ab7718ec..49df45ab3b 100644 --- a/Source/RenderPasses/Utils/Utils.vcxproj +++ b/Source/RenderPasses/Utils/Utils.vcxproj @@ -14,7 +14,7 @@ {BCA3CCD2-6118-4821-BF30-23B725B0C230} Win32Proj Utils - 10.0.18362.0 + 10.0.19041.0 Utils diff --git a/Source/RenderPasses/WhittedRayTracer/Data/WhittedRayTracer_GB_rast.py b/Source/RenderPasses/WhittedRayTracer/Data/WhittedRayTracer_GB_rast.py index dde9bda4a6..fd05e6f34f 100644 --- a/Source/RenderPasses/WhittedRayTracer/Data/WhittedRayTracer_GB_rast.py +++ b/Source/RenderPasses/WhittedRayTracer/Data/WhittedRayTracer_GB_rast.py @@ -9,7 +9,7 @@ def render_graph_WhittedRayTracer(): g.addPass(WhittedRayTracer, "WhittedRayTracer") GBufferRaster = createPass("GBufferRaster", {'samplePattern': SamplePattern.Center, 'sampleCount': 1, 'forceCullMode': True, 'cull': CullMode.CullNone,}) g.addPass(GBufferRaster, "GBufferRaster") - ToneMapper = createPass("ToneMapper", {'autoExposure': False, 'exposureValue': 1.0, 'exposureCompensation': 1.0, 'operator': ToneMapOp.Linear}) + ToneMapper = createPass("ToneMapper", {'autoExposure': False, 'exposureCompensation': 0.0, 'operator': ToneMapOp.Linear}) g.addPass(ToneMapper, "ToneMapper") g.addEdge("WhittedRayTracer.color", "ToneMapper.src") g.addEdge("GBufferRaster.posW", "WhittedRayTracer.posW") diff --git a/Source/RenderPasses/WhittedRayTracer/Data/WhittedRayTracer_GB_ray.py b/Source/RenderPasses/WhittedRayTracer/Data/WhittedRayTracer_GB_ray.py index 767bc2048c..9fc9310995 100644 --- a/Source/RenderPasses/WhittedRayTracer/Data/WhittedRayTracer_GB_ray.py +++ b/Source/RenderPasses/WhittedRayTracer/Data/WhittedRayTracer_GB_ray.py @@ -9,7 +9,7 @@ def render_graph_WhittedRayTracer(): g.addPass(WhittedRayTracer, "WhittedRayTracer") GBufferRT = createPass("GBufferRT", {'samplePattern': SamplePattern.Center, 'sampleCount': 1}) g.addPass(GBufferRT, "GBufferRT") - ToneMapper = createPass("ToneMapper", {'autoExposure': False, 'exposureValue': 1.0, 'exposureCompensation': 1.0, 'operator': ToneMapOp.Linear}) + ToneMapper = createPass("ToneMapper", {'autoExposure': False, 'exposureCompensation': 0.0, 'operator': ToneMapOp.Linear}) g.addPass(ToneMapper, "ToneMapper") g.addEdge("WhittedRayTracer.color", "ToneMapper.src") g.addEdge("GBufferRT.posW", "WhittedRayTracer.posW") diff --git a/Source/RenderPasses/WhittedRayTracer/WhittedRayTracer.cpp b/Source/RenderPasses/WhittedRayTracer/WhittedRayTracer.cpp index 240668652d..2a976db1f7 100644 --- a/Source/RenderPasses/WhittedRayTracer/WhittedRayTracer.cpp +++ b/Source/RenderPasses/WhittedRayTracer/WhittedRayTracer.cpp @@ -115,7 +115,7 @@ WhittedRayTracer::WhittedRayTracer(const Dictionary& dict) { "mtlEmissive", "gMaterialEmissive", "Material emissive color (xyz)" }, { "mtlParams", "gMaterialExtraParams", "Material parameters (IoR, flags etc)" }, { "surfSpreadAngle", "gSurfaceSpreadAngle", "surface spread angle (texlod)", true, ResourceFormat::R16Float }, - { "vbuffer", "gVBuffer", "Visibility buffer in packed 64-bit format", true, ResourceFormat::RG32Uint }, + { "vbuffer", "gVBuffer", "Visibility buffer in packed format", true, ResourceFormat::Unknown }, }; mRayConeModes = { @@ -136,7 +136,7 @@ WhittedRayTracer::WhittedRayTracer(const Dictionary& dict) { "mtlSpecRough", "gMaterialSpecularRoughness", "Material specular color (xyz) and roughness (w)" }, { "mtlEmissive", "gMaterialEmissive", "Material emissive color (xyz)" }, { "mtlParams", "gMaterialExtraParams", "Material parameters (IoR, flags etc)" }, - { "vbuffer", "gVBuffer", "Visibility buffer in packed 64-bit format", true, ResourceFormat::RG32Uint }, + { "vbuffer", "gVBuffer", "Visibility buffer in packed format", true, ResourceFormat::Unknown }, }; mRayConeModes = { diff --git a/Source/RenderPasses/WhittedRayTracer/WhittedRayTracer.rt.slang b/Source/RenderPasses/WhittedRayTracer/WhittedRayTracer.rt.slang index 5efba18f86..955188cd28 100644 --- a/Source/RenderPasses/WhittedRayTracer/WhittedRayTracer.rt.slang +++ b/Source/RenderPasses/WhittedRayTracer/WhittedRayTracer.rt.slang @@ -74,8 +74,7 @@ Texture2D gMaterialSpecularRoughness; Texture2D gMaterialEmissive; Texture2D gMaterialExtraParams; Texture2D gSurfaceSpreadAngle; -Texture2D gVBuffer; - +Texture2D gVBuffer; // Outputs RWTexture2D gOutputColor; @@ -346,13 +345,6 @@ void scatterClosestHit( } } - // Compute tangent space if it is invalid. - if (!(dot(sd.T, sd.T) > 0.f)) // Note: Comparison written so that NaNs trigger. - { - sd.T = perp_stark(sd.N); - sd.B = cross(sd.N, sd.T); - } - // Add emitted light. if (kUseEmissiveLights) { @@ -475,9 +467,9 @@ void rayGen() HitInfo hit; if (hit.decode(gVBuffer[launchIndex])) { - const float4x4 worldMat = gScene.getWorldMatrix(hit.meshInstanceID); - const float3x3 worldInvTransposeMat = gScene.getInverseTransposeWorldMatrix(hit.meshInstanceID); - const uint3 vertexIndices = gScene.getIndices(hit.meshInstanceID, hit.primitiveIndex); + const float4x4 worldMat = gScene.getWorldMatrix(hit.instanceID); + const float3x3 worldInvTransposeMat = gScene.getInverseTransposeWorldMatrix(hit.instanceID); + const uint3 vertexIndices = gScene.getIndices(hit.instanceID, hit.primitiveIndex); const float3 barycentrics = hit.getBarycentricWeights(); float2 txcoords[3], dBarydx, dBarydy, dUVdx, dUVdy; @@ -503,7 +495,7 @@ void rayGen() } else // kRayConeMode == RayConeMode::Unified { - float curvature = gScene.computeCurvatureIsotropicFirstHit(hit.meshInstanceID, hit.primitiveIndex, rayDir); + float curvature = gScene.computeCurvatureIsotropicFirstHit(hit.instanceID, hit.primitiveIndex, rayDir); float rayConeWidth = hitT * gScreenSpacePixelSpreadAngle; surfaceSpreadAngle = computeSpreadAngleFromCurvatureIso(curvature, hitT * gScreenSpacePixelSpreadAngle, rayDir, sd.N); } @@ -541,9 +533,9 @@ void rayGen() if (hit.decode(gVBuffer[launchIndex])) { - const float4x4 worldMat = gScene.getWorldMatrix(hit.meshInstanceID); - const float3x3 worldInvTransposeMat = gScene.getInverseTransposeWorldMatrix(hit.meshInstanceID); - const uint3 vertexIndices = gScene.getIndices(hit.meshInstanceID, hit.primitiveIndex); + const float4x4 worldMat = gScene.getWorldMatrix(hit.instanceID); + const float3x3 worldInvTransposeMat = gScene.getInverseTransposeWorldMatrix(hit.instanceID); + const uint3 vertexIndices = gScene.getIndices(hit.instanceID, hit.primitiveIndex); const float3 barycentrics = hit.getBarycentricWeights(); float3 unnormalizedN, normals[3], dNdx, dNdy, edge1, edge2; float2 txcoords[3], dBarydx, dBarydy, dUVdx, dUVdy; diff --git a/Source/RenderPasses/WhittedRayTracer/WhittedRayTracer.vcxproj b/Source/RenderPasses/WhittedRayTracer/WhittedRayTracer.vcxproj index 7a0f7c8f80..1e365e7fe7 100644 --- a/Source/RenderPasses/WhittedRayTracer/WhittedRayTracer.vcxproj +++ b/Source/RenderPasses/WhittedRayTracer/WhittedRayTracer.vcxproj @@ -14,7 +14,7 @@ {431C3127-E613-424C-B964-FB53DAA87789} Win32Proj WhittedRayTracer - 10.0.18362.0 + 10.0.19041.0 WhittedRayTracer diff --git a/Source/Samples/CudaInterop/CopySurface.cu b/Source/Samples/CudaInterop/CopySurface.cu index fa0311eff3..4e7a2f625b 100644 --- a/Source/Samples/CudaInterop/CopySurface.cu +++ b/Source/Samples/CudaInterop/CopySurface.cu @@ -26,6 +26,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ***************************************************************************/ #include "CopySurface.h" +#include // The CUDA kernel. This sample simply copies the input surface. template diff --git a/Source/Samples/CudaInterop/CopySurface.h b/Source/Samples/CudaInterop/CopySurface.h index 272b2fa1e8..4991fb6c52 100644 --- a/Source/Samples/CudaInterop/CopySurface.h +++ b/Source/Samples/CudaInterop/CopySurface.h @@ -25,4 +25,6 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **************************************************************************/ +#include + extern void launchCopySurface(cudaSurfaceObject_t input, cudaSurfaceObject_t output, unsigned int width, unsigned int height, unsigned int format); diff --git a/Source/Samples/CudaInterop/CudaInterop.cpp b/Source/Samples/CudaInterop/CudaInterop.cpp index 102b4ed854..49478ef4f5 100644 --- a/Source/Samples/CudaInterop/CudaInterop.cpp +++ b/Source/Samples/CudaInterop/CudaInterop.cpp @@ -56,7 +56,7 @@ void CudaInterop::onLoad(RenderContext* pRenderContext) void CudaInterop::onFrameRender(RenderContext* pRenderContext, const Fbo::SharedPtr& pTargetFbo) { - const float4 clearColor(0.38f, 0.52f, 0.10f, 1); + const Falcor::float4 clearColor(0.38f, 0.52f, 0.10f, 1); pRenderContext->clearFbo(pTargetFbo.get(), clearColor, 1.0f, 0, FboAttachmentType::All); // Call the CUDA kernel @@ -71,12 +71,13 @@ int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _ if (!FalcorCUDA::initCUDA()) { logError("CUDA driver API initialization failed"); - return -1; + exit(1); } + CudaInterop::UniquePtr pRenderer = std::make_unique(); SampleConfig config; config.windowDesc.title = "Falcor-Cuda Interop"; config.windowDesc.resizableWindow = true; + Sample::run(config, pRenderer); - return 0; } diff --git a/Source/Samples/CudaInterop/CudaInterop.h b/Source/Samples/CudaInterop/CudaInterop.h index a4b894e54b..3bd3b32da6 100644 --- a/Source/Samples/CudaInterop/CudaInterop.h +++ b/Source/Samples/CudaInterop/CudaInterop.h @@ -1,3 +1,4 @@ +#pragma once /*************************************************************************** # Copyright (c) 2020, NVIDIA CORPORATION. All rights reserved. # @@ -26,7 +27,7 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **************************************************************************/ #pragma once -#include "cuda.h" +#include #include "Falcor.h" #include "FalcorCUDA.h" diff --git a/Source/Samples/CudaInterop/CudaInterop.vcxproj b/Source/Samples/CudaInterop/CudaInterop.vcxproj index c6340901be..38a2315b04 100644 --- a/Source/Samples/CudaInterop/CudaInterop.vcxproj +++ b/Source/Samples/CudaInterop/CudaInterop.vcxproj @@ -10,55 +10,38 @@ x64 - - - - - - - - - - - - - - - {2c535635-e4c5-4098-a928-574f0e7cd5f9} - - - {B80C5BB4-A82E-4BAC-BF95-910AED1947AF} + {08E3FADA-4AAC-442A-9F84-22F95934A914} CudaInterop - 10.0.17763.0 + $(SolutionDir)Source\Externals\.packman\cuda Application true MultiByte - v141 + v142 Application false true MultiByte - v141 + v142 - + - + - + @@ -73,7 +56,7 @@ true Windows - cudart_static.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) 64 @@ -92,14 +75,28 @@ true true Windows - cudart_static.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) + kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;%(AdditionalDependencies) 64 + + + + + + {2c535635-e4c5-4098-a928-574f0e7cd5f9} + + + + + + + + + + + - - - \ No newline at end of file diff --git a/Source/Samples/CudaInterop/FalcorCUDA.cpp b/Source/Samples/CudaInterop/FalcorCUDA.cpp index a3d8f66454..6db3a5ff70 100644 --- a/Source/Samples/CudaInterop/FalcorCUDA.cpp +++ b/Source/Samples/CudaInterop/FalcorCUDA.cpp @@ -31,26 +31,26 @@ #include #include "Core/API/Device.h" -#define CU_CHECK_SUCCESS(x) \ - do { \ - CUresult result = x; \ - if (result != CUDA_SUCCESS) \ - { \ - const char *msg; \ - cuGetErrorName(result, &msg); \ - logError("CUDA Error: " #x " failed with error " + msg); \ - return 0; \ - } \ +#define CU_CHECK_SUCCESS(x) \ + do { \ + CUresult result = x; \ + if (result != CUDA_SUCCESS) \ + { \ + const char* msg; \ + cuGetErrorName(result, &msg); \ + logError("CUDA Error: " #x " failed with error " + std::string(msg)); \ + return 0; \ + } \ } while(0) -#define CUDA_CHECK_SUCCESS(x) \ - do { \ - cudaError_t result = x; \ - if (result != cudaSuccess) \ - { \ - logError("CUDA Error: " #x " failed with error " + cudaGetErrorString(result)); \ - return 0; \ - } \ +#define CUDA_CHECK_SUCCESS(x) \ + do { \ + cudaError_t result = x; \ + if (result != cudaSuccess) \ + { \ + logError("CUDA Error: " #x " failed with error " + std::string(cudaGetErrorString(result))); \ + return 0; \ + } \ } while(0) using namespace Falcor; @@ -66,11 +66,11 @@ namespace public: WindowsSecurityAttributes::WindowsSecurityAttributes() { - mWinPSecurityDescriptor = (PSECURITY_DESCRIPTOR) calloc(1, SECURITY_DESCRIPTOR_MIN_LENGTH + 2 * sizeof(void**)); - assert(mWinPSecurityDescriptor != (PSECURITY_DESCRIPTOR) NULL); + mWinPSecurityDescriptor = (PSECURITY_DESCRIPTOR)calloc(1, SECURITY_DESCRIPTOR_MIN_LENGTH + 2 * sizeof(void**)); + assert(mWinPSecurityDescriptor != (PSECURITY_DESCRIPTOR)NULL); - PSID* ppSID = (PSID*) ((PBYTE)mWinPSecurityDescriptor + SECURITY_DESCRIPTOR_MIN_LENGTH); - PACL* ppACL = (PACL*) ((PBYTE)ppSID + sizeof(PSID *)); + PSID* ppSID = (PSID*)((PBYTE)mWinPSecurityDescriptor + SECURITY_DESCRIPTOR_MIN_LENGTH); + PACL* ppACL = (PACL*)((PBYTE)ppSID + sizeof(PSID*)); InitializeSecurityDescriptor(mWinPSecurityDescriptor, SECURITY_DESCRIPTOR_REVISION); @@ -104,7 +104,7 @@ namespace if (*ppACL) LocalFree(*ppACL); free(mWinPSecurityDescriptor); } - SECURITY_ATTRIBUTES * operator&() { return &mWinSecurityAttributes; } + SECURITY_ATTRIBUTES* operator&() { return &mWinSecurityAttributes; } }; uint32_t gNodeMask; @@ -147,9 +147,9 @@ namespace FalcorCUDA return true; } - bool importTextureToMipmappedArray(Falcor::Texture::SharedPtr pTex, cudaMipmappedArray_t & mipmappedArray, uint32_t cudaUsageFlags) + bool importTextureToMipmappedArray(Falcor::Texture::SharedPtr pTex, cudaMipmappedArray_t& mipmappedArray, uint32_t cudaUsageFlags) { - HANDLE sharedHandle = pTex->createSharedApiHandle(); + HANDLE sharedHandle = pTex->getSharedApiHandle(); if (sharedHandle == NULL) { logError("FalcorCUDA::importTextureToMipmappedArray - texture shared handle creation failed"); diff --git a/Source/Samples/CudaInterop/FalcorCUDA.h b/Source/Samples/CudaInterop/FalcorCUDA.h index 534306299e..a7fb698112 100644 --- a/Source/Samples/CudaInterop/FalcorCUDA.h +++ b/Source/Samples/CudaInterop/FalcorCUDA.h @@ -26,11 +26,12 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. **************************************************************************/ #pragma once -#include "cuda_runtime.h" #include "Core/Framework.h" #include "Core/API/Texture.h" #include "Core/API/RenderContext.h" +#include + namespace FalcorCUDA { /** Initializes the CUDA driver API. Returns true if successful, false otherwise. @@ -44,7 +45,7 @@ namespace FalcorCUDA \param usageFlags The requested flags to be bound to the mipmapped array \return True if successful, false otherwise */ - bool importTextureToMipmappedArray(Falcor::Texture::SharedPtr pTex, cudaMipmappedArray_t & mipmappedArray, uint32_t cudaUsageFlags); + bool importTextureToMipmappedArray(Falcor::Texture::SharedPtr pTex, cudaMipmappedArray_t& mipmappedArray, uint32_t cudaUsageFlags); /** Maps a texture to a surface object which can be read and written within a CUDA kernel. This method should only be called once per texture on initial load. Store the returned surface object for repeated use. diff --git a/Source/Samples/CudaInterop/FalcorCUDA.props b/Source/Samples/CudaInterop/FalcorCUDA.props index cf291288fd..e874a79674 100644 --- a/Source/Samples/CudaInterop/FalcorCUDA.props +++ b/Source/Samples/CudaInterop/FalcorCUDA.props @@ -5,12 +5,14 @@ - $(CUDA_PATH)\include;%(AdditionalIncludeDirectories) + $(FALCOR_CORE_DIRECTORY)\Externals\.packman\cuda\include;%(AdditionalIncludeDirectories) - $(CUDA_PATH)\lib\x64;%(AdditionalLibraryDirectories) - cuda.lib;%(AdditionalDependencies) + $(CudaToolkitLibDir);%(AdditionalLibraryDirectories) + cudart_static.lib;%(AdditionalDependencies) - + + + \ No newline at end of file diff --git a/Source/Samples/CudaInterop/README.md b/Source/Samples/CudaInterop/README.md index 56d470d490..58480c1941 100644 --- a/Source/Samples/CudaInterop/README.md +++ b/Source/Samples/CudaInterop/README.md @@ -1,7 +1,12 @@ -# Using the CUDA interop with a project +# Using CUDA with Falcor -In order to use the CUDA interop, the following steps will need to be completed: -1. In Visual Studio, make a new CUDA Runtime project and add it to Falcor. (You must have the CUDA Toolkit installed to do so.) -2. Right-click on References under the new project in the Solution Explorer, select Add Reference, and add Falcor. -3. Open the Property Manager and add the Falcor and FalcorCUDA property sheets to both Debug and Release. -4. Open the project properties. If the project will produce a Windows application, go to General -> Configuration Type and change the setting to **Application (.exe)**, and go to Linker -> System -> SubSystem and change the setting to **Windows**. If the project is a DLL, then only the Configuration Type will need to be changed to **Dynamic Library (.dll)**. \ No newline at end of file +Download and run the CUDA Toolkit installer from [here](https://developer.nvidia.com/cuda-10.1-download-archive-update1). Navigate to the location the CUDA Toolkit was installed to (`C:\Program Files\NVIDIA GPU Computing Tools\CUDA` by default). Copy the `v.10.1` folder into `Source/Externals/.packman` and rename it `cuda`. + +The following will show how to create a CUDA project for use with Falcor: +1. Create a new CUDA Runtime project and add it to the Falcor solution. +2. In the Solution Explorer, right-click on `References` under the project and select `Add Reference`, then add `Falcor`. +3. Right-click on the project and go to `Build Dependencies -> Build Customizations`. Select `Find Existing`, and select `Source/Externals/.packman/cuda/extras/visual_studio_integration/MSBuildExtensions/CUDA 10.1.targets`. +4. Open the Property Manager and add the `Falcor` and `FalcorCUDA` property sheets to both Debug and Release. These are located in `Source/Falcor` and `Source/Samples/CudaInterop`, respectively. +5. Open the project's properties and go to `CUDA/C++` and set `CUDA Toolkit Custom Dir` to `$(SolutionDir)Source\Externals\.packman\cuda`, then go to `Linker -> System` and change the `SubSystem` to Windows. + +The CudaInterop sample is tested on CUDA Toolkit v.10.1.168. diff --git a/Source/Samples/HelloDXR/HelloDXR.cpp b/Source/Samples/HelloDXR/HelloDXR.cpp index 8f4eff58e6..31be22a108 100644 --- a/Source/Samples/HelloDXR/HelloDXR.cpp +++ b/Source/Samples/HelloDXR/HelloDXR.cpp @@ -28,7 +28,7 @@ #include "HelloDXR.h" static const float4 kClearColor(0.38f, 0.52f, 0.10f, 1); -static const std::string kDefaultScene = "Arcade/Arcade.fscene"; +static const std::string kDefaultScene = "Arcade/Arcade.pyscene"; void HelloDXR::onGuiRender(Gui* pGui) { @@ -56,7 +56,7 @@ void HelloDXR::loadScene(const std::string& filename, const Fbo* pTargetFbo) mpCamera = mpScene->getCamera(); // Update the controllers - float radius = length(mpScene->getSceneBounds().extent); + float radius = mpScene->getSceneBounds().radius(); mpScene->setCameraSpeed(radius * 0.25f); float nearZ = std::max(0.1f, radius / 750.0f); float farZ = radius * 10; diff --git a/Source/Samples/HelloDXR/HelloDXR.vcxproj b/Source/Samples/HelloDXR/HelloDXR.vcxproj index de5cf7fcc6..a23991dc56 100644 --- a/Source/Samples/HelloDXR/HelloDXR.vcxproj +++ b/Source/Samples/HelloDXR/HelloDXR.vcxproj @@ -1,4 +1,4 @@ - + @@ -29,7 +29,7 @@ {71B60B71-89A2-4196-BFB9-4A848CF6C541} Win32Proj DXRT - 10.0.18362.0 + 10.0.19041.0 HelloDXR diff --git a/Source/Samples/ModelViewer/ModelViewer.cpp b/Source/Samples/ModelViewer/ModelViewer.cpp index 47eb35c4bf..09dd4273da 100644 --- a/Source/Samples/ModelViewer/ModelViewer.cpp +++ b/Source/Samples/ModelViewer/ModelViewer.cpp @@ -49,7 +49,7 @@ void ModelViewer::loadModelFromFile(const std::string& filename, ResourceFormat SceneBuilder::Flags flags = SceneBuilder::Flags::None; if (mUseOriginalTangents) flags |= SceneBuilder::Flags::UseOriginalTangentSpace; - if (mRemoveDuplicateMaterials) flags |= SceneBuilder::Flags::RemoveDuplicateMaterials; + if (mDontMergeMaterials) flags |= SceneBuilder::Flags::DontMergeMaterials; flags |= isSrgbFormat(fboFormat) ? SceneBuilder::Flags::None : SceneBuilder::Flags::AssumeLinearSpaceTextures; SceneBuilder::SharedPtr pBuilder = SceneBuilder::create(filename, flags); @@ -93,8 +93,8 @@ void ModelViewer::onGuiRender(Gui* pGui) auto loadGroup = w.group("Load Options"); loadGroup.checkbox("Use Original Tangents", mUseOriginalTangents); loadGroup.tooltip("If this is unchecked, we will ignore the tangents that were loaded from the model and calculate them internally. Check this box if you'd like to use the original tangents"); - loadGroup.checkbox("Remove Duplicate Materials", mRemoveDuplicateMaterials); - loadGroup.tooltip("Deduplicate materials that have the same properties. The material name is ignored during the search"); + loadGroup.checkbox("Don't Merge Materials", mDontMergeMaterials); + loadGroup.tooltip("Don't merge materials that have the same properties. Use this option to preserve the original material names."); } w.separator(); @@ -190,7 +190,7 @@ void ModelViewer::onFrameRender(RenderContext* pRenderContext, const Fbo::Shared } mpGraphicsState->setProgram(mpProgram); - mpScene->render(pRenderContext, mpGraphicsState.get(), mpProgramVars.get(), renderFlags); + mpScene->rasterize(pRenderContext, mpGraphicsState.get(), mpProgramVars.get(), renderFlags); } TextRenderer::render(pRenderContext, mModelString, pTargetFbo, float2(10, 30)); diff --git a/Source/Samples/ModelViewer/ModelViewer.h b/Source/Samples/ModelViewer/ModelViewer.h index 2e1371f2cd..93f07b6efc 100644 --- a/Source/Samples/ModelViewer/ModelViewer.h +++ b/Source/Samples/ModelViewer/ModelViewer.h @@ -57,7 +57,7 @@ class ModelViewer : public IRenderer bool mDrawWireframe = false; bool mUseOriginalTangents = false; - bool mRemoveDuplicateMaterials = true; + bool mDontMergeMaterials = false; Scene::CameraControllerType mCameraType = Scene::CameraControllerType::FirstPerson; Scene::SharedPtr mpScene; diff --git a/Source/Samples/ModelViewer/ModelViewer.vcxproj b/Source/Samples/ModelViewer/ModelViewer.vcxproj index ad11f8448a..f47d46fd6b 100644 --- a/Source/Samples/ModelViewer/ModelViewer.vcxproj +++ b/Source/Samples/ModelViewer/ModelViewer.vcxproj @@ -1,4 +1,4 @@ - + @@ -28,7 +28,7 @@ {7BFFD891-AAD6-4E5C-8ADC-611C2625DCD9} Win32Proj ModelViewer - 10.0.18362.0 + 10.0.19041.0 diff --git a/Source/Samples/ProjectTemplate/ProjectTemplate.vcxproj b/Source/Samples/ProjectTemplate/ProjectTemplate.vcxproj index 7d714b4fe3..eca67439af 100644 --- a/Source/Samples/ProjectTemplate/ProjectTemplate.vcxproj +++ b/Source/Samples/ProjectTemplate/ProjectTemplate.vcxproj @@ -1,4 +1,4 @@ - + @@ -25,7 +25,7 @@ {605856E4-34D4-40DF-B859-EEA3A7D52A7B} Win32Proj ProjectTemplate - 10.0.18362.0 + 10.0.19041.0 diff --git a/Source/Samples/ShaderToy/ShaderToy.vcxproj b/Source/Samples/ShaderToy/ShaderToy.vcxproj index cadec9eb25..b7e1d15920 100644 --- a/Source/Samples/ShaderToy/ShaderToy.vcxproj +++ b/Source/Samples/ShaderToy/ShaderToy.vcxproj @@ -1,4 +1,4 @@ - + @@ -29,7 +29,7 @@ Win32Proj ShaderToy ShaderToy - 10.0.18362.0 + 10.0.19041.0 diff --git a/Source/Tools/FalcorTest/Data/pbrt_hair_bsdf.dat b/Source/Tools/FalcorTest/Data/pbrt_hair_bsdf.dat new file mode 100644 index 0000000000000000000000000000000000000000..ca10a0eafd75f9f1a0ca423fcb8d99aabef7dbc6 GIT binary patch literal 3400000 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

hLZ3#40Fw=SVGwG_8n0T@AbBh-qr=en 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